2024년 7월 03일 수요일 개발일지 / 체력 시스템과 근접 공격
2024년 7월 03일 수요일
What I did today : 체력 시스템과 근접 공격
오늘은 기술 면접 대비 꾸준 실습 시간이 끝나고, 10시에 팀원분들이랑 인사 한 뒤, 각자 해야 할 작업을 시작했습니다. 저는 일단 캐릭터랑 몬스터가 공격을 하기 위해선 체력 시스템이 필요하다 생각했고, 일단 만들려고 하다가 생각을 해보니까 스탯 관련해서도 만들어야 하는지에 대해서 고민이 생겼습니다.
체력이나 스탯 등등 초반 설계가 중요하기 때문에 제가 생각했던 고민이 걷잡을 수 없이 방대해져서 팀원분들과 담당 튜터님이랑 상담해 본 결과, 저희 게임에는 성장 요소가 따로 없기 때문에 스탯은 굳이 할 필요가 없을 것 같고, 체력 시스템만 구축하면 될 것 같다고 하셨습니다. 그리고 게임 규모가 큰 게임은 아니니까 SO 데이터 하나에 캐릭터가 가지고 있을 모든 정보를 담아두면 관리하기도 편할 것 같다고 하셨습니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthSystem : MonoBehaviour
{
public event Action OnDeath;
public event Action<float> OnDamage;
private float currentHealth;
private float maxHealth;
public void HealthSetting(float health)
{
maxHealth = health;
currentHealth = health;
UpdateHealthUI();
}
public void TakeDamage(UnitSO data)
{
float finalPhysicalDamage = data.physicalDamage * (1 - Math.Max(data.physicalDefense - data.physicalPenetration, 0) / 100);
float finalMagicDamage = data.magicDamage * (1 - Math.Max(data.magicDefense - data.magicPenetration, 0) / 100);
float totalDamage = finalPhysicalDamage + finalMagicDamage;
currentHealth -= totalDamage;
OnDamage?.Invoke(currentHealth);
if (currentHealth <= 0)
{
currentHealth = 0;
Die();
}
}
public void Heal(float amount)
{
currentHealth += amount;
if (currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
}
public float GetHealth()
{
return currentHealth;
}
private void Die()
{
OnDeath?.Invoke();
}
}
점심 회의가 끝나고, 점심을 먹은 뒤, 저는 일단 HealthSystem 클래스를 만들었고, 대미지를 받는 함수에는 SO의 데이터를 받아와서 물리와 마법 대미지 관련 공식을 토대로 최종 대미지를 계산한 뒤에 OnDamage 이벤트에 전달해주는 식으로 만들었으며, 죽는 함수는 일단 OnDeath 이벤트를 실행하는 것으로 구성을 했습니다.
public HealthSystem HealthSystem { get; private set; }
private void Awake()
{
HealthSystem = gameObject.AddComponent<HealthSystem>();
HealthSystem.HealthSetting(Data.health);
HealthSystem.OnDeath += OnDeath;
HealthSystem.OnDamage += OnDamage;
}
private void OnDamage(float currentHealth)
{
// 대미지를 입었을 때의 로직
}
private void OnDeath()
{
stateMachine.ChangeState(stateMachine.DeadState);
}
public void TakeDamage(UnitSO data)
{
HealthSystem.TakeDamage(data);
}
그다음에는 만들어진 HealthSystem을 바탕으로 Character 클래스에 추가해주고, HealthSystem의 OnDeath 이벤트에는 상태 전환을 해주는 OnDeath 함수를 연결해줬고, OnDamage 이벤트에는 대미지를 입었을 때의 상태 관련을 적어 놓기 위해 만들어 놓았습니다. 그리고 직접적인 대미지를 입는 TakeDamage 함수는 UnitSO 데이터를 받아와서 HealthSystem의 TakeDamage를 사용하게끔 구성했습니다. 그리고 나서 캐릭터와 몬스터의 DeadState도 만들어주었습니다. 오후 4시부터는 스탠다드 반 최적화 강의를 들었습니다. 강의 내용에는 다양한 최적화 방법이 있어서 이번 최종 프로젝트 작업이 끝날 때쯤에 한 번 적용을 해봐야 될 것 같습니다.
public Vector2Int[] attackRangeTiles;
그 다음 작업으로는 근접 공격에 대한 작업이었는데, 일단 캐릭터에 타일 크기만큼의 공격 범위가 필요하니까 Vector2Int[] 배열을 UnitSO 데이터에 추가하여 사용했습니다.
private void OnDrawGizmosSelected()
{
if (Data == null || Data.attackRangeTiles == null) return;
Gizmos.color = Color.yellow;
Vector3 position = transform.position;
foreach (Vector2Int offset in Data.attackRangeTiles)
{
Vector3 tilePosition = position + new Vector3(offset.x, 0, offset.y);
Gizmos.DrawWireCube(tilePosition, Vector3.one);
}
}
CharacterSO에서 공격 범위를 추가해주고, 테스트 작업하기 편하도록 Gizmo를 활용하여 공격 가능한 범위가 어디까지인지 표현했습니다.
public class CharacterMeleeState : CharacterAttackState
{
private float attackCooldown;
private float lastAttackTime;
public CharacterMeleeState(CharacterStateMachine stateMachine) : base(stateMachine)
{
attackCooldown = 1 / stateMachine.Character.Data.attackSpeed;
}
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.Character.AnimationData.MeleeParameterHash);
lastAttackTime = Time.time - attackCooldown;
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Character.AnimationData.MeleeParameterHash);
}
public override void Update()
{
base.Update();
if (Time.time >= lastAttackTime + attackCooldown)
{
Monster target = FindTargetInTileRange();
if (target != null)
{
Attack(target);
lastAttackTime = Time.time;
}
}
}
private Monster FindTargetInTileRange()
{
Vector2Int[] attackRange = stateMachine.Character.Data.attackRangeTiles;
Vector3 position = stateMachine.Character.transform.position;
// 타일 단위의 공격 범위 내에서 적을 찾는 로직
List<Monster> potentialTargets = new List<Monster>();
foreach (Vector2Int offset in attackRange)
{
Vector3 checkPosition = position + new Vector3(offset.x, 0, offset.y);
Collider[] hitColliders = Physics.OverlapSphere(checkPosition, 0.5f); // 작은 반경으로 충돌 체크
foreach (var hitCollider in hitColliders)
{
Monster enemy = hitCollider.GetComponent<Monster>();
if (enemy != null && enemy != stateMachine.Character)
{
potentialTargets.Add(enemy);
}
}
}
if (potentialTargets.Count > 0)
{
return potentialTargets[0]; // 우선순위가 높은 타겟을 반환 (예: 가장 가까운 적)
}
return null;
}
private void Attack(Monster target)
{
target.TakeDamage(stateMachine.Character.Data);
}
}
근접 공격 상태에 공격 속도에 따른 공격 쿨타임을 추가하고, 업데이트에서 조건문을 통한 공격이 가능하게 끔 만들었고, 타겟을 만들었던 범위만큼 구해서 찾고, 해당 적을 리스트에 넣어서 공격할 수 있게 만들었습니다.
그다음에는 한 번 테스트를 해보았고, 상태 전환을 하니까 공격이 되면서 몬스터의 체력이 닳는 로직이 만들어지게 되었습니다. 일단은 공격이 되게끔 설계를 한 것이기 때문에 전체적으로 다시 만들어야 할 것 같습니다. 지금 공격 로직은 업데이트에서 처리가 되지만, 추후에는 애니메이션의 Hit 이벤트를 사용하여 만들 예정이고, 근접 공격 전환 상태도 없기 때문에 아마 많은 작업이 필요하지 않을까 생각됩니다.
유한 상태 머신(FSM)을 처음 구조를 짜고 쓰려고하다보니 생각보다 너무 어렵고 전환 상태를 만드는 게 복잡하다고 생각했습니다. 지금은 일단 이렇게 만들었지만 계속 수정 작업을 해야 한다는 게 멀게만 느껴지네요... 저녁 8시에는 팀원분들이랑 각자 작업한 내용들을 공유하고, 남은 시간에는 TIL을 쓰면서 오늘 하루를 마무리했습니다.