344 lines
11 KiB
C#
344 lines
11 KiB
C#
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<TeamType> _team = new NetworkVariable<TeamType>(
|
|
TeamType.Player,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
|
100,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private Vector2 _moveInput;
|
|
private CharacterController _controller;
|
|
private PlayerInputActions _inputActions;
|
|
private Animator _animator;
|
|
|
|
void Awake()
|
|
{
|
|
_controller = GetComponent<CharacterController>();
|
|
_animator = GetComponent<Animator>();
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
base.OnNetworkSpawn();
|
|
|
|
Debug.Log($"<color=cyan>[Player] {gameObject.name} spawned. OwnerId: {OwnerClientId}, LocalClientId: {NetworkManager.Singleton.LocalClientId}, IsOwner: {IsOwner}, IsServer: {IsServer}</color>");
|
|
|
|
if (IsOwner)
|
|
{
|
|
SetSpawnPosition();
|
|
InitializePlayerServerRpc(initialTeam, maxHealth);
|
|
}
|
|
|
|
_currentHealth.OnValueChanged += OnHealthChanged;
|
|
|
|
if (!IsOwner) return;
|
|
|
|
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>();
|
|
|
|
if (vcam != null)
|
|
{
|
|
vcam.Follow = transform;
|
|
vcam.LookAt = transform;
|
|
Debug.Log("<color=green>[Camera] Camera attached to local player.</color>");
|
|
}
|
|
|
|
_inputActions = new PlayerInputActions();
|
|
_inputActions.Enable();
|
|
Debug.Log("<color=green>[Player] Input actions enabled for local player.</color>");
|
|
}
|
|
|
|
[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($"<color=cyan>[Player] {gameObject.name} initialized (Team: {TeamManager.GetTeamName(_team.Value)}, HP: {_currentHealth.Value}/{maxHealth})</color>");
|
|
}
|
|
|
|
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($"<color=yellow>[Player] Spawn position set: {spawnPos}</color>");
|
|
}
|
|
|
|
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<AttackAction>();
|
|
var playerInteraction = GetComponent<PlayerInteraction>();
|
|
|
|
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<Vector2>();
|
|
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($"<color=cyan>[Player] 팀 변경: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(team)}</color>");
|
|
}
|
|
|
|
#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<ITeamMember>();
|
|
if (attackerTeamMember != null)
|
|
{
|
|
if (!TeamManager.CanAttack(attackerTeamMember, this))
|
|
{
|
|
Debug.Log($"<color=yellow>[Player] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀을 공격할 수 없습니다.</color>");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
|
_currentHealth.Value -= actualDamage;
|
|
|
|
Debug.Log($"<color=red>[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
|
|
|
ShowDamageEffectClientRpc();
|
|
|
|
if (_currentHealth.Value <= 0)
|
|
{
|
|
Die(attackerId);
|
|
}
|
|
}
|
|
|
|
private void Die(ulong killerId)
|
|
{
|
|
Debug.Log($"<color=red>[Player] {gameObject.name} ({TeamManager.GetTeamName(_team.Value)})이(가) 사망했습니다! (킬러: {killerId})</color>");
|
|
|
|
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<PlayerSpawnPoint>(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($"<color=green>[Player] {gameObject.name} 리스폰!</color>");
|
|
}
|
|
|
|
[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($"<color=green>[Player] {gameObject.name}이(가) {healAmount} 회복되었습니다. 현재 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
|
}
|
|
|
|
private void OnHealthChanged(int previousValue, int newValue)
|
|
{
|
|
Debug.Log($"<color=yellow>[Player] 체력 변경: {previousValue} → {newValue}</color>");
|
|
}
|
|
|
|
#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
|
|
}
|
|
}
|