516 lines
15 KiB
C#
516 lines
15 KiB
C#
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<ulong> _ownerPlayerId = new NetworkVariable<ulong>(
|
|
ulong.MaxValue,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
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;
|
|
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<ulong> OnOwnerChanged;
|
|
|
|
void Awake()
|
|
{
|
|
_controller = GetComponent<CharacterController>();
|
|
_animator = GetComponent<Animator>();
|
|
_networkAnimator = GetComponent<NetworkAnimator>();
|
|
_playerStats = GetComponent<PlayerStats>();
|
|
}
|
|
|
|
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<CinemachineCamera>();
|
|
|
|
if (vcam != null)
|
|
{
|
|
vcam.Follow = transform;
|
|
vcam.LookAt = transform;
|
|
}
|
|
|
|
_inputActions = new PlayerInputActions();
|
|
_inputActions.Enable();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어 초기화 (서버에서 호출)
|
|
/// </summary>
|
|
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<AttackAction>();
|
|
var playerInteraction = GetComponent<PlayerInteraction>();
|
|
|
|
bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) ||
|
|
(playerInteraction != null && playerInteraction.IsInteracting);
|
|
|
|
if (isActionBlocked)
|
|
{
|
|
// 서버에 이동 중지 알림 (애니메이션 포함)
|
|
MoveServerRpc(Vector2.zero);
|
|
return;
|
|
}
|
|
|
|
_moveInput = _inputActions.Player.Move.ReadValue<Vector2>();
|
|
|
|
// 서버에 이동 요청 전송 (애니메이션 포함)
|
|
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<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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어가 가진 자원을 ResourcePickup으로 드랍
|
|
/// </summary>
|
|
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<NetworkPlayerController>();
|
|
if (controller != null && controller.OwnerPlayerId == playerId)
|
|
{
|
|
var inventory = kvp.Value.GetComponent<PlayerResourceInventory>();
|
|
if (inventory != null)
|
|
{
|
|
inventory.RequestResourceUpdateServerRpc(playerId);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ResourcePickup 프리팹 생성
|
|
GameObject pickupObj = Instantiate(resourcePickupPrefab, transform.position + Vector3.up, Quaternion.identity);
|
|
NetworkObject pickupNetworkObj = pickupObj.GetComponent<NetworkObject>();
|
|
ResourcePickup pickup = pickupObj.GetComponent<ResourcePickup>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사망 시 플레이어 숨김
|
|
/// </summary>
|
|
[ClientRpc]
|
|
private void HidePlayerClientRpc()
|
|
{
|
|
// CharacterController 비활성화 (이동 및 충돌 방지)
|
|
if (_controller != null)
|
|
{
|
|
_controller.enabled = false;
|
|
}
|
|
|
|
// 모든 Renderer 비활성화 (시각적으로 숨김)
|
|
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
|
foreach (Renderer renderer in renderers)
|
|
{
|
|
renderer.enabled = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 리스폰 시 플레이어 다시 보임
|
|
/// </summary>
|
|
[ClientRpc]
|
|
private void ShowPlayerClientRpc()
|
|
{
|
|
// CharacterController 활성화
|
|
if (_controller != null)
|
|
{
|
|
_controller.enabled = true;
|
|
}
|
|
|
|
// 모든 Renderer 활성화
|
|
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
|
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<PlayerSpawnPoint>(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<UnitHealthBar>();
|
|
|
|
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
|
|
}
|