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, IHealthProvider { [Header("Movement Settings")] public float rotationSpeed = 10f; [Header("Team Settings")] [SerializeField] private TeamType initialTeam = TeamType.Player; [Header("Visual Effects")] [SerializeField] private GameObject damageEffectPrefab; [SerializeField] private GameObject deathEffectPrefab; [Header("Death Settings")] [SerializeField] private GameObject resourcePickupPrefab; // 자원 드랍 프리팹 [SerializeField] private float respawnDelay = 10f; // 부활 대기 시간 (초) [Header("Health Bar")] [SerializeField] private bool showHealthBar = true; [SerializeField] private GameObject healthBarPrefab; // 이 플레이어를 제어하는 클라이언트 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; private PlayerStats _playerStats; private UnitHealthBar _healthBar; // 이 플레이어가 로컬 플레이어인지 확인 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(); _playerStats = GetComponent(); } public override void OnNetworkSpawn() { base.OnNetworkSpawn(); // 서버에서 초기화 if (IsServer) { if (_team.Value == TeamType.Neutral) { _team.Value = initialTeam; } if (_currentHealth.Value == 0) { _currentHealth.Value = GetMaxHealth(); } } // 이벤트 구독 _currentHealth.OnValueChanged += OnHealthChanged; _ownerPlayerId.OnValueChanged += OnOwnerPlayerIdChanged; // 체력바 생성 if (showHealthBar && healthBarPrefab != null) { CreateHealthBar(); } // 이미 로컬 플레이어로 설정되어 있으면 입력 초기화 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 * (_playerStats?.GetMoveSpeed() ?? 5f) * 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); } } /// /// 플레이어가 가진 자원을 ResourcePickup으로 드랍 /// private void DropPlayerResources() { if (!IsServer) return; // ServerResourceManager에서 플레이어의 자원 확인 var resourceManager = ServerResourceManager.Instance; if (resourceManager == null || resourcePickupPrefab == null) return; ulong playerId = _ownerPlayerId.Value; int resourceAmount = resourceManager.GetPlayerResourceAmount(playerId); if (resourceAmount > 0) { // 자원을 0으로 설정 (드랍 후 소멸) resourceManager.RemoveResource(playerId, resourceAmount); // 플레이어의 UI 업데이트 요청 var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; foreach (var kvp in spawnedObjects) { var controller = kvp.Value.GetComponent(); if (controller != null && controller.OwnerPlayerId == playerId) { var inventory = kvp.Value.GetComponent(); if (inventory != null) { inventory.RequestResourceUpdateServerRpc(playerId); break; } } } // ResourcePickup 프리팹 생성 GameObject pickupObj = Instantiate(resourcePickupPrefab, transform.position + Vector3.up, Quaternion.identity); NetworkObject pickupNetworkObj = pickupObj.GetComponent(); ResourcePickup pickup = pickupObj.GetComponent(); if (pickupNetworkObj != null && pickup != null) { // 드랍된 자원량 설정 pickup.resourceAmount = resourceAmount; pickup.resourceName = "Dropped Resource"; // NetworkObject로 스폰 pickupNetworkObj.Spawn(); } else { Debug.LogError("[NetworkPlayerController] ResourcePickup 프리팹이 올바르지 않습니다."); Destroy(pickupObj); } } } private void Die(ulong killerId) { if (!IsServer) return; // 플레이어가 가진 자원을 ResourcePickup으로 드랍 DropPlayerResources(); // 사망 이펙트 ShowDeathEffectClientRpc(); // 애니메이션 - NetworkAnimator로 동기화 if (_networkAnimator != null) { _networkAnimator.SetTrigger("Die"); } // 사망 즉시 플레이어를 숨김 (크립의 감지 방지) HidePlayerClientRpc(); // 일정 시간 후 리스폰 Invoke(nameof(HandleDeath), respawnDelay); } /// /// 사망 시 플레이어 숨김 /// [ClientRpc] private void HidePlayerClientRpc() { // CharacterController 비활성화 (이동 및 충돌 방지) if (_controller != null) { _controller.enabled = false; } // 모든 Renderer 비활성화 (시각적으로 숨김) Renderer[] renderers = GetComponentsInChildren(); foreach (Renderer renderer in renderers) { renderer.enabled = false; } } /// /// 리스폰 시 플레이어 다시 보임 /// [ClientRpc] private void ShowPlayerClientRpc() { // CharacterController 활성화 if (_controller != null) { _controller.enabled = true; } // 모든 Renderer 활성화 Renderer[] renderers = GetComponentsInChildren(); foreach (Renderer renderer in renderers) { renderer.enabled = true; } } private void HandleDeath() { if (!IsServer) return; Respawn(); } private void Respawn() { if (!IsServer) return; // 체력 회복 _currentHealth.Value = GetMaxHealth(); // 스폰 포인트로 이동 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"); } // 플레이어 다시 보임 (이동 후 실행) ShowPlayerClientRpc(); } [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() => _playerStats?.GetMaxHp() ?? 100; public float GetHealthPercentage() { int max = GetMaxHealth(); return max > 0 ? (float)_currentHealth.Value / max : 0f; } public bool IsDead() => _currentHealth.Value <= 0; public void Heal(int amount) { if (!IsServer) return; int healAmount = Mathf.Min(amount, GetMaxHealth() - _currentHealth.Value); _currentHealth.Value += healAmount; } private void OnHealthChanged(int previousValue, int newValue) { // 체력바 업데이트 if (_healthBar != null) { _healthBar.UpdateHealth(); } if (IsLocalPlayer) { // UI 업데이트 등 } } #endregion #region Health Bar private void CreateHealthBar() { if (_healthBar != null) return; if (healthBarPrefab == null) return; GameObject healthBarObj = Instantiate(healthBarPrefab, transform); _healthBar = healthBarObj.GetComponent(); if (_healthBar != null) { // 플레이어 체력바는 상시 표시 _healthBar.hideWhenFull = false; _healthBar.Initialize(this); } } #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}/{GetMaxHealth()}"); } else { string teamName = TeamManager.GetTeamName(initialTeam); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"Player: {gameObject.name}\nTeam: {teamName}"); } #endif } #endregion }