diff --git a/Assets/Scripts/Player/PlayerCamera.cs b/Assets/Scripts/Player/PlayerCamera.cs
index 4c86b7e7..71c76db3 100644
--- a/Assets/Scripts/Player/PlayerCamera.cs
+++ b/Assets/Scripts/Player/PlayerCamera.cs
@@ -1,5 +1,6 @@
using UnityEngine;
using UnityEngine.InputSystem;
+using UnityEngine.SceneManagement;
namespace Colosseum.Player
{
@@ -20,11 +21,35 @@ namespace Colosseum.Player
private float pitch;
private Camera cameraInstance;
private InputSystem_Actions inputActions;
+ private bool isSpectating = false;
+
+ public Transform Target => target;
+ public bool IsSpectating => isSpectating;
+
+ private void OnEnable()
+ {
+ SceneManager.sceneLoaded += OnSceneLoaded;
+ }
+
+ private void OnDisable()
+ {
+ SceneManager.sceneLoaded -= OnSceneLoaded;
+ }
+
+ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
+ {
+ // 씬 로드 시 카메라 참조 갱신
+ RefreshCamera();
+ SnapToTarget();
+
+ Debug.Log($"[PlayerCamera] Scene loaded, camera refreshed");
+ }
public void Initialize(Transform playerTransform, InputSystem_Actions actions)
{
target = playerTransform;
inputActions = actions;
+ isSpectating = false;
// 기존 메인 카메라 사용 또는 새로 생성
cameraInstance = Camera.main;
@@ -36,12 +61,73 @@ namespace Colosseum.Player
}
// 초기 각도
- yaw = target.eulerAngles.y;
+ if (target != null)
+ {
+ yaw = target.eulerAngles.y;
+ }
pitch = 20f;
+
+ // 카메라 위치를 즉시 타겟 위치로 초기화
+ SnapToTarget();
+ }
+
+ ///
+ /// 관전 대상 변경
+ ///
+ public void SetTarget(Transform newTarget)
+ {
+ if (newTarget == null) return;
+
+ target = newTarget;
+ isSpectating = true;
+
+ // 부드러운 전환을 위해 현재 카메라 위치에서 새 타겟으로
+ yaw = target.eulerAngles.y;
+
+ Debug.Log($"[PlayerCamera] Now spectating: {target.name}");
+ }
+
+ ///
+ /// 원래 플레이어로 복귀
+ ///
+ public void ResetToPlayer(Transform playerTransform)
+ {
+ target = playerTransform;
+ isSpectating = false;
+ }
+
+ ///
+ /// 카메라 위치를 타겟 위치로 즉시 이동 (부드러운 전환 없이)
+ ///
+ public void SnapToTarget()
+ {
+ if (target == null || cameraInstance == null) return;
+
+ Quaternion rotation = Quaternion.Euler(pitch, yaw, 0f);
+ Vector3 offset = rotation * new Vector3(0f, 0f, -distance);
+ offset.y += height;
+
+ cameraInstance.transform.position = target.position + offset;
+ cameraInstance.transform.LookAt(target.position + Vector3.up * height * 0.5f);
+ }
+
+ ///
+ /// 카메라 참조 갱신 (씬 전환 후 호출)
+ ///
+ public void RefreshCamera()
+ {
+ // 씬 전환 시 항상 새 카메라 참조 획득
+ cameraInstance = Camera.main;
}
private void LateUpdate()
{
+ // 카메라 참조가 없으면 갱신 시도
+ if (cameraInstance == null)
+ {
+ RefreshCamera();
+ }
+
if (target == null || cameraInstance == null) return;
HandleRotation();
diff --git a/Assets/Scripts/Player/PlayerMovement.cs b/Assets/Scripts/Player/PlayerMovement.cs
index 3f13bbed..e5fe7b4c 100644
--- a/Assets/Scripts/Player/PlayerMovement.cs
+++ b/Assets/Scripts/Player/PlayerMovement.cs
@@ -75,6 +75,17 @@ namespace Colosseum.Player
SetSpawnPosition();
// Input Actions 초기화
+ InitializeInputActions();
+
+ // 카메라 설정
+ SetupCamera();
+ }
+
+ ///
+ /// 입력 액션 초기화
+ ///
+ private void InitializeInputActions()
+ {
inputActions = new InputSystem_Actions();
inputActions.Player.Enable();
@@ -84,11 +95,42 @@ namespace Colosseum.Player
// Jump 액션 콜백 등록
inputActions.Player.Jump.performed += OnJumpPerformed;
-
- // 카메라 설정
- SetupCamera();
}
+ ///
+ /// 입력 액션 해제
+ ///
+ private void CleanupInputActions()
+ {
+ if (inputActions != null)
+ {
+ inputActions.Player.Move.performed -= OnMovePerformed;
+ inputActions.Player.Move.canceled -= OnMoveCanceled;
+ inputActions.Player.Jump.performed -= OnJumpPerformed;
+ inputActions.Player.Disable();
+ }
+ }
+
+ private void OnDisable()
+ {
+ // 컴포넌트 비활성화 시 입력 해제
+ CleanupInputActions();
+
+ // 입력 초기화
+ moveInput = Vector2.zero;
+ }
+
+ private void OnEnable()
+ {
+ // 컴포넌트 재활성화 시 입력 다시 등록
+ if (IsOwner && inputActions != null)
+ {
+ inputActions.Player.Enable();
+ inputActions.Player.Move.performed += OnMovePerformed;
+ inputActions.Player.Move.canceled += OnMoveCanceled;
+ inputActions.Player.Jump.performed += OnJumpPerformed;
+ }
+ }
///
/// 스폰 위치 설정
@@ -107,34 +149,24 @@ namespace Colosseum.Player
}
}
-
///
- /// 네트워크 정리리
+ /// 네트워크 정리
///
public override void OnNetworkDespawn()
{
- if (inputActions != null)
- {
- inputActions.Player.Move.performed -= OnMovePerformed;
- inputActions.Player.Move.canceled -= OnMoveCanceled;
- inputActions.Player.Jump.performed -= OnJumpPerformed;
- inputActions.Disable();
- }
+ CleanupInputActions();
}
-
private void OnMovePerformed(InputAction.CallbackContext context)
{
moveInput = context.ReadValue();
}
-
private void OnMoveCanceled(InputAction.CallbackContext context)
{
moveInput = Vector2.zero;
}
-
private void OnJumpPerformed(InputAction.CallbackContext context)
{
if (!isJumping && characterController.isGrounded)
@@ -143,7 +175,6 @@ namespace Colosseum.Player
}
}
-
private void SetupCamera()
{
var cameraController = GetComponent();
@@ -154,6 +185,13 @@ namespace Colosseum.Player
cameraController.Initialize(transform, inputActions);
}
+ ///
+ /// 카메라 재설정 (씬 로드 후 호출)
+ ///
+ public void RefreshCamera()
+ {
+ SetupCamera();
+ }
private void Update()
{
@@ -163,7 +201,6 @@ namespace Colosseum.Player
Move();
}
-
private void ApplyGravity()
{
if (wasGrounded && velocity.y < 0)
@@ -176,7 +213,6 @@ namespace Colosseum.Player
}
}
-
private void Move()
{
if (characterController == null) return;
@@ -218,7 +254,6 @@ namespace Colosseum.Player
wasGrounded = characterController.isGrounded;
}
-
private void Jump()
{
isJumping = true;
@@ -232,7 +267,6 @@ namespace Colosseum.Player
}
}
-
///
/// 점프 중 상태가 끝나면 IsJumping = false;
///
@@ -241,7 +275,6 @@ namespace Colosseum.Player
isJumping = false;
}
-
private Vector3 TransformDirectionByCamera(Vector3 direction)
{
if (Camera.main == null) return direction;
@@ -259,7 +292,6 @@ namespace Colosseum.Player
return cameraRight * direction.x + cameraForward * direction.z;
}
-
///
/// 루트 모션 처리. 스킬 애니메이션 중에 애니메이션의 이동/회전 데이터를 적용합니다.
///
diff --git a/Assets/Scripts/Player/PlayerSpectator.cs b/Assets/Scripts/Player/PlayerSpectator.cs
new file mode 100644
index 00000000..55026425
--- /dev/null
+++ b/Assets/Scripts/Player/PlayerSpectator.cs
@@ -0,0 +1,195 @@
+using System.Collections.Generic;
+using UnityEngine;
+using Colosseum.Player;
+
+namespace Colosseum.Player
+{
+ ///
+ /// 플레이어 관전 시스템.
+ /// 사망한 플레이어가 살아있는 플레이어를 관찰할 수 있게 합니다.
+ ///
+ public class PlayerSpectator : MonoBehaviour
+ {
+ [Header("References")]
+ [Tooltip("PlayerCamera 컴포넌트")]
+ [SerializeField] private PlayerCamera playerCamera;
+
+ [Tooltip("PlayerNetworkController 컴포넌트")]
+ [SerializeField] private PlayerNetworkController networkController;
+
+ [Header("Spectate Settings")]
+ [Tooltip("관전 대상 전환 키")]
+ [SerializeField] private KeyCode nextTargetKey = KeyCode.Tab;
+
+ [Tooltip("관전 UI 표시 여부")]
+ [SerializeField] private bool showSpectateUI = true;
+
+ // 관전 상태
+ private bool isSpectating = false;
+ private int currentSpectateIndex = 0;
+ private List alivePlayers = new List();
+
+ // 이벤트
+ public event System.Action OnSpectateModeChanged; // (isSpectating)
+ public event System.Action OnSpectateTargetChanged;
+
+ // Properties
+ public bool IsSpectating => isSpectating;
+ public PlayerNetworkController CurrentTarget => alivePlayers.Count > currentSpectateIndex ? alivePlayers[currentSpectateIndex] : null;
+
+ private void Awake()
+ {
+ // 컴포넌트 자동 참조
+ if (playerCamera == null)
+ playerCamera = GetComponent();
+ if (networkController == null)
+ networkController = GetComponentInParent();
+ }
+
+ private void Start()
+ {
+ if (networkController != null)
+ {
+ networkController.OnDeathStateChanged += HandleDeathStateChanged;
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (networkController != null)
+ {
+ networkController.OnDeathStateChanged -= HandleDeathStateChanged;
+ }
+ }
+
+ private void Update()
+ {
+ if (!isSpectating) return;
+
+ // Tab 키로 다음 관전 대상 전환
+ if (Input.GetKeyDown(nextTargetKey))
+ {
+ CycleToNextTarget();
+ }
+ }
+
+ private void HandleDeathStateChanged(bool dead)
+ {
+ if (dead)
+ {
+ StartSpectating();
+ }
+ else
+ {
+ StopSpectating();
+ }
+ }
+
+ ///
+ /// 관전 모드 시작
+ ///
+ private void StartSpectating()
+ {
+ // 살아있는 플레이어 목록 갱신
+ RefreshAlivePlayers();
+
+ if (alivePlayers.Count == 0)
+ {
+ // 관전할 플레이어가 없음 (게임 오버)
+ Debug.Log("[PlayerSpectator] No alive players to spectate");
+ return;
+ }
+
+ isSpectating = true;
+ currentSpectateIndex = 0;
+
+ // 첫 번째 살아있는 플레이어 관전
+ SetSpectateTarget(alivePlayers[currentSpectateIndex]);
+
+ OnSpectateModeChanged?.Invoke(true);
+
+ Debug.Log($"[PlayerSpectator] Started spectating. {alivePlayers.Count} players alive.");
+ }
+
+ ///
+ /// 관전 모드 종료
+ ///
+ private void StopSpectating()
+ {
+ isSpectating = false;
+ alivePlayers.Clear();
+ currentSpectateIndex = 0;
+
+ // 원래 플레이어로 카메라 복귀
+ if (playerCamera != null && networkController != null)
+ {
+ playerCamera.ResetToPlayer(networkController.transform);
+ }
+
+ OnSpectateModeChanged?.Invoke(false);
+
+ Debug.Log("[PlayerSpectator] Stopped spectating");
+ }
+
+ ///
+ /// 살아있는 플레이어 목록 갱신
+ ///
+ private void RefreshAlivePlayers()
+ {
+ alivePlayers.Clear();
+
+ var allPlayers = FindObjectsByType(FindObjectsSortMode.None);
+ foreach (var player in allPlayers)
+ {
+ // 자신이 아니고, 살아있는 플레이어만 추가
+ if (player != networkController && !player.IsDead)
+ {
+ alivePlayers.Add(player);
+ }
+ }
+ }
+
+ ///
+ /// 다음 관전 대상으로 전환
+ ///
+ private void CycleToNextTarget()
+ {
+ if (alivePlayers.Count == 0) return;
+
+ // 목록 갱신 (중간에 사망했을 수 있음)
+ RefreshAlivePlayers();
+
+ if (alivePlayers.Count == 0)
+ {
+ Debug.Log("[PlayerSpectator] No more alive players");
+ return;
+ }
+
+ currentSpectateIndex = (currentSpectateIndex + 1) % alivePlayers.Count;
+ SetSpectateTarget(alivePlayers[currentSpectateIndex]);
+ }
+
+ ///
+ /// 관전 대상 설정
+ ///
+ private void SetSpectateTarget(PlayerNetworkController target)
+ {
+ if (target == null || playerCamera == null) return;
+
+ playerCamera.SetTarget(target.transform);
+ OnSpectateTargetChanged?.Invoke(target);
+
+ Debug.Log($"[PlayerSpectator] Now spectating: Player {target.OwnerClientId}");
+ }
+
+ ///
+ /// 현재 관전 대상 이름 반환 (UI용)
+ ///
+ public string GetCurrentTargetName()
+ {
+ var target = CurrentTarget;
+ if (target == null) return "None";
+ return $"Player {target.OwnerClientId}";
+ }
+ }
+}
diff --git a/Assets/Scripts/Player/PlayerSpectator.cs.meta b/Assets/Scripts/Player/PlayerSpectator.cs.meta
new file mode 100644
index 00000000..c9e83e05
--- /dev/null
+++ b/Assets/Scripts/Player/PlayerSpectator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ace7240dc9e5c834892fc1a0e4ea657e
\ No newline at end of file