이렇게 정리를 하는 시간을 가지다 보면 깨달음을 얻을 때가 있다.

그래서 오늘은 내 작업 방식도 함께 정리해보려고 한다.

 

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--;
        }
    }

 

다음화에서..