feat: 게임 상태 관리 시스템 추가

게임 전체 상태(Waiting, Playing, GameOver, Victory)를 관리하는 GameManager 구현.
플레이어 전멸 시 게임 오버, 보스 처치 시 승리 처리.
씬 로드 시 플레이어 리스폰 및 카메라 재설정 지원.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-14 15:07:49 +09:00
parent d28ff21213
commit 5e61dbf7e6
2 changed files with 517 additions and 0 deletions

View File

@@ -0,0 +1,515 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using Unity.Netcode;
using Colosseum.Player;
using Colosseum.Enemy;
namespace Colosseum
{
/// <summary>
/// 게임 상태 열거형
/// </summary>
public enum GameState
{
Waiting, // 대기 중
Playing, // 게임 진행 중
GameOver, // 게임 오버
Victory // 승리
}
/// <summary>
/// 게임 전체를 관리하는 매니저.
/// 게임 상태, 플레이어 사망 체크, 승리/패배 조건을 처리합니다.
/// </summary>
public class GameManager : NetworkBehaviour
{
[Header("UI Prefabs")]
[Tooltip("게임 오버 UI 프리팹")]
[SerializeField] private GameObject gameOverUIPrefab;
[Tooltip("승리 UI 프리팹")]
[SerializeField] private GameObject victoryUIPrefab;
[Tooltip("승리 연출 이펙트 프리팹")]
[SerializeField] private GameObject victoryEffectPrefab;
[Header("Settings")]
[Tooltip("게임 오버 후 재시작까지 대기 시간")]
[SerializeField] private float gameOverRestartDelay = 5f;
[Tooltip("승리 후 로비로 이동까지 대기 시간")]
[SerializeField] private float victoryToLobbyDelay = 5f;
[Header("Debug")]
[SerializeField] private bool debugMode = true;
// 싱글톤
public static GameManager Instance { get; private set; }
// 게임 상태
private NetworkVariable<GameState> currentState = new NetworkVariable<GameState>(GameState.Waiting);
// 인스턴스화된 UI
private GameObject gameOverUIInstance;
private GameObject victoryUIInstance;
private GameObject victoryEffectInstance;
// 이벤트
public event Action<GameState> OnGameStateChanged;
public event Action OnGameOver;
public event Action OnVictory;
// Properties
public GameState CurrentState => currentState.Value;
public bool IsGameOver => currentState.Value == GameState.GameOver;
public bool IsVictory => currentState.Value == GameState.Victory;
private void Awake()
{
// 싱글톤 설정
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public override void OnNetworkSpawn()
{
currentState.OnValueChanged += HandleGameStateChanged;
// 네트워크 씬 로드 이벤트 구독
if (NetworkManager.Singleton.SceneManager != null)
{
NetworkManager.Singleton.SceneManager.OnLoadEventCompleted += OnSceneLoadCompleted;
}
// UI 인스턴스화 (모든 클라이언트에서)
SpawnUI();
if (IsServer)
{
// 플레이어 사망 이벤트 구독
StartCoroutine(WaitForPlayersAndSubscribe());
// 보스 사망 이벤트 구독
SubscribeToBossEvents();
}
}
private void OnSceneLoadCompleted(string sceneName, LoadSceneMode loadSceneMode, List<ulong> clientsCompleted, List<ulong> clientsTimedOut)
{
if (loadSceneMode == LoadSceneMode.Single)
{
if (debugMode)
{
Debug.Log($"[GameManager] Scene loaded: {sceneName}");
}
// 씬 로드 완료 시 플레이어 리스폰
if (IsServer)
{
RespawnAllPlayersClientRpc();
}
}
}
[Rpc(SendTo.ClientsAndHost)]
private void RespawnAllPlayersClientRpc()
{
// 모든 플레이어 리스폰
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
foreach (var player in players)
{
player.Respawn();
}
// 카메라 재설정
var playerMovement = FindObjectsByType<PlayerMovement>(FindObjectsSortMode.None);
foreach (var movement in playerMovement)
{
movement.RefreshCamera();
}
}
public override void OnNetworkDespawn()
{
currentState.OnValueChanged -= HandleGameStateChanged;
// 네트워크 씬 로드 이벤트 구독 해제
if (NetworkManager.Singleton.SceneManager != null)
{
NetworkManager.Singleton.SceneManager.OnLoadEventCompleted -= OnSceneLoadCompleted;
}
// UI 정리
CleanupUI();
if (IsServer)
{
UnsubscribeFromPlayerEvents();
UnsubscribeFromBossEvents();
}
}
#region UI Management
/// <summary>
/// UI 프리팹 인스턴스화
/// </summary>
private void SpawnUI()
{
// Canvas 찾기 또는 생성
Canvas canvas = FindOrCreateCanvas();
// 게임 오버 UI
if (gameOverUIPrefab != null)
{
gameOverUIInstance = Instantiate(gameOverUIPrefab, canvas.transform);
gameOverUIInstance.name = "GameOverUI";
gameOverUIInstance.SetActive(false);
if (debugMode)
{
Debug.Log("[GameManager] GameOverUI instantiated");
}
}
// 승리 UI
if (victoryUIPrefab != null)
{
victoryUIInstance = Instantiate(victoryUIPrefab, canvas.transform);
victoryUIInstance.name = "VictoryUI";
victoryUIInstance.SetActive(false);
if (debugMode)
{
Debug.Log("[GameManager] VictoryUI instantiated");
}
}
// 승리 연출 이펙트
if (victoryEffectPrefab != null)
{
victoryEffectInstance = Instantiate(victoryEffectPrefab);
victoryEffectInstance.name = "VictoryEffect";
victoryEffectInstance.SetActive(false);
if (debugMode)
{
Debug.Log("[GameManager] VictoryEffect instantiated");
}
}
}
/// <summary>
/// Canvas 찾기 또는 생성
/// </summary>
private Canvas FindOrCreateCanvas()
{
// 기존 Canvas 찾기
Canvas canvas = FindFirstObjectByType<Canvas>();
if (canvas == null)
{
// 새 Canvas 생성
var canvasObject = new GameObject("GameUI Canvas");
canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObject.AddComponent<UnityEngine.UI.CanvasScaler>();
canvasObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
if (debugMode)
{
Debug.Log("[GameManager] Created new Canvas");
}
}
return canvas;
}
/// <summary>
/// UI 정리
/// </summary>
private void CleanupUI()
{
if (gameOverUIInstance != null)
{
Destroy(gameOverUIInstance);
}
if (victoryUIInstance != null)
{
Destroy(victoryUIInstance);
}
if (victoryEffectInstance != null)
{
Destroy(victoryEffectInstance);
}
}
#endregion
private void HandleGameStateChanged(GameState oldValue, GameState newValue)
{
OnGameStateChanged?.Invoke(newValue);
// UI 활성화/비활성화
UpdateUIVisibility(newValue);
if (debugMode)
{
Debug.Log($"[GameManager] State changed: {oldValue} -> {newValue}");
}
}
private void UpdateUIVisibility(GameState state)
{
// 게임 오버 UI
if (gameOverUIInstance != null)
{
gameOverUIInstance.SetActive(state == GameState.GameOver);
}
// 승리 UI
if (victoryUIInstance != null)
{
victoryUIInstance.SetActive(state == GameState.Victory);
}
// 승리 연출
if (victoryEffectInstance != null && state == GameState.Victory)
{
victoryEffectInstance.SetActive(true);
}
}
#region Player Death Tracking
private List<PlayerNetworkController> alivePlayers = new List<PlayerNetworkController>();
private IEnumerator WaitForPlayersAndSubscribe()
{
// 플레이어들이 스폰될 때까지 대기
yield return new WaitForSeconds(1f);
SubscribeToPlayerEvents();
// 게임 시작
SetGameState(GameState.Playing);
}
private void SubscribeToPlayerEvents()
{
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
foreach (var player in players)
{
player.OnDeath += HandlePlayerDeath;
if (!player.IsDead)
{
alivePlayers.Add(player);
}
}
if (debugMode)
{
Debug.Log($"[GameManager] Subscribed to {players.Length} players, {alivePlayers.Count} alive");
}
}
private void UnsubscribeFromPlayerEvents()
{
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
foreach (var player in players)
{
player.OnDeath -= HandlePlayerDeath;
}
alivePlayers.Clear();
}
private void HandlePlayerDeath(PlayerNetworkController player)
{
alivePlayers.Remove(player);
if (debugMode)
{
Debug.Log($"[GameManager] Player died. Alive: {alivePlayers.Count}");
}
// 모든 플레이어 사망 체크
if (alivePlayers.Count == 0)
{
TriggerGameOver();
}
}
#endregion
#region Boss Death Tracking
private void SubscribeToBossEvents()
{
BossEnemy.OnBossSpawned += HandleBossSpawned;
// 이미 스폰된 보스가 있는지 확인
if (BossEnemy.ActiveBoss != null)
{
SubscribeToBossDeath(BossEnemy.ActiveBoss);
}
}
private void UnsubscribeFromBossEvents()
{
BossEnemy.OnBossSpawned -= HandleBossSpawned;
}
private void HandleBossSpawned(BossEnemy boss)
{
SubscribeToBossDeath(boss);
if (debugMode)
{
Debug.Log($"[GameManager] Boss spawned: {boss.name}");
}
}
private void SubscribeToBossDeath(BossEnemy boss)
{
boss.OnDeath += HandleBossDeath;
}
private void HandleBossDeath()
{
if (debugMode)
{
Debug.Log("[GameManager] Boss died!");
}
TriggerVictory();
}
#endregion
#region Game State Management
private void SetGameState(GameState newState)
{
if (!IsServer) return;
currentState.Value = newState;
}
/// <summary>
/// 게임 오버 처리 (서버에서만 실행)
/// </summary>
public void TriggerGameOver()
{
if (!IsServer || currentState.Value != GameState.Playing) return;
SetGameState(GameState.GameOver);
OnGameOver?.Invoke();
if (debugMode)
{
Debug.Log("[GameManager] Game Over!");
}
// N초 후 씬 재시작
StartCoroutine(RestartSceneAfterDelay(gameOverRestartDelay));
}
/// <summary>
/// 승리 처리 (서버에서만 실행)
/// </summary>
public void TriggerVictory()
{
if (!IsServer || currentState.Value != GameState.Playing) return;
SetGameState(GameState.Victory);
OnVictory?.Invoke();
if (debugMode)
{
Debug.Log("[GameManager] Victory!");
}
// N초 후 씬 재시작 (또는 로비로 이동)
StartCoroutine(RestartSceneAfterDelay(victoryToLobbyDelay));
}
private IEnumerator RestartSceneAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
// 현재 씬 다시 로드
string currentScene = SceneManager.GetActiveScene().name;
if (IsServer)
{
// 네트워크 씬 관리 사용
NetworkManager.Singleton.SceneManager.LoadScene(currentScene, LoadSceneMode.Single);
}
}
#endregion
#region Utility
/// <summary>
/// 살아있는 플레이어 목록 반환
/// </summary>
public List<PlayerNetworkController> GetAlivePlayers()
{
return alivePlayers.Where(p => p != null && !p.IsDead).ToList();
}
/// <summary>
/// 랜덤한 살아있는 플레이어 반환 (관전용)
/// </summary>
public PlayerNetworkController GetRandomAlivePlayer()
{
var alive = GetAlivePlayers();
if (alive.Count == 0) return null;
return alive[UnityEngine.Random.Range(0, alive.Count)];
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7bde02fc6ca2ab0468bb3ce777206089