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
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 추가