Files
Northbound/Assets/Scripts/Core.cs
dal4segno 4beb2fb0bb 몬스터가 코어를 제대로 공격하지 못하는 문제 수정
navMesh는 다시 이전으로 되돌림
코어의 중심이 아닌, 코어의 표면을 기준으로 공격하도록 함
2026-02-19 17:04:46 +09:00

376 lines
12 KiB
C#

using Unity.Netcode;
using UnityEngine;
using UnityEngine.AI;
namespace Northbound
{
/// <summary>
/// 플레이어가 자원을 건내받아 게임의 전역 자원으로 관리하는 중앙 허브
/// </summary>
public class Core : NetworkBehaviour, IInteractable, IDamageable, ITeamMember
{
[Header("Core Settings")]
public int maxStorageCapacity = 1000; // 코어의 최대 저장 용량
public bool unlimitedStorage = false; // 무제한 저장소
[Header("Health")]
public int maxHealth = 1000;
public GameObject damageEffectPrefab;
public GameObject destroyEffectPrefab;
[Header("Deposit Settings")]
public bool depositAll = true; // true: 전부 건네기, false: 일부만 건네기
public int depositAmountPerInteraction = 10; // depositAll이 false일 때 한 번에 건네는 양
[Header("Animation")]
public string interactionAnimationTrigger = "Deposit"; // 플레이어 애니메이션 트리거
[Header("Equipment")]
public EquipmentData equipmentData = null; // 도구 필요 없음
[Header("Visual")]
public GameObject depositEffectPrefab;
public Transform effectSpawnPoint;
private NetworkVariable<int> _totalResources = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
public int TotalResources => _totalResources.Value;
public int MaxStorageCapacity => maxStorageCapacity;
public int CurrentHealth => _currentHealth.Value;
public int MaxHealth => maxHealth;
public override void OnNetworkSpawn()
{
if (IsServer)
{
_totalResources.Value = 0;
_currentHealth.Value = maxHealth;
}
_currentHealth.OnValueChanged += OnHealthChanged;
}
public override void OnNetworkDespawn()
{
_currentHealth.OnValueChanged -= OnHealthChanged;
}
private void OnHealthChanged(int previousValue, int newValue)
{
}
#region ITeamMember Implementation
public bool IsDead() => _currentHealth.Value <= 0;
public TeamType GetTeam()
{
return TeamType.Player; // 코어는 플레이어 팀
}
public void SetTeam(TeamType team)
{
// 코어의 팀은 변경할 수 없음 (항상 플레이어 팀)
Debug.LogWarning("[Core] 코어의 팀은 변경할 수 없습니다.");
}
#endregion
#region IDamageable Implementation
public void TakeDamage(int damage, ulong attackerId)
{
if (!IsServer) return;
if (_currentHealth.Value <= 0) return;
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
_currentHealth.Value -= actualDamage;
Debug.Log($"<color=red>[Core] 코어가 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
// 데미지 이펙트
ShowDamageEffectClientRpc();
// 체력이 0이 되면 게임 오버
if (_currentHealth.Value <= 0)
{
OnCoreDestroyed();
}
}
private void OnCoreDestroyed()
{
if (!IsServer) return;
// 파괴 이펙트
ShowDestroyEffectClientRpc();
// 게임 오버 로직 (추후 구현)
// GameManager.Instance?.OnGameOver();
}
[Rpc(SendTo.ClientsAndHost)]
private void ShowDamageEffectClientRpc()
{
if (damageEffectPrefab != null)
{
GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up * 2f, Quaternion.identity);
Destroy(effect, 2f);
}
}
[Rpc(SendTo.ClientsAndHost)]
private void ShowDestroyEffectClientRpc()
{
if (destroyEffectPrefab != null)
{
GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity);
Destroy(effect, 5f);
}
}
#endregion
#region Resource Management
/// <summary>
/// 자원을 소비할 수 있는지 확인
/// </summary>
public bool CanConsumeResource(int amount)
{
return _totalResources.Value >= amount;
}
/// <summary>
/// 자원 소비 (서버에서만)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void ConsumeResourceServerRpc(int amount)
{
if (!CanConsumeResource(amount))
{
Debug.LogWarning($"[Core] 자원이 부족합니다. 필요: {amount}, 보유: {_totalResources.Value}");
return;
}
int previousAmount = _totalResources.Value;
_totalResources.Value -= amount;
}
/// <summary>
/// 자원 추가 (서버에서만)
/// </summary>
public void AddResource(int amount)
{
if (!IsServer) return;
if (!unlimitedStorage)
{
int availableSpace = maxStorageCapacity - _totalResources.Value;
amount = Mathf.Min(amount, availableSpace);
}
if (amount > 0)
{
_totalResources.Value += amount;
}
}
#endregion
#region IInteractable Implementation
public bool CanInteract(ulong playerId)
{
// 저장소가 가득 찼는지 확인 (무제한이 아닐 때)
if (!unlimitedStorage && _totalResources.Value >= maxStorageCapacity)
return false;
// 플레이어가 자원을 가지고 있는지 확인
// NetworkPlayerController로 찾기 (서버 소유권으로 스폰한 경우 PlayerObject가 null일 수 있음)
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
foreach (var kvp in spawnedObjects)
{
var controller = kvp.Value.GetComponent<NetworkPlayerController>();
if (controller != null && controller.OwnerPlayerId == playerId)
{
var playerInventory = kvp.Value.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
return playerInventory.CurrentResourceAmount > 0;
}
}
}
return false;
}
public void Interact(ulong playerId)
{
if (!CanInteract(playerId))
return;
DepositResourceServerRpc(playerId);
}
public string GetInteractionPrompt()
{
if (unlimitedStorage)
{
return "[E] Deposit (No Limit)";
}
else
{
return $"[E] Deposit ({_totalResources.Value}/{maxStorageCapacity})";
}
}
public string GetInteractionAnimation()
{
return interactionAnimationTrigger;
}
public EquipmentData GetEquipmentData()
{
return equipmentData;
}
public Transform GetTransform()
{
return transform;
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void DepositResourceServerRpc(ulong playerId)
{
if (!CanInteract(playerId))
return;
var resourceManager = ServerResourceManager.Instance;
if (resourceManager == null)
{
Debug.LogWarning("ServerResourceManager 인스턴스를 찾을 수 없습니다.");
return;
}
int playerResourceAmount = resourceManager.GetPlayerResourceAmount(playerId);
if (playerResourceAmount <= 0)
{
return;
}
int depositAmount;
if (depositAll)
{
depositAmount = playerResourceAmount;
}
else
{
depositAmount = Mathf.Min(depositAmountPerInteraction, playerResourceAmount);
}
if (!unlimitedStorage)
{
int availableSpace = maxStorageCapacity - _totalResources.Value;
depositAmount = Mathf.Min(depositAmount, availableSpace);
}
if (depositAmount <= 0)
{
return;
}
resourceManager.RemoveResource(playerId, depositAmount);
UpdatePlayerResourcesClientRpc(playerId);
_totalResources.Value += depositAmount;
ShowDepositEffectClientRpc();
}
[Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId)
{
// 해당 플레이어만 업데이트
if (NetworkManager.Singleton.LocalClientId != playerId)
return;
// 로컬 플레이어의 PlayerResourceInventory 찾아서 업데이트 요청
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
foreach (var kvp in spawnedObjects)
{
var controller = kvp.Value.GetComponent<NetworkPlayerController>();
if (controller != null && controller.IsLocalPlayer)
{
var inventory = kvp.Value.GetComponent<PlayerResourceInventory>();
if (inventory != null)
{
inventory.RequestResourceUpdateServerRpc(playerId);
return;
}
}
}
}
[Rpc(SendTo.ClientsAndHost)]
private void ShowDepositEffectClientRpc()
{
if (depositEffectPrefab != null && effectSpawnPoint != null)
{
GameObject effect = Instantiate(depositEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
Destroy(effect, 2f);
}
}
#endregion
#region Navigation
/// <summary>
/// AI가 코어에 접근할 수 있는 NavMesh 위치 반환 (콜라이더 표면 근처)
/// </summary>
public Vector3 GetNavMeshPosition()
{
Collider coreCollider = GetComponent<Collider>();
if (coreCollider == null)
{
return transform.position;
}
// 1. 코어 콜라이더 표면에서 남쪽(앞쪽) 방향으로 검색 시작
// 적들이 보통 남쪽에서 오기 때문에 전면부 검색
Vector3 searchDirection = Vector3.back; // -Z 방향 (남쪽)
Vector3 searchCenter = coreCollider.ClosestPoint(transform.position + searchDirection * 5f);
// 2. 표면 방향으로 attackRange(약 2m)보다 조금 더 큰 거리에서 NavMesh 샘플링
NavMeshHit hit;
float maxDistance = 3f; // 콜라이더 표면에서 탐색할 거리
if (NavMesh.SamplePosition(searchCenter, out hit, maxDistance, NavMesh.AllAreas))
{
return hit.position;
}
// 3. 실패 시 코어 중심에서 더 넓게 탐색 (fallback)
if (NavMesh.SamplePosition(transform.position, out hit, 10f, NavMesh.AllAreas))
{
return hit.position;
}
Debug.LogWarning("[Core] 코어 주변에서 NavMesh를 찾을 수 없습니다.");
return transform.position;
}
#endregion
}
}