feat: 플레이어 관전 시스템 추가
- PlayerSpectator: 사망한 플레이어가 살아있는 플레이어 관찰 - PlayerCamera: SetTarget/ResetToPlayer/SnapToTarget 메서드 추가 - PlayerMovement: OnEnable/OnDisable에서 입력 액션 관리 - Tab 키로 관전 대상 전환 기능 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
195
Assets/Scripts/Player/PlayerSpectator.cs
Normal file
195
Assets/Scripts/Player/PlayerSpectator.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어 관전 시스템.
|
||||
/// 사망한 플레이어가 살아있는 플레이어를 관찰할 수 있게 합니다.
|
||||
/// </summary>
|
||||
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<PlayerNetworkController> alivePlayers = new List<PlayerNetworkController>();
|
||||
|
||||
// 이벤트
|
||||
public event System.Action<bool> OnSpectateModeChanged; // (isSpectating)
|
||||
public event System.Action<PlayerNetworkController> OnSpectateTargetChanged;
|
||||
|
||||
// Properties
|
||||
public bool IsSpectating => isSpectating;
|
||||
public PlayerNetworkController CurrentTarget => alivePlayers.Count > currentSpectateIndex ? alivePlayers[currentSpectateIndex] : null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 컴포넌트 자동 참조
|
||||
if (playerCamera == null)
|
||||
playerCamera = GetComponent<PlayerCamera>();
|
||||
if (networkController == null)
|
||||
networkController = GetComponentInParent<PlayerNetworkController>();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 관전 모드 시작
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 관전 모드 종료
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 살아있는 플레이어 목록 갱신
|
||||
/// </summary>
|
||||
private void RefreshAlivePlayers()
|
||||
{
|
||||
alivePlayers.Clear();
|
||||
|
||||
var allPlayers = FindObjectsByType<PlayerNetworkController>(FindObjectsSortMode.None);
|
||||
foreach (var player in allPlayers)
|
||||
{
|
||||
// 자신이 아니고, 살아있는 플레이어만 추가
|
||||
if (player != networkController && !player.IsDead)
|
||||
{
|
||||
alivePlayers.Add(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 관전 대상으로 전환
|
||||
/// </summary>
|
||||
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]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 관전 대상 설정
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 관전 대상 이름 반환 (UI용)
|
||||
/// </summary>
|
||||
public string GetCurrentTargetName()
|
||||
{
|
||||
var target = CurrentTarget;
|
||||
if (target == null) return "None";
|
||||
return $"Player {target.OwnerClientId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user