using System; using Unity.Netcode; using Unity.Netcode.Components; using UnityEngine; using UnityEngine.InputSystem; using Unity.Cinemachine; using Northbound; public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageable { [Header("Movement Settings")] public float moveSpeed = 5f; public float rotationSpeed = 10f; [Header("Team Settings")] [SerializeField] private TeamType initialTeam = TeamType.Player; [Header("Health Settings")] [SerializeField] private int maxHealth = 100; [Header("Visual Effects")] [SerializeField] private GameObject damageEffectPrefab; [SerializeField] private GameObject deathEffectPrefab; // 이 플레이어를 제어하는 클라이언트 ID (서버 소유권이지만 논리적 소유자) private NetworkVariable _ownerPlayerId = new NetworkVariable( ulong.MaxValue, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _team = new NetworkVariable( TeamType.Player, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _currentHealth = new NetworkVariable( 100, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private Vector2 _moveInput; private CharacterController _controller; private PlayerInputActions _inputActions; private Animator _animator; private NetworkAnimator _networkAnimator; // 이 플레이어가 로컬 플레이어인지 확인 public bool IsLocalPlayer => _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId; public ulong OwnerPlayerId => _ownerPlayerId.Value; // 소유자 변경 이벤트 public event Action OnOwnerChanged; void Awake() { _controller = GetComponent(); _animator = GetComponent(); _networkAnimator = GetComponent(); } public override void OnNetworkSpawn() { base.OnNetworkSpawn(); // 서버에서 초기화 if (IsServer) { if (_team.Value == TeamType.Neutral) { _team.Value = initialTeam; } if (_currentHealth.Value == 0) { _currentHealth.Value = maxHealth; } } // 이벤트 구독 _currentHealth.OnValueChanged += OnHealthChanged; _ownerPlayerId.OnValueChanged += OnOwnerPlayerIdChanged; // 이미 로컬 플레이어로 설정되어 있으면 입력 초기화 TryInitializeLocalPlayer(); } private void OnOwnerPlayerIdChanged(ulong previousValue, ulong newValue) { OnOwnerChanged?.Invoke(newValue); TryInitializeLocalPlayer(); } private void TryInitializeLocalPlayer() { if (!IsLocalPlayer) return; if (_inputActions != null) return; // 이미 초기화됨 var vcam = GameObject.FindFirstObjectByType(); if (vcam != null) { vcam.Follow = transform; vcam.LookAt = transform; } _inputActions = new PlayerInputActions(); _inputActions.Enable(); } /// /// 플레이어 초기화 (서버에서 호출) /// public void Initialize(ulong ownerPlayerId) { if (!IsServer) return; _ownerPlayerId.Value = ownerPlayerId; } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthChanged; _ownerPlayerId.OnValueChanged -= OnOwnerPlayerIdChanged; if (IsLocalPlayer && _inputActions != null) { _inputActions.Disable(); } base.OnNetworkDespawn(); } void Update() { // 로컬 플레이어만 입력 처리 if (!IsLocalPlayer) return; // 입력 시스템이 초기화되지 않았으면 스킵 if (_inputActions == null) return; // 죽었으면 이동 불가 if (_currentHealth.Value <= 0) return; // 액션/상호작용 중이면 이동 불가 var attackAction = GetComponent(); var playerInteraction = GetComponent(); bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) || (playerInteraction != null && playerInteraction.IsInteracting); if (isActionBlocked) { // 서버에 이동 중지 알림 (애니메이션 포함) MoveServerRpc(Vector2.zero); return; } _moveInput = _inputActions.Player.Move.ReadValue(); // 서버에 이동 요청 전송 (애니메이션 포함) MoveServerRpc(_moveInput); } [Rpc(SendTo.Server)] private void MoveServerRpc(Vector2 moveInput) { // 죽었으면 이동 불가 if (_currentHealth.Value <= 0) return; Vector3 move = new Vector3(moveInput.x, 0, moveInput.y).normalized; // NetworkAnimator로 애니메이션 동기화 if (_networkAnimator != null) { _networkAnimator.Animator.SetFloat("MoveSpeed", move.magnitude); } if (move.magnitude >= 0.1f) { Quaternion targetRotation = Quaternion.LookRotation(move); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); if (_controller != null) { _controller.Move(move * moveSpeed * Time.deltaTime); } } } #region ITeamMember Implementation public TeamType GetTeam() => _team.Value; public void SetTeam(TeamType team) { if (!IsServer) return; TeamType previousTeam = _team.Value; _team.Value = team; } #endregion #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsServer) return; // 이미 죽었으면 무시 if (_currentHealth.Value <= 0) return; // 공격자의 팀 확인 if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj)) { var attackerTeamMember = attackerObj.GetComponent(); if (attackerTeamMember != null) { if (!TeamManager.CanAttack(attackerTeamMember, this)) { return; } } } // 데미지 적용 int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; // 데미지 이펙트 ShowDamageEffectClientRpc(); // 체력이 0이 되면 사망 if (_currentHealth.Value <= 0) { Die(attackerId); } } private void Die(ulong killerId) { if (!IsServer) return; // 사망 이펙트 ShowDeathEffectClientRpc(); // 애니메이션 - NetworkAnimator로 동기화 if (_networkAnimator != null) { _networkAnimator.SetTrigger("Die"); } // 일정 시간 후 리스폰 또는 디스폰 Invoke(nameof(HandleDeath), 3f); } private void HandleDeath() { if (!IsServer) return; Respawn(); } private void Respawn() { if (!IsServer) return; // 체력 회복 _currentHealth.Value = maxHealth; // 스폰 포인트로 이동 var spawnPoints = FindObjectsByType(FindObjectsSortMode.None); if (spawnPoints.Length > 0) { var spawnPoint = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)]; transform.position = spawnPoint.transform.position; transform.rotation = spawnPoint.transform.rotation; } // NetworkAnimator로 애니메이션 동기화 if (_networkAnimator != null) { _networkAnimator.SetTrigger("Revive"); } } [ClientRpc] private void ShowDamageEffectClientRpc() { if (damageEffectPrefab != null) { GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up, Quaternion.identity); Destroy(effect, 2f); } } [ClientRpc] private void ShowDeathEffectClientRpc() { if (deathEffectPrefab != null) { GameObject effect = Instantiate(deathEffectPrefab, transform.position, Quaternion.identity); Destroy(effect, 3f); } } #endregion #region Health Management public int GetCurrentHealth() => _currentHealth.Value; public int GetMaxHealth() => maxHealth; public float GetHealthPercentage() { return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f; } public bool IsDead() => _currentHealth.Value <= 0; public void Heal(int amount) { if (!IsServer) return; int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value); _currentHealth.Value += healAmount; } private void OnHealthChanged(int previousValue, int newValue) { if (IsLocalPlayer) { // UI 업데이트 등 } } #endregion #region Gizmos private void OnDrawGizmosSelected() { #if UNITY_EDITOR if (Application.isPlaying) { string teamName = TeamManager.GetTeamName(_team.Value); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"Player: {gameObject.name}\nTeam: {teamName}\nHP: {_currentHealth.Value}/{maxHealth}"); } else { string teamName = TeamManager.GetTeamName(initialTeam); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"Player: {gameObject.name}\nTeam: {teamName}\nHP: {maxHealth}/{maxHealth}"); } #endif } #endregion }