chore: Assets 디렉토리 구조 정리 및 네이밍 컨벤션 적용
- Assets/_Game/ 하위로 게임 에셋 통합 - External/ 패키지 벤더별 분류 (Synty, Animations, UI) - 에셋 네이밍 컨벤션 확립 및 적용 (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등) - pre-commit hook으로 네이밍 컨벤션 자동 검사 추가 - RESTRUCTURE_CHECKLIST.md 작성 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
98
Assets/_Game/Scripts/Player/PlayerAnimationController.cs
Normal file
98
Assets/_Game/Scripts/Player/PlayerAnimationController.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어 애니메이션 컨트롤러
|
||||
/// 이동 속도에 따라 Idle/Walk/Run 애니메이션 제어
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Animator))]
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
public class PlayerAnimationController : NetworkBehaviour
|
||||
{
|
||||
[Header("Animation Parameters")]
|
||||
[SerializeField] private string speedParam = "Speed";
|
||||
[SerializeField] private string isGroundedParam = "IsGrounded";
|
||||
[SerializeField] private string jumpTriggerParam = "Jump";
|
||||
[SerializeField] private string landTriggerParam = "Land";
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float speedSmoothTime = 0.1f;
|
||||
|
||||
private Animator animator;
|
||||
private PlayerMovement playerMovement;
|
||||
private CharacterController characterController;
|
||||
private float currentSpeed;
|
||||
private float speedVelocity;
|
||||
private bool wasGrounded = true;
|
||||
private bool isJumpingAnimation;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
playerMovement = GetComponent<PlayerMovement>();
|
||||
characterController = GetComponent<CharacterController>();
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (!IsOwner)
|
||||
{
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
UpdateAnimationParameters();
|
||||
}
|
||||
|
||||
private void UpdateAnimationParameters()
|
||||
{
|
||||
// PlayerMovement에서 직접 속도 가져오기
|
||||
float targetSpeed = playerMovement.CurrentMoveSpeed;
|
||||
|
||||
// 부드러운 속도 변화
|
||||
currentSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedVelocity, speedSmoothTime);
|
||||
|
||||
// 지면 접촉 상태
|
||||
bool isGrounded = characterController.isGrounded;
|
||||
|
||||
// 착지 감지 (공중에서 지면으로)
|
||||
if (!wasGrounded && isGrounded && isJumpingAnimation)
|
||||
{
|
||||
PlayLand();
|
||||
}
|
||||
|
||||
// 애니메이터 파라미터 설정
|
||||
animator.SetFloat(speedParam, currentSpeed);
|
||||
animator.SetBool(isGroundedParam, isGrounded);
|
||||
|
||||
wasGrounded = isGrounded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점프 애니메이션 트리거 (외부에서 호출)
|
||||
/// </summary>
|
||||
public void PlayJump()
|
||||
{
|
||||
if (IsOwner)
|
||||
{
|
||||
isJumpingAnimation = true;
|
||||
animator.SetTrigger(jumpTriggerParam);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 착지 애니메이션 트리거
|
||||
/// </summary>
|
||||
private void PlayLand()
|
||||
{
|
||||
isJumpingAnimation = false;
|
||||
animator.SetTrigger(landTriggerParam);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dafca28b13e62ee43893a43187dc3535
|
||||
162
Assets/_Game/Scripts/Player/PlayerCamera.cs
Normal file
162
Assets/_Game/Scripts/Player/PlayerCamera.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 3인칭 카메라 컨트롤러
|
||||
/// </summary>
|
||||
public class PlayerCamera : MonoBehaviour
|
||||
{
|
||||
[Header("Camera Settings")]
|
||||
[SerializeField] private float distance = 5f;
|
||||
[SerializeField] private float height = 2f;
|
||||
[SerializeField] private float rotationSpeed = 2f;
|
||||
[SerializeField] private float minPitch = -30f;
|
||||
[SerializeField] private float maxPitch = 60f;
|
||||
|
||||
private Transform target;
|
||||
private float yaw;
|
||||
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;
|
||||
if (cameraInstance == null)
|
||||
{
|
||||
var cameraObject = new GameObject("PlayerCamera");
|
||||
cameraInstance = cameraObject.AddComponent<Camera>();
|
||||
cameraObject.tag = "MainCamera";
|
||||
}
|
||||
|
||||
// 초기 각도
|
||||
if (target != null)
|
||||
{
|
||||
yaw = target.eulerAngles.y;
|
||||
}
|
||||
pitch = 20f;
|
||||
|
||||
// 카메라 위치를 즉시 타겟 위치로 초기화
|
||||
SnapToTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 관전 대상 변경
|
||||
/// </summary>
|
||||
public void SetTarget(Transform newTarget)
|
||||
{
|
||||
if (newTarget == null) return;
|
||||
|
||||
target = newTarget;
|
||||
isSpectating = true;
|
||||
|
||||
// 부드러운 전환을 위해 현재 카메라 위치에서 새 타겟으로
|
||||
yaw = target.eulerAngles.y;
|
||||
|
||||
Debug.Log($"[PlayerCamera] Now spectating: {target.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 원래 플레이어로 복귀
|
||||
/// </summary>
|
||||
public void ResetToPlayer(Transform playerTransform)
|
||||
{
|
||||
target = playerTransform;
|
||||
isSpectating = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라 위치를 타겟 위치로 즉시 이동 (부드러운 전환 없이)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라 참조 갱신 (씬 전환 후 호출)
|
||||
/// </summary>
|
||||
public void RefreshCamera()
|
||||
{
|
||||
// 씬 전환 시 항상 새 카메라 참조 획득
|
||||
cameraInstance = Camera.main;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// 카메라 참조가 없으면 갱신 시도
|
||||
if (cameraInstance == null)
|
||||
{
|
||||
RefreshCamera();
|
||||
}
|
||||
|
||||
if (target == null || cameraInstance == null) return;
|
||||
|
||||
HandleRotation();
|
||||
UpdateCameraPosition();
|
||||
}
|
||||
|
||||
private void HandleRotation()
|
||||
{
|
||||
if (inputActions == null) return;
|
||||
|
||||
// Input Actions에서 Look 입력 받기
|
||||
Vector2 lookInput = inputActions.Player.Look.ReadValue<Vector2>();
|
||||
float mouseX = lookInput.x * rotationSpeed * 0.1f;
|
||||
float mouseY = lookInput.y * rotationSpeed * 0.1f;
|
||||
|
||||
yaw += mouseX;
|
||||
pitch -= mouseY;
|
||||
pitch = Mathf.Clamp(pitch, minPitch, maxPitch);
|
||||
}
|
||||
|
||||
private void UpdateCameraPosition()
|
||||
{
|
||||
// 구면 좌표로 카메라 위치 계산
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Player/PlayerCamera.cs.meta
Normal file
2
Assets/_Game/Scripts/Player/PlayerCamera.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ffd5f6c47d39e94f92515c69e69a9a1
|
||||
393
Assets/_Game/Scripts/Player/PlayerMovement.cs
Normal file
393
Assets/_Game/Scripts/Player/PlayerMovement.cs
Normal file
@@ -0,0 +1,393 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Unity.Netcode;
|
||||
using Colosseum.Network;
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 3인칭 플레이어 이동 (네트워크 동기화)
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class PlayerMovement : NetworkBehaviour
|
||||
{
|
||||
[Header("Movement Settings")]
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
[SerializeField] private float rotationSpeed = 10f;
|
||||
[SerializeField] private float gravity = -9.81f;
|
||||
|
||||
[Header("Jump Settings")]
|
||||
[SerializeField] private float jumpForce = 5f;
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private SkillController skillController;
|
||||
[SerializeField] private Animator animator;
|
||||
|
||||
private CharacterController characterController;
|
||||
private Vector3 velocity;
|
||||
private Vector2 moveInput;
|
||||
private InputSystem_Actions inputActions;
|
||||
private bool isJumping;
|
||||
private bool wasGrounded;
|
||||
|
||||
// 적 충돌 방향 (이동 차단용)
|
||||
private Vector3 blockedDirection;
|
||||
private int enemyLayerMask;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 이동 속도 (애니메이션용)
|
||||
/// </summary>
|
||||
public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 현재 지면 접촉 상태
|
||||
/// </summary>
|
||||
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 점프 중 상태
|
||||
/// </summary>
|
||||
public bool IsJumping => isJumping;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// 로컬 플레이어가 아니면 입력 비활성화
|
||||
if (!IsOwner)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
characterController = GetComponent<CharacterController>();
|
||||
|
||||
// SkillController 참조
|
||||
if (skillController == null)
|
||||
{
|
||||
skillController = GetComponent<SkillController>();
|
||||
}
|
||||
|
||||
// Animator 참조
|
||||
if (animator == null)
|
||||
{
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
}
|
||||
|
||||
// 스폰 포인트에서 위치 설정
|
||||
SetSpawnPosition();
|
||||
|
||||
// Input Actions 초기화
|
||||
InitializeInputActions();
|
||||
|
||||
// 카메라 설정
|
||||
SetupCamera();
|
||||
|
||||
// 적 레이어 마스크 초기화
|
||||
enemyLayerMask = LayerMask.GetMask("Enemy");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입력 액션 초기화
|
||||
/// </summary>
|
||||
private void InitializeInputActions()
|
||||
{
|
||||
inputActions = new InputSystem_Actions();
|
||||
inputActions.Player.Enable();
|
||||
|
||||
// Move 액션 콜백 등록
|
||||
inputActions.Player.Move.performed += OnMovePerformed;
|
||||
inputActions.Player.Move.canceled += OnMoveCanceled;
|
||||
|
||||
// Jump 액션 콜백 등록
|
||||
inputActions.Player.Jump.performed += OnJumpPerformed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입력 액션 해제
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스폰 위치 설정
|
||||
/// </summary>
|
||||
private void SetSpawnPosition()
|
||||
{
|
||||
Transform spawnPoint = PlayerSpawnPoint.GetRandomSpawnPoint();
|
||||
|
||||
if (spawnPoint != null)
|
||||
{
|
||||
// CharacterController 비활성화 후 위치 설정 (충돌 문제 방지)
|
||||
characterController.enabled = false;
|
||||
transform.position = spawnPoint.position;
|
||||
transform.rotation = spawnPoint.rotation;
|
||||
characterController.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 네트워크 정리
|
||||
/// </summary>
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
CleanupInputActions();
|
||||
}
|
||||
|
||||
private void OnMovePerformed(InputAction.CallbackContext context)
|
||||
{
|
||||
moveInput = context.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
private void OnMoveCanceled(InputAction.CallbackContext context)
|
||||
{
|
||||
moveInput = Vector2.zero;
|
||||
}
|
||||
|
||||
private void OnJumpPerformed(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isJumping && characterController.isGrounded)
|
||||
{
|
||||
Jump();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupCamera()
|
||||
{
|
||||
var cameraController = GetComponent<PlayerCamera>();
|
||||
if (cameraController == null)
|
||||
{
|
||||
cameraController = gameObject.AddComponent<PlayerCamera>();
|
||||
}
|
||||
cameraController.Initialize(transform, inputActions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라 재설정 (씬 로드 후 호출)
|
||||
/// </summary>
|
||||
public void RefreshCamera()
|
||||
{
|
||||
SetupCamera();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
ApplyGravity();
|
||||
Move();
|
||||
}
|
||||
|
||||
private void ApplyGravity()
|
||||
{
|
||||
if (wasGrounded && velocity.y < 0)
|
||||
{
|
||||
velocity.y = -2f;
|
||||
}
|
||||
else
|
||||
{
|
||||
velocity.y += gravity * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
private void Move()
|
||||
{
|
||||
if (characterController == null) return;
|
||||
|
||||
// 스킬 애니메이션 재생 중에는 이동 불가 (루트 모션은 OnAnimatorMove에서 처리)
|
||||
if (skillController != null && skillController.IsPlayingAnimation)
|
||||
{
|
||||
// 루트 모션을 사용하지 않는 경우 중력만 적용
|
||||
if (!skillController.UsesRootMotion)
|
||||
{
|
||||
characterController.Move(velocity * Time.deltaTime);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 이동 방향 계산 (카메라 기준)
|
||||
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y);
|
||||
moveDirection = TransformDirectionByCamera(moveDirection);
|
||||
moveDirection.Normalize();
|
||||
|
||||
// 충돌 방향으로의 이동 차단 (미끄러짐 방지)
|
||||
if (blockedDirection != Vector3.zero)
|
||||
{
|
||||
float blockedAmount = Vector3.Dot(moveDirection, blockedDirection);
|
||||
if (blockedAmount > 0f)
|
||||
{
|
||||
moveDirection -= blockedDirection * blockedAmount;
|
||||
moveDirection.Normalize();
|
||||
}
|
||||
}
|
||||
|
||||
// 이동 적용
|
||||
Vector3 moveVector = moveDirection * moveSpeed * Time.deltaTime;
|
||||
characterController.Move(moveVector + velocity * Time.deltaTime);
|
||||
|
||||
// 충돌 방향 리셋
|
||||
blockedDirection = Vector3.zero;
|
||||
|
||||
// 회전 (이동 중일 때만)
|
||||
if (moveDirection != Vector3.zero)
|
||||
{
|
||||
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
// 착지 체크 (Move 후에 isGrounded가 업데이트됨)
|
||||
if (!wasGrounded && characterController.isGrounded && isJumping)
|
||||
{
|
||||
OnJumpEnd();
|
||||
}
|
||||
|
||||
// 다음 프레임을 위해 현재 상태 저장
|
||||
wasGrounded = characterController.isGrounded;
|
||||
}
|
||||
|
||||
private void Jump()
|
||||
{
|
||||
isJumping = true;
|
||||
velocity.y = jumpForce;
|
||||
|
||||
// 애니메이션 컨트롤러에 점프 알림
|
||||
var animController = GetComponent<PlayerAnimationController>();
|
||||
if (animController != null)
|
||||
{
|
||||
animController.PlayJump();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 점프 중 상태가 끝나면 IsJumping = false;
|
||||
/// </summary>
|
||||
public void OnJumpEnd()
|
||||
{
|
||||
isJumping = false;
|
||||
}
|
||||
|
||||
private Vector3 TransformDirectionByCamera(Vector3 direction)
|
||||
{
|
||||
if (Camera.main == null) return direction;
|
||||
|
||||
Transform cameraTransform = Camera.main.transform;
|
||||
Vector3 cameraForward = cameraTransform.forward;
|
||||
Vector3 cameraRight = cameraTransform.right;
|
||||
|
||||
// Y축 제거
|
||||
cameraForward.y = 0f;
|
||||
cameraRight.y = 0f;
|
||||
cameraForward.Normalize();
|
||||
cameraRight.Normalize();
|
||||
|
||||
return cameraRight * direction.x + cameraForward * direction.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루트 모션 처리. 스킬 애니메이션 중에 애니메이션의 이동/회전 데이터를 적용합니다.
|
||||
/// </summary>
|
||||
private void OnAnimatorMove()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
if (animator == null || characterController == null) return;
|
||||
if (skillController == null || !skillController.IsPlayingAnimation) return;
|
||||
if (!skillController.UsesRootMotion) return;
|
||||
|
||||
// 루트 모션 이동 적용
|
||||
Vector3 deltaPosition = animator.deltaPosition;
|
||||
|
||||
// Y축 무시 설정 시 중력 유지
|
||||
if (skillController.IgnoreRootMotionY)
|
||||
{
|
||||
deltaPosition.y = 0f;
|
||||
characterController.Move(deltaPosition + velocity * Time.deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
characterController.Move(deltaPosition);
|
||||
}
|
||||
|
||||
// 루트 모션 회전 적용
|
||||
if (animator.deltaRotation != Quaternion.identity)
|
||||
{
|
||||
transform.rotation *= animator.deltaRotation;
|
||||
}
|
||||
|
||||
// 착지 체크
|
||||
if (!wasGrounded && characterController.isGrounded && isJumping)
|
||||
{
|
||||
OnJumpEnd();
|
||||
}
|
||||
|
||||
wasGrounded = characterController.isGrounded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CharacterController 충돌 처리. 적과 충돌 시 해당 방향 이동을 차단합니다.
|
||||
/// 충돌 normal을 8방향으로 양자화하여 각진 충돌 느낌을 줍니다.
|
||||
/// </summary>
|
||||
private void OnControllerColliderHit(ControllerColliderHit hit)
|
||||
{
|
||||
// 적과의 충돌인지 확인
|
||||
if ((enemyLayerMask & (1 << hit.gameObject.layer)) != 0)
|
||||
{
|
||||
// 충돌 방향 저장 (이동 차단용)
|
||||
blockedDirection = hit.normal;
|
||||
blockedDirection.y = 0f;
|
||||
blockedDirection.Normalize();
|
||||
|
||||
// 8방향으로 양자화 (45도 간격)
|
||||
blockedDirection = QuantizeToOctagon(blockedDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향을 8각형(45도 간격) 방향으로 양자화합니다.
|
||||
/// </summary>
|
||||
private Vector3 QuantizeToOctagon(Vector3 direction)
|
||||
{
|
||||
if (direction == Vector3.zero)
|
||||
return direction;
|
||||
|
||||
// 각도 계산
|
||||
float angle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;
|
||||
|
||||
// 45도 단위로 반올림
|
||||
float snappedAngle = Mathf.Round(angle / 45f) * 45f;
|
||||
|
||||
// 다시 벡터로 변환
|
||||
float radians = snappedAngle * Mathf.Deg2Rad;
|
||||
return new Vector3(Mathf.Sin(radians), 0f, Mathf.Cos(radians));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Player/PlayerMovement.cs.meta
Normal file
2
Assets/_Game/Scripts/Player/PlayerMovement.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db1d9c2d6f86e254f9889e2fa9d41e31
|
||||
242
Assets/_Game/Scripts/Player/PlayerNetworkController.cs
Normal file
242
Assets/_Game/Scripts/Player/PlayerNetworkController.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Combat;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어 네트워크 상태 관리 (HP, MP 등)
|
||||
/// </summary>
|
||||
public class PlayerNetworkController : NetworkBehaviour, IDamageable
|
||||
{
|
||||
[Header("References")]
|
||||
[Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")]
|
||||
[SerializeField] private CharacterStats characterStats;
|
||||
|
||||
// 네트워크 동기화 변수
|
||||
private NetworkVariable<float> currentHealth = new NetworkVariable<float>(100f);
|
||||
private NetworkVariable<float> currentMana = new NetworkVariable<float>(50f);
|
||||
private NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);
|
||||
|
||||
public float Health => currentHealth.Value;
|
||||
public float Mana => currentMana.Value;
|
||||
public float MaxHealth => characterStats != null ? characterStats.MaxHealth : 100f;
|
||||
public float MaxMana => characterStats != null ? characterStats.MaxMana : 50f;
|
||||
public CharacterStats Stats => characterStats;
|
||||
|
||||
// 체력/마나 변경 이벤트
|
||||
public event Action<float, float> OnHealthChanged; // (oldValue, newValue)
|
||||
public event Action<float, float> OnManaChanged; // (oldValue, newValue)
|
||||
|
||||
// 사망 이벤트
|
||||
public event Action<PlayerNetworkController> OnDeath;
|
||||
public event Action<bool> OnDeathStateChanged; // (isDead)
|
||||
|
||||
// IDamageable 구현
|
||||
public float CurrentHealth => currentHealth.Value;
|
||||
public bool IsDead => isDead.Value;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// CharacterStats 참조 확인
|
||||
if (characterStats == null)
|
||||
{
|
||||
characterStats = GetComponent<CharacterStats>();
|
||||
}
|
||||
|
||||
// 네트워크 변수 변경 콜백 등록
|
||||
currentHealth.OnValueChanged += HandleHealthChanged;
|
||||
currentMana.OnValueChanged += HandleManaChanged;
|
||||
isDead.OnValueChanged += HandleDeathStateChanged;
|
||||
|
||||
// 초기화
|
||||
if (IsServer)
|
||||
{
|
||||
currentHealth.Value = MaxHealth;
|
||||
currentMana.Value = MaxMana;
|
||||
isDead.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
// 콜백 해제
|
||||
currentHealth.OnValueChanged -= HandleHealthChanged;
|
||||
currentMana.OnValueChanged -= HandleManaChanged;
|
||||
isDead.OnValueChanged -= HandleDeathStateChanged;
|
||||
}
|
||||
|
||||
private void HandleHealthChanged(float oldValue, float newValue)
|
||||
{
|
||||
OnHealthChanged?.Invoke(oldValue, newValue);
|
||||
}
|
||||
|
||||
private void HandleManaChanged(float oldValue, float newValue)
|
||||
{
|
||||
OnManaChanged?.Invoke(oldValue, newValue);
|
||||
}
|
||||
|
||||
private void HandleDeathStateChanged(bool oldValue, bool newValue)
|
||||
{
|
||||
OnDeathStateChanged?.Invoke(newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대미지 적용 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void TakeDamageRpc(float damage)
|
||||
{
|
||||
if (isDead.Value) return;
|
||||
|
||||
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage);
|
||||
|
||||
if (currentHealth.Value <= 0f)
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마나 소모 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void UseManaRpc(float amount)
|
||||
{
|
||||
currentMana.Value = Mathf.Max(0f, currentMana.Value - amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RestoreHealthRpc(float amount)
|
||||
{
|
||||
currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마나 회복 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RestoreManaRpc(float amount)
|
||||
{
|
||||
currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사망 애니메이션 재생 (모든 클라이언트에서 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Everyone)]
|
||||
private void PlayDeathAnimationRpc()
|
||||
{
|
||||
var animator = GetComponentInChildren<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Die");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사망 처리 (서버에서만 실행)
|
||||
/// </summary>
|
||||
private void HandleDeath()
|
||||
{
|
||||
if (isDead.Value) return;
|
||||
|
||||
isDead.Value = true;
|
||||
|
||||
// 이동 비활성화
|
||||
var movement = GetComponent<PlayerMovement>();
|
||||
if (movement != null)
|
||||
{
|
||||
movement.enabled = false;
|
||||
}
|
||||
|
||||
// 스킬 입력 비활성화
|
||||
var skillInput = GetComponent<PlayerSkillInput>();
|
||||
if (skillInput != null)
|
||||
{
|
||||
skillInput.enabled = false;
|
||||
}
|
||||
|
||||
// 모든 클라이언트에서 사망 애니메이션 재생
|
||||
PlayDeathAnimationRpc();
|
||||
|
||||
// 사망 이벤트 발생
|
||||
OnDeath?.Invoke(this);
|
||||
|
||||
Debug.Log($"[Player] Player {OwnerClientId} died!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리스폰 (서버에서만 실행)
|
||||
/// </summary>
|
||||
public void Respawn()
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
isDead.Value = false;
|
||||
currentHealth.Value = MaxHealth;
|
||||
currentMana.Value = MaxMana;
|
||||
|
||||
// 이동 재활성화
|
||||
var movement = GetComponent<PlayerMovement>();
|
||||
if (movement != null)
|
||||
{
|
||||
movement.enabled = true;
|
||||
}
|
||||
|
||||
// 스킬 입력 재활성화
|
||||
var skillInput = GetComponent<PlayerSkillInput>();
|
||||
if (skillInput != null)
|
||||
{
|
||||
skillInput.enabled = true;
|
||||
}
|
||||
|
||||
// 애니메이션 리셋
|
||||
var animator = GetComponentInChildren<Animator>();
|
||||
if (animator != null)
|
||||
{
|
||||
animator.Rebind();
|
||||
}
|
||||
|
||||
Debug.Log($"[Player] Player {OwnerClientId} respawned!");
|
||||
}
|
||||
|
||||
#region IDamageable
|
||||
/// <summary>
|
||||
/// 대미지 적용 (서버에서만 호출)
|
||||
/// </summary>
|
||||
public float TakeDamage(float damage, object source = null)
|
||||
{
|
||||
if (!IsServer || isDead.Value) return 0f;
|
||||
|
||||
float actualDamage = Mathf.Min(damage, currentHealth.Value);
|
||||
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage);
|
||||
|
||||
if (currentHealth.Value <= 0f)
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
|
||||
return actualDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복 (서버에서만 호출)
|
||||
/// </summary>
|
||||
public float Heal(float amount)
|
||||
{
|
||||
if (!IsServer || isDead.Value) return 0f;
|
||||
|
||||
float actualHeal = Mathf.Min(amount, MaxHealth - currentHealth.Value);
|
||||
currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount);
|
||||
|
||||
return actualHeal;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5b322d198fc60a41ae175ea9c58a337
|
||||
246
Assets/_Game/Scripts/Player/PlayerSkillInput.cs
Normal file
246
Assets/_Game/Scripts/Player/PlayerSkillInput.cs
Normal file
@@ -0,0 +1,246 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Unity.Netcode;
|
||||
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Weapons;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어 스킬 입력 처리.
|
||||
/// 논타겟 방식: 입력 시 즉시 스킬 시전
|
||||
/// </summary>
|
||||
public class PlayerSkillInput : NetworkBehaviour
|
||||
{
|
||||
[Header("Skill Slots")]
|
||||
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개)")]
|
||||
[SerializeField] private SkillData[] skillSlots = new SkillData[6];
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("SkillController (없으면 자동 검색)")]
|
||||
[SerializeField] private SkillController skillController;
|
||||
[Tooltip("PlayerNetworkController (없으면 자동 검색)")]
|
||||
[SerializeField] private PlayerNetworkController networkController;
|
||||
[Tooltip("WeaponEquipment (없으면 자동 검색)")]
|
||||
[SerializeField] private WeaponEquipment weaponEquipment;
|
||||
|
||||
private InputSystem_Actions inputActions;
|
||||
|
||||
public SkillData[] SkillSlots => skillSlots;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (!IsOwner)
|
||||
{
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// SkillController 참조 확인
|
||||
if (skillController == null)
|
||||
{
|
||||
skillController = GetComponent<SkillController>();
|
||||
if (skillController == null)
|
||||
{
|
||||
Debug.LogError("PlayerSkillInput: SkillController not found!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// PlayerNetworkController 참조 확인
|
||||
if (networkController == null)
|
||||
{
|
||||
networkController = GetComponent<PlayerNetworkController>();
|
||||
}
|
||||
|
||||
// WeaponEquipment 참조 확인
|
||||
if (weaponEquipment == null)
|
||||
{
|
||||
weaponEquipment = GetComponent<WeaponEquipment>();
|
||||
}
|
||||
|
||||
InitializeInputActions();
|
||||
}
|
||||
|
||||
private void InitializeInputActions()
|
||||
{
|
||||
inputActions = new InputSystem_Actions();
|
||||
inputActions.Player.Enable();
|
||||
|
||||
// 스킬 액션 콜백 등록
|
||||
inputActions.Player.Skill1.performed += _ => OnSkillInput(0);
|
||||
inputActions.Player.Skill2.performed += _ => OnSkillInput(1);
|
||||
inputActions.Player.Skill3.performed += _ => OnSkillInput(2);
|
||||
inputActions.Player.Skill4.performed += _ => OnSkillInput(3);
|
||||
inputActions.Player.Skill5.performed += _ => OnSkillInput(4);
|
||||
inputActions.Player.Skill6.performed += _ => OnSkillInput(5);
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (inputActions != null)
|
||||
{
|
||||
inputActions.Player.Skill1.performed -= _ => OnSkillInput(0);
|
||||
inputActions.Player.Skill2.performed -= _ => OnSkillInput(1);
|
||||
inputActions.Player.Skill3.performed -= _ => OnSkillInput(2);
|
||||
inputActions.Player.Skill4.performed -= _ => OnSkillInput(3);
|
||||
inputActions.Player.Skill5.performed -= _ => OnSkillInput(4);
|
||||
inputActions.Player.Skill6.performed -= _ => OnSkillInput(5);
|
||||
inputActions.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 입력 처리
|
||||
/// </summary>
|
||||
private void OnSkillInput(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
if (skill == null)
|
||||
{
|
||||
Debug.Log($"Skill slot {slotIndex + 1} is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// 사망 상태 체크
|
||||
if (networkController != null && networkController.IsDead)
|
||||
return;
|
||||
|
||||
// 로컬 체크 (빠른 피드백용)
|
||||
if (skillController.IsExecutingSkill)
|
||||
{
|
||||
Debug.Log($"Already executing skill");
|
||||
return;
|
||||
}
|
||||
|
||||
if (skillController.IsOnCooldown(skill))
|
||||
{
|
||||
Debug.Log($"Skill {skill.SkillName} is on cooldown");
|
||||
return;
|
||||
}
|
||||
|
||||
// 마나 비용 체크 (무기 배율 적용)
|
||||
float actualManaCost = GetActualManaCost(skill);
|
||||
if (networkController != null && networkController.Mana < actualManaCost)
|
||||
{
|
||||
Debug.Log($"Not enough mana for skill: {skill.SkillName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버에 스킬 실행 요청
|
||||
RequestSkillExecutionRpc(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서버에 스킬 실행 요청
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
private void RequestSkillExecutionRpc(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
if (skill == null) return;
|
||||
|
||||
// 서버에서 다시 검증
|
||||
// 사망 상태 체크
|
||||
if (networkController != null && networkController.IsDead)
|
||||
return;
|
||||
|
||||
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
||||
return;
|
||||
|
||||
// 마나 비용 체크 (무기 배율 적용)
|
||||
float actualManaCost = GetActualManaCost(skill);
|
||||
if (networkController != null && networkController.Mana < actualManaCost)
|
||||
return;
|
||||
|
||||
// 마나 소모 (무기 배율 적용)
|
||||
if (networkController != null && actualManaCost > 0)
|
||||
{
|
||||
networkController.UseManaRpc(actualManaCost);
|
||||
}
|
||||
|
||||
// 모든 클라이언트에 스킬 실행 전파
|
||||
BroadcastSkillExecutionRpc(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 클라이언트에 스킬 실행 전파
|
||||
/// </summary>
|
||||
[Rpc(SendTo.ClientsAndHost)]
|
||||
private void BroadcastSkillExecutionRpc(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
SkillData skill = skillSlots[slotIndex];
|
||||
if (skill == null) return;
|
||||
|
||||
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
|
||||
skillController.ExecuteSkill(skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 무기 마나 배율이 적용된 실제 마나 비용 계산
|
||||
/// </summary>
|
||||
private float GetActualManaCost(SkillData skill)
|
||||
{
|
||||
if (skill == null) return 0f;
|
||||
|
||||
float baseCost = skill.ManaCost;
|
||||
float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
|
||||
|
||||
return baseCost * multiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 슬롯 접근자
|
||||
/// </summary>
|
||||
public SkillData GetSkill(int slotIndex)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return null;
|
||||
return skillSlots[slotIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 슬롯 변경
|
||||
/// </summary>
|
||||
public void SetSkill(int slotIndex, SkillData skill)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
||||
return;
|
||||
|
||||
skillSlots[slotIndex] = skill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 남은 쿨타임 조회
|
||||
/// </summary>
|
||||
public float GetRemainingCooldown(int slotIndex)
|
||||
{
|
||||
SkillData skill = GetSkill(slotIndex);
|
||||
if (skill == null) return 0f;
|
||||
|
||||
return skillController.GetRemainingCooldown(skill);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 사용 가능 여부
|
||||
/// </summary>
|
||||
public bool CanUseSkill(int slotIndex)
|
||||
{
|
||||
SkillData skill = GetSkill(slotIndex);
|
||||
if (skill == null) return false;
|
||||
|
||||
return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Player/PlayerSkillInput.cs.meta
Normal file
2
Assets/_Game/Scripts/Player/PlayerSkillInput.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5b5422385dda854cbe61b01950f06da
|
||||
195
Assets/_Game/Scripts/Player/PlayerSpectator.cs
Normal file
195
Assets/_Game/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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Player/PlayerSpectator.cs.meta
Normal file
2
Assets/_Game/Scripts/Player/PlayerSpectator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ace7240dc9e5c834892fc1a0e4ea657e
|
||||
Reference in New Issue
Block a user