내일배움캠프 77일차 TIL : 스테이지 관련 코드들 정리

woollim·2025년 1월 13일
0

내일배움캠프TIL

목록 보기
70/74
post-thumbnail

■ 오늘계획

○ 할 일 목록

  • 다음 보상 정보 안맞는거 수정
  • 보상 관련 오브젝트 및 UI 코드 정리
  • 최고층 도달 함수 위치 변경으로 인한 오류 탐색(특히 보물방)
  • 게임 매니저 대공사
  • 휴게층 입장 로직 수정(현재는 UI에서 진행되고 있는데 매우 옳지 않다)
  • 보물방 추가 명성치, 골드 빼버리기
  • 던전에서 '도망가기' 버튼 만들기
  • 무한 스테이지 모드
  • 스테이지 나가기 버튼 클릭시 몬스터 정리안되는 버그 확인


■ 보상관련 오브젝트

○ UIRewardBox

  • 보상 아이콘 표기
public class UIRewardBox : UIPopUp
{
    [SerializeField] private Image currentWeaponImage;          // (보상) 현재 무기 이미지
    [SerializeField] private TextMeshProUGUI currentWeaponName; // (보상) 현재 무기 이름
    [SerializeField] private TextMeshProUGUI currentWeaponDesc; // (보상) 현재 무기 설명

    [SerializeField] private GameObject nextStagePanel;         // 다음 보상 정보칸
    [SerializeField] private Image nextWeaponImage;             // (보상) 다음 무기 이미지
    [SerializeField] private TextMeshProUGUI nextWeaponName;    // (보상) 다음 무기 이름
    [SerializeField] private TextMeshProUGUI nextWeaponDesc;    // (보상) 다음 무기 설명

    private void UIViewUpdate()
    {
        topCurFloor.text = $"{Managers.Game.TopCurFloor}층 보상 정보";
        // 현재 층에 맞는 랜덤 무기 획득
        WeaponInfo current = Managers.Game.RandomCurrentWeapon();

        // 현재 랜덤 무기 정보
        reputeNumber.text = $"명성: {(int)Managers.Game.TopReputeScore}";
        goldNumber.text = $"골드: {Managers.Game.TopGold}";
        currentWeaponImage.sprite = current.GetIcon();
        currentWeaponName.text = $"{current.ItemName}";
        currentWeaponDesc.text = $"무기 등급 : {current.grade}\n옵션 갯수 : {current.optionCount}개\n기본 공격력 : {current.AD}";

        // 다음 층이 있다면
        // 다음 층 무기 정보 보여줌
        if (Managers.Game.TopCurFloor <= Managers.Data.GameDataBase.FloorDropGradeDatas.Count - 1)
        {
            // 다음 층에 맞는 랜덤 무기 불러오기
            WeaponInfo next = Managers.Game.RandomNextWeapon();

            nextWeaponImage.sprite = next.GetIcon();
            nextWeaponName.text = $"{next.ItemName}";
            nextWeaponDesc.text = $"무기 등급 : {next.grade}\n옵션 갯수 : {next.optionCount}개\n기본 공격력 : {next.AD}";

            nextStageBtn.interactable = true;
        }
        else
        {
            // 없다면 '다음층 무기정보 칸'과 다음스테이지 도전 버튼 비활성화
            nextStagePanel.SetActive(false);
            nextStageBtn.interactable = false;
        }
    }    
}



■ 휴게층 입장 로직 수정

○ 기존

  • 현재는 UI에서 진행되고 있는데 매우 옳지 않다
  • UIRewardBox 클래스의 OnNextStageBtn() 함수에서 다음으로 갈층이 5층단위면 휴게층으로 이동했다

○ 수정

  • 모든층은 생성전 스테이지매니저의 CreateStage() 함수로 생성을 시작한다.
  • CreateStage() 함수 초반에 5층 단위인지 검사하는 코드 추가. 결과적으로 UI에 있던 코드가 매니저로 이동했다.
  • 장점 : 이렇게 하면 새로운 입장 오브젝트를 만들거나 로직을 만들어도 일일히 검사로직을 추가할 필요없다. 그리고 층 생성관련 로직을 한곳에서 관리하여 유지보수성도 좋다.
  • 스테이지 타입에 휴게층 추가
public enum StageType
{
    Boss = 0,
    Normal,
    Event,
    Shelter
}
  • CreateStage() 함수에서 층수와 현재 휴게층인지 검사하고 스테이지 타입 결정
public void CreateStage()
{
    int floor = Managers.Game.TopCurFloor;

    // 5층 단위면 입장전이고, 이미 휴게층이 아니라면 휴게층으로 이동
    StageType = ((floor % StageScene.ShelterStageInterval == 0) && (!isShelterFloor))? StageType.Shelter : RandomStageType(floor);
    Logger.Log(StageType.ToString());

    // 스테이지 특성에 맞는 몬스터 생성
    switch (StageType)
    {
        case StageType.Boss:
            StageID = RandomStageID<BossStageData>(Managers.Data.GameDataBase.BossStageDatas);
            // --- 임시 코드 ---
            if (StageID == lastBossStageID)
            {
                StageID = (lastBossStageID == 101) ? 102 : 101;
            }
            lastBossStageID = StageID;
            // -----------------
            Managers.Scene.LoadScene(Managers.Data.GameDataBase.BossStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
            break;
        case StageType.Normal:
            StageID = RandomStageID<NormalStageData>(Managers.Data.GameDataBase.NormalStageDatas);
            Managers.Scene.LoadScene(Managers.Data.GameDataBase.NormalStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
            break;
        case StageType.Event:
            StageID = RandomStageID<EventStageData>(Managers.Data.GameDataBase.EventStageDatas);
            Managers.Scene.LoadScene(Managers.Data.GameDataBase.EventStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
            break;
        case StageType.Shelter:
            isShelterFloor = true;
            Managers.Scene.LoadScene(ESceneName.Shelter);
            break;
    }
}



■ 스테이지 흐름 리팩토링

○ UIFloorChallengeController

using TMPro;
using UnityEngine;
using UnityEngine.UI;
using static GameConstants;

public class UIFloorChallengeController : UIPopUp
{
    [SerializeField] private TextMeshProUGUI floorNumber;
    [SerializeField] private Button downButton;
    [SerializeField] private Button upButton;
    [SerializeField] private Button enterButton;

    private int seletedFloor;

    protected override void Awake()
    {
        base.Awake();

        downButton.onClick.AddListener(OnDownButton);
        upButton.onClick.AddListener(OnUpButton);
        enterButton.onClick.AddListener(OnEnterButton);
    }

    private void Start()
    {
        // 게임 내 최대 층수 보다, 플레이어 기록최대 층수가 작거나 같으면 기록최대 층수로 표기
        if (Managers.Character.Player.saveData.HighestFloorRecord <= Managers.Data.GameDataBase.FloorDropGradeDatas.Count)
            floorNumber.text = Managers.Character.Player.saveData.HighestFloorRecord.ToString();

        SelectedFloor();
        UpdateUpDownButtonCheck();
    }

    public override void Hide()
    {
        base.Hide();
        Managers.UI.Hide<UIFloorChallengeController>();
    }

    /// <summary>
    /// 다운 버튼을 눌렀을때 호출되는 함수입니다. 선택 층수를 내려줍니다.
    /// </summary>
    public void OnDownButton()
    {
        SelectedFloor();

        seletedFloor -= StageScene.floorGap;

        if (seletedFloor <= 0)
            seletedFloor = 1;

        if (seletedFloor > 0)
        {
            floorNumber.text = seletedFloor.ToString();
            UpdateUpDownButtonCheck();
        }
    }

    /// <summary>
    /// 업 버튼을 눌렀을때 호출되는 함수입니다. 선택 층수를 올려줍니다.
    /// </summary>
    public void OnUpButton()
    {
        SelectedFloor();

        // 선택된 이전 층이 1층이면 floorGap층으로 변환
        // 그 외에는 + floorGap
        seletedFloor += StageScene.floorGap;

        // 선택된 층이 플레이어 최고 도달층 보다 작거나 같으면
        if (seletedFloor <= Managers.Character.Player.saveData.HighestFloorRecord)
        {
            // 선택된층을  UI에 반영
            floorNumber.text = seletedFloor.ToString();
            UpdateUpDownButtonCheck();
        }
    }

    /// <summary>
    /// 입장 버튼입력시 호출되는 함수입니다.
    /// </summary>
    public void OnEnterButton()
    {
        // 무기 장착 여부 확인
        if (Managers.Character.Player.combat.EquippedWeapon == WeaponType.None)
        {
            Managers.UI.Show<UINotification>()?.ShowNotification("무기를 장착해야 입장할 수 있습니다.");
            return;
        }

        if (Managers.Character.Player.saveData.CurrentQuestProgress < 1)
        {
            Managers.UI.Show<UINotification>()?.ShowNotification("퀘스트를 완료해야 입장할 수 있습니다.");
            Managers.UI.Show<UIQuestPanel>()?.PlayBlinkEffect();
            return;
        }

        Managers.Sound.PlaySFX(SFX.NPC_OpenGate);

        Hide(); // UI 숨김
        Managers.Game.InitChallengeFloor(seletedFloor);
        Managers.Stage.CreateStage();
    }

    /// <summary>
    /// 현재 선택된 층(UI 텍스트로 보여지는)을 선택층 변수에 반환해주는 함수입니다.
    /// </summary>
    public void SelectedFloor()
    {
        seletedFloor = int.Parse(floorNumber.text);
    }

    /// <summary>
    /// 업다운 버튼의 활성화 여부를 판단해주는 함수입니다.
    /// </summary>
    public void UpdateUpDownButtonCheck()
    {
        // 선택된 층에서 아래로 더 내릴 수 있는지 확인
        if (seletedFloor - StageScene.floorGap <= 0)
            downButton.interactable = false;
        else
            downButton.interactable = true;

        // 선택된 층에서 위로 더 올릴 수 있는지 확인
        if (seletedFloor + StageScene.floorGap > Managers.Character.Player.saveData.HighestFloorRecord || seletedFloor + StageScene.floorGap > Managers.Data.GameDataBase.FloorDropGradeDatas.Count)
            upButton.interactable = false;
        else
            upButton.interactable = true;
    }
}

○ StageManager

using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using static GameConstants;

public class StageManager : IManager
{
    public StageType StageType { get; set; }
    public int StageID { get; set; }
    public bool isShelterFloor;
    private int lastBossStageID;
    public void Init()
    {
        StageType = StageType.Normal;
        StageID = 0;
        isShelterFloor = false;
    }

    /// <summary>
    /// 랜덤 스테이지 ID 지정 후 해당 스테이지 씬으로 이동하는 함수입니다. StageType, stageID를 초기화 합니다
    /// </summary>
    public void CreateStage()
    {
        int floor = Managers.Game.TopCurFloor;

        // 5층 단위면 입장전이고, 이미 휴게층이 아니라면 휴게층으로 이동
        StageType = ((floor % StageScene.ShelterStageInterval == 0) && (!isShelterFloor))? StageType.Shelter : RandomStageType(floor);
        Logger.Log(StageType.ToString());

        // 스테이지 특성에 맞는 몬스터 생성
        switch (StageType)
        {
            case StageType.Boss:
                StageID = RandomStageID<BossStageData>(Managers.Data.GameDataBase.BossStageDatas);
                // --- 임시 코드 ---
                if (StageID == lastBossStageID)
                {
                    StageID = (lastBossStageID == 101) ? 102 : 101;
                }
                lastBossStageID = StageID;
                // -----------------
                Managers.Scene.LoadScene(Managers.Data.GameDataBase.BossStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
                break;
            case StageType.Normal:
                StageID = RandomStageID<NormalStageData>(Managers.Data.GameDataBase.NormalStageDatas);
                Managers.Scene.LoadScene(Managers.Data.GameDataBase.NormalStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
                break;
            case StageType.Event:
                StageID = RandomStageID<EventStageData>(Managers.Data.GameDataBase.EventStageDatas);
                Managers.Scene.LoadScene(Managers.Data.GameDataBase.EventStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
                break;
            case StageType.Shelter:
                isShelterFloor = true;
                Managers.Scene.LoadScene(ESceneName.Shelter);
                break;
        }

        Managers.Game.StageStart();
    }

    /// <summary>
    /// 스테이지 ID 에 맞는 몬스터를 소환해주는 함수 입니다
    /// </summary>
    public void EnterStageMonsterSpawn()
    {
        switch (StageType)
        {
            case StageType.Boss:
                BossStageData bossStage = Managers.Data.GameDataBase.BossStageDatas[StageID];
                Managers.Character.SpawnMonster(bossStage);
                break;
            case StageType.Normal:
                NormalStageData normalStage = Managers.Data.GameDataBase.NormalStageDatas[StageID];
                Managers.Character.SpawnMonster(normalStage);
                break;
        }
    }

    /// <summary>
    /// 리스트에 저장된 스테이지타입을 랜덤으로 선택하여 랜덤스테이지 타입 ID(인덱스) 값을 반환하는 함수 입니다.
    /// </summary>
    /// <returns>랜덤 스테이지타입 ID</returns>
    private StageType RandomStageType(int floor)
    {
        int _stageType;

        if (floor % GameConstants.StageScene.BossStageInterval == 0) // 10층단위로 보스 등장
        {
            _stageType = 0; // 보스타입 고정
        }
        else
        {
            float randomValue = Random.value; // 0.0 ~ 1.0 사이의 값을 반환
            if (randomValue <= 0.75f) // 75% 확률로 1 반환
            {
                _stageType = 1;
            }
            else // 25% 확률로 2 반환
            {
                _stageType = 2;
            }
        }
        isShelterFloor = false; // 휴게층이 아니므로

        return (StageType)_stageType; // 이넘으로 변환
    }

    /// <summary>
    /// 랜덤 스테이지 ID를 반환하는 함수 입니다.
    /// </summary>
    /// <typeparam name="T">해당 스테이지 데이터테이블</typeparam>
    /// <param name="stageMonster"> 게임DB에 저장된 스테이지 Enemy 딕셔너리</param>
    /// <returns>스테이지 ID</returns>
    private int RandomStageID<T>(Dictionary<int, T> stageMonster)
    {
        if (stageMonster == null || stageMonster.Count == 0)
        {
            Logger.WarningLog("stageMonster딕셔너리가 비어있습니다.");
        }

        List<int> keys = new List<int>(stageMonster.Keys);
        // 딕셔너리에서 키 추출하여 리스트 만듬

        // 뒤에서 3개는 테스트용이라 제외
        int randomIndex = Random.Range(0, keys.Count - 3);
        // 키 리스트 카운트 범위내에서 랜덤 인덱스

        return (int)keys[randomIndex];
    }
}

○ GameManager

using Common.Event;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using Unity.Services.Analytics;
using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Pool;
using static GameConstants;

public class GameManager : MonoBehaviour, IManager
{
    // -------- 스테이지 공통 정보 --------
    public int TopCurFloor { get; private set; }        // 현재 층수
    public bool IsDamaged { get; private set; }         // 해당 스테이지에서 피격받았는지 확인하는 불 변수
    public float StageReputeScore { get; private set; } // (보상)누적 명성치 점수
    public int StageGold { get; private set; }          // (보상)누적 골드
    public WeaponInfo CurrentRewardWeaponInfo { get; private set; } // (보상) 현재 보상 무기 정보
    public WeaponInfo NextRewardWeaponInfo { get; private set; }    // (보상) 다음 보상 무기 정보
    public GameObject StageRewardBox { get; private set; }          // 보상 박스 오브젝트

    // -------- 노말 스테이지 정보 --------
    private Dictionary<int, int> stageEnemyCount;           // 해당 스테이지에 있는 에너미 카운트 (페이즈 / 페이즈당 수)
    public int CurrentEnemyPhase { get; private set; }      // 현재 페이즈
    public int RecentHalfEnemyPhase { get; private set; }   // 가장 최근에 몬스터가 수가 절반이 된 페이즈
    public int LastEnemyPhase { get; private set; }         // 마지막 페이즈
    public int MaxEnemyCountPerPhase { get; private set; }  // 페이즈당 최대 에너미 수

    // -------- 기타 정보 --------
    private string pendingDeathNotification;

    #region 초기화
    public void Init()
    {
        StageRewardBox = Managers.Resource.LoadAsset<GameObject>("Prefab/Stage/RewardBox");

        StageEnd();
        GameStartPlayerInit();

        EventManager.Subscribe(GameEventType.PlayerDamaged, IsDamagedTrue);
    }

    private void OnDestroy()
    {
        EventManager.Unsubscribe(GameEventType.PlayerDamaged, IsDamagedTrue);
    }

    /// <summary>
    /// 게임 시작시 플레이어 설정 하는 함수 입니다.
    /// </summary>
    public void GameStartPlayerInit()
    {
        Managers.Character.SpawnPlayer();
        var inventory = Managers.UI.IsOpened<UIInventory>();
        if (inventory == null)
        {
            inventory = Managers.UI.Show<UIInventory>();
            // 기본 무기 지급
            if (!Managers.SaveLoad.SaveFileExists())
            {
                var weaponBow = new WeaponInfo(1, WeaponType.Bow);
                var wrapperBow = ItemFactory.GetItemEventData(weaponBow, 1);
                EventManager.Dispatch(GameEventType.ItemGet, wrapperBow);
                ItemFactory.ReleaseItemEventData(wrapperBow);

                var weaponSword = new WeaponInfo(1, WeaponType.Sword);
                var wrapperSword = ItemFactory.GetItemEventData(weaponSword, 1);
                EventManager.Dispatch(GameEventType.ItemGet, wrapperSword);
                ItemFactory.ReleaseItemEventData(wrapperSword);

                var healthPotion = ItemFactory.CreateItem(2110);
                var wrapperHealthPotion = ItemFactory.GetItemEventData(healthPotion, 3);
                EventManager.Dispatch(GameEventType.ItemGet, wrapperHealthPotion);
                ItemFactory.ReleaseItemEventData(wrapperHealthPotion);

                var manaPotion = ItemFactory.CreateItem(2210);
                var wrapperManaPotion = ItemFactory.GetItemEventData(manaPotion, 3);
                EventManager.Dispatch(GameEventType.ItemGet, wrapperManaPotion);
                ItemFactory.ReleaseItemEventData(wrapperManaPotion);

                var bomb = ItemFactory.CreateItem(4111);
                var wrapperBomb = ItemFactory.GetItemEventData(bomb, 3);
                EventManager.Dispatch(GameEventType.ItemGet, wrapperBomb);
                ItemFactory.ReleaseItemEventData(wrapperBomb);
            }

            Managers.UI.Hide<UIInventory>();
        }
    }
    #endregion

    #region EnemyCount
    /// <summary>
    /// 에너미 카운트 딕셔너리에 에너미 종류와 수를 추가하는 함수입니다.
    /// </summary>
    /// <param name="spawnPhase">몬스터의 소속 페이즈, 딕셔너리 키</param>
    /// <param name="quantity">몬스터 수, 딕셔너리 밸류</param>
    public void AddEnemyCount(int spawnPhase, int quantity)
    {
        if (stageEnemyCount.ContainsKey(spawnPhase))
        {
            stageEnemyCount[spawnPhase] += quantity;
        }
        else
        {
            stageEnemyCount.Add(spawnPhase, quantity);
        }
    }

    /// <summary>
    /// 에너미를 처치할때마다 호출되는 함수입니다. 에너미가 처치되면 하는 후속작업을 합니다.
    /// </summary>
    public void SubEnemyCount(int enemySpawnPhase, Enemy enemy)
    {
        // 딕셔너리에서 phase를 키로 해당되는 밸류(몬스터 수)를 찾습니다.
        if (!stageEnemyCount.TryGetValue(enemySpawnPhase, out int monsterCount))
            return;

        StageReputeScore += enemy.Stat.ReputeNumber;
        stageEnemyCount[enemySpawnPhase]--;

        // 노말 스테이지이고, 처치된 에너미가 소속된 페이즈가 가장 최근에 몬스터 수가 절반이 된 페이즈가 아니라면
        // 즉, 새로운 페이즈를 소환하는 트리거가 될 수 있는 최신 페이즈의 에너미라면
        if (Managers.Stage.StageType == StageType.Normal && RecentHalfEnemyPhase != enemySpawnPhase)
        {
            // 마지막 페이즈가 아니고, 페이즈 별 몬스터 절반이 죽었을 경우에
            if (CurrentEnemyPhase < LastEnemyPhase && stageEnemyCount[enemySpawnPhase] < MaxEnemyCountPerPhase / 2)
            {
                CurrentEnemyPhase++; // 현재 페이즈 증가
                RecentHalfEnemyPhase = enemySpawnPhase;
                // 다음 페이즈 몬스터 소환
                Managers.Character.SpawnMonster(Managers.Data.GameDataBase.NormalStageDatas[Managers.Stage.StageID]);
                Logger.Log($"{CurrentEnemyPhase}페이즈 시작, 최대페이즈 : {LastEnemyPhase}");
            }
        }
        if (stageEnemyCount[enemySpawnPhase] <= 0) // 다 죽였다면
        {
            stageEnemyCount.Remove(enemySpawnPhase);
            if (stageEnemyCount.Count == 0) // 모든 종류의 몬스터가 죽었다면
            {
                StageClear(enemy.gameObject.transform.position);
            }
        }
    }

    /// <summary>
    /// 스테이지의 에너미 정보를 설정합니다.
    /// </summary>
    public void SetStageEnemyInfo()
    {
        // 마지막 페이즈 숫자
        LastEnemyPhase = Managers.Data.GameDataBase.NormalStageDatas[Managers.Stage.StageID]._phaseCount - 1;
        // 페이즈별 몬스터수
        for (int i = 0; i < Managers.Data.GameDataBase.NormalStageDatas[Managers.Stage.StageID]._monsterQuantity.Count; i++)
        {
            MaxEnemyCountPerPhase += Managers.Data.GameDataBase.NormalStageDatas[Managers.Stage.StageID]._monsterQuantity[i];
        }
    }

    /// <summary>
    /// 스테이지의 에너미 정보를 리셋합니다.
    /// </summary>
    public void ResetStageEnemyInfo()
    {
        if (stageEnemyCount == null)
            stageEnemyCount = new Dictionary<int, int>();
        else
            stageEnemyCount.Clear();

        CurrentEnemyPhase = 0;
        RecentHalfEnemyPhase = -1;
        LastEnemyPhase = 0;
        MaxEnemyCountPerPhase = 0;
    }
    #endregion

    #region 스테이지 진행
    /// <summary>
    ///  스테이지 시작시 호출되는 함수 입니다.
    /// </summary>
    public void StageStart()
    {
        Managers.UI.Show<UICurrentFloor>();
        if (Managers.Stage.StageType == StageType.Normal)   
            SetStageEnemyInfo();
    }

    /// <summary>
    /// 스테이지 클리어했을때 호출되는 함수입니다.
    /// </summary>
    private void StageClear(Vector3 enemyPosition)
    {
        if (!IsDamaged)
        {
            // 노피격 상태면 보너스 점수 지급
            StageReputeScore += StageScene.RewardBonusRepute * TopCurFloor;
        }
        StageGold = (int)StageReputeScore * 3;

        GiveCurrentReward(); // 재화 보상 지급

        Instantiate(StageRewardBox, enemyPosition, Quaternion.identity);
        UpdateHighestFloorRecord(TopCurFloor + 1); // 최고 도달층 변경
    }

    /// <summary>
    /// 탑 도전 실패시 호출되는 함수입니다.
    /// 에너미 수를 저장한 딕셔너리와 명성치점수를 초기화 합니다.
    /// </summary>
    public void StageFail()
    {
        // UGS
        if (Unity.Services.Core.UnityServices.State == Unity.Services.Core.ServicesInitializationState.Initialized)
        {
            TopFailEvent topFailEvent = new TopFailEvent
            {
                TopFail_curFloor = TopCurFloor
            };
            AnalyticsService.Instance.RecordEvent(topFailEvent);
        }

        pendingDeathNotification = "탑 등반에 실패하여 탑 입장 전의 기억으로 돌아왔습니다.";
        TopExit();
    }

    /// <summary>
    /// 탑을 계속 도전하기를 선택했을 때 호출되는 함수 입니다. 
    /// </summary>
    public void TopContinue()
    {
        // 스테이지데이터 리셋
        StageEnd();

        if (TopCurFloor + 1 <= Managers.Data.GameDataBase.FloorDropGradeDatas.Count)
        {
            TopCurFloor++;
            // 층수 도달 이벤트 발생
            EventManager.Dispatch(GameEventType.ReachFloor, TopCurFloor);
        }
    }

    /// <summary>
    /// 탑을 나갈때 (보상열기, 실패) 호출되는 함수 입니다.
    /// </summary>
    public void TopExit()
    {
        // 스테이지데이터 리셋
        StageEnd();
        // 에너미 풀 클리어
        Managers.Character.ResetEnemyPool();

        // 최대 체력 회복
        if (Managers.Character.Player != null)
            Managers.Character.Player.stat.RestoreHealth(Managers.Character.Player.stat.MaxHP);

        Managers.Scene.LoadScene(ESceneName.Village);
    }

    /// <summary>
    /// 스테이지 종료(성공, 실패 둘다)후 데이터를 리셋하는 함수입니다.
    /// </summary>
    private void StageEnd()
    {
        // 최고 도달층 변경
        UpdateHighestFloorRecord();

        // 에너미 정보 초기화
        ResetStageEnemyInfo();

        // 보상정보, 피격여부 초기화
        CurrentRewardWeaponInfo = null;
        NextRewardWeaponInfo = null;
        StageReputeScore = 0f;
        StageGold = 0;
        IsDamaged = false;
    }
    #endregion

    #region 탑 층수 관리
    /// <summary>
    /// 입장 층수를 설정할때 호출하는 함수입니다.
    /// </summary>
    /// <param name="floor"></param>
    public void InitChallengeFloor(int floor)
    {
        TopCurFloor = floor;
        Managers.Stage.isShelterFloor = true; // 쉘터층을 들리지 않도록 방지

        // 초기 층수 설정 시에도 이벤트 발생
        EventManager.Dispatch(GameEventType.ReachFloor, TopCurFloor);
    }

    /// <summary>
    /// 도달한 최고층 기록을 바꿔주는 함수 입니다.
    /// </summary>
    public void UpdateHighestFloorRecord(int floor = 0)
    {
        if (floor == 0)
            floor = TopCurFloor;

        // 게임내 최대 층수 이상으로 못 넘게 하기
        if (floor > Managers.Data.GameDataBase.FloorDropGradeDatas.Count)
            floor = Managers.Data.GameDataBase.FloorDropGradeDatas.Count;

        // 기록 층수 보다 크다면
        if (Managers.Character.Player.saveData.HighestFloorRecord < floor)
        {
            // 플레이어의 도달 최고층 바꾸기
            Managers.Character.Player.saveData.UpHighestFloorRecord(floor);
        }
    }
    #endregion

    #region 피격 여부
    /// <summary>
    /// 해당 스테이지에서 피격받았을때 호출되는 함수입니다.
    /// </summary>
    public void IsDamagedTrue(object arg = null)
    {
        IsDamaged = true;
    }
    #endregion

    #region 보상관련
    public void GiveCurrentReward()
    {
        Managers.Character.Player.saveData.AddReputation((int)StageReputeScore); // 플레이어 명성치에 보상 명성치 더하기
        Managers.Character.Player.saveData.AddGold(StageGold); // 플레이어 골드에 보상 골드 더하기
    }

    /// <summary>
    /// 현재 층 도전데이터에 맞는 무기를 랜덤으로 선택하는 함수입니다.
    /// </summary>
    /// <returns>현재 층 보상무기 정보</returns>
    public WeaponInfo RandomCurrentWeaponReward(int minus = 0)
    {
        // 무기 타입 랜덤 결정
        WeaponType randomType;
        float randomValue = Random.value; // 0.0 ~ 1.0 사이의 값을 반환
        if (randomValue <= 0.5f) // 50% 확률
            randomType = WeaponType.Bow;
        else
            randomType = WeaponType.Sword;

        // 다음 무기정보가 없다면 지금이 첫 입장한 층
        if (NextRewardWeaponInfo == null)
        {
            CurrentRewardWeaponInfo = new WeaponInfo(TopCurFloor + minus, randomType);
        }
        else
        {
            CurrentRewardWeaponInfo = NextRewardWeaponInfo;
        }

        return CurrentRewardWeaponInfo;
    }

    /// <summary>
    /// 다음 층 도전데이터에 맞는 무기를 랜덤으로 선택하는 함수입니다.
    /// </summary>
    /// <returns>다음 층 보상무기 정보</returns>
    public WeaponInfo RandomNextWeaponReward()
    {
        NextRewardWeaponInfo = new WeaponInfo(TopCurFloor + 1, Managers.Character.Player.combat.CurWeapon.weaponInfo.weaponType);

        return NextRewardWeaponInfo;
    }

    /// <summary>
    /// 현재 무기 보상을 지급합니다.
    /// </summary>
    public void GiveCurrentWeaponReward()
    {
        ItemEventData currentRewardWeapon = ItemFactory.GetItemEventData(CurrentRewardWeaponInfo, 1);
        EventManager.Dispatch(GameEventType.ItemGet, currentRewardWeapon); // 아이템 지급
        ItemFactory.ReleaseItemEventData(currentRewardWeapon);
    }
    #endregion

    /// <summary>
    /// 씬 로드 완료 후 보류 중인 알림을 표시합니다.
    /// </summary>
    public void ShowPendingDeathNotification()
    {
        if (!string.IsNullOrEmpty(pendingDeathNotification))
        {
            Managers.UI.Show<UINotification>()?.ShowNotification(pendingDeathNotification, NotificationType.Warning);
            pendingDeathNotification = null;
        }
    }
}



■ 층수 표기 UI

○ UICurrentFloor

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class UICurrentFloor : UIBase
{
    [SerializeField] private TextMeshProUGUI floorNumber;
    [SerializeField] private Button exitButton;

    private void Awake()
    {
        base.Awake();
        exitButton.onClick.AddListener(OnExitButton);
    }

    public override void Opened(object[] param)
    {
        floorNumber.text = Managers.Game.TopCurFloor.ToString();
    }

    public void SwitchOnButtonState()
    {
        exitButton.interactable = true;
    }
    public void SwitchOffButtonState()
    {
        exitButton.interactable = false;
    }

    private void OnExitButton()
    {
        Managers.Sound.PlaySFX(SFX.UI_OpenBox);
        Managers.UI.Hide<UICurrentFloor>();
        Managers.Character.Player.stat.TakeDamage(Managers.Character.Player.stat.MaxHP+1000);
    }
}



■ 인피니트 스테이지

  • 흑흑 기껏 리팩토링한 스테이지 코드가 못생겨져서 슬프다.. 주말에 다시 리팩토링 해야지🥲

○ StageManager

    public void CreateStage()
   {
       int floor = Managers.Game.TopCurFloor;


       // 5층 단위면 입장전이고, 이미 휴게층이 아니라면 휴게층으로 이동
       StageType = RandomStageType(floor);
       Logger.Log(StageType.ToString());

       // 스테이지 특성에 맞는 몬스터 생성
       switch (StageType)
       {
           case StageType.Boss:
               StageID = RandomStageID<BossStageData>(Managers.Data.GameDataBase.BossStageDatas);
               // --- 임시 코드 ---
               if (StageID == lastBossStageID)
               {
                   StageID = (lastBossStageID == 101) ? 102 : 101;
               }
               lastBossStageID = StageID;
               // -----------------
               Managers.Scene.LoadScene(Managers.Data.GameDataBase.BossStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
               Managers.UI.Show<UICurrentFloor>();
               break;
           case StageType.Normal:
               StageID = RandomStageID<NormalStageData>(Managers.Data.GameDataBase.NormalStageDatas);
               Managers.Scene.LoadScene(Managers.Data.GameDataBase.NormalStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
               Managers.UI.Show<UICurrentFloor>();
               break;
           case StageType.Event:
               StageID = RandomStageID<EventStageData>(Managers.Data.GameDataBase.EventStageDatas);
               Managers.Scene.LoadScene(Managers.Data.GameDataBase.EventStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
               Managers.UI.Show<UICurrentFloor>();
               break;
           case StageType.Shelter:
               isShelterFloor = true;
               Managers.Scene.LoadScene(ESceneName.Shelter);
               break;
           case StageType.Infinite:
               StageID = RandomStageID<NormalStageData>(Managers.Data.GameDataBase.NormalStageDatas);
               NormalStageData normalStage = Managers.Data.GameDataBase.NormalStageDatas[StageID];
               Managers.Character.SpawnMonster(normalStage);
               break;
       }
       Managers.Game.StageStart();
   }

   public void StartInfiniteStage()
   {
       Managers.Game.MaxGameTopFloorSet();
       StageType = StageType.Infinite;
       StageID = RandomStageID<NormalStageData>(Managers.Data.GameDataBase.NormalStageDatas);
       Managers.Scene.LoadScene(Managers.Data.GameDataBase.NormalStageDatas[StageID]._sceneName); // 해당 스테이지의 씬으로 이동
       Managers.Game.StageStart();
   }

   /// <summary>
   /// 스테이지 ID 에 맞는 몬스터를 소환해주는 함수 입니다
   /// </summary>
   public void EnterStageMonsterSpawn()
   {
       switch (StageType)
       {
           case StageType.Boss:
               BossStageData bossStage = Managers.Data.GameDataBase.BossStageDatas[StageID];
               Managers.Character.SpawnMonster(bossStage);
               break;
           case StageType.Infinite:
           case StageType.Normal:
               NormalStageData normalStage = Managers.Data.GameDataBase.NormalStageDatas[StageID];
               Managers.Character.SpawnMonster(normalStage);
               break;
       }
   }

   /// <summary>
   /// 리스트에 저장된 스테이지타입을 랜덤으로 선택하여 랜덤스테이지 타입 ID(인덱스) 값을 반환하는 함수 입니다.
   /// </summary>
   /// <returns>랜덤 스테이지타입 ID</returns>
   private StageType RandomStageType(int floor)
   {
       if (StageType == StageType.Infinite)
           return StageType.Infinite;

       if ((floor % StageScene.ShelterStageInterval == 0) && (!isShelterFloor))
           return StageType.Shelter;

       int _stageType;

       if (floor % GameConstants.StageScene.BossStageInterval == 0) // 10층단위로 보스 등장
       {
           _stageType = 0; // 보스타입 고정
       }
       else
       {
           float randomValue = Random.value; // 0.0 ~ 1.0 사이의 값을 반환
           if (randomValue <= 0.75f) // 75% 확률로 1 반환
           {
               _stageType = 1;
           }
           else // 25% 확률로 2 반환
           {
               _stageType = 2;
           }
       }
       isShelterFloor = false; // 휴게층이 아니므로

       return (StageType)_stageType; // 이넘으로 변환
   }

○ GameManager

    public void SubEnemyCount(int enemySpawnPhase, Enemy enemy)
    {
        // 딕셔너리에서 phase를 키로 해당되는 밸류(몬스터 수)를 찾습니다.
        if (!stageEnemyCount.TryGetValue(enemySpawnPhase, out int monsterCount))
            return;

        StageReputeScore += enemy.Stat.ReputeNumber;
        stageEnemyCount[enemySpawnPhase]--;
        EnemyKillCount++;

        // 노말 스테이지이고, 처치된 에너미가 소속된 페이즈가 가장 최근에 몬스터 수가 절반이 된 페이즈가 아니라면
        // 즉, 새로운 페이즈를 소환하는 트리거가 될 수 있는 최신 페이즈의 에너미라면
        if ((Managers.Stage.StageType == StageType.Normal || Managers.Stage.StageType == StageType.Infinite )&& RecentHalfEnemyPhase != enemySpawnPhase)
        {
            // 마지막 페이즈가 아니고, 페이즈 별 몬스터 절반이 죽었을 경우에
            if (CurrentEnemyPhase < LastEnemyPhase && stageEnemyCount[enemySpawnPhase] < MaxEnemyCountPerPhase / 2)
            {
                CurrentEnemyPhase++; // 현재 페이즈 증가
                RecentHalfEnemyPhase = enemySpawnPhase;
                // 다음 페이즈 몬스터 소환
                Managers.Character.SpawnMonster(Managers.Data.GameDataBase.NormalStageDatas[Managers.Stage.StageID]);
                Logger.Log($"{CurrentEnemyPhase}페이즈 시작, 최대페이즈 : {LastEnemyPhase}");
            }
        }
        if (stageEnemyCount[enemySpawnPhase] <= 0) // 다 죽였다면
        {
            stageEnemyCount.Remove(enemySpawnPhase);
            if (stageEnemyCount.Count == 0) // 모든 종류의 몬스터가 죽었다면
            {
                if(Managers.Stage.StageType == StageType.Infinite)
                {
                    // 인피니트 스테이지라면
                    if (stageEnemyCount == null)
                        stageEnemyCount = new Dictionary<int, int>();
                    else
                        stageEnemyCount.Clear();

                    CurrentEnemyPhase = 0;
                    RecentHalfEnemyPhase = -1;
                    LastEnemyPhase = 0;
                    MaxEnemyCountPerPhase = 0;

                    Managers.Stage.CreateStage();
                    return;
                }
                StageClear(enemy.gameObject.transform.position);
            }
        }
    }
        public void StageStart()
    {
        if (Managers.Stage.StageType == StageType.Normal || Managers.Stage.StageType == StageType.Infinite)
            SetStageEnemyInfo();
    }
    
        /// <summary>
    /// 스테이지 종료(성공, 실패 둘다)후 데이터를 리셋하는 함수입니다.
    /// </summary>
    private void StageEnd()
    {
        if (Managers.Stage.StageType == StageType.Infinite)
        {
            Managers.Character.Player.saveData.UpdateChallengeKillCount(EnemyKillCount);
            Managers.Stage.StageType = StageType.Normal;
        }
        else
            UpdateHighestFloorRecord();// 최고 도달층 변경

        // 에너미 정보 초기화
        ResetStageEnemyInfo();

        // 보상정보, 피격여부 초기화
        CurrentRewardWeaponInfo = null;
        NextRewardWeaponInfo = null;
        StageReputeScore = 0f;
        StageGold = 0;
        IsDamaged = false;
    }

0개의 댓글

관련 채용 정보