Files
Northbound/Assets/Scripts/Resource.cs
dal4segno 63a742d5d4 네트워크 환경에서의 비정상 동작 수정
클라이언트 접속 전에 스폰되어 있는 오브젝트의 경우, Ownership이 Distributable일 경우 클라이언트 접속 시점에 Ownership을 호스트로부터 분배받는다.
서버만 데이터를 수정해야 하는 환경이기 때문에 대부분 Distributable 대신 None을 사용하면 된다.
2026-02-17 01:53:06 +09:00

402 lines
12 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;
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
if (resourceManager.GetAvailableSpace(playerId) <= 0)
return false;
}
return true;
}
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.Owner)]
private void UpdatePlayerResourcesClientRpc(ulong playerId)
{
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId]?.PlayerObject;
if (playerObject == null)
return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
playerInventory?.RequestResourceUpdateServerRpc();
}
[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;
}
}
}