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을 지닌 객체의 체력에 변화가 생길 경우,
HealthSystem은 이 게임에 등장하는 적, 캐릭터 모두에게 적용해야 한다!
적, 플레이어에게 모두 HealthSystem.cs를 추가해준다.
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; // 무적 상태 종료
}
}
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.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);
}
}
이후 모든 적에게 해당 스크립트를 추가해주자!!!