일꾼(Worker) 개발 및 Kaykit Adventurer 애셋 추가
코어 오른쪽 건물에서 상호작용 하면 Worker 생성 Worker와 상호작용하면 Worker가 플레이어를 따라옴 그 상태에서 자원과 상호작용하면 Worker를 자원에 배치할 수 있음.
This commit is contained in:
@@ -29,6 +29,9 @@ namespace Northbound
|
||||
[Header("Debug")]
|
||||
public bool showDebugRay = true;
|
||||
|
||||
[Header("Worker")]
|
||||
public Worker assignedWorker;
|
||||
|
||||
private PlayerInputActions _inputActions;
|
||||
private IInteractable _currentInteractable;
|
||||
private Camera _mainCamera;
|
||||
@@ -40,7 +43,6 @@ namespace Northbound
|
||||
private bool _isInteracting = false;
|
||||
private Coroutine _interactionTimeoutCoroutine;
|
||||
|
||||
// 다른 컴포넌트가 이동 차단 여부를 확인할 수 있도록 public 프로퍼티 제공
|
||||
public bool IsInteracting => _isInteracting;
|
||||
public float WorkPower => workPower;
|
||||
|
||||
@@ -124,14 +126,28 @@ namespace Northbound
|
||||
|
||||
if (_currentInteractable != null)
|
||||
{
|
||||
bool isResource = _currentInteractable is Resource;
|
||||
bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId;
|
||||
bool isWorkerFollowing = hasWorker && (int)assignedWorker.CurrentState == 1;
|
||||
bool shouldPlayAnimation = !isResource || !hasWorker || !isWorkerFollowing;
|
||||
|
||||
_isInteracting = true;
|
||||
_pendingEquipmentData = _currentInteractable.GetEquipmentData();
|
||||
|
||||
string animTrigger = _currentInteractable.GetInteractionAnimation();
|
||||
bool hasAnimation = !string.IsNullOrEmpty(animTrigger);
|
||||
|
||||
// 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
|
||||
if (!useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null)
|
||||
if (playAnimations && _animator != null && hasAnimation && shouldPlayAnimation)
|
||||
{
|
||||
_animator.SetTrigger(animTrigger);
|
||||
_interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f));
|
||||
}
|
||||
else
|
||||
{
|
||||
_isInteracting = false;
|
||||
}
|
||||
|
||||
if (shouldPlayAnimation && !useAnimationEvents && useEquipment && _equipmentSocket != null && _pendingEquipmentData != null)
|
||||
{
|
||||
if (_pendingEquipmentData.attachOnStart && _pendingEquipmentData.equipmentPrefab != null)
|
||||
{
|
||||
@@ -144,20 +160,6 @@ namespace Northbound
|
||||
}
|
||||
}
|
||||
|
||||
// 애니메이션 재생
|
||||
if (playAnimations && _animator != null && hasAnimation)
|
||||
{
|
||||
_animator.SetTrigger(animTrigger);
|
||||
// 애니메이션 완료를 보장하기 위해 타임아웃 코루틴 시작
|
||||
_interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 애니메이션이 없으면 즉시 상호작용 완료
|
||||
_isInteracting = false;
|
||||
}
|
||||
|
||||
// 상호작용 실행 (서버에서 처리)
|
||||
_currentInteractable.Interact(OwnerClientId);
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,6 @@ namespace Northbound
|
||||
yield return new WaitForSeconds(timeout);
|
||||
if (_isInteracting)
|
||||
{
|
||||
Debug.LogWarning("[PlayerInteraction] Interaction timeout - forcing completion");
|
||||
_isInteracting = false;
|
||||
_interactionTimeoutCoroutine = null;
|
||||
}
|
||||
@@ -268,7 +269,7 @@ namespace Northbound
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!IsOwner || _currentInteractable == null) return;
|
||||
if (!IsOwner) return;
|
||||
|
||||
GUIStyle style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
@@ -277,15 +278,50 @@ namespace Northbound
|
||||
};
|
||||
style.normal.textColor = Color.white;
|
||||
|
||||
string prompt = _currentInteractable.GetInteractionPrompt();
|
||||
|
||||
if (_isInteracting)
|
||||
string prompt = "";
|
||||
|
||||
if (_currentInteractable != null)
|
||||
{
|
||||
prompt += " (진행 중...)";
|
||||
style.normal.textColor = Color.yellow;
|
||||
prompt = _currentInteractable.GetInteractionPrompt();
|
||||
|
||||
bool isResource = _currentInteractable is Resource;
|
||||
if (isResource && assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId)
|
||||
{
|
||||
prompt += " (워커 할당 가능)";
|
||||
}
|
||||
|
||||
if (_isInteracting)
|
||||
{
|
||||
prompt += " (진행 중...)";
|
||||
style.normal.textColor = Color.yellow;
|
||||
}
|
||||
}
|
||||
|
||||
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 50), prompt, style);
|
||||
if (assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId)
|
||||
{
|
||||
string workerStatus = assignedWorker.CurrentState switch
|
||||
{
|
||||
WorkerState.Following => "따라가는 중",
|
||||
WorkerState.Mining => "채굴 중",
|
||||
WorkerState.Returning => "반환 중",
|
||||
_ => "대기 중"
|
||||
};
|
||||
|
||||
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 400, 50),
|
||||
$"워커: {workerStatus} (가방: {assignedWorker.CurrentBagResources}/{assignedWorker.maxBagCapacity})",
|
||||
style);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 150, 400, 50),
|
||||
$"워커: 미할당",
|
||||
style);
|
||||
}
|
||||
|
||||
if (_currentInteractable != null)
|
||||
{
|
||||
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 50), prompt, style);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
|
||||
@@ -34,12 +34,75 @@ namespace Northbound
|
||||
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 void TakeResourcesForWorker(int amount, ulong workerId)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (!allowMultipleWorkers)
|
||||
{
|
||||
if (_currentWorkerId.Value != ulong.MaxValue && _currentWorkerId.Value != workerId)
|
||||
return;
|
||||
}
|
||||
|
||||
int availableResources = _currentResources.Value;
|
||||
int actualAmount = Mathf.Min(amount, availableResources);
|
||||
|
||||
if (actualAmount <= 0)
|
||||
return;
|
||||
|
||||
_currentResources.Value -= actualAmount;
|
||||
_lastGatheringTime = Time.time;
|
||||
|
||||
if (!allowMultipleWorkers && actualAmount > 0)
|
||||
{
|
||||
_currentWorkerId.Value = workerId;
|
||||
}
|
||||
|
||||
if (_currentResources.Value <= 0 && !allowMultipleWorkers)
|
||||
{
|
||||
_currentWorkerId.Value = ulong.MaxValue;
|
||||
}
|
||||
|
||||
ShowGatheringEffectClientRpc();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -103,19 +166,51 @@ namespace Northbound
|
||||
|
||||
public void Interact(ulong playerId)
|
||||
{
|
||||
if (!CanInteract(playerId))
|
||||
return;
|
||||
|
||||
GatherResourceServerRpc(playerId);
|
||||
AssignOrGatherResourceServerRpc(playerId, NetworkObject.NetworkObjectId);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void GatherResourceServerRpc(ulong playerId)
|
||||
private void AssignOrGatherResourceServerRpc(ulong playerId, ulong resourceId)
|
||||
{
|
||||
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
|
||||
if (playerObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerInteraction = playerObject.GetComponent<PlayerInteraction>();
|
||||
|
||||
bool workerAssigned = false;
|
||||
|
||||
if (allowWorkerAssignment && playerInteraction != null && playerInteraction.assignedWorker != null)
|
||||
{
|
||||
var worker = playerInteraction.assignedWorker;
|
||||
|
||||
if (worker.OwnerPlayerId == playerId && (int)worker.CurrentState == 1)
|
||||
{
|
||||
worker.AssignMiningTargetServerRpc(resourceId);
|
||||
workerAssigned = true;
|
||||
|
||||
ShowGatheringEffectClientRpc();
|
||||
}
|
||||
}
|
||||
|
||||
if (!workerAssigned)
|
||||
{
|
||||
if (!CanInteract(playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GatherResource(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void GatherResource(ulong playerId)
|
||||
{
|
||||
if (!CanInteract(playerId))
|
||||
return;
|
||||
|
||||
// 플레이어의 인벤토리 확인
|
||||
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
|
||||
if (playerObject == null)
|
||||
return;
|
||||
@@ -123,14 +218,11 @@ namespace Northbound
|
||||
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
|
||||
if (playerInventory == null)
|
||||
{
|
||||
Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 플레이어가 받을 수 있는 최대량 계산
|
||||
int playerAvailableSpace = playerInventory.GetAvailableSpace();
|
||||
|
||||
// 자원 노드가 줄 수 있는 양과 플레이어가 받을 수 있는 양 중 작은 값 선택
|
||||
int gatheredAmount = Mathf.Min(
|
||||
resourcesPerGathering,
|
||||
_currentResources.Value,
|
||||
@@ -139,19 +231,14 @@ namespace Northbound
|
||||
|
||||
if (gatheredAmount <= 0)
|
||||
{
|
||||
Debug.Log($"플레이어 {playerId}의 인벤토리가 가득 찼습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 자원 차감
|
||||
_currentResources.Value -= gatheredAmount;
|
||||
_lastGatheringTime = Time.time;
|
||||
|
||||
// 플레이어에게 자원 추가
|
||||
playerInventory.AddResourceServerRpc(gatheredAmount);
|
||||
|
||||
Debug.Log($"플레이어 {playerId}가 {gatheredAmount} {resourceName}을(를) 채집했습니다. 남은 자원: {_currentResources.Value}");
|
||||
|
||||
ShowGatheringEffectClientRpc();
|
||||
}
|
||||
|
||||
|
||||
623
Assets/Scripts/Worker.cs
Normal file
623
Assets/Scripts/Worker.cs
Normal file
@@ -0,0 +1,623 @@
|
||||
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;
|
||||
|
||||
[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)
|
||||
{
|
||||
return _ownerPlayerId.Value == ulong.MaxValue || _ownerPlayerId.Value == playerId;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
return "[E] 워커 채용";
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (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
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Worker.cs.meta
Normal file
2
Assets/Scripts/Worker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8cb09d7a5404d34da082430986bd31c
|
||||
227
Assets/Scripts/WorkerSpawner.cs
Normal file
227
Assets/Scripts/WorkerSpawner.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class WorkerSpawner : NetworkBehaviour, IInteractable
|
||||
{
|
||||
[Header("Spawner Settings")]
|
||||
public GameObject workerPrefab;
|
||||
public Transform spawnPoint;
|
||||
public float spawnRadius = 2f;
|
||||
public int maxWorkers = 5;
|
||||
public float spawnCooldown = 5f;
|
||||
|
||||
[Header("Interaction")]
|
||||
public string interactionAnimationTrigger = "Build";
|
||||
public string interactionPrompt = "[E] 워커 생성";
|
||||
|
||||
[Header("Visual")]
|
||||
public GameObject spawnEffectPrefab;
|
||||
|
||||
private NetworkVariable<int> _workerCount = new NetworkVariable<int>(
|
||||
0,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server
|
||||
);
|
||||
|
||||
private float _lastSpawnTime;
|
||||
|
||||
public int WorkerCount => _workerCount.Value;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
_workerCount.OnValueChanged += OnWorkerCountChanged;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
_workerCount.OnValueChanged -= OnWorkerCountChanged;
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
|
||||
private void OnWorkerCountChanged(int previousValue, int newValue)
|
||||
{
|
||||
Debug.Log($"[WorkerSpawner] 워커 수 변경: {previousValue} → {newValue}");
|
||||
}
|
||||
|
||||
#region IInteractable Implementation
|
||||
|
||||
public bool CanInteract(ulong playerId)
|
||||
{
|
||||
if (_workerCount.Value >= maxWorkers)
|
||||
return false;
|
||||
|
||||
if (Time.time - _lastSpawnTime < spawnCooldown)
|
||||
return false;
|
||||
|
||||
if (workerPrefab == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Interact(ulong playerId)
|
||||
{
|
||||
if (!CanInteract(playerId))
|
||||
return;
|
||||
|
||||
SpawnWorkerServerRpc(playerId);
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void SpawnWorkerServerRpc(ulong playerId)
|
||||
{
|
||||
if (!CanInteract(playerId))
|
||||
return;
|
||||
|
||||
if (_workerCount.Value >= maxWorkers)
|
||||
{
|
||||
Debug.LogWarning("[WorkerSpawner] 최대 워커 수에 도달함");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = spawnPoint != null ? spawnPoint.position : transform.position;
|
||||
|
||||
float randomAngle = Random.Range(0f, 360f);
|
||||
Vector3 offset = new Vector3(
|
||||
Mathf.Cos(randomAngle * Mathf.Deg2Rad) * spawnRadius,
|
||||
0f,
|
||||
Mathf.Sin(randomAngle * Mathf.Deg2Rad) * spawnRadius
|
||||
);
|
||||
spawnPosition += offset;
|
||||
|
||||
GameObject workerObj = Instantiate(workerPrefab, spawnPosition, Quaternion.identity);
|
||||
NetworkObject workerNetObj = workerObj.GetComponent<NetworkObject>();
|
||||
|
||||
if (workerNetObj == null)
|
||||
{
|
||||
Debug.LogError("[WorkerSpawner] Worker prefab must have NetworkObject component");
|
||||
Destroy(workerObj);
|
||||
return;
|
||||
}
|
||||
|
||||
workerNetObj.Spawn();
|
||||
_lastSpawnTime = Time.time;
|
||||
_workerCount.Value++;
|
||||
|
||||
ShowSpawnEffectClientRpc(spawnPosition);
|
||||
|
||||
Debug.Log($"[WorkerSpawner] 워커 생성됨 (총 워커: {_workerCount.Value}/{maxWorkers})");
|
||||
|
||||
ScheduleWorkerAssignment(playerId, workerNetObj.NetworkObjectId);
|
||||
}
|
||||
|
||||
private void ScheduleWorkerAssignment(ulong playerId, ulong workerNetObjectId)
|
||||
{
|
||||
StartCoroutine(AssignWorkerAfterFrame(playerId, workerNetObjectId));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator AssignWorkerAfterFrame(ulong playerId, ulong workerNetObjectId)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(workerNetObjectId, out var workerObj))
|
||||
{
|
||||
var worker = workerObj.GetComponent<Worker>();
|
||||
if (worker != null)
|
||||
{
|
||||
SetPlayerAssignedWorkerClientRpc(playerId, workerNetObjectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
Debug.Log($"[WorkerSpawner] Worker assigned to player {playerId}'s PlayerInteraction");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.ClientsAndHost)]
|
||||
private void ShowSpawnEffectClientRpc(Vector3 position)
|
||||
{
|
||||
if (spawnEffectPrefab != null)
|
||||
{
|
||||
GameObject effect = Instantiate(spawnEffectPrefab, position, Quaternion.identity);
|
||||
Destroy(effect, 2f);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetInteractionPrompt()
|
||||
{
|
||||
if (_workerCount.Value >= maxWorkers)
|
||||
return $"워커 수 최대 ({_workerCount.Value}/{maxWorkers})";
|
||||
|
||||
float cooldownRemaining = Mathf.Max(0, spawnCooldown - (Time.time - _lastSpawnTime));
|
||||
if (cooldownRemaining > 0)
|
||||
return $"워커 생성 대기 중 ({cooldownRemaining:F1}s)";
|
||||
|
||||
return $"{interactionPrompt} ({_workerCount.Value}/{maxWorkers})";
|
||||
}
|
||||
|
||||
public string GetInteractionAnimation()
|
||||
{
|
||||
return interactionAnimationTrigger;
|
||||
}
|
||||
|
||||
public EquipmentData GetEquipmentData()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Transform GetTransform()
|
||||
{
|
||||
return transform;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void OnWorkerDestroyed()
|
||||
{
|
||||
if (IsServer)
|
||||
{
|
||||
_workerCount.Value = Mathf.Max(0, _workerCount.Value - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Gizmos.color = Color.green;
|
||||
Vector3 spawnCenter = spawnPoint != null ? spawnPoint.position : transform.position;
|
||||
Gizmos.DrawWireSphere(spawnCenter, spawnRadius);
|
||||
|
||||
UnityEditor.Handles.Label(spawnCenter + Vector3.up * 2f,
|
||||
$"Worker Spawner\nWorkers: {_workerCount.Value}/{maxWorkers}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/WorkerSpawner.cs.meta
Normal file
2
Assets/Scripts/WorkerSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3c0ae3369326994da60b1f4ede9d7c1
|
||||
Reference in New Issue
Block a user