Files
Colosseum/Assets/_Game/Scripts/Core/GameManager.cs
dal4segno e5ef94da85 feat: 멀티플레이어 네트워크 동기화 구현
- 로비 씬 추가 및 LobbyManager/LobbyUI/LobbySceneBuilder 구현
- NetworkPrefabsList로 플레이어 프리팹 등록 (PlayerPrefab 자동스폰 비활성화)
- PlayerMovement 서버 권한 이동 아키텍처로 전환
  - NetworkVariable<Vector2>로 클라이언트 입력 → 서버 전달
  - 점프 JumpRequestRpc로 서버 검증 후 실행
- 보스 프리팹에 NetworkTransform/NetworkAnimator 추가 (서버 권한)
- SkillController를 NetworkBehaviour로 전환
  - PlaySkillClipClientRpc로 클립 override + 재생 원자적 동기화
  - OnEffect/OnSkillEnd 클라이언트 실행 차단
- WeaponEquipment 클라이언트 무기 시각화 동기화 수정
  - registeredWeapons 인덱스 기반 NetworkVariable 동기화
  - SpawnWeaponVisualsLocal로 클라이언트 무기 생성
  - 중복 Instantiate 버그 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:46:45 +09:00

549 lines
15 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("Player")]
[Tooltip("플레이어 프리팹 (NetworkObject 포함)")]
[SerializeField] private GameObject playerPrefab;
[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)
StartCoroutine(SpawnPlayersAndRespawn(clientsCompleted));
}
}
private IEnumerator SpawnPlayersAndRespawn(List<ulong> clientsCompleted)
{
if (playerPrefab == null)
{
Debug.LogWarning("[GameManager] playerPrefab이 설정되지 않았습니다. 인스펙터에서 할당하세요.");
RespawnAllPlayersClientRpc();
yield break;
}
Debug.Log($"[GameManager] SpawnPlayersAndRespawn: clientsCompleted=[{string.Join(",", clientsCompleted)}]");
// 씬 로드를 완료한 클라이언트마다 플레이어 스폰
foreach (ulong clientId in clientsCompleted)
{
var go = Instantiate(playerPrefab);
var no = go.GetComponent<NetworkObject>();
if (no != null)
{
no.SpawnAsPlayerObject(clientId, true);
Debug.Log($"[GameManager] Spawned player for clientId={clientId}");
}
else
{
Debug.LogError($"[GameManager] playerPrefab에 NetworkObject가 없습니다!");
Destroy(go);
}
}
// 클라이언트가 스폰된 오브젝트를 받을 시간 여유
yield return new WaitForSeconds(0.5f);
RespawnAllPlayersClientRpc();
}
[Rpc(SendTo.ClientsAndHost)]
private void RespawnAllPlayersClientRpc()
{
// 서버: 모든 플레이어 체력/상태 리셋
var players = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
foreach (var player in players)
{
player.Respawn();
}
// 카메라 재설정 — 자신이 소유한 플레이어만
var playerMovements = FindObjectsByType<PlayerMovement>(FindObjectsSortMode.None);
foreach (var movement in playerMovements)
{
if (movement.IsOwner)
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
}
}