모든 네트워크 오브젝트의 소유권을 서버가 갖도록 함

Distributable -> None
관련 사이드 이펙트로 인한 버그 수정
This commit is contained in:
2026-02-18 02:18:42 +09:00
parent da8c87d082
commit 4ffbbb0aff
10 changed files with 388 additions and 129 deletions

View File

@@ -194,15 +194,16 @@ namespace Northbound
return false; return false;
// 플레이어가 자원을 가지고 있는지 확인 // 플레이어가 자원을 가지고 있는지 확인
if (NetworkManager.Singleton != null && // NetworkPlayerController로 찾기 (서버 소유권으로 스폰한 경우 PlayerObject가 null일 수 있음)
NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client)) var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
foreach (var kvp in spawnedObjects)
{ {
if (client.PlayerObject != null) var controller = kvp.Value.GetComponent<NetworkPlayerController>();
if (controller != null && controller.OwnerPlayerId == playerId)
{ {
var playerInventory = client.PlayerObject.GetComponent<PlayerResourceInventory>(); var playerInventory = kvp.Value.GetComponent<PlayerResourceInventory>();
if (playerInventory != null) if (playerInventory != null)
{ {
// 플레이어가 자원을 가지고 있어야 함
return playerInventory.CurrentResourceAmount > 0; return playerInventory.CurrentResourceAmount > 0;
} }
} }
@@ -297,13 +298,23 @@ namespace Northbound
[Rpc(SendTo.ClientsAndHost)] [Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId) private void UpdatePlayerResourcesClientRpc(ulong playerId)
{ {
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject; // 해당 플레이어만 업데이트
if (playerObject != null) if (NetworkManager.Singleton.LocalClientId != playerId)
return;
// 로컬 플레이어의 PlayerResourceInventory 찾아서 업데이트 요청
var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
foreach (var kvp in spawnedObjects)
{ {
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>(); var controller = kvp.Value.GetComponent<NetworkPlayerController>();
if (playerInventory != null) if (controller != null && controller.IsLocalPlayer)
{ {
playerInventory.RequestResourceUpdateServerRpc(); var inventory = kvp.Value.GetComponent<PlayerResourceInventory>();
if (inventory != null)
{
inventory.RequestResourceUpdateServerRpc(playerId);
return;
}
} }
} }
} }

View File

@@ -171,9 +171,26 @@ namespace Northbound
ulong localClientId = NetworkManager.Singleton.LocalClientId; ulong localClientId = NetworkManager.Singleton.LocalClientId;
// PlayerObject가 있으면 사용
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(localClientId, out var client)) if (NetworkManager.Singleton.ConnectedClients.TryGetValue(localClientId, out var client))
{ {
return client.PlayerObject?.gameObject; if (client.PlayerObject != null)
{
return client.PlayerObject.gameObject;
}
}
// PlayerObject가 없으면 NetworkPlayerController로 찾기
// (서버 소유권으로 스폰한 경우 PlayerObject가 null일 수 있음)
var allNetworkObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects;
foreach (var kvp in allNetworkObjects)
{
var networkObj = kvp.Value;
var playerController = networkObj.GetComponent<NetworkPlayerController>();
if (playerController != null && playerController.IsLocalPlayer)
{
return networkObj.gameObject;
}
} }
return null; return null;

View File

@@ -104,10 +104,10 @@ namespace Northbound
if (playerInteraction == null) if (playerInteraction == null)
return false; return false;
var networkObject = playerInteraction.GetComponent<Unity.Netcode.NetworkObject>(); var networkPlayerController = playerInteraction.GetComponent<NetworkPlayerController>();
if (networkObject != null) if (networkPlayerController != null)
{ {
return networkObject.IsOwner; return networkPlayerController.IsLocalPlayer;
} }
return false; return false;
} }

View File

@@ -110,8 +110,16 @@ namespace Northbound
{ {
return; return;
} }
networkObject.SpawnAsPlayerObject(clientId); // 서버 소유권으로 스폰
networkObject.SpawnWithOwnership(NetworkManager.ServerClientId);
// 플레이어 컨트롤러 초기화
var playerController = playerObject.GetComponent<NetworkPlayerController>();
if (playerController != null)
{
playerController.Initialize(clientId);
}
} }
private void ApprovalCheck( private void ApprovalCheck(

View File

@@ -1,4 +1,6 @@
using System;
using Unity.Netcode; using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using Unity.Cinemachine; using Unity.Cinemachine;
@@ -20,6 +22,13 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
[SerializeField] private GameObject damageEffectPrefab; [SerializeField] private GameObject damageEffectPrefab;
[SerializeField] private GameObject deathEffectPrefab; [SerializeField] private GameObject deathEffectPrefab;
// 이 플레이어를 제어하는 클라이언트 ID (서버 소유권이지만 논리적 소유자)
private NetworkVariable<ulong> _ownerPlayerId = new NetworkVariable<ulong>(
ulong.MaxValue,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>( private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
TeamType.Player, TeamType.Player,
NetworkVariableReadPermission.Everyone, NetworkVariableReadPermission.Everyone,
@@ -36,11 +45,21 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
private CharacterController _controller; private CharacterController _controller;
private PlayerInputActions _inputActions; private PlayerInputActions _inputActions;
private Animator _animator; private Animator _animator;
private NetworkAnimator _networkAnimator;
// 이 플레이어가 로컬 플레이어인지 확인
public bool IsLocalPlayer => _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId;
public ulong OwnerPlayerId => _ownerPlayerId.Value;
// 소유자 변경 이벤트
public event Action<ulong> OnOwnerChanged;
void Awake() void Awake()
{ {
_controller = GetComponent<CharacterController>(); _controller = GetComponent<CharacterController>();
_animator = GetComponent<Animator>(); _animator = GetComponent<Animator>();
_networkAnimator = GetComponent<NetworkAnimator>();
} }
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
@@ -61,28 +80,52 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
} }
} }
// 체력 변경 이벤트 구독 // 이벤트 구독
_currentHealth.OnValueChanged += OnHealthChanged; _currentHealth.OnValueChanged += OnHealthChanged;
_ownerPlayerId.OnValueChanged += OnOwnerPlayerIdChanged;
if (!IsOwner) return; // 이미 로컬 플레이어로 설정되어 있으면 입력 초기화
TryInitializeLocalPlayer();
}
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>(); private void OnOwnerPlayerIdChanged(ulong previousValue, ulong newValue)
{
OnOwnerChanged?.Invoke(newValue);
TryInitializeLocalPlayer();
}
if (vcam != null) private void TryInitializeLocalPlayer()
{ {
vcam.Follow = transform; if (!IsLocalPlayer) return;
vcam.LookAt = transform; if (_inputActions != null) return; // 이미 초기화됨
}
var vcam = GameObject.FindFirstObjectByType<CinemachineCamera>();
if (vcam != null)
{
vcam.Follow = transform;
vcam.LookAt = transform;
}
_inputActions = new PlayerInputActions(); _inputActions = new PlayerInputActions();
_inputActions.Enable(); _inputActions.Enable();
} }
/// <summary>
/// 플레이어 초기화 (서버에서 호출)
/// </summary>
public void Initialize(ulong ownerPlayerId)
{
if (!IsServer) return;
_ownerPlayerId.Value = ownerPlayerId;
}
public override void OnNetworkDespawn() public override void OnNetworkDespawn()
{ {
_currentHealth.OnValueChanged -= OnHealthChanged; _currentHealth.OnValueChanged -= OnHealthChanged;
_ownerPlayerId.OnValueChanged -= OnOwnerPlayerIdChanged;
if (IsOwner && _inputActions != null) if (IsLocalPlayer && _inputActions != null)
{ {
_inputActions.Disable(); _inputActions.Disable();
} }
@@ -92,7 +135,11 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
void Update() void Update()
{ {
if (!IsOwner) return; // 로컬 플레이어만 입력 처리
if (!IsLocalPlayer) return;
// 입력 시스템이 초기화되지 않았으면 스킵
if (_inputActions == null) return;
// 죽었으면 이동 불가 // 죽었으면 이동 불가
if (_currentHealth.Value <= 0) return; if (_currentHealth.Value <= 0) return;
@@ -106,16 +153,30 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
if (isActionBlocked) if (isActionBlocked)
{ {
// 이동 불가 시 애니메이션 속도를 0으로 // 서버에 이동 중지 알림 (애니메이션 포함)
if (_animator != null) MoveServerRpc(Vector2.zero);
{
_animator.SetFloat("MoveSpeed", 0f);
}
return; return;
} }
_moveInput = _inputActions.Player.Move.ReadValue<Vector2>(); _moveInput = _inputActions.Player.Move.ReadValue<Vector2>();
Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
// 서버에 이동 요청 전송 (애니메이션 포함)
MoveServerRpc(_moveInput);
}
[Rpc(SendTo.Server)]
private void MoveServerRpc(Vector2 moveInput)
{
// 죽었으면 이동 불가
if (_currentHealth.Value <= 0) return;
Vector3 move = new Vector3(moveInput.x, 0, moveInput.y).normalized;
// NetworkAnimator로 애니메이션 동기화
if (_networkAnimator != null)
{
_networkAnimator.Animator.SetFloat("MoveSpeed", move.magnitude);
}
if (move.magnitude >= 0.1f) if (move.magnitude >= 0.1f)
{ {
@@ -127,11 +188,6 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
_controller.Move(move * moveSpeed * Time.deltaTime); _controller.Move(move * moveSpeed * Time.deltaTime);
} }
} }
if (_animator != null)
{
_animator.SetFloat("MoveSpeed", move.magnitude);
}
} }
#region ITeamMember Implementation #region ITeamMember Implementation
@@ -149,7 +205,6 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
#endregion #endregion
#region IDamageable Implementation #region IDamageable Implementation
public void TakeDamage(int damage, ulong attackerId) public void TakeDamage(int damage, ulong attackerId)
{ {
if (!IsServer) return; if (!IsServer) return;
@@ -184,17 +239,17 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
} }
} }
private void Die(ulong killerId) private void Die(ulong killerId)
{ {
if (!IsServer) return; if (!IsServer) return;
// 사망 이펙트 // 사망 이펙트
ShowDeathEffectClientRpc(); ShowDeathEffectClientRpc();
// 애니메이션 (있는 경우) // 애니메이션 - NetworkAnimator로 동기화
if (_animator != null) if (_networkAnimator != null)
{ {
_animator.SetTrigger("Die"); _networkAnimator.SetTrigger("Die");
} }
// 일정 시간 후 리스폰 또는 디스폰 // 일정 시간 후 리스폰 또는 디스폰
@@ -205,8 +260,6 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
{ {
if (!IsServer) return; if (!IsServer) return;
// 여기서 리스폰 로직을 추가하거나 게임 오버 처리
// 예: 리스폰 위치로 이동 및 체력 회복
Respawn(); Respawn();
} }
@@ -217,16 +270,20 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
// 체력 회복 // 체력 회복
_currentHealth.Value = maxHealth; _currentHealth.Value = maxHealth;
// 스폰 포인트로 이동 (PlayerSpawnPoint 활용) // 스폰 포인트로 이동
var spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None); var spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
if (spawnPoints.Length > 0) if (spawnPoints.Length > 0)
{ {
var spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)]; var spawnPoint = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)];
transform.position = spawnPoint.transform.position; transform.position = spawnPoint.transform.position;
transform.rotation = spawnPoint.transform.rotation; transform.rotation = spawnPoint.transform.rotation;
} }
_animator.SetTrigger("Revive"); // NetworkAnimator로 애니메이션 동기화
if (_networkAnimator != null)
{
_networkAnimator.SetTrigger("Revive");
}
} }
[ClientRpc] [ClientRpc]
@@ -253,32 +310,17 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
#region Health Management #region Health Management
/// <summary>
/// 현재 체력
/// </summary>
public int GetCurrentHealth() => _currentHealth.Value; public int GetCurrentHealth() => _currentHealth.Value;
/// <summary>
/// 최대 체력
/// </summary>
public int GetMaxHealth() => maxHealth; public int GetMaxHealth() => maxHealth;
/// <summary>
/// 체력 비율 (0.0 ~ 1.0)
/// </summary>
public float GetHealthPercentage() public float GetHealthPercentage()
{ {
return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f; return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f;
} }
/// <summary>
/// 죽었는지 여부
/// </summary>
public bool IsDead() => _currentHealth.Value <= 0; public bool IsDead() => _currentHealth.Value <= 0;
/// <summary>
/// 체력 회복
/// </summary>
public void Heal(int amount) public void Heal(int amount)
{ {
if (!IsServer) return; if (!IsServer) return;
@@ -289,10 +331,7 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
private void OnHealthChanged(int previousValue, int newValue) private void OnHealthChanged(int previousValue, int newValue)
{ {
// 체력바 UI 업데이트 또는 체력 변경 시각 효과 if (IsLocalPlayer)
// 클라이언트에서도 체력 변경 인지 가능
if (IsOwner)
{ {
// UI 업데이트 등 // UI 업데이트 등
} }

View File

@@ -1,4 +1,5 @@
using Unity.Netcode; using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using System.Collections.Generic; using System.Collections.Generic;
@@ -19,17 +20,23 @@ namespace Northbound
private PlayerInputActions _inputActions; private PlayerInputActions _inputActions;
private Dictionary<string, IAction> _actions = new Dictionary<string, IAction>(); private Dictionary<string, IAction> _actions = new Dictionary<string, IAction>();
private Animator _animator; private Animator _animator;
private NetworkAnimator _networkAnimator;
private NetworkPlayerController _networkPlayerController;
// 로컬 플레이어인지 확인
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
private void Awake() private void Awake()
{ {
_animator = GetComponent<Animator>(); _animator = GetComponent<Animator>();
_networkAnimator = GetComponent<NetworkAnimator>();
_networkPlayerController = GetComponent<NetworkPlayerController>();
} }
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
{ {
if (!IsOwner) return; // 액션 컴포넌트들을 딕셔너리에 등록 (모든 클라이언트에서)
// 액션 컴포넌트들을 딕셔너리에 등록
foreach (var component in actionComponents) foreach (var component in actionComponents)
{ {
if (component is IAction action) if (component is IAction action)
@@ -38,15 +45,39 @@ namespace Northbound
} }
} }
// _ownerPlayerId 변경 이벤트 구독
if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged += OnOwnerPlayerIdChanged;
}
// 이미 로컬 플레이어면 입력 초기화
TryInitializeInput();
}
private void OnOwnerPlayerIdChanged(ulong newOwnerId)
{
TryInitializeInput();
}
private void TryInitializeInput()
{
if (!IsLocalPlayer) return;
if (_inputActions != null) return;
_inputActions = new PlayerInputActions(); _inputActions = new PlayerInputActions();
_inputActions.Player.Attack.performed += OnAttack; _inputActions.Player.Attack.performed += OnAttack;
// 다른 액션들도 여기에 바인딩
_inputActions.Enable(); _inputActions.Enable();
} }
public override void OnNetworkDespawn() public override void OnNetworkDespawn()
{ {
if (IsOwner && _inputActions != null) if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged -= OnOwnerPlayerIdChanged;
}
if (_inputActions != null)
{ {
_inputActions.Player.Attack.performed -= OnAttack; _inputActions.Player.Attack.performed -= OnAttack;
_inputActions.Disable(); _inputActions.Disable();
@@ -63,23 +94,30 @@ namespace Northbound
{ {
if (_actions.TryGetValue(actionName, out IAction action)) if (_actions.TryGetValue(actionName, out IAction action))
{ {
if (action.CanExecute(OwnerClientId)) if (action.CanExecute(LocalPlayerId))
{ {
// 애니메이션 재생 (액션 실행 전) string animTrigger = action.GetActionAnimation();
if (playAnimations && _animator != null)
// 서버에서 애니메이션 실행 (동기화를 위해)
if (playAnimations && !string.IsNullOrEmpty(animTrigger))
{ {
string animTrigger = action.GetActionAnimation(); PlayAnimationServerRpc(animTrigger);
if (!string.IsNullOrEmpty(animTrigger))
{
_animator.SetTrigger(animTrigger);
}
} }
action.Execute(OwnerClientId); action.Execute(LocalPlayerId);
} }
} }
} }
[Rpc(SendTo.Server)]
private void PlayAnimationServerRpc(string animTrigger)
{
if (_networkAnimator != null && !string.IsNullOrEmpty(animTrigger))
{
_networkAnimator.SetTrigger(animTrigger);
}
}
override public void OnDestroy() override public void OnDestroy()
{ {
if (_inputActions != null) if (_inputActions != null)

View File

@@ -1,4 +1,5 @@
using Unity.Netcode; using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
@@ -37,6 +38,7 @@ namespace Northbound
private IInteractable _unavailableInteractable; private IInteractable _unavailableInteractable;
private Camera _mainCamera; private Camera _mainCamera;
private Animator _animator; private Animator _animator;
private NetworkAnimator _networkAnimator;
private EquipmentSocket _equipmentSocket; private EquipmentSocket _equipmentSocket;
private EquipmentData _pendingEquipmentData; private EquipmentData _pendingEquipmentData;
@@ -44,21 +46,51 @@ namespace Northbound
private bool _isInteracting = false; private bool _isInteracting = false;
private Coroutine _interactionTimeoutCoroutine; private Coroutine _interactionTimeoutCoroutine;
private NetworkPlayerController _networkPlayerController;
public bool IsInteracting => _isInteracting; public bool IsInteracting => _isInteracting;
public float WorkPower => workPower; public float WorkPower => workPower;
public IInteractable CurrentUnavailableInteractable => _unavailableInteractable; public IInteractable CurrentUnavailableInteractable => _unavailableInteractable;
// 로컬 플레이어인지 확인
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
private void Awake()
{
_networkPlayerController = GetComponent<NetworkPlayerController>();
_networkAnimator = GetComponent<NetworkAnimator>();
}
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
{ {
if (!IsOwner) return;
_mainCamera = Camera.main;
_animator = GetComponent<Animator>(); _animator = GetComponent<Animator>();
_equipmentSocket = GetComponent<EquipmentSocket>(); _equipmentSocket = GetComponent<EquipmentSocket>();
if (rayOrigin == null) if (rayOrigin == null)
rayOrigin = transform; rayOrigin = transform;
// _ownerPlayerId 변경 이벤트 구독
if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged += OnOwnerPlayerIdChanged;
}
// 이미 로컬 플레이어면 입력 초기화
TryInitializeInput();
}
private void OnOwnerPlayerIdChanged(ulong newOwnerId)
{
TryInitializeInput();
}
private void TryInitializeInput()
{
if (!IsLocalPlayer) return;
if (_inputActions != null) return;
_mainCamera = Camera.main;
_inputActions = new PlayerInputActions(); _inputActions = new PlayerInputActions();
_inputActions.Player.Interact.performed += OnInteract; _inputActions.Player.Interact.performed += OnInteract;
_inputActions.Enable(); _inputActions.Enable();
@@ -66,7 +98,12 @@ namespace Northbound
public override void OnNetworkDespawn() public override void OnNetworkDespawn()
{ {
if (IsOwner && _inputActions != null) if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged -= OnOwnerPlayerIdChanged;
}
if (_inputActions != null)
{ {
_inputActions.Player.Interact.performed -= OnInteract; _inputActions.Player.Interact.performed -= OnInteract;
_inputActions.Disable(); _inputActions.Disable();
@@ -76,12 +113,11 @@ namespace Northbound
private void Update() private void Update()
{ {
if (!IsOwner) return; if (!IsLocalPlayer) return;
// Check if current interactable is no longer valid (e.g., building completed)
if (_isInteracting && _currentInteractable != null) if (_isInteracting && _currentInteractable != null)
{ {
if (!_currentInteractable.CanInteract(OwnerClientId)) if (!_currentInteractable.CanInteract(LocalPlayerId))
{ {
EndInteraction(); EndInteraction();
} }
@@ -92,6 +128,13 @@ namespace Northbound
private void DetectInteractable() private void DetectInteractable()
{ {
// 카메라가 없으면 다시 시도
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null) return;
}
Vector3 origin = rayOrigin.position; Vector3 origin = rayOrigin.position;
Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward; Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward;
@@ -112,7 +155,7 @@ namespace Northbound
if (interactable != null) if (interactable != null)
{ {
if (interactable.CanInteract(OwnerClientId)) if (interactable.CanInteract(LocalPlayerId))
{ {
_currentInteractable = interactable; _currentInteractable = interactable;
_unavailableInteractable = null; _unavailableInteractable = null;
@@ -120,7 +163,6 @@ namespace Northbound
} }
else else
{ {
// CanInteract가 false인 경우 추적
_currentInteractable = null; _currentInteractable = null;
_unavailableInteractable = interactable; _unavailableInteractable = interactable;
return; return;
@@ -139,8 +181,14 @@ namespace Northbound
if (_currentInteractable != null) if (_currentInteractable != null)
{ {
// 상호작용 가능 여부 다시 확인 (NetworkVariable 동기화 지연 대응)
if (!_currentInteractable.CanInteract(LocalPlayerId))
{
return;
}
bool isResource = _currentInteractable is Resource; bool isResource = _currentInteractable is Resource;
bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId; bool hasWorker = assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId;
bool isWorkerFollowing = hasWorker && (int)assignedWorker.CurrentState == 1; bool isWorkerFollowing = hasWorker && (int)assignedWorker.CurrentState == 1;
bool shouldPlayAnimation = !isResource || !hasWorker || !isWorkerFollowing; bool shouldPlayAnimation = !isResource || !hasWorker || !isWorkerFollowing;
@@ -150,9 +198,10 @@ namespace Northbound
string animTrigger = _currentInteractable.GetInteractionAnimation(); string animTrigger = _currentInteractable.GetInteractionAnimation();
bool hasAnimation = !string.IsNullOrEmpty(animTrigger); bool hasAnimation = !string.IsNullOrEmpty(animTrigger);
if (playAnimations && _animator != null && hasAnimation && shouldPlayAnimation) if (playAnimations && hasAnimation && shouldPlayAnimation)
{ {
_animator.SetTrigger(animTrigger); // 서버에서 애니메이션 실행 (동기화를 위해)
PlayAnimationServerRpc(animTrigger);
_interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f)); _interactionTimeoutCoroutine = StartCoroutine(InteractionTimeout(3f));
} }
else else
@@ -173,13 +222,18 @@ namespace Northbound
} }
} }
_currentInteractable.Interact(OwnerClientId); _currentInteractable.Interact(LocalPlayerId);
} }
} }
// ======================================== [Rpc(SendTo.Server)]
// Animation Event 함수들 private void PlayAnimationServerRpc(string animTrigger)
// ======================================== {
if (_networkAnimator != null && !string.IsNullOrEmpty(animTrigger))
{
_networkAnimator.SetTrigger(animTrigger);
}
}
public void OnEquipTool() public void OnEquipTool()
{ {
@@ -207,7 +261,7 @@ namespace Northbound
public void OnInteractionComplete() public void OnInteractionComplete()
{ {
if (!IsOwner) return; if (!IsLocalPlayer) return;
if (_interactionTimeoutCoroutine != null) if (_interactionTimeoutCoroutine != null)
{ {
@@ -217,10 +271,6 @@ namespace Northbound
_isInteracting = false; _isInteracting = false;
} }
// ========================================
// 내부 헬퍼 함수들
// ========================================
private void AttachEquipment(string socketName = null) private void AttachEquipment(string socketName = null)
{ {
if (_equipmentSocket == null || _pendingEquipmentData == null) if (_equipmentSocket == null || _pendingEquipmentData == null)
@@ -283,7 +333,13 @@ namespace Northbound
private void OnGUI() private void OnGUI()
{ {
if (!IsOwner) return; if (!IsLocalPlayer) return;
// 디버그: _mainCamera 상태 확인
if (_mainCamera == null)
{
_mainCamera = Camera.main;
}
GUIStyle style = new GUIStyle(GUI.skin.label) GUIStyle style = new GUIStyle(GUI.skin.label)
{ {
@@ -299,7 +355,7 @@ namespace Northbound
prompt = _currentInteractable.GetInteractionPrompt(); prompt = _currentInteractable.GetInteractionPrompt();
bool isResource = _currentInteractable is Resource; bool isResource = _currentInteractable is Resource;
if (isResource && assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId) if (isResource && assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId)
{ {
prompt += " (워커 할당 가능)"; prompt += " (워커 할당 가능)";
} }
@@ -311,7 +367,7 @@ namespace Northbound
} }
} }
if (assignedWorker != null && assignedWorker.OwnerPlayerId == OwnerClientId) if (assignedWorker != null && assignedWorker.OwnerPlayerId == LocalPlayerId)
{ {
string workerStatus = assignedWorker.CurrentState switch string workerStatus = assignedWorker.CurrentState switch
{ {

View File

@@ -11,32 +11,65 @@ namespace Northbound
public int CurrentResourceAmount => _displayAmount; public int CurrentResourceAmount => _displayAmount;
public int MaxResourceCapacity => maxResourceCapacity; public int MaxResourceCapacity => maxResourceCapacity;
private NetworkPlayerController _networkPlayerController;
private bool IsLocalPlayer => _networkPlayerController != null && _networkPlayerController.IsLocalPlayer;
private ulong LocalPlayerId => _networkPlayerController != null ? _networkPlayerController.OwnerPlayerId : OwnerClientId;
private void Awake()
{
_networkPlayerController = GetComponent<NetworkPlayerController>();
}
[Rpc(SendTo.Server)] [Rpc(SendTo.Server)]
private void SetMaxCapacityServerRpc(int maxCapacity) private void SetMaxCapacityServerRpc(int maxCapacity, ulong playerId)
{ {
var resourceManager = ServerResourceManager.Instance; var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null) if (resourceManager != null)
{ {
resourceManager.SetPlayerMaxCapacity(OwnerClientId, maxCapacity); resourceManager.SetPlayerMaxCapacity(playerId, maxCapacity);
} }
} }
public override void OnNetworkSpawn() public override void OnNetworkSpawn()
{ {
if (IsClient && IsOwner) // _ownerPlayerId 변경 이벤트 구독
if (_networkPlayerController != null)
{ {
SetMaxCapacityServerRpc(maxResourceCapacity); _networkPlayerController.OnOwnerChanged += OnOwnerPlayerIdChanged;
RequestResourceUpdateServerRpc(); }
TryInitialize();
}
private void OnOwnerPlayerIdChanged(ulong newOwnerId)
{
TryInitialize();
}
private void TryInitialize()
{
if (!IsLocalPlayer) return;
SetMaxCapacityServerRpc(maxResourceCapacity, LocalPlayerId);
RequestResourceUpdateServerRpc(LocalPlayerId);
}
public override void OnNetworkDespawn()
{
if (_networkPlayerController != null)
{
_networkPlayerController.OnOwnerChanged -= OnOwnerPlayerIdChanged;
} }
} }
[Rpc(SendTo.Server)] [Rpc(SendTo.Server)]
public void RequestResourceUpdateServerRpc() public void RequestResourceUpdateServerRpc(ulong playerId)
{ {
var resourceManager = ServerResourceManager.Instance; var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null) if (resourceManager != null)
{ {
int amount = resourceManager.GetPlayerResourceAmount(OwnerClientId); int amount = resourceManager.GetPlayerResourceAmount(playerId);
UpdateResourceAmountClientRpc(amount); UpdateResourceAmountClientRpc(amount);
} }
} }
@@ -57,7 +90,7 @@ namespace Northbound
var resourceManager = ServerResourceManager.Instance; var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null) if (resourceManager != null)
{ {
return resourceManager.CanAddResource(OwnerClientId, amount); return resourceManager.CanAddResource(LocalPlayerId, amount);
} }
return false; return false;
} }
@@ -67,7 +100,7 @@ namespace Northbound
var resourceManager = ServerResourceManager.Instance; var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null) if (resourceManager != null)
{ {
return resourceManager.GetAvailableSpace(OwnerClientId); return resourceManager.GetAvailableSpace(LocalPlayerId);
} }
return 0; return 0;
} }

View File

@@ -235,14 +235,45 @@ namespace Northbound
if (Time.time - _lastGatheringTime < gatheringCooldown) if (Time.time - _lastGatheringTime < gatheringCooldown)
return false; 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; var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null) if (resourceManager != null)
{ {
if (resourceManager.GetAvailableSpace(playerId) <= 0) return resourceManager.GetAvailableSpace(playerId);
return false;
} }
return true; // 클라이언트에서는 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) public void Interact(ulong playerId)
@@ -324,15 +355,28 @@ namespace Northbound
ShowGatheringEffectClientRpc(); ShowGatheringEffectClientRpc();
} }
[Rpc(SendTo.Owner)] [Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId) private void UpdatePlayerResourcesClientRpc(ulong playerId)
{ {
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId]?.PlayerObject; // 해당 플레이어만 업데이트
if (playerObject == null) if (NetworkManager.Singleton.LocalClientId != playerId)
return; return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>(); // 로컬 플레이어의 PlayerResourceInventory 찾아서 업데이트 요청
playerInventory?.RequestResourceUpdateServerRpc(); 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)] [Rpc(SendTo.NotServer)]

View File

@@ -88,15 +88,28 @@ namespace Northbound
Invoke(nameof(DestroyPickup), 0.1f); Invoke(nameof(DestroyPickup), 0.1f);
} }
[Rpc(SendTo.Owner)] [Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId) private void UpdatePlayerResourcesClientRpc(ulong playerId)
{ {
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId]?.PlayerObject; // 해당 플레이어만 업데이트
if (playerObject == null) if (NetworkManager.Singleton.LocalClientId != playerId)
return; return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>(); // 로컬 플레이어의 PlayerResourceInventory 찾아서 업데이트 요청
playerInventory?.RequestResourceUpdateServerRpc(); 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.ClientsAndHost)] [Rpc(SendTo.ClientsAndHost)]