Files
Northbound/Assets/Scripts/Resource.cs
dal4segno 4ffbbb0aff 모든 네트워크 오브젝트의 소유권을 서버가 갖도록 함
Distributable -> None
관련 사이드 이펙트로 인한 버그 수정
2026-02-18 02:18:42 +09:00

446 lines
14 KiB
C#

using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
namespace Northbound
{
/// <summary>
/// 상호작용 대상 - 자원 채집
/// </summary>
public class Resource : NetworkBehaviour, IInteractable
{
[Header("Resource Settings")]
public int maxResources = 100;
public int resourcesPerGathering = 10;
public float gatheringCooldown = 2f;
public string resourceName = "광석";
[Header("Resource Recharge")]
public float rechargeInterval = 5f; // 충전 주기 (초)
public int rechargeAmount = 10; // 주기당 충전량
[Header("Quality (Runtime)")]
[SerializeField] private NetworkVariable<float> _qualityPercentage = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
[Tooltip("품질 보정율 (-30% ~ +30%)")]
[SerializeField] private float _displayQuality = 0f;
[Tooltip("품질 적용 후 최대 자원량")]
[SerializeField] private int _displayMaxResources = 100;
[Tooltip("품질 적용 후 충전량")]
[SerializeField] private int _displayRechargeAmount = 10;
private bool _isQualityInitialized = false;
[Header("Animation")]
public string interactionAnimationTrigger = "Mining"; // 플레이어 애니메이션 트리거
[Header("Equipment")]
public EquipmentData equipmentData = new EquipmentData
{
socketName = "RightHand",
equipmentPrefab = null, // Inspector에서 곡괭이 프리팹 할당
attachOnStart = true,
detachOnEnd = true
};
[Header("Visual")]
public GameObject gatheringEffectPrefab;
public Transform effectSpawnPoint;
[Header("Worker Assignment")]
public bool allowWorkerAssignment = true;
[Header("Multi-worker")]
public bool allowMultipleWorkers = false; // 한 자원에 여러 워커 허용 여부
public bool HasResourcesAvailable()
{
return _currentResources.Value > 0;
}
public bool CanWorkerMineResource(ulong workerId)
{
if (!HasResourcesAvailable())
return false;
if (allowMultipleWorkers)
return true;
if (_currentWorkerId.Value == ulong.MaxValue)
return true;
return _currentWorkerId.Value == workerId;
}
public int TakeResourcesForWorker(int amount, ulong workerId)
{
if (!IsServer) return 0;
if (!allowMultipleWorkers)
{
if (_currentWorkerId.Value != ulong.MaxValue && _currentWorkerId.Value != workerId)
return 0;
}
int availableResources = _currentResources.Value;
int actualAmount = Mathf.Min(amount, availableResources);
if (actualAmount <= 0)
return 0;
_currentResources.Value -= actualAmount;
_lastGatheringTime = Time.time;
if (!allowMultipleWorkers && actualAmount > 0)
{
_currentWorkerId.Value = workerId;
}
if (_currentResources.Value <= 0 && !allowMultipleWorkers)
{
_currentWorkerId.Value = ulong.MaxValue;
}
ShowGatheringEffectClientRpc();
return actualAmount;
}
private NetworkVariable<int> _currentResources = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private NetworkVariable<ulong> _currentWorkerId = new NetworkVariable<ulong>(
ulong.MaxValue,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private float _lastGatheringTime;
private float _lastRechargeTime;
public float QualityPercentage => _qualityPercentage.Value;
public int ActualMaxResources
{
get
{
float multiplier = 1f + (_qualityPercentage.Value / 100f);
return Mathf.RoundToInt(maxResources * multiplier);
}
}
public int ActualRechargeAmount
{
get
{
float multiplier = 1f + (_qualityPercentage.Value / 100f);
return Mathf.RoundToInt(rechargeAmount * multiplier);
}
}
public override void OnNetworkSpawn()
{
_qualityPercentage.OnValueChanged += OnQualityChanged;
if (IsServer)
{
if (!_isQualityInitialized)
{
_qualityPercentage.Value = Random.Range(-30f, 30f);
}
_currentResources.Value = ActualMaxResources;
_lastRechargeTime = Time.time;
_lastGatheringTime = Time.time - gatheringCooldown;
}
_displayQuality = _qualityPercentage.Value;
UpdateDisplayValues();
}
public void InitializeQuality(float qualityPercentage)
{
if (IsServer)
{
_qualityPercentage.Value = qualityPercentage;
_displayQuality = qualityPercentage;
_isQualityInitialized = true;
}
else if (IsClient)
{
_displayQuality = qualityPercentage;
UpdateDisplayValues();
}
}
public override void OnNetworkDespawn()
{
_qualityPercentage.OnValueChanged -= OnQualityChanged;
}
private void OnQualityChanged(float previous, float current)
{
_displayQuality = current;
UpdateDisplayValues();
}
private void UpdateDisplayValues()
{
if (IsClient || IsServer)
{
_displayMaxResources = ActualMaxResources;
_displayRechargeAmount = ActualRechargeAmount;
}
else
{
_displayMaxResources = maxResources;
_displayRechargeAmount = rechargeAmount;
}
}
private void Update()
{
if (!IsServer)
return;
// 자원 충전 로직
if (Time.time - _lastRechargeTime >= rechargeInterval)
{
if (_currentResources.Value < ActualMaxResources)
{
int rechargeAmountToAdd = Mathf.Min(ActualRechargeAmount, ActualMaxResources - _currentResources.Value);
_currentResources.Value += rechargeAmountToAdd;
// Debug.Log($"{resourceName} {rechargeAmountToAdd} 충전됨. 현재: {_currentResources.Value}/{ActualMaxResources}");
}
_lastRechargeTime = Time.time;
}
}
public bool CanInteract(ulong playerId)
{
if (_currentResources.Value <= 0)
return false;
if (Time.time - _lastGatheringTime < gatheringCooldown)
return false;
// 플레이어의 자원 공간 확인
int availableSpace = GetPlayerAvailableSpace(playerId);
if (availableSpace <= 0)
return false;
return true;
}
/// <summary>
/// 플레이어의 자원 공간 확인 (서버와 클라이언트 모두 지원)
/// </summary>
private int GetPlayerAvailableSpace(ulong playerId)
{
// 서버에서는 ServerResourceManager 사용
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
return resourceManager.GetAvailableSpace(playerId);
}
// 클라이언트에서는 PlayerResourceInventory 사용
if (NetworkManager.Singleton != 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 inventory = kvp.Value.GetComponent<PlayerResourceInventory>();
if (inventory != null)
{
return inventory.MaxResourceCapacity - inventory.CurrentResourceAmount;
}
}
}
}
return 0;
}
public void Interact(ulong playerId)
{
AssignOrGatherResourceServerRpc(playerId, NetworkObject.NetworkObjectId);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void AssignOrGatherResourceServerRpc(ulong playerId, ulong resourceId)
{
if (!IsServer) return;
bool workerAssigned = false;
if (allowWorkerAssignment)
{
// Find worker owned by player in Following state (server-side, not client-side)
Worker assignedWorker = FindWorkerForPlayer(playerId);
if (assignedWorker != null)
{
if ((int)assignedWorker.CurrentState == 1) // 1 = Following
{
assignedWorker.AssignMiningTargetServerRpc(resourceId);
workerAssigned = true;
ShowGatheringEffectClientRpc();
}
}
}
if (!workerAssigned)
{
if (!CanInteract(playerId))
{
return;
}
GatherResource(playerId);
}
}
private void GatherResource(ulong playerId)
{
Debug.Log($"[Resource] GatherResource called - IsServer: {IsServer}, OwnerClientId: {OwnerClientId}, Current: {_currentResources.Value}");
if (!IsServer)
{
Debug.LogError($"[Resource] GatherResource called on CLIENT! This should not happen!");
return;
}
if (!CanInteract(playerId))
return;
var resourceManager = ServerResourceManager.Instance;
if (resourceManager == null)
return;
int playerAvailableSpace = resourceManager.GetAvailableSpace(playerId);
int gatheredAmount = Mathf.Min(
resourcesPerGathering,
_currentResources.Value,
playerAvailableSpace
);
if (gatheredAmount <= 0)
{
return;
}
_currentResources.Value -= gatheredAmount;
_lastGatheringTime = Time.time;
resourceManager.AddResource(playerId, gatheredAmount);
UpdatePlayerResourcesClientRpc(playerId);
ShowGatheringEffectClientRpc();
}
[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.NotServer)]
private void ShowGatheringEffectClientRpc()
{
if (gatheringEffectPrefab != null && effectSpawnPoint != null)
{
GameObject effect = Instantiate(gatheringEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
Destroy(effect, 2f);
}
}
public string GetInteractionPrompt()
{
if (_currentResources.Value <= 0)
return "Recharging...";
return $"[E] {resourceName} Mining ({_currentResources.Value}/{ActualMaxResources})";
}
public string GetInteractionAnimation()
{
return interactionAnimationTrigger;
}
public EquipmentData GetEquipmentData()
{
return equipmentData;
}
public Transform GetTransform()
{
return transform;
}
private Worker FindWorkerForPlayer(ulong playerId)
{
if (NetworkManager.Singleton == null || NetworkManager.Singleton.SpawnManager == null)
{
return null;
}
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
int workersFound = 0;
foreach (var kvp in spawnedObjects)
{
var networkObj = kvp.Value;
var worker = networkObj.GetComponent<Worker>();
if (worker != null)
{
workersFound++;
// Use worker's internal OwnerPlayerId, NOT NetworkObject.OwnerClientId!
if (worker.OwnerPlayerId == playerId && (int)worker.CurrentState == 1) // 1 = Following
{
return worker;
}
}
}
return null;
}
}
}