654 lines
20 KiB
C#
654 lines
20 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace Northbound
|
|
{
|
|
public enum WorkerState
|
|
{
|
|
Idle = 0,
|
|
Following = 1,
|
|
Mining = 2,
|
|
Returning = 3
|
|
}
|
|
|
|
[RequireComponent(typeof(Collider))]
|
|
public class Worker : NetworkBehaviour, IInteractable, ITeamMember
|
|
{
|
|
[Header("Worker Settings")]
|
|
public int maxBagCapacity = 50;
|
|
public float miningSpeed = 5f;
|
|
public float followDistance = 3f;
|
|
public float movementSpeed = 3.5f;
|
|
public int resourcesPerMining = 5;
|
|
public int recruitmentCost = 10;
|
|
|
|
[Header("Interaction")]
|
|
public string interactionAnimationTrigger = "Recruit";
|
|
|
|
[Header("Visual")]
|
|
public GameObject miningEffectPrefab;
|
|
public GameObject depositEffectPrefab;
|
|
public Transform effectSpawnPoint;
|
|
|
|
[Header("Team")]
|
|
public TeamType teamType = TeamType.Player;
|
|
|
|
private NetworkVariable<int> _currentBagResources = new NetworkVariable<int>(
|
|
0,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<ulong> _ownerPlayerId = new NetworkVariable<ulong>(
|
|
ulong.MaxValue,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<WorkerState> _currentState = new NetworkVariable<WorkerState>(
|
|
WorkerState.Idle,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private NetworkVariable<ulong> _targetResourceId = new NetworkVariable<ulong>(
|
|
ulong.MaxValue,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
private Resource _targetResource;
|
|
private Transform _playerTransform;
|
|
private Core _core;
|
|
private float _lastMiningTime;
|
|
private Rigidbody _rb;
|
|
private Collider _collider;
|
|
private Animator _animator;
|
|
|
|
[Header("Animation")]
|
|
public string moveAnimation = "Walk";
|
|
public string idleAnimation = "Idle";
|
|
public string mineAnimation = "Mine";
|
|
public bool useAnimations = true;
|
|
public bool playAnimationsByName = false; // true = play by name, false = use bools
|
|
|
|
public int CurrentBagResources => _currentBagResources.Value;
|
|
public WorkerState CurrentState => _currentState.Value;
|
|
public bool IsBagFull => _currentBagResources.Value >= maxBagCapacity;
|
|
public ulong OwnerPlayerId => _ownerPlayerId.Value;
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
base.OnNetworkSpawn();
|
|
|
|
_rb = GetComponent<Rigidbody>();
|
|
_collider = GetComponent<Collider>();
|
|
_animator = GetComponent<Animator>();
|
|
|
|
if (IsServer)
|
|
{
|
|
_rb.constraints = RigidbodyConstraints.FreezeRotation;
|
|
FindCore();
|
|
}
|
|
|
|
_currentState.OnValueChanged += OnStateChanged;
|
|
_ownerPlayerId.OnValueChanged += OnOwnerChanged;
|
|
_targetResourceId.OnValueChanged += OnTargetResourceChanged;
|
|
|
|
PlayIdleAnimation();
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
_currentState.OnValueChanged -= OnStateChanged;
|
|
_ownerPlayerId.OnValueChanged -= OnOwnerChanged;
|
|
_targetResourceId.OnValueChanged -= OnTargetResourceChanged;
|
|
base.OnNetworkDespawn();
|
|
}
|
|
|
|
private void FindCore()
|
|
{
|
|
Core[] cores = FindObjectsOfType<Core>();
|
|
if (cores.Length > 0)
|
|
{
|
|
_core = cores[0];
|
|
Debug.Log($"[Worker] 코어 찾음: {_core.name}");
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsServer) return;
|
|
|
|
if (_playerTransform == null && _ownerPlayerId.Value != ulong.MaxValue)
|
|
{
|
|
UpdatePlayerTransform();
|
|
}
|
|
|
|
switch (_currentState.Value)
|
|
{
|
|
case WorkerState.Following:
|
|
HandleFollowing();
|
|
break;
|
|
case WorkerState.Mining:
|
|
HandleMining();
|
|
break;
|
|
case WorkerState.Returning:
|
|
HandleReturning();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HandleFollowing()
|
|
{
|
|
if (_ownerPlayerId.Value == ulong.MaxValue)
|
|
{
|
|
SetState(WorkerState.Idle);
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
if (_playerTransform == null)
|
|
{
|
|
UpdatePlayerTransform();
|
|
}
|
|
|
|
if (_playerTransform == null)
|
|
{
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
float distance = Vector3.Distance(transform.position, _playerTransform.position);
|
|
if (distance > followDistance)
|
|
{
|
|
MoveTowards(_playerTransform.position);
|
|
}
|
|
else
|
|
{
|
|
PlayIdleAnimation();
|
|
}
|
|
}
|
|
|
|
private void HandleMining()
|
|
{
|
|
if (_targetResource == null)
|
|
{
|
|
SetState(WorkerState.Following);
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
if (IsBagFull)
|
|
{
|
|
SetState(WorkerState.Returning);
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
float distance = GetDistanceToCollider(_targetResource.GetComponent<Collider>());
|
|
|
|
if (distance > 2f)
|
|
{
|
|
MoveTowards(_targetResource.transform.position);
|
|
}
|
|
else
|
|
{
|
|
PlayMineAnimation();
|
|
MineResource();
|
|
}
|
|
}
|
|
|
|
private void HandleReturning()
|
|
{
|
|
if (_core == null)
|
|
{
|
|
FindCore();
|
|
if (_core == null)
|
|
{
|
|
SetState(WorkerState.Idle);
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
}
|
|
|
|
float distance = GetDistanceToCollider(_core.GetComponent<Collider>());
|
|
|
|
if (distance > 2f)
|
|
{
|
|
MoveTowards(_core.transform.position);
|
|
}
|
|
else
|
|
{
|
|
PlayIdleAnimation();
|
|
DepositToCore();
|
|
}
|
|
}
|
|
|
|
private float GetDistanceToCollider(Collider targetCollider)
|
|
{
|
|
if (targetCollider == null)
|
|
return float.MaxValue;
|
|
|
|
Vector3 closestPoint = targetCollider.ClosestPoint(transform.position);
|
|
float distance = Vector3.Distance(transform.position, closestPoint);
|
|
|
|
return distance;
|
|
}
|
|
|
|
private void MoveTowards(Vector3 targetPosition)
|
|
{
|
|
float distance = Vector3.Distance(transform.position, targetPosition);
|
|
|
|
if (distance <= 0.1f)
|
|
{
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
Vector3 direction = (targetPosition - transform.position).normalized;
|
|
direction.y = 0;
|
|
|
|
float moveDistance = Mathf.Min(movementSpeed * Time.deltaTime, distance);
|
|
transform.position += direction * moveDistance;
|
|
|
|
transform.rotation = Quaternion.LookRotation(direction);
|
|
|
|
PlayMoveAnimation();
|
|
}
|
|
|
|
private void MineResource()
|
|
{
|
|
if (Time.time - _lastMiningTime < miningSpeed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_targetResource.CanWorkerMineResource(NetworkObject.NetworkObjectId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int availableSpace = maxBagCapacity - _currentBagResources.Value;
|
|
int mineAmount = Mathf.Min(resourcesPerMining, availableSpace);
|
|
|
|
if (mineAmount > 0)
|
|
{
|
|
_targetResource.TakeResourcesForWorker(mineAmount, NetworkObject.NetworkObjectId);
|
|
_currentBagResources.Value += mineAmount;
|
|
_lastMiningTime = Time.time;
|
|
|
|
ShowMiningEffectClientRpc();
|
|
|
|
if (IsBagFull)
|
|
{
|
|
SetState(WorkerState.Returning);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DepositToCore()
|
|
{
|
|
if (_currentBagResources.Value > 0 && _core != null)
|
|
{
|
|
_core.AddResource(_currentBagResources.Value);
|
|
_currentBagResources.Value = 0;
|
|
|
|
ShowDepositEffectClientRpc();
|
|
}
|
|
|
|
bool shouldReturnToMining = _targetResource != null && _targetResource.HasResourcesAvailable();
|
|
|
|
if (shouldReturnToMining)
|
|
{
|
|
SetState(WorkerState.Mining);
|
|
}
|
|
else
|
|
{
|
|
SetState(WorkerState.Following);
|
|
}
|
|
}
|
|
|
|
private void SetState(WorkerState newState)
|
|
{
|
|
if (IsServer)
|
|
{
|
|
_currentState.Value = newState;
|
|
}
|
|
}
|
|
|
|
#region IInteractable Implementation
|
|
|
|
public bool CanInteract(ulong playerId)
|
|
{
|
|
if (_ownerPlayerId.Value != ulong.MaxValue && _ownerPlayerId.Value != playerId)
|
|
return false;
|
|
|
|
if (_ownerPlayerId.Value == ulong.MaxValue)
|
|
{
|
|
var coreResourceManager = CoreResourceManager.Instance;
|
|
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Interact(ulong playerId)
|
|
{
|
|
if (!CanInteract(playerId)) return;
|
|
|
|
if (_ownerPlayerId.Value == ulong.MaxValue)
|
|
{
|
|
RecruitWorkerServerRpc(playerId, NetworkObject.NetworkObjectId);
|
|
}
|
|
else if (_ownerPlayerId.Value == playerId)
|
|
{
|
|
StopWorkerServerRpc();
|
|
}
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void StopWorkerServerRpc()
|
|
{
|
|
_targetResourceId.Value = ulong.MaxValue;
|
|
_targetResource = null;
|
|
SetState(WorkerState.Following);
|
|
}
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
private void RecruitWorkerServerRpc(ulong playerId, ulong workerNetObjectId)
|
|
{
|
|
var coreResourceManager = CoreResourceManager.Instance;
|
|
if (coreResourceManager == null)
|
|
{
|
|
Debug.LogWarning("[Worker] CoreResourceManager 인스턴스를 찾을 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
if (!coreResourceManager.CanAfford(recruitmentCost))
|
|
{
|
|
Debug.LogWarning($"[Worker] 코어 자원이 부족합니다. 필요: {recruitmentCost}");
|
|
return;
|
|
}
|
|
|
|
coreResourceManager.SpendResources(recruitmentCost);
|
|
_ownerPlayerId.Value = playerId;
|
|
SetState(WorkerState.Following);
|
|
UpdatePlayerTransform();
|
|
}
|
|
|
|
[Rpc(SendTo.ClientsAndHost)]
|
|
private void SetPlayerAssignedWorkerClientRpc(ulong playerId, ulong workerNetObjectId)
|
|
{
|
|
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
|
|
|
|
NetworkObject playerObj = null;
|
|
foreach (var kvp in spawnedObjects)
|
|
{
|
|
if (kvp.Value != null && kvp.Value.OwnerClientId == playerId)
|
|
{
|
|
playerObj = kvp.Value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (playerObj != null)
|
|
{
|
|
var playerInteraction = playerObj.GetComponent<PlayerInteraction>();
|
|
if (playerInteraction != null && playerInteraction.IsOwner)
|
|
{
|
|
if (spawnedObjects.TryGetValue(workerNetObjectId, out var workerObj))
|
|
{
|
|
var worker = workerObj.GetComponent<Worker>();
|
|
if (worker != null)
|
|
{
|
|
playerInteraction.assignedWorker = worker;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public string GetInteractionPrompt()
|
|
{
|
|
if (_ownerPlayerId.Value == ulong.MaxValue)
|
|
{
|
|
var coreResourceManager = CoreResourceManager.Instance;
|
|
if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost))
|
|
return $"자원 부족 (필요: {recruitmentCost})";
|
|
|
|
return $"[E] 워커 채용 - 비용: {recruitmentCost}";
|
|
}
|
|
else if (NetworkManager.Singleton != null && _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId)
|
|
{
|
|
string stateText = _currentState.Value switch
|
|
{
|
|
WorkerState.Following => "따라가는 중",
|
|
WorkerState.Mining => "채굴 중",
|
|
WorkerState.Returning => "반환 중",
|
|
_ => "대기 중"
|
|
};
|
|
return $"워커 (가방: {_currentBagResources.Value}/{maxBagCapacity}) - {stateText}";
|
|
}
|
|
return "다른 플레이어의 워커";
|
|
}
|
|
|
|
public string GetInteractionAnimation()
|
|
{
|
|
return interactionAnimationTrigger;
|
|
}
|
|
|
|
public EquipmentData GetEquipmentData()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public Transform GetTransform()
|
|
{
|
|
return transform;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ITeamMember Implementation
|
|
|
|
public TeamType GetTeam() => teamType;
|
|
|
|
public void SetTeam(TeamType team)
|
|
{
|
|
if (!IsServer) return;
|
|
teamType = team;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ClientRPC Effects
|
|
|
|
[Rpc(SendTo.ClientsAndHost)]
|
|
private void ShowMiningEffectClientRpc()
|
|
{
|
|
if (miningEffectPrefab != null && effectSpawnPoint != null)
|
|
{
|
|
GameObject effect = Instantiate(miningEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
|
|
Destroy(effect, 2f);
|
|
}
|
|
}
|
|
|
|
[Rpc(SendTo.ClientsAndHost)]
|
|
private void ShowDepositEffectClientRpc()
|
|
{
|
|
if (depositEffectPrefab != null && effectSpawnPoint != null)
|
|
{
|
|
GameObject effect = Instantiate(depositEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation);
|
|
Destroy(effect, 2f);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
private void OnStateChanged(WorkerState previousState, WorkerState newState)
|
|
{
|
|
if (!useAnimations || _animator == null) return;
|
|
|
|
switch (newState)
|
|
{
|
|
case WorkerState.Following:
|
|
PlayIdleAnimation();
|
|
break;
|
|
case WorkerState.Idle:
|
|
PlayIdleAnimation();
|
|
break;
|
|
case WorkerState.Mining:
|
|
PlayMoveAnimation();
|
|
break;
|
|
case WorkerState.Returning:
|
|
PlayMoveAnimation();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OnOwnerChanged(ulong previousOwner, ulong newOwner)
|
|
{
|
|
if (IsServer && newOwner != ulong.MaxValue)
|
|
{
|
|
UpdatePlayerTransform();
|
|
}
|
|
}
|
|
|
|
private bool UpdatePlayerTransform()
|
|
{
|
|
if (!IsServer || _ownerPlayerId.Value == ulong.MaxValue)
|
|
return false;
|
|
|
|
if (NetworkManager.Singleton != null &&
|
|
NetworkManager.Singleton.SpawnManager != null)
|
|
{
|
|
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
|
|
|
|
foreach (var kvp in spawnedObjects)
|
|
{
|
|
var networkObj = kvp.Value;
|
|
if (networkObj != null && networkObj.OwnerClientId == _ownerPlayerId.Value)
|
|
{
|
|
var playerInteraction = networkObj.GetComponent<PlayerInteraction>();
|
|
if (playerInteraction != null && networkObj.gameObject != null)
|
|
{
|
|
_playerTransform = networkObj.transform;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void OnTargetResourceChanged(ulong previousResource, ulong newResource)
|
|
{
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(newResource, out var resourceObj))
|
|
{
|
|
_targetResource = resourceObj.GetComponent<Resource>();
|
|
}
|
|
else
|
|
{
|
|
_targetResource = null;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
|
public void AssignMiningTargetServerRpc(ulong resourceId)
|
|
{
|
|
if (!NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(resourceId, out var resourceObj))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var resource = resourceObj.GetComponent<Resource>();
|
|
if (resource == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_targetResourceId.Value = resourceId;
|
|
_targetResource = resource;
|
|
|
|
if (_ownerPlayerId.Value != ulong.MaxValue)
|
|
{
|
|
SetState(WorkerState.Mining);
|
|
}
|
|
}
|
|
|
|
private void PlayIdleAnimation()
|
|
{
|
|
if (!useAnimations || _animator == null) return;
|
|
|
|
if (playAnimationsByName)
|
|
{
|
|
_animator.Play(idleAnimation);
|
|
}
|
|
else
|
|
{
|
|
_animator.SetBool(moveAnimation, false);
|
|
_animator.SetBool(mineAnimation, false);
|
|
_animator.SetBool(idleAnimation, true);
|
|
}
|
|
}
|
|
|
|
private void PlayMoveAnimation()
|
|
{
|
|
if (!useAnimations || _animator == null) return;
|
|
|
|
if (playAnimationsByName)
|
|
{
|
|
_animator.Play(moveAnimation);
|
|
}
|
|
else
|
|
{
|
|
_animator.SetBool(idleAnimation, false);
|
|
_animator.SetBool(mineAnimation, false);
|
|
_animator.SetBool(moveAnimation, true);
|
|
}
|
|
}
|
|
|
|
private void PlayMineAnimation()
|
|
{
|
|
if (!useAnimations || _animator == null) return;
|
|
|
|
if (playAnimationsByName)
|
|
{
|
|
_animator.Play(mineAnimation);
|
|
}
|
|
else
|
|
{
|
|
_animator.SetBool(idleAnimation, false);
|
|
_animator.SetBool(moveAnimation, false);
|
|
_animator.SetBool(mineAnimation, true);
|
|
}
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
#if UNITY_EDITOR
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawWireSphere(transform.position, followDistance);
|
|
|
|
if (_currentState.Value == WorkerState.Mining && _targetResource != null)
|
|
{
|
|
Gizmos.color = Color.red;
|
|
Gizmos.DrawLine(transform.position, _targetResource.transform.position);
|
|
}
|
|
|
|
if (_currentState.Value == WorkerState.Returning && _core != null)
|
|
{
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawLine(transform.position, _core.transform.position);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|