- 로비 씬 추가 및 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>
549 lines
15 KiB
C#
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
|
|
}
|
|
}
|