■ 오늘계획
○ 할 일 목록
- 다음 보상 정보 안맞는거 수정
- 보상 관련 오브젝트 및 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;
}