일꾼(Worker) 개발 및 Kaykit Adventurer 애셋 추가

코어 오른쪽 건물에서 상호작용 하면 Worker 생성
Worker와 상호작용하면 Worker가 플레이어를 따라옴
그 상태에서 자원과 상호작용하면 Worker를 자원에 배치할 수 있음.
This commit is contained in:
2026-02-01 21:55:14 +09:00
parent 78791649ae
commit 3e747a9d97
799 changed files with 2085932 additions and 80 deletions

View File

@@ -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()

View File

@@ -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
View 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
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c8cb09d7a5404d34da082430986bd31c

View 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
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3c0ae3369326994da60b1f4ede9d7c1