Unity 입문 TopDown Shooting - 데미지 피격

Amberjack·2024년 1월 21일
0

Unity

목록 보기
17/44

🩻 HealthSystem.cs

Entities 폴더 밑에 HealthSystem.cs를 생성하자!!!

public class HealthSystem : MonoBehaviour
{
    [SerializeField] private float healthChangeDelay = .5f;     // 무적 시간

    private CharacterStatsHandler _statsHandler;
    private float _timeSinceLastChange = float.MaxValue;        // 시간 저장

    public event Action OnDamage;
    public event Action OnHeal;
    public event Action OnDeath;
    public event Action OnInvincibilityEnd;

    public float CurrentHealth { get; private set; }        // 현재 체력

    public float MaxHealth => _statsHandler.currentStats.maxHealth;     // 최대 체력

    private void Awake()
    {
        _statsHandler = GetComponent<CharacterStatsHandler>();
    }

    private void Start()
    {
        CurrentHealth = _statsHandler.currentStats.maxHealth;       // 최대체력값 세팅하기
    }

    private void Update()
    {
        // _timeSinceLastChange : 체력이 닳은 순간의 시간을 기록
        if (_timeSinceLastChange < healthChangeDelay)       // 체력이 닳으면
        {
            _timeSinceLastChange += Time.deltaTime;     // 체력이 닳은 순간부터 시간을 기록하기
            if (_timeSinceLastChange >= healthChangeDelay)  // _timeSinceLastChange >= healthChangeDelay : 체력이 닳은 시간이 무적 시간보다 길어지면
            {
                OnInvincibilityEnd?.Invoke();       // 무적 상태 해제하기
            }
        }
    }

    public bool ChangeHealth(float change)
    {
        if (change == 0 || _timeSinceLastChange < healthChangeDelay)    // 체력에 변화가 없거나, 무적 시간일 경우 return false.
        {
            return false;
        }

        _timeSinceLastChange = 0f;      // 체력이 닳은 시간 초기화
        CurrentHealth += change;

        // 캐릭터가 치유를 받았을 경우, 치유 받은 HP가 최대 체력보다 높아지면 
        // 최대체력으로 변경하기
        CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;      
        CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth;

        if (change > 0)
        {
            OnHeal?.Invoke();       // 체력의 변화량이 양수라면, 힐!
        }
        else
        {
            OnDamage?.Invoke();     // 음수라면, 데미지!
        }

        // 현재 체력이 0 이하가 될 경우, 죽음!
        if (CurrentHealth <= 0f)
        {
            CallDeath();
        }

        return true;
    }

    private void CallDeath()
    {
        OnDeath?.Invoke();
    }
}

HealthSystem을 지닌 객체의 체력에 변화가 생길 경우,

  • 체력의 변화량이 양수일 경우 : 체력을 채워주되, 채워진 체력이 해당 객체의 최대 체력보다 커질 경우, 최대 체력으로 고정하기.
  • 체력의 변화량이 음수일 경우 : 체력을 감소시키고, 해당 객체에게 0.5초의 무적 시간을 준다. 만약 무적 시간 내에 데미지를 다시 받을 경우, 체력이 변화되지 않는다. 만약 체력이 0 이하가 될 경우, 죽는다.

🩻 HealthSystem 적용시키기!!!

HealthSystem은 이 게임에 등장하는 적, 캐릭터 모두에게 적용해야 한다!

적, 플레이어에게 모두 HealthSystem.cs를 추가해준다.

🫥 HealthSystem 이벤트 처리하기 - Animation Controller

TopDownAnimationController.cs에서 HeathSystem을 추가하자.

--------------------- 생략--------------------- 
private HealthSystem _healthSystem;

protected override void Awake()
{
    base.Awake();
    _healthSystem = GetComponent<HealthSystem>();
}

--------------------- 생략--------------------- 

void Start()
{
    controller.OnAttackEvent += Attacking;
    controller.OnMoveEvent += Move;
	
    // HealthSystem 이벤트 구독하기
    if(_healthSystem != null)
    {
        _healthSystem.OnDamage += Hit;		// 데미지 피격 시
        _healthSystem.OnInvincibilityEnd += InvincibilityEnd;		// 무적 상태 종료
    }
}

🎯 HealthSystem 이벤트 처리하기 - RangedAttackController

RangedAttackController에서 상대를 공격했을 때의 HealthSystem 처리를 하자.

// 현재 Arrow의 rigidbody2d에 is Trigger를 켜놓았기 때문에 트리거 충돌이 발생한다.
private void OnTriggerEnter2D(Collider2D collision)
{
    // LayerMask의 비트 연산.
    // 현재 Level의 layer 값은 7이다. 즉, 1 << collision.gameObject.layer를 진행하면 1을 7번 left shift 한 값이 나온다. 1 0 0 0 0 0 0 0
    // 이 때, OR 연산을 통해 내가 찾는 layer값과 비교를 한다. 만약 levelCollisioinLayer.value 도 1 0 0 0 0 0 0 0 이라면 OR를 해도 1 0 0 0 0 0 0 0 이다.
    // 이 경우, levelCollisionLayer.value 값과 같기 때문에 if문이 실행된다.
    if (levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))       // 만약 부딪힌 collision의 레이어 값이 Level 이라면
    {
        // 발사체 삭제.
        // collision.ClosestPoint(transform.position) - _direction * .2f : 부딪힌 지점(positioni)에서 0.2f 만큼 안쪽
        DestroyProjectile(collision.ClosestPoint(transform.position) - _direction * .2f, fxOnDestroy);
    }
    // 만약 발사체가 공격 대상과 부딪히면
    else if(_attackData.target.value == ((_attackData.target.value) | (1 << collision.gameObject.layer)))
    {
        HealthSystem healthSystem = collision.GetComponent<HealthSystem>();     // 공격 대상의 HealthSystem을 가져온다.

        // 공격 대상이 HealthSystem가 null이라면 = 해당 대상은 체력이 닳지 않는다.
        // 공격 대상이 HealthSystem가 null이 아니라면 = 해당 대상의 체력을 깎는다.
        if (healthSystem != null)
        {
            healthSystem.ChangeHealth(_attackData.power);       // 체력을 공격력만큼 차감.
            if (_attackData.isOnKnockback)
            {
                TopDownMovement movement = collision.GetComponent<TopDownMovement>();
                if(movement != null)
                {
                    movement.ApplyKnockback(transform, _attackData.knockbackPower, _attackData.knockbackTime);      // 넉벡 발생시키기
                }
            }
        }
        DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestroy);      // 공격 대상에 명중한 발사체 제거
    }
}

이후, 플레이어의 AttackData에 target이 enemy로 설정이 되어 있는지 체크한다.

그리고 적들의 layer가 enemy로 설정되어 있는지 확인한다.

확인해보기!!!

이제 근접 공격의 데미지 구현을 하자!

💥 근접 공격 데미지 구현하기

현재 오크의 공격에 플레이어가 반응하지 않는 것을 확인할 수 있다. 근접 공격의 데미지 구현을 하자.

private HealthSystem healthSystem;		// 근접 공격을 하는 적
private HealthSystem _collidingTargetHealthSystem;		// 플레이어
private TopDownMovement _collidingMovement;

protected override void Start()
{
    base.Start();
    
    healthSystem  = GetComponent<HealthSystem>();	// 근접 공격하는 적의 HealthSystem 가져오기
    healthSystem.OnDamage += OnDamage;		// 플레이어가 근접 공격 받았을 경우 OnDamage
}

private void OnDamage()
{
	// 플레이어가 공격을 받고 넉벡에 의해 멀어졌을 때, 계속 쫓아오도록
    // 색적 거리를 늘려준다.
    followRange = 100f;		
}

protected override void FixedUpdate()
{
    base.FixedUpdate();

	// 만약 근접 공격을 했으면
    if(_isCollidingWithTarget)
    {
        ApplyHealthChange();
    }

-------------------------- 생략 -------------------------- 

private void OnTriggerEnter2D(Collider2D collision)
{
    GameObject receiver = collision.gameObject;	// 부딪힌 객체 가져오기

    if(!receiver.CompareTag(targetTag))		// 부딪힌 객체가 player가 아닐 경우 return
    {
        return;
    }

    _collidingTargetHealthSystem = receiver.GetComponent<HealthSystem>();	// 부딪힌 플레이어의 HealthSystem 가져오기
    if( _collidingTargetHealthSystem != null )
    {
        _isCollidingWithTarget = true;	// 부딪힌 타겟이 존재한다. -> ApplyHealthChange를 실행하기 위한 flag
    }

    _collidingMovement = receiver.GetComponent<TopDownMovement>();	// 충돌한 플레이어 TopDownMovement 가져오기
}

private void OnTriggerExit2D(Collider2D collision)
{
    if (!collision.CompareTag(targetTag))
    {
        return;
    }

	// 충돌 된 후, 더 이상 충돌 상태가 아니면
    _isCollidingWithTarget = false;
}

private void ApplyHealthChange()
{
    AttackSO attackSO = Stats.CurrentStats.attackSO;
    bool hasBeenChanged = _collidingTargetHealthSystem.ChangeHealth(-attackSO.power);	// 플레이어 체력 감소
    if(attackSO.isOnKnockback && _collidingMovement != null )
    {
        _collidingMovement.ApplyKnockback(transform, attackSO.knockbackPower, attackSO.knockbackTime);
    }
}

이후, Goblin에게 Circle Collider 2D를 추가하고 Is Trigger를 켜준다.

☠️ DisappearOnDeath

DisappearOnDeath.cs를 만들어 죽으면 사라지도록 만들어 보자!

Entities 폴더 밑에 DisappearOnDeath.cs 만들기.

public class DisappearOnDeath : MonoBehaviour
{
    private HealthSystem _healthSystem;
    private Rigidbody2D _rigidbody;

    private void Start()
    {
        _healthSystem = GetComponent<HealthSystem>();
        _rigidbody = GetComponent<Rigidbody2D>();
        _healthSystem.OnDeath += OnDeath;       // 죽을 경우 이벤트 구독하기
    }

    void OnDeath()
    {
        _rigidbody.velocity = Vector3.zero;     // 죽을 경우 사라지면서 움직이지 않도록 제자리에 고정시키기

        foreach (SpriteRenderer renderer in transform.GetComponentsInChildren<SpriteRenderer>())
        {
            // 죽는 객체의 스프라이트를 투명하게 만든다.
            Color color = renderer.color;
            color.a = 0.3f;
            renderer.color = color;
        }

        // 죽은 객체의 모든 Behaviour를 동작하지 않도록 꺼버리기
        foreach (Behaviour component in transform.GetComponentsInChildren<Behaviour>())
        {
            component.enabled = false;
        }

        Destroy(gameObject, 2f);
    }
}

이후 모든 적에게 해당 스크립트를 추가해주자!!!

확인해보기!!!

0개의 댓글