using Unity.Netcode; 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; [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(); // 서버에서 초기화 if (IsServer) { if (_team.Value == TeamType.Neutral) { _team.Value = initialTeam; } if (_currentHealth.Value == 0) { _currentHealth.Value = maxHealth; } Debug.Log($"[Player] {gameObject.name} 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})"); } // 체력 변경 이벤트 구독 _currentHealth.OnValueChanged += OnHealthChanged; if (!IsOwner) return; var vcam = GameObject.FindFirstObjectByType(); if (vcam != null) { vcam.Follow = transform; vcam.LookAt = transform; Debug.Log("[Camera] 로컬 플레이어에게 카메라가 연결되었습니다."); } _inputActions = new PlayerInputActions(); _inputActions.Enable(); } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthChanged; if (IsOwner && _inputActions != null) { _inputActions.Disable(); } 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) { // 이동 불가 시 애니메이션 속도를 0으로 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 (!IsServer) return; 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 (!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)) { 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(); // 체력이 0이 되면 사망 if (_currentHealth.Value <= 0) { Die(attackerId); } } private void Die(ulong killerId) { if (!IsServer) return; Debug.Log($"[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})"); // 사망 이펙트 ShowDeathEffectClientRpc(); // 애니메이션 (있는 경우) if (_animator != null) { _animator.SetTrigger("Die"); } // 일정 시간 후 리스폰 또는 디스폰 Invoke(nameof(HandleDeath), 3f); } private void HandleDeath() { if (!IsServer) return; // 여기서 리스폰 로직을 추가하거나 게임 오버 처리 // 예: 리스폰 위치로 이동 및 체력 회복 Respawn(); } private void Respawn() { if (!IsServer) return; // 체력 회복 _currentHealth.Value = maxHealth; // 스폰 포인트로 이동 (PlayerSpawnPoint 활용) 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; /// /// 체력 비율 (0.0 ~ 1.0) /// 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; Debug.Log($"[Player] {gameObject.name}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{maxHealth}"); } private void OnHealthChanged(int previousValue, int newValue) { // 체력바 UI 업데이트 또는 체력 변경 시각 효과 Debug.Log($"[Player] 체력 변경: {previousValue} → {newValue}"); // 클라이언트에서도 체력 변경 인지 가능 if (IsOwner) { // 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 }