네트워크 멀티플레이 환경 문제 수정

관련 문제가 다시 발생하면 이 커밋으로 돌아올 것
This commit is contained in:
2026-02-02 04:24:14 +09:00
parent 3e747a9d97
commit 10b496dfae
49 changed files with 2860 additions and 1792 deletions

View File

@@ -259,19 +259,14 @@ namespace Northbound
if (!CanInteract(playerId))
return;
// 플레이어 인벤토리 가져오기
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject == null)
return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory == null)
var resourceManager = ServerResourceManager.Instance;
if (resourceManager == null)
{
Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
Debug.LogWarning("ServerResourceManager 인스턴스를 찾을 수 없습니다.");
return;
}
int playerResourceAmount = playerInventory.CurrentResourceAmount;
int playerResourceAmount = resourceManager.GetPlayerResourceAmount(playerId);
if (playerResourceAmount <= 0)
{
Debug.Log($"플레이어 {playerId}가 건낼 자원이 없습니다.");
@@ -282,16 +277,13 @@ namespace Northbound
if (depositAll)
{
// 전부 건네기
depositAmount = playerResourceAmount;
}
else
{
// 일부만 건네기
depositAmount = Mathf.Min(depositAmountPerInteraction, playerResourceAmount);
}
// 무제한 저장소가 아니면 용량 제한 확인
if (!unlimitedStorage)
{
int availableSpace = maxStorageCapacity - _totalResources.Value;
@@ -304,10 +296,8 @@ namespace Northbound
return;
}
// 플레이어로부터 자원 차감
playerInventory.RemoveResourceServerRpc(depositAmount);
// 코어에 자원 추가
resourceManager.RemoveResource(playerId, depositAmount);
UpdatePlayerResourcesClientRpc(playerId);
_totalResources.Value += depositAmount;
Debug.Log($"<color=green>[Core] 플레이어 {playerId}가 {depositAmount} 자원을 건넸습니다. 코어 총 자원: {_totalResources.Value}" +
@@ -316,6 +306,20 @@ namespace Northbound
ShowDepositEffectClientRpc();
}
[Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId)
{
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject != null)
{
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
playerInventory.RequestResourceUpdateServerRpc();
}
}
}
[Rpc(SendTo.ClientsAndHost)]
private void ShowDepositEffectClientRpc()
{

View File

@@ -0,0 +1,160 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
namespace Northbound
{
public class DebugLogUI : MonoBehaviour
{
[Header("Settings")]
public int maxLogMessages = 50;
public float logDisplayDuration = 5f;
public KeyCode toggleKey = KeyCode.BackQuote;
[Header("UI References")]
public GameObject logPanel;
public Text logText;
public GameObject scrollContent;
public ScrollRect scrollRect;
private Queue<string> _logMessages = new Queue<string>();
private bool _isVisible = true;
private void Start()
{
// 이미 씬에 있는지 확인
if (logPanel != null)
{
logPanel = CreateLogPanel();
_isVisible = true;
}
}
private void Update()
{
if (Input.GetKeyDown(toggleKey))
{
ToggleLogPanel();
}
}
private void OnEnable()
{
if (logPanel != null)
{
Application.logMessageReceived += HandleLog;
}
}
private void OnDisable()
{
if (logPanel != null)
{
Application.logMessageReceived -= HandleLog;
}
}
private GameObject CreateLogPanel()
{
GameObject panel = new GameObject("DebugLogPanel");
panel.AddComponent<Canvas>();
Canvas canvas = panel.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 9999;
RectTransform canvasRect = canvas.GetComponent<RectTransform>();
canvasRect.anchorMin = Vector2.zero;
canvasRect.anchorMax = new Vector2(Screen.width, Screen.height);
canvasRect.sizeDelta = Vector2.zero;
canvasRect.localScale = Vector3.one;
// 배경
GameObject background = new GameObject("Background");
background.transform.SetParent(canvas.transform, false);
Image bgImage = background.AddComponent<Image>();
bgImage.color = new Color(0, 0, 0, 0.8f);
RectTransform bgRect = background.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.sizeDelta = new Vector2(600, 300);
bgRect.localPosition = new Vector2(-300, -150);
// 로그 내용
GameObject content = new GameObject("Content");
content.transform.SetParent(background.transform, false);
RectTransform contentRect = content.AddComponent<RectTransform>();
contentRect.anchorMin = Vector2.zero;
contentRect.anchorMax = Vector2.one;
contentRect.sizeDelta = new Vector2(580, 280);
contentRect.localPosition = new Vector2(-290, -140);
// 텍스트
Text logTextComponent = content.AddComponent<Text>();
logTextComponent.fontSize = 14;
logTextComponent.alignment = TextAnchor.UpperLeft;
logTextComponent.color = Color.white;
// 스크롤뷰
GameObject scrollView = new GameObject("ScrollView");
scrollView.transform.SetParent(content.transform, false);
scrollRect = scrollView.AddComponent<ScrollRect>();
scrollRect.content = contentRect;
scrollRect.viewport = new RectTransform();
scrollRect.horizontal = false;
scrollRect.vertical = true;
scrollRect.scrollSensitivity = 1;
return panel;
}
private void HandleLog(string logString, string stackTrace, LogType type)
{
string timestamp = System.DateTime.Now.ToString("HH:mm:ss");
string logMessage = $"[{timestamp}] {logString}";
_logMessages.Enqueue(logMessage);
if (_logMessages.Count > maxLogMessages)
{
_logMessages.Dequeue();
}
UpdateLogText();
}
private void UpdateLogText()
{
if (logText == null) return;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (string msg in _logMessages)
{
sb.AppendLine(msg);
}
logText.text = sb.ToString();
}
public void ToggleLogPanel()
{
if (logPanel != null)
{
_isVisible = !_isVisible;
logPanel.SetActive(_isVisible);
}
}
public void Log(string message)
{
if (logPanel != null)
{
logPanel.SetActive(true);
_isVisible = true;
}
Debug.Log($"[DebugLogUI] {message}");
Debug.Log($"[DebugLogUI] 로그 패널 활성됨: {message}");
}
}
}

View File

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

View File

@@ -1,29 +1,34 @@
using UnityEngine;
using System.Collections.Generic;
using Unity.Netcode;
namespace Northbound
{
/// <summary>
/// 플레이어의 장비 소켓 관리 (손, 등, 허리 등)
/// </summary>
public class EquipmentSocket : MonoBehaviour
public class EquipmentSocket : NetworkBehaviour
{
[System.Serializable]
public class Socket
{
public string socketName; // "RightHand", "LeftHand", "Back" 등
public Transform socketTransform; // 실제 본 Transform
[HideInInspector] public GameObject currentEquipment; // 현재 장착된 장비
public string socketName;
public Transform socketTransform;
[HideInInspector] public GameObject currentEquipment;
}
[Header("Available Sockets")]
public List<Socket> sockets = new List<Socket>();
[Header("Equipment Prefabs")]
public GameObject[] equipmentPrefabs;
private Dictionary<string, Socket> _socketDict = new Dictionary<string, Socket>();
private Dictionary<string, GameObject> _prefabDict = new Dictionary<string, GameObject>();
private void Awake()
{
// 빠른 검색을 위한 딕셔너리 생성
_socketDict.Clear();
foreach (var socket in sockets)
{
if (!string.IsNullOrEmpty(socket.socketName))
@@ -31,82 +36,124 @@ namespace Northbound
_socketDict[socket.socketName] = socket;
}
}
}
/// <summary>
/// 소켓에 장비 부착
/// </summary>
public GameObject AttachToSocket(string socketName, GameObject equipmentPrefab)
{
if (!_socketDict.TryGetValue(socketName, out Socket socket))
_prefabDict.Clear();
if (equipmentPrefabs != null)
{
Debug.LogWarning($"소켓을 찾을 수 없습니다: {socketName}");
return null;
}
if (socket.socketTransform == null)
{
Debug.LogWarning($"소켓 Transform이 없습니다: {socketName}");
return null;
}
// 기존 장비 제거
DetachFromSocket(socketName);
// 새 장비 생성
if (equipmentPrefab != null)
{
GameObject equipment = Instantiate(equipmentPrefab, socket.socketTransform);
equipment.transform.localPosition = Vector3.zero;
equipment.transform.localRotation = Quaternion.identity;
socket.currentEquipment = equipment;
return equipment;
}
return null;
}
/// <summary>
/// 소켓에서 장비 제거
/// </summary>
public void DetachFromSocket(string socketName)
{
if (!_socketDict.TryGetValue(socketName, out Socket socket))
return;
if (socket.currentEquipment != null)
{
Destroy(socket.currentEquipment);
socket.currentEquipment = null;
foreach (var prefab in equipmentPrefabs)
{
if (prefab != null)
{
_prefabDict[prefab.name] = prefab;
}
}
}
}
/// <summary>
/// 모든 소켓에서 장비 제거
/// </summary>
public void DetachAll()
public override void OnNetworkDespawn()
{
foreach (var socket in sockets)
{
if (socket.currentEquipment != null)
{
Destroy(socket.currentEquipment);
Object.Destroy(socket.currentEquipment);
socket.currentEquipment = null;
}
}
base.OnNetworkDespawn();
}
public GameObject AttachToSocket(string socketName, GameObject equipmentPrefab)
{
if (equipmentPrefab != null)
{
AttachToSocketServerRpc(socketName, equipmentPrefab.name);
}
return null;
}
[Rpc(SendTo.Server)]
private void AttachToSocketServerRpc(string socketName, string prefabName)
{
AttachToSocketClientRpc(socketName, prefabName);
}
[Rpc(SendTo.ClientsAndHost)]
private void AttachToSocketClientRpc(string socketName, string prefabName)
{
if (!_socketDict.ContainsKey(socketName))
return;
var socket = sockets.Find(s => s.socketName == socketName);
if (socket == null || socket.socketTransform == null)
return;
DetachFromSocketInternal(socketName);
GameObject prefab = FindPrefab(prefabName);
if (prefab != null)
{
GameObject equipment = Object.Instantiate(prefab, socket.socketTransform);
equipment.transform.localPosition = Vector3.zero;
equipment.transform.localRotation = Quaternion.identity;
socket.currentEquipment = equipment;
}
}
public void DetachFromSocket(string socketName)
{
DetachFromSocketServerRpc(socketName);
}
[Rpc(SendTo.Server)]
private void DetachFromSocketServerRpc(string socketName)
{
DetachFromSocketClientRpc(socketName);
}
[Rpc(SendTo.ClientsAndHost)]
private void DetachFromSocketClientRpc(string socketName)
{
DetachFromSocketInternal(socketName);
}
private void DetachFromSocketInternal(string socketName)
{
var socket = sockets.Find(s => s.socketName == socketName);
if (socket == null) return;
if (socket.currentEquipment != null)
{
Object.Destroy(socket.currentEquipment);
socket.currentEquipment = null;
}
}
/// <summary>
/// 특정 소켓에 장비가 있는지 확인
/// </summary>
public bool HasEquipment(string socketName)
{
if (_socketDict.TryGetValue(socketName, out Socket socket))
var socket = sockets.Find(s => s.socketName == socketName);
return socket != null && socket.currentEquipment != null;
}
public GameObject GetEquipment(string socketName)
{
var socket = sockets.Find(s => s.socketName == socketName);
return socket != null ? socket.currentEquipment : null;
}
private GameObject FindPrefab(string name)
{
if (_prefabDict.TryGetValue(name, out var prefab))
{
return socket.currentEquipment != null;
return prefab;
}
return false;
prefab = Resources.Load<GameObject>($"Prefabs/{name}");
if (prefab != null)
{
return prefab;
}
return Resources.Load<GameObject>(name);
}
}
}
}

View File

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 871f9f40b5b3cb54ab7c0a76b592bc47
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -32,6 +32,7 @@ namespace Northbound
{
NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
Debug.Log("<color=cyan>[Connection] ConnectionApprovalCallback 등록됨</color>");
}
else
@@ -50,7 +51,7 @@ namespace Northbound
spawnPoints.Clear();
foreach (var point in sortedPoints)
{
if (point.isAvailable)
if (point != null && point.transform != null && point.isAvailable)
{
spawnPoints.Add(point.transform);
}
@@ -62,30 +63,92 @@ namespace Northbound
private void OnServerStarted()
{
Debug.Log("<color=green>[Connection] 서버 시작됨</color>");
if (ServerResourceManager.Instance == null)
{
GameObject resourceManagerObj = new GameObject("ServerResourceManager");
ServerResourceManager manager = resourceManagerObj.AddComponent<ServerResourceManager>();
NetworkObject networkObject = resourceManagerObj.GetComponent<NetworkObject>();
if (networkObject == null)
{
networkObject = resourceManagerObj.AddComponent<NetworkObject>();
}
networkObject.Spawn();
Debug.Log("[Connection] ServerResourceManager spawned.");
}
if (NetworkManager.Singleton.IsHost)
{
SpawnPlayer(NetworkManager.Singleton.LocalClientId);
}
}
private void OnClientConnected(ulong clientId)
{
if (!NetworkManager.Singleton.IsServer) return;
if (clientId == NetworkManager.Singleton.LocalClientId) return;
Debug.Log($"<color=cyan>[Connection] 클라이언트 {clientId} 연결됨</color>");
if (!NetworkManager.Singleton.ConnectedClients.TryGetValue(clientId, out var client) || client.PlayerObject != null)
{
Debug.Log($"<color=yellow>[Connection] 클라이언트 {clientId}의 플레이어가 이미 존재합니다.</color>");
return;
}
SpawnPlayer(clientId);
}
private void SpawnPlayer(ulong clientId)
{
Vector3 spawnPosition = GetSpawnPosition(clientId);
Quaternion spawnRotation = GetSpawnRotation(clientId);
GameObject playerPrefab = NetworkManager.Singleton.NetworkConfig.PlayerPrefab;
if (playerPrefab == null)
{
Debug.LogError("[Connection] PlayerPrefab이 null입니다!");
return;
}
GameObject playerObject = Instantiate(playerPrefab, spawnPosition, spawnRotation);
NetworkObject networkObject = playerObject.GetComponent<NetworkObject>();
if (networkObject == null)
{
Debug.LogError("[Connection] PlayerPrefab에 NetworkObject가 없습니다!");
return;
}
networkObject.SpawnAsPlayerObject(clientId);
Debug.Log($"<color=green>[Connection] 플레이어 {clientId} 스폰됨: {spawnPosition}</color>");
}
private void ApprovalCheck(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
// 🔍 디버깅: 스폰 포인트 상태 확인
spawnPoints.RemoveAll(p => p == null);
if (spawnPoints.Count == 0)
{
Debug.LogError($"<color=red>[Connection] 스폰 포인트가 없습니다! 씬에 PlayerSpawnPoint가 있는지 확인하세요.</color>");
}
response.Approved = true;
response.CreatePlayerObject = true;
// 스폰 위치 설정
response.Position = GetSpawnPosition(request.ClientNetworkId);
response.Rotation = GetSpawnRotation(request.ClientNetworkId);
Debug.Log($"<color=green>[Connection] 클라이언트 {request.ClientNetworkId} 승인됨. 스폰 위치: {response.Position}</color>");
response.CreatePlayerObject = false;
response.Position = Vector3.zero;
response.Rotation = Quaternion.identity;
Debug.Log($"<color=green>[Connection] 클라이언트 {request.ClientNetworkId} 승인됨. 수동 스폰으로 대기.</color>");
}
private Vector3 GetSpawnPosition(ulong clientId)
{
spawnPoints.RemoveAll(p => p == null);
if (spawnPoints.Count == 0)
{
Debug.LogWarning("[Connection] 스폰 포인트가 없습니다. 기본 위치 반환.");
@@ -106,6 +169,12 @@ namespace Northbound
_nextSpawnIndex = (_nextSpawnIndex + 1) % spawnPoints.Count;
}
spawnIndex = _clientSpawnIndices[clientId];
if (spawnIndex >= spawnPoints.Count)
{
Debug.LogWarning($"<color=yellow>[Connection] 스폰 인덱스 {spawnIndex}가 범위를 벗어났습니다. 기본값으로 조정.</color>");
spawnIndex = spawnIndex % spawnPoints.Count;
}
}
Debug.Log($"<color=yellow>[Connection] 클라이언트 {clientId}에게 스폰 인덱스 {spawnIndex} 할당</color>");
@@ -114,12 +183,19 @@ namespace Northbound
private Quaternion GetSpawnRotation(ulong clientId)
{
spawnPoints.RemoveAll(p => p == null);
if (spawnPoints.Count == 0)
return Quaternion.identity;
int spawnIndex = _clientSpawnIndices.ContainsKey(clientId)
? _clientSpawnIndices[clientId]
: 0;
if (spawnIndex >= spawnPoints.Count)
{
spawnIndex = spawnIndex % spawnPoints.Count;
}
return spawnPoints[spawnIndex].rotation;
}
@@ -130,6 +206,7 @@ namespace Northbound
{
NetworkManager.Singleton.ConnectionApprovalCallback = null;
NetworkManager.Singleton.OnServerStarted -= OnServerStarted;
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
}
}
}

View File

@@ -0,0 +1,86 @@
using System.Collections;
using UnityEngine;
using Unity.Netcode;
using Northbound;
public class NetworkDebug : MonoBehaviour
{
private void Start()
{
Debug.Log("=== NETWORK DEBUG INFO ===");
// 1. NetworkManager 상태
if (NetworkManager.Singleton != null)
{
Debug.Log($"NetworkManager: IsServer={NetworkManager.Singleton.IsServer}, IsClient={NetworkManager.Singleton.IsClient}, IsHost={NetworkManager.Singleton.IsHost}");
}
// 2. 스폰 포인트 검색
PlayerSpawnPoint[] spawnPoints = FindObjectsByType<PlayerSpawnPoint>(FindObjectsSortMode.None);
Debug.Log($"PlayerSpawnPoints: Found {spawnPoints.Length} objects");
foreach (var sp in spawnPoints)
{
Debug.Log($" - {sp.name} at {sp.transform.position}, isAvailable={sp.isAvailable}, spawnIndex={sp.spawnIndex}");
}
// 3. NetworkConnectionHandler 확인
var connectionHandler = FindObjectOfType<NetworkConnectionHandler>();
if (connectionHandler != null)
{
Debug.Log($"NetworkConnectionHandler: spawnPoints.Count={connectionHandler.spawnPoints.Count}, findAuto={connectionHandler.findSpawnPointsAutomatically}");
}
// 4. NetworkSpawnManager 확인
var spawnManager = FindObjectOfType<Northbound.NetworkSpawnManager>();
if (spawnManager != null)
{
Debug.Log($"NetworkSpawnManager: spawnPoints.Count={spawnManager.spawnPoints.Count}, findAuto={spawnManager.findSpawnPointsAutomatically}");
}
// 플레이어가 스폰될 때까지 대기
StartCoroutine(CheckPlayerAfterSpawn());
}
private IEnumerator CheckPlayerAfterSpawn()
{
// 플레이어가 스폰될 때까지 최대 5초 대기
for (int i = 0; i < 50; i++)
{
yield return new WaitForSeconds(0.1f);
var players = GameObject.FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
if (players != null && players.Length > 0)
{
Debug.Log($"Found {players.Length} player(s) - checking equipment...");
foreach (var playerCtrl in players)
{
Debug.Log($"Player found: {playerCtrl.name}, IsOwner={playerCtrl.IsOwner}");
var equipmentSocketOnPlayer = playerCtrl.GetComponent<Northbound.EquipmentSocket>();
var networkEquipSocketOnPlayer = playerCtrl.GetComponent<Northbound.NetworkEquipmentSocket>();
if (equipmentSocketOnPlayer != null)
Debug.Log($"Player has EquipmentSocket: YES");
else
Debug.LogWarning($"Player has EquipmentSocket: NO");
if (networkEquipSocketOnPlayer != null)
Debug.Log($"Player has NetworkEquipmentSocket: YES");
else
Debug.LogWarning($"Player has NetworkEquipmentSocket: NO");
}
Debug.Log("=== DEBUG INFO END ===");
yield break;
}
}
// 5초 후에도 플레이어가 없는지 확인
var finalCheck = GameObject.FindObjectsByType<NetworkPlayerController>(FindObjectsSortMode.None);
if (finalCheck == null || finalCheck.Length == 0)
{
Debug.LogWarning($"Player did not spawn within 5 seconds!");
}
}
}

View File

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

View File

@@ -0,0 +1,162 @@
using UnityEngine;
using System.Collections.Generic;
using Unity.Netcode;
namespace Northbound
{
/// <summary>
/// 네트워크 환경에서 작동하는 장비 소켓 관리
/// </summary>
public class NetworkEquipmentSocket : NetworkBehaviour
{
[System.Serializable]
public class Socket
{
public string socketName; // "RightHand", "LeftHand", "Back" 등
public Transform socketTransform; // 실제 본 Transform
[HideInInspector] public NetworkObject currentEquipmentNetworkObj; // 현재 장착된 장비 NetworkObject
[HideInInspector] public GameObject currentEquipment; // 현재 장착된 장비 GameObject
}
[Header("Available Sockets")]
public List<Socket> sockets = new List<Socket>();
private Dictionary<string, Socket> _socketDict = new Dictionary<string, Socket>();
private void Awake()
{
// 빠른 검색을 위한 딕셔너리 생성
foreach (var socket in sockets)
{
if (!string.IsNullOrEmpty(socket.socketName))
{
_socketDict[socket.socketName] = socket;
}
}
}
/// <summary>
/// 소켓에 장비 부착 (네트워크 스폰)
/// </summary>
public GameObject AttachToSocket(string socketName, GameObject equipmentPrefab)
{
if (!IsServer)
{
Debug.LogWarning("[NetworkEquipmentSocket] 서버에서만 장비를 장착할 수 있습니다.");
return null;
}
if (!_socketDict.TryGetValue(socketName, out Socket socket))
{
Debug.LogWarning($"[NetworkEquipmentSocket] 소켓을 찾을 수 없습니다: {socketName}");
return null;
}
if (socket.socketTransform == null)
{
Debug.LogWarning($"[NetworkEquipmentSocket] 소켓 Transform이 없습니다: {socketName}");
return null;
}
// 기존 장비 제거
DetachFromSocket(socketName);
// 새 장비 생성
if (equipmentPrefab != null)
{
GameObject equipment = Instantiate(equipmentPrefab, socket.socketTransform);
equipment.transform.localPosition = Vector3.zero;
equipment.transform.localRotation = Quaternion.identity;
// NetworkObject 확인
NetworkObject netObj = equipment.GetComponent<NetworkObject>();
if (netObj == null)
{
Debug.LogWarning($"[NetworkEquipmentSocket] 장비 프리팹에 NetworkObject가 없습니다: {equipmentPrefab.name}");
Destroy(equipment);
return null;
}
// 네트워크에 스폰
netObj.Spawn(true); // true = 소유자가 파괴되면 장비도 파괴
socket.currentEquipment = equipment;
socket.currentEquipmentNetworkObj = netObj;
Debug.Log($"<color=green>[NetworkEquipmentSocket] 장비 장착됨: {socketName} -> {equipmentPrefab.name}</color>");
return equipment;
}
return null;
}
/// <summary>
/// 소켓에서 장비 제거 (네트워크 디스폰)
/// </summary>
public void DetachFromSocket(string socketName)
{
if (!IsServer)
{
Debug.LogWarning("[NetworkEquipmentSocket] 서버에서만 장비를 제거할 수 있습니다.");
return;
}
if (!_socketDict.TryGetValue(socketName, out Socket socket))
return;
if (socket.currentEquipment != null)
{
// 네트워크에서 디스폰
if (socket.currentEquipmentNetworkObj != null && socket.currentEquipmentNetworkObj.IsSpawned)
{
socket.currentEquipmentNetworkObj.Despawn(true);
}
// 로컬 파괴
if (socket.currentEquipment != null)
{
Destroy(socket.currentEquipment);
}
socket.currentEquipment = null;
socket.currentEquipmentNetworkObj = null;
Debug.Log($"<color=yellow>[NetworkEquipmentSocket] 장비 제거됨: {socketName}</color>");
}
}
/// <summary>
/// 모든 소켓에서 장비 제거
/// </summary>
public void DetachAll()
{
if (!IsServer) return;
foreach (var socket in sockets)
{
if (socket.currentEquipment != null)
{
if (socket.currentEquipmentNetworkObj != null && socket.currentEquipmentNetworkObj.IsSpawned)
{
socket.currentEquipmentNetworkObj.Despawn(true);
}
Destroy(socket.currentEquipment);
socket.currentEquipment = null;
socket.currentEquipmentNetworkObj = null;
}
}
}
/// <summary>
/// 특정 소켓에 장비가 있는지 확인
/// </summary>
public bool HasEquipment(string socketName)
{
if (_socketDict.TryGetValue(socketName, out Socket socket))
{
return socket.currentEquipment != null;
}
return false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 58c239d2e984593429db90ada66cbdcf

View File

@@ -1,35 +1,30 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
public class NetworkManagerUI : MonoBehaviour
namespace Northbound
{
[Header("UI Buttons")]
[SerializeField] private Button hostButton;
[SerializeField] private Button clientButton;
[SerializeField] private Button serverButton;
private void Start()
/// <summary>
/// 키보드 단축키로 클라이언트/호스트 모드 시작
/// </summary>
public class ShortcutNetworkStarterUI : MonoBehaviour
{
if (hostButton != null)
hostButton.onClick.AddListener(() => NetworkManager.Singleton.StartHost());
[Header("UI Reference")]
[SerializeField] private Text statusText;
if (clientButton != null)
clientButton.onClick.AddListener(() => NetworkManager.Singleton.StartClient());
private void Start()
{
if (statusText != null)
{
statusText.text = "'C' 키를 누르면 클라이언트 모드 시작";
}
}
if (serverButton != null)
serverButton.onClick.AddListener(() => NetworkManager.Singleton.StartServer());
}
private void OnDestroy()
{
if (hostButton != null)
hostButton.onClick.RemoveAllListeners();
if (clientButton != null)
clientButton.onClick.RemoveAllListeners();
if (serverButton != null)
serverButton.onClick.RemoveAllListeners();
private void Update()
{
if (Input.GetKeyDown(KeyCode.BackQuote))
{
statusText.gameObject.SetActive(!statusText.gameObject.activeSelf);
}
}
}
}

View File

@@ -194,13 +194,15 @@ namespace Northbound
public void OnInteractionComplete()
{
if (!IsOwner) return;
if (_interactionTimeoutCoroutine != null)
{
StopCoroutine(_interactionTimeoutCoroutine);
_interactionTimeoutCoroutine = null;
}
_isInteracting = false;
Debug.Log("[PlayerInteraction] 상호작용 완료");
Debug.Log($"[PlayerInteraction] Owner {OwnerClientId} - 상호작용 완료");
}
// ========================================

View File

@@ -3,74 +3,57 @@ using UnityEngine;
namespace Northbound
{
/// <summary>
/// 플레이어의 자원 인벤토리 관리
/// </summary>
public class PlayerResourceInventory : NetworkBehaviour
{
[Header("Inventory Settings")]
public int maxResourceCapacity = 100; // 최대 자원 보유량
public int maxResourceCapacity = 100;
private int _displayAmount = 0;
private NetworkVariable<int> _currentResourceAmount = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
public int CurrentResourceAmount => _currentResourceAmount.Value;
public int CurrentResourceAmount => _displayAmount;
public int MaxResourceCapacity => maxResourceCapacity;
/// <summary>
/// 자원을 추가할 수 있는지 확인
/// </summary>
public override void OnNetworkSpawn()
{
if (IsClient && IsOwner)
{
RequestResourceUpdateServerRpc();
}
}
[Rpc(SendTo.Server)]
public void RequestResourceUpdateServerRpc()
{
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
int amount = resourceManager.GetPlayerResourceAmount(OwnerClientId);
UpdateResourceAmountClientRpc(amount);
}
}
[Rpc(SendTo.ClientsAndHost)]
private void UpdateResourceAmountClientRpc(int amount)
{
_displayAmount = amount;
}
public bool CanAddResource(int amount)
{
return _currentResourceAmount.Value + amount <= maxResourceCapacity;
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
return resourceManager.CanAddResource(OwnerClientId, amount);
}
return false;
}
/// <summary>
/// 추가 가능한 최대 자원량 계산
/// </summary>
public int GetAvailableSpace()
{
return maxResourceCapacity - _currentResourceAmount.Value;
}
/// <summary>
/// 자원 추가 (서버에서만 호출)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void AddResourceServerRpc(int amount)
{
if (amount <= 0) return;
int actualAmount = Mathf.Min(amount, maxResourceCapacity - _currentResourceAmount.Value);
_currentResourceAmount.Value += actualAmount;
Debug.Log($"플레이어 {OwnerClientId} - 자원 추가: +{actualAmount}, 현재: {_currentResourceAmount.Value}/{maxResourceCapacity}");
}
/// <summary>
/// 자원 제거 (서버에서만 호출)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void RemoveResourceServerRpc(int amount)
{
if (amount <= 0) return;
int actualAmount = Mathf.Min(amount, _currentResourceAmount.Value);
_currentResourceAmount.Value -= actualAmount;
Debug.Log($"플레이어 {OwnerClientId} - 자원 사용: -{actualAmount}, 현재: {_currentResourceAmount.Value}/{maxResourceCapacity}");
}
/// <summary>
/// 자원 설정 (서버에서만 호출)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void SetResourceServerRpc(int amount)
{
_currentResourceAmount.Value = Mathf.Clamp(amount, 0, maxResourceCapacity);
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
return resourceManager.GetAvailableSpace(OwnerClientId);
}
return 0;
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
namespace Northbound
{
@@ -112,6 +113,7 @@ namespace Northbound
{
_currentResources.Value = maxResources;
_lastRechargeTime = Time.time;
_lastGatheringTime = Time.time - gatheringCooldown;
}
}
@@ -137,28 +139,17 @@ namespace Northbound
public bool CanInteract(ulong playerId)
{
// 자원 노드에 자원이 없으면 상호작용 불가
if (_currentResources.Value <= 0)
return false;
// 쿨다운 확인
if (Time.time - _lastGatheringTime < gatheringCooldown)
return false;
// 플레이어 인벤토리 확인
if (NetworkManager.Singleton != null &&
NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
if (client.PlayerObject != null)
{
var playerInventory = client.PlayerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
// 플레이어가 받을 수 있는 공간이 없으면 상호작용 불가
if (playerInventory.GetAvailableSpace() <= 0)
return false;
}
}
if (resourceManager.GetAvailableSpace(playerId) <= 0)
return false;
}
return true;
@@ -211,17 +202,11 @@ namespace Northbound
if (!CanInteract(playerId))
return;
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject == null)
var resourceManager = ServerResourceManager.Instance;
if (resourceManager == null)
return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory == null)
{
return;
}
int playerAvailableSpace = playerInventory.GetAvailableSpace();
int playerAvailableSpace = resourceManager.GetAvailableSpace(playerId);
int gatheredAmount = Mathf.Min(
resourcesPerGathering,
@@ -237,12 +222,27 @@ namespace Northbound
_currentResources.Value -= gatheredAmount;
_lastGatheringTime = Time.time;
playerInventory.AddResourceServerRpc(gatheredAmount);
resourceManager.AddResource(playerId, gatheredAmount);
UpdatePlayerResourcesClientRpc(playerId);
ShowGatheringEffectClientRpc();
}
[Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId)
{
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject != null)
{
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
playerInventory.RequestResourceUpdateServerRpc();
}
}
}
[Rpc(SendTo.NotServer)]
private void ShowGatheringEffectClientRpc()
{
if (gatheringEffectPrefab != null && effectSpawnPoint != null)
@@ -275,4 +275,4 @@ namespace Northbound
return transform;
}
}
}
}

View File

@@ -32,24 +32,14 @@ namespace Northbound
public bool CanInteract(ulong playerId)
{
// 이미 수집됨
if (_isCollected)
return false;
// 플레이어 인벤토리 확인
if (NetworkManager.Singleton != null &&
NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
var resourceManager = ServerResourceManager.Instance;
if (resourceManager != null)
{
if (client.PlayerObject != null)
{
var playerInventory = client.PlayerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
// 플레이어가 받을 수 있는 공간이 없으면 상호작용 불가
if (playerInventory.GetAvailableSpace() <= 0)
return false;
}
}
if (resourceManager.GetAvailableSpace(playerId) <= 0)
return false;
}
return true;
@@ -69,49 +59,52 @@ namespace Northbound
if (!CanInteract(playerId))
return;
// 중복 수집 방지
if (_isCollected)
return;
_isCollected = true;
// 플레이어의 인벤토리 확인
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject == null)
return;
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory == null)
var resourceManager = ServerResourceManager.Instance;
if (resourceManager == null)
{
Debug.LogWarning($"플레이어 {playerId}에게 PlayerResourceInventory 컴포넌트가 없습니다.");
Debug.LogWarning("ServerResourceManager 인스턴스를 찾을 수 없습니다.");
_isCollected = false;
return;
}
// 플레이어가 받을 수 있는 최대량 계산
int playerAvailableSpace = playerInventory.GetAvailableSpace();
// 실제 지급할 양 계산
int playerAvailableSpace = resourceManager.GetAvailableSpace(playerId);
int collectedAmount = Mathf.Min(resourceAmount, playerAvailableSpace);
if (collectedAmount <= 0)
{
Debug.Log($"플레이어 {playerId}의 인벤토리가 가득 찼습니다.");
_isCollected = false; // 수집 실패 시 다시 시도 가능하도록
_isCollected = false;
return;
}
// 플레이어에게 자원 추가
playerInventory.AddResourceServerRpc(collectedAmount);
resourceManager.AddResource(playerId, collectedAmount);
UpdatePlayerResourcesClientRpc(playerId);
Debug.Log($"플레이어 {playerId}가 {collectedAmount} {resourceName}을(를) 획득했습니다.");
// 이펙트 표시 및 오브젝트 제거
ShowPickupEffectClientRpc();
// 짧은 딜레이 후 제거 (이펙트를 위해)
Invoke(nameof(DestroyPickup), 0.1f);
}
[Rpc(SendTo.ClientsAndHost)]
private void UpdatePlayerResourcesClientRpc(ulong playerId)
{
var playerObject = NetworkManager.Singleton.ConnectedClients[playerId].PlayerObject;
if (playerObject != null)
{
var playerInventory = playerObject.GetComponent<PlayerResourceInventory>();
if (playerInventory != null)
{
playerInventory.RequestResourceUpdateServerRpc();
}
}
}
[Rpc(SendTo.ClientsAndHost)]
private void ShowPickupEffectClientRpc()
{

View File

@@ -0,0 +1,101 @@
using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
namespace Northbound
{
public class ServerResourceManager : NetworkBehaviour
{
public static ServerResourceManager Instance { get; private set; }
private Dictionary<ulong, int> _playerResources = new Dictionary<ulong, int>();
private NetworkVariable<int> _resourcesData = new NetworkVariable<int>();
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
Instance = this;
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
}
}
public override void OnNetworkDespawn()
{
if (IsServer)
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnected;
}
}
}
private void OnClientConnected(ulong clientId)
{
_playerResources[clientId] = 0;
}
private void OnClientDisconnected(ulong clientId)
{
_playerResources.Remove(clientId);
}
public int GetPlayerResourceAmount(ulong clientId)
{
if (_playerResources.TryGetValue(clientId, out var resource))
{
return resource;
}
return 0;
}
public bool CanAddResource(ulong clientId, int amount)
{
if (_playerResources.TryGetValue(clientId, out var resource))
{
return resource + amount <= 100;
}
return false;
}
public int GetAvailableSpace(ulong clientId)
{
if (_playerResources.TryGetValue(clientId, out var resource))
{
return 100 - resource;
}
return 0;
}
public void AddResource(ulong clientId, int amount)
{
if (_playerResources.TryGetValue(clientId, out var resource))
{
int actualAmount = Mathf.Min(amount, 100 - resource);
_playerResources[clientId] = resource + actualAmount;
}
}
public void RemoveResource(ulong clientId, int amount)
{
if (_playerResources.TryGetValue(clientId, out var resource))
{
int actualAmount = Mathf.Min(amount, resource);
_playerResources[clientId] = resource - actualAmount;
}
}
}
}

View File

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

View File

@@ -0,0 +1,50 @@
using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Netcode;
namespace Northbound
{
public class ShortcutNetworkStarter : MonoBehaviour
{
[Header("Input Actions")]
[SerializeField] private InputActionReference startAsClientAction;
private bool _isClientStarted = false;
private void Start()
{
if (startAsClientAction != null && startAsClientAction.action != null)
{
startAsClientAction.action.performed += StartAsClient;
}
}
private void OnDestroy()
{
if (startAsClientAction != null && startAsClientAction.action != null)
{
startAsClientAction.action.performed -= StartAsClient;
}
}
private void StartAsClient(InputAction.CallbackContext context)
{
if (NetworkManager.Singleton == null)
{
Debug.LogError("[ShortcutNetworkStarter] NetworkManager.Singleton이 null입니다!");
return;
}
if (NetworkManager.Singleton.IsClient)
{
Debug.Log("[ShortcutNetworkStarter] 이미 클라이언트 모드입니다.");
return;
}
_isClientStarted = true;
Debug.Log("[ShortcutNetworkStarter] 클라이언트 모드로 시작합니다...");
NetworkManager.Singleton.StartClient();
}
}
}

View File

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