Files
Northbound/Assets/Scripts/NetworkPlayerController.cs

325 lines
8.6 KiB
C#

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;
[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();
// 서버에서 초기화
if (IsServer)
{
if (_team.Value == TeamType.Neutral)
{
_team.Value = initialTeam;
}
if (_currentHealth.Value == 0)
{
_currentHealth.Value = maxHealth;
}
}
// 체력 변경 이벤트 구독
_currentHealth.OnValueChanged += OnHealthChanged;
if (!IsOwner) return;
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>();
if (vcam != null)
{
vcam.Follow = transform;
vcam.LookAt = transform;
}
_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<AttackAction>();
var playerInteraction = GetComponent<PlayerInteraction>();
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<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 (!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<ITeamMember>();
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();
// 애니메이션 (있는 경우)
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<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;
}
_animator.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
/// <summary>
/// 현재 체력
/// </summary>
public int GetCurrentHealth() => _currentHealth.Value;
/// <summary>
/// 최대 체력
/// </summary>
public int GetMaxHealth() => maxHealth;
/// <summary>
/// 체력 비율 (0.0 ~ 1.0)
/// </summary>
public float GetHealthPercentage()
{
return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
}
/// <summary>
/// 죽었는지 여부
/// </summary>
public bool IsDead() => _currentHealth.Value <= 0;
/// <summary>
/// 체력 회복
/// </summary>
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)
{
// 체력바 UI 업데이트 또는 체력 변경 시각 효과
// 클라이언트에서도 체력 변경 인지 가능
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
}