diff --git a/Assets/Scripts/GameManager.cs b/Assets/Scripts/GameManager.cs new file mode 100644 index 00000000..b60f023a --- /dev/null +++ b/Assets/Scripts/GameManager.cs @@ -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 +{ + /// + /// 게임 상태 열거형 + /// + public enum GameState + { + Waiting, // 대기 중 + Playing, // 게임 진행 중 + GameOver, // 게임 오버 + Victory // 승리 + } + + /// + /// 게임 전체를 관리하는 매니저. + /// 게임 상태, 플레이어 사망 체크, 승리/패배 조건을 처리합니다. + /// + 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 currentState = new NetworkVariable(GameState.Waiting); + + // 인스턴스화된 UI + private GameObject gameOverUIInstance; + private GameObject victoryUIInstance; + private GameObject victoryEffectInstance; + + // 이벤트 + public event Action 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 clientsCompleted, List 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(FindObjectsSortMode.None); + foreach (var player in players) + { + player.Respawn(); + } + + // 카메라 재설정 + var playerMovement = FindObjectsByType(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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /// + /// UI 프리팹 인스턴스화 + /// + 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"); + } + } + } + + /// + /// Canvas 찾기 또는 생성 + /// + private Canvas FindOrCreateCanvas() + { + // 기존 Canvas 찾기 + Canvas canvas = FindFirstObjectByType(); + + if (canvas == null) + { + // 새 Canvas 생성 + var canvasObject = new GameObject("GameUI Canvas"); + canvas = canvasObject.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + canvasObject.AddComponent(); + canvasObject.AddComponent(); + + if (debugMode) + { + Debug.Log("[GameManager] Created new Canvas"); + } + } + + return canvas; + } + + /// + /// UI 정리 + /// + 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 alivePlayers = new List(); + + private IEnumerator WaitForPlayersAndSubscribe() + { + // 플레이어들이 스폰될 때까지 대기 + yield return new WaitForSeconds(1f); + + SubscribeToPlayerEvents(); + + // 게임 시작 + SetGameState(GameState.Playing); + } + + private void SubscribeToPlayerEvents() + { + var players = FindObjectsByType(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(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; + } + + /// + /// 게임 오버 처리 (서버에서만 실행) + /// + 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)); + } + + /// + /// 승리 처리 (서버에서만 실행) + /// + 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 + + /// + /// 살아있는 플레이어 목록 반환 + /// + public List GetAlivePlayers() + { + return alivePlayers.Where(p => p != null && !p.IsDead).ToList(); + } + + /// + /// 랜덤한 살아있는 플레이어 반환 (관전용) + /// + public PlayerNetworkController GetRandomAlivePlayer() + { + var alive = GetAlivePlayers(); + if (alive.Count == 0) return null; + + return alive[UnityEngine.Random.Range(0, alive.Count)]; + } + + #endregion + } +} diff --git a/Assets/Scripts/GameManager.cs.meta b/Assets/Scripts/GameManager.cs.meta new file mode 100644 index 00000000..a6282ee4 --- /dev/null +++ b/Assets/Scripts/GameManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7bde02fc6ca2ab0468bb3ce777206089 \ No newline at end of file