네트워크 동기화 문제 해결

몬스터와 크립에 네트워크 관련 컴포넌트가 없는 문제 수정
포탈/캠프와 몬스터/크립 간의 계층 구조 해제
 - 네트워크 오브젝트끼리 계층 구조로 둘 수 없음
This commit is contained in:
2026-02-04 18:16:59 +09:00
parent ea80402fc1
commit 4bd46b2a0a
25 changed files with 1241 additions and 174 deletions

View File

@@ -190,9 +190,7 @@ namespace Northbound
networkObj = creep.AddComponent<NetworkObject>();
}
networkObj.Spawn();
creep.transform.SetParent(transform);
networkObj.SpawnWithOwnership(NetworkManager.Singleton.LocalClientId);
}
private void OnDrawGizmos()

View File

@@ -4,7 +4,7 @@ using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public class EnemyPortal : MonoBehaviour
public class EnemyPortal : NetworkBehaviour
{
[System.Serializable]
public class MonsterEntry
@@ -131,6 +131,8 @@ public class EnemyPortal : MonoBehaviour
private void SpawnEnemy(GameObject prefab)
{
if (!IsServer) return;
GameObject enemy = Instantiate(prefab, transform);
if (enemy.GetComponent<FogOfWarVisibility>() == null)
@@ -140,7 +142,7 @@ public class EnemyPortal : MonoBehaviour
visibility.updateInterval = 0.2f;
}
enemy.GetComponent<NetworkObject>().Spawn();
enemy.GetComponent<NetworkObject>().SpawnWithOwnership(NetworkManager.Singleton.LocalClientId);
}
private void IncreaseCost()

View File

@@ -0,0 +1,334 @@
# 🎯 최종 진단 및 해결 가이드
## 📊 현재 상황 분석
### 성공한 사항
- ✅ yougetsignal: 포트 4045 접근 가능 (외부에서 열림)
- ✅ 방화벽: 허용 (yougetsignal 성공으로 증명)
- ✅ OS 바인딩: 가능 (PortListenerTest 증명)
- ✅ NetworkManager: IsServer=True, IsHost=True
- ✅ Player Prefab: Player (할당됨)
- ✅ Unity Transport: 4045, 0.0.0.0
- ✅ 모든 설정: 올바름
### 문제: 유니티 Netcode가 반복적으로 네트워크를 종료
```
상황:
- 게임 시작 → 호스트 시작됨
- 잠시후 (1-30초) → 서버 종료됨
- 포트 4045: 외부에서 닫힘
- yougetsignal: "closed"
```
---
## 🔍 문제 원인
### 가장 흔한 가능성 1: AutoHost 간섭
```
AutoHost가 다음과 같은 행동:
- Start()에서 자동으로 StartHost() 호출
- NetworkManager가 이미 실행 중이면
→ 다시 시도하거나
→ 무시하고 넘어감
- 하지만 연결 승인 처리 중 오류가 발생하면
→ 자동으로 종료하거나
→ 포트가 닫힘
```
**해결책:**
```
씬에서 AutoHost 컴포넌트 제거
또는 Enable Auto Host 체크 해제
```
### 가장 흔한 가능성 2: NetworkConnectionHandler 승인 오류
```
NetworkConnectionHandler.ApprovalCheck()에서:
- 스폰 포인트 모두 찾았음
- 하지만 승인: response.Approved = true
- response.CreatePlayerObject = false
- response.Position/Rotation 설정
이 상황에서:
- Netcode는 "수동 스폰으로 대기" 상태로 됨
- 하지만 실제 플레이어 스폰하지 않음 (CreatePlayerObject=false)
- 연결 시간초과 또는 서버 측에서 종료
```
**해결책:**
```
NetworkConnectionHandler.ApprovalCheck() 수정:
response.Approved = true;
response.CreatePlayerObject = false;
response.PlayerPrefabHash = 0;
response.Position = Vector3.zero;
response.Rotation = Quaternion.identity;
response.Approved = true;
response.CreatePlayerObject = true; // 스폰 생성!
response.PlayerPrefabHash = NetworkManager.Singleton.NetworkConfig.PlayerPrefab.GetComponent<NetworkObject>().GlobalObjectIdHash;
```
### 가능성 3: 다른 스크립트에서 Shutdown() 호출
```
AutoHost, 네트워크 관련 스크립트 등에서
- 명시적이든 아니든 NetworkManager.Shutdown()를 호출
- 연결 제대로 되지 않아도 호출됨
```
**해결책:**
```
- 모든 스크립트에서 Shutdown() 호출 제거
- 또는 명시적인 조건문 추가
```
### 가능성 4: 네트워크 이벤트에서 예외 발생
```
NetworkConnectionHandler의 다른 이벤트 핸들러에서
- 예외가 발생해 NetworkManager.Shutdown() 트리거
```
**해결책:**
```
try-catch로 래핑 및 로깅 추가
```
---
## ✅ 해결 단계 (순서대로 수행)
### 1단계: AutoHost 제거 (가장 중요!)
```
1. 씬에서 AutoHost 게임오브젝트 찾기
2. 제거 (컴포넌트 우클릭 → Remove Component)
3. 또는 Enable Auto Host 체크 해제
```
### 2단계: SimpleNetworkMonitor 추가
```
1. SimpleNetworkMonitor를 씬에 추가
2. 플레이하여 5-10초간 관찰
3. "Server stopped X times!" 메시지가 나오면
→ 이것이 문제 원인 확인!
```
### 3단계: 문제 원인 찾기
```
Monitor에서 다음을 확인:
정상적인 경우:
- Server started (Count: 1)
- 그 후 중지 중단 없음
- Server uptime: 계속 증가
불안정적인 경우 (현재 문제):
- Server started (Count: 1)
- Server stopped (Count: 1+)
- Server started (Count: 2+)
- Server stopped (Count: 2+)
- 짧은 업타임 (1-30초)
```
### 4단계: AutoHost가 원인인지 확인
```
1. AutoHost 제거 후 테스트
2. 호스트 시작
3. Monitor 관찰
4. 포트 열린 상태 확인 (yougetsignal)
이것으로 해결되면:
→ AutoHost가 문제였음!
해결되지 않으면:
→ 다른 원인 찾아야 함
```
---
## 🎯 팀원과 테스트 방법
### 호스트 (당신)
```
1. AutoHost 제거
2. SimpleNetworkMonitor 추가
3. 호스트 시작
4. Monitor에서 "Server uptime" 관찰
5. yougetsignal에서 포트 4045 확인
6. 팀원에게 퍼블릭 IP 공유
```
### 팀원
```
1. 에디터에서 클라이언트 모드 시작
2. 당신의 퍼블릭 IP 입력: 4045
3. "연결" 클릭
4. 성공: 플레이어 스폰, 게임 시작! 🎉
```
---
## 📊 예상 결과
### 성공한 경우
```
Monitor 로그:
[SimpleMonitor] Server started (Count: 1)
[SimpleMonitor] Server uptime: 10s, 20s, 30s...
[SimpleMonitor] Client connected: [팀원 ID]
팀원:
- 접속 성공!
- 플레이어 스폰됨
- 게임 시작 가능!
yougetsignal:
- Port 4045: open
```
### 실패하는 경우 (현재)
```
Monitor 로그:
[SimpleMonitor] Server started (Count: 1)
[SimpleMonitor] Server stopped (Count: 1, Uptime: 5s)
[SimpleMonitor] Server started (Count: 2)
[SimpleMonitor] Server stopped (Count: 2, Uptime: 8s)
팀원:
- 접속 시도
- "Failed to connect" 에러
- 포트가 닫힘 있어서 실패
```
---
## 🔧 필수 조치
### 1. AutoHost 제거 (즉시)
```
AutoHost는 게임에 필요 없음
- 네트워크 연결 UI를 사용하세요
- NetworkConnectionHelper나
- 또는 직접 NetworkManager.StartHost()
```
### 2. 설정 확인
```
NetworkManager → Network Config:
✓ Player Prefab: Player
✓ Connection Approval: 체크
✓ Enable Scene Management: 체크
Unity Transport:
✓ Port: 4045
✓ Address: 0.0.0.0
✓ ServerListenAddress: 0.0.0.0
```
### 3. 방화벽 확인
```
Windows 방화벽:
- 포트 4045 허용
- Unity Editor 허용
- 빌드 실행파일 허용
라우터:
- 포트 포워딩 구성됨
```
---
## 📋 테스트 체크리스트
### 호스트 측
- [ ] AutoHost 제거됨
- [ ] SimpleNetworkMonitor 추가됨
- [ ] 호스트 시작 성공
- [ ] 서버 중지 없음 (최소 30초)
- [ ] Monitor: "Server uptime: 30s+" 표시
- [ ] yougetsignal: "Port 4045 is open"
### 팀원 측
- [ ] 퍼블릭 IP 입력: 4045
- [ ] "연결" 클릭
- [ ] 접속 성공
- [ ] 플레이어 스폰됨
- [ ] 게임 시작 가능
---
## 🚀 다음 액션
### 지금 당장 할 것
```
1. AutoHost 제거 (씬에서 찾기 → Remove Component)
2. 플레이하여 10초간 관찰
3. Monitor 로그 확인
4. "Server stopped X times!" 보이면
→ 이것이 해결해야 할 포인트!
```
### Monitor가 보여주는 것
```
정상:
✓ Server started (Count: 1)
✓ Uptime: 계속 증가
✓ Client connected: [ID]
불안정 (현재):
✗ Server stopped (Count: 1)
✗ Server stopped (Count: 2+)
✗ 짧은 업타임
✗ Uptime: 5s, 10s, 15s
```
---
## 🎯 해결 우선순위
### 1순위: AutoHost 제거 (가장 중요!)
- 이것이 가장 흔한 원인
- 완전히 제거 필요
### 2순위: Monitor 관찰
- 5-10초 관찰
- Server가 안정적인지 확인
### 3순위: 팀원 테스트
- 설정이 안정적이면 테스트
- 문제 지속되면 추가 조치
### 4순위: 네트워크 로그 분석
- Console에서 경고/에러 확인
- Shutdown()이 언제 호출되는지 파악
---
## 💡 성공하면 보게 될 것
```
Monitor: Server uptime: 60s+
yougetsignal: Port 4045 is open
팀원: 접속 성공!
게임: 팀원 플레이어 스폰됨, 시작 가능! 🎉
```
---
## 📞 참고
이 가이드의 모든 설정이:
- ✅ yougetsignal 성공으로 검증됨 (외부 접근 가능)
- ✅ 포트 포워딩 작동 중
- ✅ 방화벽 허용
- ✅ OS 바인딩 가능
현재 유일한 문제:
- 유니티 Netcode가 반복적으로 서버를 중지
- 포트가 1-30초 후 닫힘
해결책: AutoHost 제거가 이 문제를 해결할 가능성이 가장 높음!

View File

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

View File

@@ -25,21 +25,21 @@ namespace Northbound
private NetworkVariable<float> _currentTime = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
NetworkVariableWritePermission.Owner
);
// 타이머 실행 중 여부
private NetworkVariable<bool> _isRunning = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
NetworkVariableWritePermission.Owner
);
// 현재 사이클 번호
private NetworkVariable<int> _cycleCount = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
NetworkVariableWritePermission.Owner
);
// 이벤트
@@ -83,7 +83,7 @@ namespace Northbound
private void Update()
{
if (!IsServer || !_isRunning.Value)
if (!IsServer || !IsOwner || !_isRunning.Value)
return;
_currentTime.Value -= Time.deltaTime;

View File

@@ -116,7 +116,6 @@ namespace Northbound
private List<Vector3> _spawnedPositions = new List<Vector3>();
private List<Vector3> _creepCampPositions = new List<Vector3>();
private List<GameObject> _creepPrefabs = new List<GameObject>();
private Transform _objectsParent;
[System.Serializable]
public class ObstacleEntry
@@ -188,19 +187,6 @@ namespace Northbound
private void GenerateMap()
{
if (groupUnderParent)
{
_objectsParent = new GameObject("Generated Map Objects").transform;
_objectsParent.SetParent(transform);
NetworkObject parentNetworkObj = _objectsParent.gameObject.GetComponent<NetworkObject>();
if (parentNetworkObj == null)
{
parentNetworkObj = _objectsParent.gameObject.AddComponent<NetworkObject>();
}
parentNetworkObj.Spawn();
}
_spawnedPositions.Clear();
_creepCampPositions.Clear();
@@ -412,7 +398,7 @@ namespace Northbound
for (int i = 0; i < _generatedResources.Length; i++)
{
GameObject resourceObj = Instantiate(resourcePrefab, new Vector3(_generatedResources[i].position.x, 1f, _generatedResources[i].position.y), Quaternion.identity);
Resource resource = resourceObj.GetComponent<Resource>();
if (resource == null)
{
@@ -422,15 +408,11 @@ namespace Northbound
}
resource.InitializeQuality(_generatedResources[i].qualityModifier);
NetworkObject networkObj = resourceObj.GetComponent<NetworkObject>();
if (networkObj != null)
{
networkObj.Spawn();
if (groupUnderParent && _objectsParent != null)
{
resourceObj.transform.SetParent(_objectsParent);
}
}
else
{
@@ -557,11 +539,6 @@ namespace Northbound
networkObj.Spawn();
if (groupUnderParent && _objectsParent != null)
{
obstacle.transform.SetParent(_objectsParent);
}
if (obstacle.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = obstacle.AddComponent<FogOfWarVisibility>();
@@ -657,18 +634,7 @@ namespace Northbound
private int CountObstacleType(GameObject prefab)
{
int count = 0;
if (_objectsParent != null)
{
foreach (Transform child in _objectsParent)
{
if (child.name.StartsWith(prefab.name))
{
count++;
}
}
}
return count;
return 0;
}
#endregion
@@ -860,6 +826,13 @@ namespace Northbound
{
GameObject campObj = Instantiate(creepCampPrefab, position, Quaternion.identity);
if (campObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = campObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false;
visibility.updateInterval = 0.2f;
}
CreepCamp creepCamp = campObj.GetComponent<CreepCamp>();
if (creepCamp == null)
{
@@ -879,18 +852,6 @@ namespace Northbound
}
networkObj.Spawn();
if (groupUnderParent && _objectsParent != null)
{
campObj.transform.SetParent(_objectsParent);
}
if (campObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = campObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false;
visibility.updateInterval = 0.2f;
}
}
#endregion
@@ -912,34 +873,6 @@ namespace Northbound
public void ClearGeneratedObjects()
{
if (_objectsParent != null)
{
NetworkObject parentNetworkObj = _objectsParent.GetComponent<NetworkObject>();
if (parentNetworkObj != null && parentNetworkObj.IsSpawned)
{
parentNetworkObj.Despawn(true);
}
foreach (Transform child in _objectsParent)
{
NetworkObject networkObj = child.GetComponent<NetworkObject>();
if (networkObj != null && networkObj.IsSpawned)
{
networkObj.Despawn(true);
}
}
if (Application.isPlaying)
{
Destroy(_objectsParent.gameObject);
}
else
{
DestroyImmediate(_objectsParent.gameObject);
}
_objectsParent = null;
}
_spawnedPositions.Clear();
}

View File

@@ -32,13 +32,13 @@ namespace Northbound
private NetworkVariable<float> _networkSpeed = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
NetworkVariableWritePermission.Owner
);
private NetworkVariable<bool> _networkIsMoving = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
NetworkVariableWritePermission.Owner
);
public override void OnNetworkSpawn()
@@ -101,7 +101,7 @@ namespace Northbound
{
if (!IsSpawned) return;
if (IsServer)
if (IsServer && IsOwner)
{
UpdateServerSide();
}

View File

@@ -0,0 +1,67 @@
using UnityEngine;
using Unity.Netcode;
namespace Northbound
{
/// <summary>
/// NetworkConfig의 모든 설정을 로그로 출력하여 서버/클라이언트 차이점을 확인
/// </summary>
public class NetworkConfigDebugger : MonoBehaviour
{
[ContextMenu("Log NetworkConfig Details")]
public void LogNetworkConfig()
{
if (NetworkManager.Singleton == null)
{
Debug.LogError("[NetworkConfigDebugger] NetworkManager not found!");
return;
}
Debug.Log("=== NETWORK CONFIG DEBUGGER ===");
var config = NetworkManager.Singleton.NetworkConfig;
var transport = NetworkManager.Singleton.GetComponent<Unity.Netcode.Transports.UTP.UnityTransport>();
Debug.Log("<color=cyan>Basic Config:</color>");
Debug.Log($" ProtocolVersion: {config.ProtocolVersion}");
Debug.Log($" TickRate: {config.TickRate}");
Debug.Log($" ClientConnectionBufferTimeout: {config.ClientConnectionBufferTimeout}");
Debug.Log($" EnsureNetworkVariableLengthSafety: {config.EnsureNetworkVariableLengthSafety}");
Debug.Log("<color=cyan>Connection Settings:</color>");
Debug.Log($" ConnectionApproval: {config.ConnectionApproval}");
Debug.Log($" EnableSceneManagement: {config.EnableSceneManagement}");
Debug.Log($" EnableNetworkLogs: {config.EnableNetworkLogs}");
Debug.Log("<color=cyan>Spawn Settings:</color>");
Debug.Log($" ForceSamePrefabs: {config.ForceSamePrefabs}");
Debug.Log($" RecycleNetworkIds: {config.RecycleNetworkIds}");
Debug.Log($" PlayerPrefab: {config.PlayerPrefab?.name ?? "NULL"}");
Debug.Log("<color=cyan>NetworkPrefabs ({config.Prefabs.Prefabs.Count}):</color>");
foreach (var prefab in config.Prefabs.Prefabs)
{
Debug.Log($" - {prefab.Prefab.name} (Hash: {prefab.Prefab.GetHashCode()})");
}
Debug.Log("<color=cyan>Transport:</color>");
if (transport != null)
{
Debug.Log($" Address: {transport.ConnectionData.Address}");
Debug.Log($" Port: {transport.ConnectionData.Port}");
Debug.Log($" ServerData: {transport.ConnectionData.ServerListenAddress}:{transport.ConnectionData.Port}");
}
else
{
Debug.LogError(" Transport not found!");
}
Debug.Log("================================");
}
private void Start()
{
Invoke(nameof(LogNetworkConfig), 1f);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8ab98e4f98fbe4e44a4a2f7cb8fe59ca

View File

@@ -0,0 +1,115 @@
using UnityEngine;
using Unity.Netcode;
namespace Northbound
{
/// <summary>
/// Simple monitor for network restarts
/// Detects if Unity Netcode is shutting down repeatedly
/// </summary>
public class SimpleNetworkMonitor : MonoBehaviour
{
[Header("Settings")]
[SerializeField] private bool enableOnStart = true;
[SerializeField] private float checkInterval = 2f;
private NetworkManager _networkManager;
private float _lastCheckTime;
private int _serverStopCount = 0;
private int _serverStartCount = 0;
private float _lastServerStartTime = 0f;
private int _eventsLogged = 0;
private void Start()
{
if (enableOnStart)
{
_networkManager = NetworkManager.Singleton;
Debug.Log("[SimpleMonitor] Simple Network Monitor started");
}
}
private void Update()
{
if (_networkManager == null)
{
_networkManager = NetworkManager.Singleton;
}
if (Time.time - _lastCheckTime >= checkInterval)
{
_lastCheckTime = Time.time;
CheckNetworkStatus();
}
}
private void CheckNetworkStatus()
{
if (_networkManager == null)
{
Debug.LogWarning("[SimpleMonitor] NetworkManager not found");
return;
}
bool isServer = _networkManager.IsServer;
bool isClient = _networkManager.IsClient;
bool isHost = _networkManager.IsHost;
if (isServer && !isClient && _eventsLogged < 3)
{
_serverStartCount++;
_lastServerStartTime = Time.time;
Debug.Log("[SimpleMonitor] Server started (Count: " + _serverStartCount + ")");
_eventsLogged++;
}
if (!isServer && !isClient && _lastServerStartTime > 0f && _eventsLogged < 50)
{
float uptime = Time.time - _lastServerStartTime;
_serverStopCount++;
Debug.LogWarning("[SimpleMonitor] Server stopped (Count: " + _serverStopCount + ", Uptime: " + uptime.ToString("F1") + "s)");
_eventsLogged++;
if (_serverStopCount >= 3)
{
Debug.LogError("[SimpleMonitor] SERVER STOPPED " + _serverStopCount + " TIMES!");
Debug.LogError("[SimpleMonitor] This is why port closes!");
Debug.LogError("[SimpleMonitor] Something is calling NetworkManager.Shutdown()");
}
}
if (isServer && _serverStartCount > 0)
{
if (_eventsLogged % 10 == 0)
{
float uptime = Time.time - _lastServerStartTime;
Debug.Log("[SimpleMonitor] Server uptime: " + uptime.ToString("F1") + "s");
_eventsLogged++;
}
}
}
[ContextMenu("Reset Counters")]
public void ResetCounters()
{
_serverStopCount = 0;
_serverStartCount = 0;
_lastServerStartTime = 0f;
_eventsLogged = 0;
Debug.Log("[SimpleMonitor] Counters reset");
}
[ContextMenu("Show Stats")]
public void ShowStats()
{
Debug.Log("=== SIMPLE MONITOR STATS ===");
Debug.Log("Server Stops: " + _serverStopCount);
Debug.Log("Server Starts: " + _serverStartCount);
Debug.Log("NetworkManager:");
Debug.Log(" IsServer: " + (_networkManager != null ? _networkManager.IsServer.ToString() : "null"));
Debug.Log(" IsClient: " + (_networkManager != null ? _networkManager.IsClient.ToString() : "null"));
Debug.Log(" IsHost: " + (_networkManager != null ? _networkManager.IsHost.ToString() : "null"));
Debug.Log("============================");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 977685b2792b7684dbc782d410338a51