이렇게 정리를 하는 시간을 가지다 보면 깨달음을 얻을 때가 있다.
그래서 오늘은 내 작업 방식도 함께 정리해보려고 한다.
1. 회의를 통해 플로우 차트 그리기
(부족한 기획 보충요청 및 이해관계 재확인 등의 작업)
2. 작업할 내용 및 순서 정리
1. 플레이어 사망 처리 시스템 구현하기(현재 StatSystem은 스탯관리와 장비 장착의 스탯 등 여기에 구현하는 건 맞지 않다고 판단)
2. Spawner에서 사용 중인 풀링을 스테이지 전환 시 정리하는 로직 추가하기
3. 스테이지 시스템 구현하기(ㅜ_ㅜ)
시작
1. PlayerHealth 구현
중요하게 생각한것 : 추후 필요하다고 생각되는 것은 미리 구현, 이벤트발생처리 해두기, 현재 시스템에 연결해 줄 것들이 있는지 확인(미루면 힘듦 or 체크리스트 작성)
using UnityEngine;
using System.Numerics;
/// <summary>
/// 플레이어의 체력과 사망 상태를 관리하는 클래스
/// </summary>
public class PlayerHealth : MonoBehaviour, IDamageable
{
private Player player;
private BigInteger currentHealth;
private bool isDead;
/// <summary>
/// 현재 체력 비율을 반환합니다.
/// </summary>
public float CurrentHPRatio =>
(float)((decimal)currentHealth / (decimal)MaxHealth);
/// <summary>
/// 사망 상태를 반환합니다.
/// </summary>
public bool IsDead => isDead;
/// <summary>
/// 최대 체력을 StatSystem에서 가져옵니다.
/// </summary>
private BigInteger MaxHealth => player.Stats.GetStatValue(StatType.Health);
public void Initialize(Player owner)
{
player = owner;
ResetHealth();
EventManager.Subscribe(EnumTypes.GameEventType.StatChanged, OnStatChanged);
}
private void OnDestroy()
{
EventManager.Unsubscribe(EnumTypes.GameEventType.StatChanged, OnStatChanged);
}
/// <summary>
/// 데미지를 받아 체력을 감소시킵니다.
/// </summary>
public void TakeDamage(BigInteger damage)
{
if (isDead) return;
currentHealth = BigInteger.Max(currentHealth - damage, 0);
EventManager.Dispatch(EnumTypes.GameEventType.PlayerHealthChanged, CurrentHPRatio);
if (currentHealth <= 0)
{
Die();
}
}
/// <summary>
/// 체력을 회복합니다.
/// </summary>
public void Heal(BigInteger amount)
{
if (isDead) return;
currentHealth = BigInteger.Min(currentHealth + amount, MaxHealth);
EventManager.Dispatch(EnumTypes.GameEventType.PlayerHealthChanged, CurrentHPRatio);
}
/// <summary>
/// 체력을 최대치로 회복합니다.
/// </summary>
public void ResetHealth()
{
isDead = false;
currentHealth = MaxHealth;
EventManager.Dispatch(EnumTypes.GameEventType.PlayerHealthChanged, CurrentHPRatio);
}
/// <summary>
/// 플레이어 사망 처리
/// </summary>
private void Die()
{
if (isDead) return;
isDead = true;
EventManager.Dispatch(EnumTypes.GameEventType.PlayerDied, null);
}
/// <summary>
/// 스탯 변경 시 체력 조정
/// </summary>
private void OnStatChanged(object statTypeObj)
{
if (statTypeObj is StatType statType && statType == StatType.Health)
{
BigInteger newMaxHealth = MaxHealth;
// 현재 체력 비율을 유지하면서 최대 체력 변경
decimal ratio = (decimal)currentHealth / (decimal)MaxHealth;
// decimal을 double로 변환하여 계산
double newHealth = (double)newMaxHealth * (double)ratio;
currentHealth = new BigInteger(newHealth);
EventManager.Dispatch(EnumTypes.GameEventType.PlayerHealthChanged, CurrentHPRatio);
}
}
/// <summary>
/// 부활 처리
/// </summary>
public void Revive()
{
isDead = false;
ResetHealth();
EventManager.Dispatch(EnumTypes.GameEventType.PlayerRevived, null);
}
}
2. CombatSystem
(IsDead 플래그를 이용하여 전투멈춤)
private void HandleCombatLoop()
{
if (spawner == null || player.Health.IsDead) return; // 사망확인
if (currentTarget == null || !currentTarget.gameObject.activeSelf)
{
FindTarget();
}
if (currentTarget != null)
{
_ = UpdateCombatAsync();
}
}
3. ObjectPoolManager
/// <summary>
/// 현재 사용 중인 풀의 프리팹 ID 목록을 반환합니다.
/// </summary>
public HashSet<int> GetActivePoolPrefabIds()
{
lock (poolLock)
{
return new HashSet<int>(poolCache.Keys);
}
}
/// <summary>
/// 특정 프리팹 ID의 풀을 제거합니다.
/// </summary>
public void RemovePool(int prefabId)
{
lock (poolLock)
{
if (poolCache.TryGetValue(prefabId, out var pool))
{
if (pool is IObjectPool<Component> componentPool)
{
componentPool.Clear();
}
poolCache.Remove(prefabId);
}
}
}
/// <summary>
/// 사용하지 않는 풀들을 정리합니다.
/// </summary>
/// <param name="activePoolIds">현재 활성화된 풀의 ID 목록</param>
public void CleanupUnusedPools(HashSet<int> activePoolIds)
{
lock (poolLock)
{
var unusedPoolIds = poolCache.Keys.Where(id => !activePoolIds.Contains(id)).ToList();
foreach (var id in unusedPoolIds)
{
RemovePool(id);
}
}
}
4. StageManager
(다음 스테이지에서 사용하지 않는 몬스터는 풀에서 정리)
private HashSet<int> GetRequiredPoolIds(StageData stageData)
{
var poolIds = new HashSet<int>();
// 일반 몬스터 프리팹 ID 수집
foreach (var monsterData in stageData.normalMonsters)
{
if (monsterData.monsterPrefab != null)
{
poolIds.Add(monsterData.monsterPrefab.GetInstanceID());
}
}
// 보스 몬스터 프리팹 ID 추가
if (stageData.bossMonster?.monsterPrefab != null)
{
poolIds.Add(stageData.bossMonster.monsterPrefab.GetInstanceID());
}
return poolIds;
}
StageManager의 이미 구현된 기능과 구현이 필요한 기능 정리해 보기
1. 구현된 기능
- 몬스터 스폰 및 처치 처리
- 킬카운트 체크
- 보스 소환 기능
- 풀링 시스템
- 스테이지 로드/언로드
- 스폰 위치 및 범위 관리
2. 구현이 필요한 기능
자동 보스 스폰 여부
public bool IsAutoBossEnabled
{
get => isAutoBossEnabled;
set
{
isAutoBossEnabled = value;
EventManager.Dispatch(EnumTypes.GameEventType.AutoBossChanged, value);
}
}
스테이지 데이터 로드
private void LoadAllStageData()
{
stageDataCache.Clear();
var allStages = Resources.LoadAll<StageData>(STAGE_RESOURCE_PATH);
foreach (var stage in allStages)
{
if (stageDataCache.ContainsKey(stage.stageId))
{
Logger.ErrorLog($"중복된 스테이지 ID가 발견되었습니다: {stage.stageId}");
continue;
}
stageDataCache[stage.stageId] = stage;
}
Logger.Log($"총 {stageDataCache.Count}개의 스테이지 데이터를 로드했습니다.");
}
(스테이지 ID로 가져오기, 사용가능한 ID목록 반환, 스테이지 유효성검사, 첫 번째 스테이지로 이동 등 로직 생략)
다음, 이전 스테이지 이동
/// <summary>
/// 다음 스테이지로 이동
/// </summary>
public async Awaitable MoveToNextStage()
{
var nextStageId = currentStageId + 1;
if (!StageExists(nextStageId))
{
Logger.Log("마지막 스테이지입니다!");
return;
}
var nextStage = GetStageData(nextStageId);
if (nextStage != null)
{
if (nextStageId > highestStageId)
{
highestStageId = nextStageId;
EventManager.Dispatch(EnumTypes.GameEventType.NewStageUnlocked, highestStageId);
}
await LoadStageAsync(nextStage);
currentStageId = nextStageId;
}
}
/// <summary>
/// 이전 스테이지로 이동
/// </summary>
public async Awaitable MoveToPreviousStage()
{
if (currentStageId <= 1) return;
var prevStage = GetStageData(currentStageId - 1);
if (prevStage != null)
{
await LoadStageAsync(prevStage);
currentStageId--;
}
}
다음화에서..