유니티2D 입문 정리 14 - 데미지 피격 구현

woollim·2024년 11월 9일
0

1. Health System 만들기

○ Health System 만들기

using System;
using UnityEngine;

public class HealthSystem : MonoBehaviour
{
    [SerializeField] private float healthChangeDelay = .5f;

   private CharacterStatHandler statsHandler;
    private float timeSinceLastChange = float.MaxValue;
		private bool isAttacked = false;

    // 체력이 변했을 때 할 행동들을 정의하고 적용 가능
    public event Action OnDamage;
    public event Action OnHeal;
    public event Action OnDeath;
    public event Action OnInvincibilityEnd;

    public float CurrentHealth { get; private set; }

    // get만 구현된 것처럼 프로퍼티를 사용하는 것
    // 이렇게 하면 데이터의 복제본이 여기저기 돌아다니다가 싱크가 깨지는 문제를 막을 수 있어요!
    public float MaxHealth => statsHandler.CurrentStat.maxHealth;

    private void Awake()
    {
        statsHandler = GetComponent<CharacterStatHandler>();
    }

    private void Start()
    {
        CurrentHealth = statsHandler.CurrentStat.maxHealth;
    }

    private void Update()
    {
        if (isAttacked && timeSinceLastChange < healthChangeDelay)
        {
            timeSinceLastChange += Time.deltaTime;
            if (timeSinceLastChange >= healthChangeDelay)
            {
                OnInvincibilityEnd?.Invoke();
	              isAttacked = false;
            }
        }
    }

    public bool ChangeHealth(float change)
    {
		    // 무적 시간에는 체력이 달지 않음
        if (timeSinceLastChange < healthChangeDelay)
        {
            return false;
        }

        timeSinceLastChange = 0f;
        CurrentHealth += change;
        // [최솟값을 0, 최댓값을 MaxHealth로 하는 구문]
        CurrentHealth = Mathf.Clamp(CurrentHealth, 0, MaxHealth);
        // CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;
        // CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth; 와 같아요!

        if (CurrentHealth <= 0f)
        {
            CallDeath();
            return true;
        }
        
        if (change >= 0)
        {
            OnHeal?.Invoke();
        }
        else
        {
            OnDamage?.Invoke();
	          isAttacked = true;
        }


        return true;
    }

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

○ 시스템 적용하기

  • Player - HealthSystem 추가
  • Goblin, OrcShaman Prefab - HealthSystem 추가

○ TopDownAnimationController 수정

public class TopDownAnimationController : AnimationController
{
	--------------------- 생략--------------------- 
		private HealthSystem healthSystem;

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

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

		void Start()
		{
		    controller.OnAttackEvent += Attacking;
		    controller.OnMoveEvent += Move;

		    if(healthSystem != null)
		    {
		        healthSystem.OnDamage += Hit;
		        healthSystem.OnInvincibilityEnd += InvincibilityEnd;
				}
		}
}

○ ProjectileController 수정

--------------------- 생략--------------------- 
private void OnTriggerEnter2D(Collider2D collision)
{
    // levelCollisionLayer에 포함되는 레이어인지 확인합니다.
    if (IsLayerMatched(levelCollisionLayer.value, collision.gameObject.layer))
    {
        // 벽에서는 충돌한 지점으로부터 약간 앞 쪽에서 발사체를 파괴합니다.
        Vector2 destroyPosition = collision.ClosestPoint(transform.position) - direction * .2f;
        DestroyProjectile(destroyPosition, fxOnDestory);
    }
    // _attackData.target에 포함되는 레이어인지 확인합니다.
    else if (IsLayerMatched(attackData.target.value, collision.gameObject.layer))
    {
        // 충돌한 오브젝트에서 HealthSystem 컴포넌트를 가져옵니다.
	      HealthSystem healthSystem = collision.GetComponent<HealthSystem>();
				if (healthSystem != null)
						{
						// 충돌한 오브젝트의 체력을 감소시킵니다.
						bool isAttackApplied = healthSystem.ChangeHealth(-attackData.power);
						
						// 넉백이 활성화된 경우, ★드★디★어★ 넉백을 적용합니다.
						if (isAttackApplied && attackData.isOnKnockback)
						{
								ApplyKnockback(collision);
						}
				}
        // 충돌한 지점에서 프로젝타일을 파괴합니다.
        DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestory);
    }
}

// 레이어가 일치하는지 확인하는 메소드입니다.
private bool IsLayerMatched(int layerMask, int objectLayer)
{
    return layerMask == (layerMask | (1 << objectLayer));
}

// 넉백을 적용하는 메소드입니다.
private void ApplyKnockback(Collider2D collision)
{
    TopDownMovement movement = collision.GetComponent<TopDownMovement>();
    if (movement != null)
    {
        movement.ApplyKnockback(transform, attackData.knockbackPower, attackData.knockbackTime);
    }
}
--------------------- 생략--------------------- 

○ 적 Layer 설정하기

  • Goblin Prefab - Layer → Enemy
  • OrcShaman 도 동일하게 적용

AttackData 수정하기

  • PlayerRangedAttack → Target → Enemy


○ TopDownContactEnemyController 수정

using UnityEngine;

public class TopDownContactEnemyController : TopDownEnemyController
{
    [SerializeField][Range(0f, 100f)] private float followRange;
    [SerializeField] private string targetTag = "Player";
    private bool isCollidingWithTarget;

    [SerializeField] private SpriteRenderer characterRenderer;

    private HealthSystem healthSystem;
    private HealthSystem collidingTargetHealthSystem;
    private TopDownMovement collidingMovement;

    protected override void Start()
    {
        base.Start();
    
        healthSystem = GetComponent<HealthSystem>();
        healthSystem.OnDamage += OnDamage;
    }

    private void OnDamage()
    {
        followRange = 100f;
    }

    protected override void FixedUpdate()
    {
        // 단거리적은 플레이어처럼 입력을 받아서 움직이는 것은 아닙니다.
        // 그래서 단거리적은 어떤 식으로 움직일 지 로직을 저희가 직접 구현해야 합니다.

        base.FixedUpdate();

        if(isCollidingWithTarget)
        {
            ApplyHealthChange();
        }
        
        Vector2 direction = Vector2.zero;
        if (DistanceToTarget() < followRange)
        {
            direction = DirectionToTarget();
        }

        CallMoveEvent(direction);
        Rotate(direction);
    }

    private void Rotate(Vector2 direction)
    {
        // TopDownAimRotation에서 했었죠? 
        // Atan2는 가로와 세로의 비율을 바탕으로 -파이~파이(-180도~180도에 대응, * Rad2Deg가 그 기능)하는 값을 나타내주는 함수였다는 것 기억하시죠?
        float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        characterRenderer.flipX = Mathf.Abs(rotZ) > 90f;
    }
    
    private void OnTriggerEnter2D(Collider2D collision)
    {
        GameObject receiver = collision.gameObject;

        if(!receiver.CompareTag(targetTag))
        {
            return;
        }

        collidingTargetHealthSystem = receiver.GetComponent<HealthSystem>();
        if(collidingTargetHealthSystem != null )
        {
            isCollidingWithTarget = true;
        }

        collidingMovement = receiver.GetComponent<TopDownMovement>();
    }

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

        isCollidingWithTarget = false;
    }

    private void ApplyHealthChange()
    {
        AttackSO attackSO = stats.CurrentStat.attackSO;
        bool hasBeenChanged = collidingTargetHealthSystem.ChangeHealth(-attackSO.power);
        if(attackSO.isOnKnockback && collidingMovement != null )
        {
            collidingMovement.ApplyKnockback(transform, attackSO.knockbackPower, attackSO.knockbackTime);
        }
    }
}

○ Goblin 오브젝트 수정

  • Circle Collider 2D 추가
  • isTrigger 체크


2. 적 죽이기

○ DestoryOnDeath 만들기

using UnityEngine;

public class DestroyOnDeath : MonoBehaviour
{
    private HealthSystem healthSystem;
    private Rigidbody2D rigidbody;

    private void Start()
    {
        healthSystem = GetComponent<HealthSystem>();
        rigidbody = GetComponent<Rigidbody2D>();
        // 실제 실행 주체는 healthSystem임
        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;
        }

        // 스크립트 더이상 작동 안하도록 함
        foreach (Behaviour component in transform.GetComponentsInChildren<Behaviour>())
        {
            component.enabled = false;
        }

        // 2초뒤에 파괴
        Destroy(gameObject, 2f);
    }
}

○ 추가하기

  • Goblin, Orc_Shaman → DestoryOnDeath 추가

0개의 댓글