using Unity.Netcode; using UnityEngine; using Unity.Cinemachine; namespace 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; [SerializeField] private bool showHealthBar = true; [Header("Visual Effects")] [SerializeField] private GameObject damageEffectPrefab; [SerializeField] private GameObject deathEffectPrefab; 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; void Awake() { _controller = GetComponent(); _animator = GetComponent(); } public override void OnNetworkSpawn() { base.OnNetworkSpawn(); Debug.Log($"[Player] {gameObject.name} spawned. OwnerId: {OwnerClientId}, LocalClientId: {NetworkManager.Singleton.LocalClientId}, IsOwner: {IsOwner}, IsServer: {IsServer}"); if (IsOwner) { SetSpawnPosition(); InitializePlayerServerRpc(initialTeam, maxHealth); } _currentHealth.OnValueChanged += OnHealthChanged; if (!IsOwner) return; var vcam = GameObject.FindFirstObjectByType(); if (vcam != null) { vcam.Follow = transform; vcam.LookAt = transform; Debug.Log("[Camera] Camera attached to local player."); } _inputActions = new PlayerInputActions(); _inputActions.Enable(); Debug.Log("[Player] Input actions enabled for local player."); } [ServerRpc] private void InitializePlayerServerRpc(TeamType team, int health) { if (_team.Value == TeamType.Neutral) _team.Value = team; if (_currentHealth.Value == 0) _currentHealth.Value = health; Debug.Log($"[Player] {gameObject.name} initialized (Team: {TeamManager.GetTeamName(_team.Value)}, HP: {_currentHealth.Value}/{maxHealth})"); } private void SetSpawnPosition() { if (PlayerSpawnPositionSetter.Instance == null) { Debug.LogWarning("[Player] PlayerSpawnPositionSetter not found. Using default spawn position."); return; } Vector3 spawnPos = PlayerSpawnPositionSetter.Instance.GetSpawnPosition(OwnerClientId); Quaternion spawnRot = PlayerSpawnPositionSetter.Instance.GetSpawnRotation(OwnerClientId); transform.position = spawnPos; transform.rotation = spawnRot; Debug.Log($"[Player] Spawn position set: {spawnPos}"); } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthChanged; if (IsOwner && _inputActions != null) { _inputActions.Disable(); _inputActions.Dispose(); } base.OnNetworkDespawn(); } void Update() { if (!IsOwner) return; if (_currentHealth.Value <= 0) return; var attackAction = GetComponent(); var playerInteraction = GetComponent(); bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) || (playerInteraction != null && playerInteraction.IsInteracting); if (isActionBlocked) { if (_animator != null) { _animator.SetFloat("MoveSpeed", 0f); } return; } _moveInput = _inputActions.Player.Move.ReadValue(); Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized; 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); } } if (_animator != null) { _animator.SetFloat("MoveSpeed", move.magnitude); } } #region ITeamMember Implementation public TeamType GetTeam() => _team.Value; public void SetTeam(TeamType team) { if (!IsOwner) return; SetTeamServerRpc(team); } [ServerRpc] private void SetTeamServerRpc(TeamType team) { TeamType previousTeam = _team.Value; _team.Value = team; Debug.Log($"[Player] 팀 변경: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}"); } #endregion #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsOwner) { TakeDamageServerRpc(damage, attackerId); return; } TakeDamageServerRpc(damage, attackerId); } [ServerRpc] private void TakeDamageServerRpc(int damage, ulong attackerId) { 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)) { Debug.Log($"[Player] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다."); return; } } } int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}"); ShowDamageEffectClientRpc(); if (_currentHealth.Value <= 0) { Die(attackerId); } } private void Die(ulong killerId) { Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})"); ShowDeathEffectClientRpc(); if (_animator != null) { _animator.SetTrigger("Die"); } Invoke(nameof(HandleDeath), 3f); } private void HandleDeath() { RespawnServerRpc(); } [ServerRpc] private void RespawnServerRpc() { _currentHealth.Value = maxHealth; var spawnPoints = FindObjectsByType(FindObjectsSortMode.None); if (spawnPoints.Length > 0) { var spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)]; transform.position = spawnPoint.transform.position; transform.rotation = spawnPoint.transform.rotation; } Debug.Log($"[Player] {gameObject.name} 리스폰!"); } [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 (!IsOwner) { HealServerRpc(amount); return; } HealServerRpc(amount); } [ServerRpc] private void HealServerRpc(int amount) { int healAmount = Mathf.Min(amount, maxHealth - _currentHealth.Value); _currentHealth.Value += healAmount; Debug.Log($"[Player] {gameObject.name}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{maxHealth}"); } private void OnHealthChanged(int previousValue, int newValue) { Debug.Log($"[Player] 체력 변경: {previousValue} → {newValue}"); } #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 } }