- Assets/_Game/ 하위로 게임 에셋 통합 - External/ 패키지 벤더별 분류 (Synty, Animations, UI) - 에셋 네이밍 컨벤션 확립 및 적용 (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등) - pre-commit hook으로 네이밍 컨벤션 자동 검사 추가 - RESTRUCTURE_CHECKLIST.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
516 lines
14 KiB
C#
516 lines
14 KiB
C#
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
|
|
}
|
|
}
|