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