Photon Fusion 2 멀티씬 네트워킹 완전 가이드
Photon Fusion 2 멀티씬 네트워킹 완전 가이드
개요
본 문서는 Photon Fusion 2에서 공유모드(Shared Mode)와 멀티씬 로딩(Scene.LoadSceneMode.Additive)을 활용한 네트워킹 구현에 대한 완전한 가이드를 제공합니다. 방 생성부터 통신, 게임 결과 반영까지의 전체 프로세스를 다룹니다.
1. Photon Fusion 2 아키텍처 개요
1.1 Network Runner와 API 구조
Photon Fusion 2는 NetworkRunner를 핵심 컴포넌트로 사용하여 네트워크 시뮬레이션을 실행합니다. NetworkRunner는 Fusion API를 통해 다음과 같은 역할을 수행합니다:
- 네트워크 세션 생성 및 관리
- 틱 기반 시뮬레이션 실행
- 클라이언트 연결 상태 관리
- Unity 씬과의 동기화 처리
// 기본 NetworkRunner 설정
private NetworkRunner _runner;
async void InitializeNetworking()
{
_runner = gameObject.AddComponent<NetworkRunner>();
_runner.ProvideInput = true;
// NetworkRunner가 INetworkRunnerCallbacks를 자동으로 감지
var result = await _runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Shared,
SessionName = "MultiSceneRoom",
Scene = SceneRef.FromIndex(0),
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
});
}
1.2 데이터 흐름 메커니즘
- NetworkRunner → Fusion API를 통해 네트워크 이벤트 처리
- 네트워크 상태 동기화 → Unity GameObject와 실시간 연동
- NetworkBehaviour → 네트워크 데이터 송수신 담당
2. 개발환경 설정
2.1 시스템 요구사항
- Unity 버전: Unity 2022.3 LTS 이상 권장
- .NET 지원: .NET Standard 2.1
- Photon AppId: Photon 대시보드에서 발급 필요
2.2 프로젝트 초기 설정
[CreateAssetMenu(menuName = "Fusion/App Settings")]
public class MyAppSettings : ScriptableObject
{
[Header("Photon Configuration")]
public string AppId;
public string Region = "asia";
[Header("Game Settings")]
public int MaxPlayers = 8;
public float TickRate = 60f;
}
3. 기본 모듈 구성
3.1 NetworkBehaviour 구현
public class PlayerController : NetworkBehaviour
{
[Networked] public Vector3 NetworkPosition { get; set; }
[Networked] public float Health { get; set; }
[Networked] public NetworkButtons ButtonsPrevious { get; set; }
public override void FixedUpdateNetwork()
{
// 네트워크 틱마다 실행되는 로직
if (GetInput<NetworkInputData>(out var input))
{
ProcessMovement(input);
ProcessActions(input);
}
}
private void ProcessMovement(NetworkInputData input)
{
var movement = input.MovementInput * Runner.DeltaTime * 5f;
NetworkPosition += new Vector3(movement.x, 0, movement.y);
}
}
3.2 입력 시스템 구성
public struct NetworkInputData : INetworkInput
{
public Vector2 MovementInput;
public NetworkButtons Buttons;
public float MouseX;
public float MouseY;
}
public class InputHandler : MonoBehaviour, INetworkBehaviour
{
public void OnInput(NetworkRunner runner, NetworkInputData input)
{
input.MovementInput = new Vector2(
Input.GetAxis("Horizontal"),
Input.GetAxis("Vertical")
);
input.Buttons.Set(InputButtons.Fire, Input.GetKey(KeyCode.Space));
input.Buttons.Set(InputButtons.Jump, Input.GetKey(KeyCode.LeftShift));
}
}
4. NetworkSceneManager 생성 및 구현
참고 문서: [Fusion 2 - Setting Up A Scene Photon Engine](https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/2-setting-up-a-scene)
4.1 기본 NetworkSceneManagerDefault 사용
공식 문서에 따르면, StartGame
메서드에서 gameObject.AddComponent<NetworkSceneManagerDefault>()
를 사용합니다:
public class BasicSpawner : MonoBehaviour, INetworkRunnerCallbacks
{
private NetworkRunner _runner;
async void StartGame()
{
_runner = gameObject.AddComponent<NetworkRunner>();
_runner.ProvideInput = true;
await _runner.StartGame(new StartGameArgs()
{
GameMode = GameMode.Host,
SessionName = "TestRoom",
Scene = SceneRef.FromIndex(0),
// NetworkSceneManagerDefault 컴포넌트 추가
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
});
}
}
4.2 커스텀 NetworkSceneManager 구현
참고 문서: [Fusion 2 - Scene Loading Photon Engine](https://doc.photonengine.com/fusion/current/manual/scene-loading)
NetworkSceneManagerDefault 구현에서는 Version 필드가 카운터로 작동하여 씬이 로드되거나 언로드될 때마다 증가합니다:
public class CustomNetworkSceneManager : NetworkSceneManagerDefault
{
[Header("Multi-Scene Configuration")]
public List<SceneReference> additiveScenes = new List<SceneReference>();
public override Task LoadSceneAsync(NetworkSceneInfo sceneInfo,
LoadSceneParameters sceneParameters,
bool clientSynchronization = true)
{
Debug.Log($"[SceneManager] Loading scene: {sceneInfo.Scene}");
// 멀티씬을 위한 커스텀 로직
if (sceneParameters.loadSceneMode == LoadSceneMode.Additive)
{
return LoadAdditiveSceneWithNetworking(sceneInfo, sceneParameters);
}
return base.LoadSceneAsync(sceneInfo, sceneParameters, clientSynchronization);
}
private async Task LoadAdditiveSceneWithNetworking(NetworkSceneInfo sceneInfo,
LoadSceneParameters parameters)
{
// Unity SceneManager로 additive 로드
var operation = SceneManager.LoadSceneAsync(sceneInfo.Scene.AssetGuidString,
LoadSceneMode.Additive);
// 로딩 진행률 모니터링
while (!operation.isDone)
{
Debug.Log($"Scene loading progress: {operation.progress * 100f}%");
await Task.Yield();
}
// 로드된 씬의 NetworkObject들을 Runner에 등록
var loadedScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
await RegisterSceneNetworkObjects(loadedScene);
Debug.Log($"[SceneManager] Scene {sceneInfo.Scene} loaded successfully");
}
private async Task RegisterSceneNetworkObjects(Scene scene)
{
var networkObjects = new List<NetworkObject>();
// 씬의 모든 루트 오브젝트에서 NetworkObject 수집
foreach (GameObject rootObj in scene.GetRootGameObjects())
{
networkObjects.AddRange(rootObj.GetComponentsInChildren<NetworkObject>());
}
// NetworkRunner에 씬 오브젝트들 등록
if (networkObjects.Count > 0)
{
Runner.RegisterSceneObjects(networkObjects.ToArray());
Debug.Log($"[SceneManager] Registered {networkObjects.Count} NetworkObjects");
}
}
}
5. 공유모드 멀티씬 네트워킹 구현
5.1 방 생성 및 참여 프로세스
public class MultiSceneGameManager : MonoBehaviour, INetworkRunnerCallbacks
{
[Header("Scene Configuration")]
[SerializeField] private SceneReference baseScene;
[SerializeField] private SceneReference uiScene;
[SerializeField] private SceneReference gameplayScene;
[SerializeField] private SceneReference environmentScene;
private NetworkRunner _runner;
private CustomNetworkSceneManager _sceneManager;
async void Start()
{
await InitializeSharedMode();
}
async Task InitializeSharedMode()
{
// 커스텀 SceneManager 생성
_sceneManager = gameObject.AddComponent<CustomNetworkSceneManager>();
_runner = gameObject.AddComponent<NetworkRunner>();
var startGameArgs = new StartGameArgs()
{
GameMode = GameMode.Shared,
SessionName = "MultiSceneRoom",
Scene = SceneRef.FromIndex(0), // 기본 씬
SceneManager = _sceneManager,
PlayerCount = 8
};
var result = await _runner.StartGame(startGameArgs);
if (result.Ok)
{
Debug.Log("[GameManager] Game session started successfully!");
// Scene Authority를 가진 클라이언트만 추가 씬 로드
if (_runner.HasStateAuthority)
{
await LoadAdditionalScenes();
}
}
else
{
Debug.LogError($"[GameManager] Failed to start game: {result.ShutdownReason}");
}
}
}
5.2 멀티씬 로딩 및 동기화
public class MultiSceneNetworkManager : NetworkBehaviour
{
[Networked] public NetworkString<_32> CurrentGameScene { get; set; }
[Networked] public int LoadedSceneCount { get; set; }
[Networked] public GamePhase CurrentPhase { get; set; }
public async Task LoadAdditionalScenes()
{
if (!Object.HasStateAuthority) return;
CurrentPhase = GamePhase.LoadingScenes;
try
{
// UI 씬 로드
await LoadSceneAdditive("UIScene", 1);
// 게임플레이 씬 로드
await LoadSceneAdditive("GameplayScene", 2);
// 환경 씬 로드
await LoadSceneAdditive("EnvironmentScene", 3);
// 모든 씬 로드 완료 알림
RPC_NotifyAllScenesLoaded();
}
catch (Exception ex)
{
Debug.LogError($"[SceneManager] Error loading scenes: {ex.Message}");
}
}
private async Task LoadSceneAdditive(string sceneName, int sceneIndex)
{
Debug.Log($"[SceneManager] Loading {sceneName}...");
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
LoadedSceneCount++;
// 로드된 씬의 NetworkObject들을 등록
var scene = SceneManager.GetSceneByName(sceneName);
RegisterSceneNetworkObjects(scene);
RPC_NotifySceneLoaded(sceneName, sceneIndex);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_NotifySceneLoaded(NetworkString<_32> sceneName, int sceneIndex)
{
Debug.Log($"[Network] Scene {sceneName} loaded on all clients");
// 클라이언트별 씬 로딩 후 처리
OnSceneLoadedCallback?.Invoke(sceneName.ToString(), sceneIndex);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_NotifyAllScenesLoaded()
{
CurrentPhase = GamePhase.Ready;
Debug.Log("[Network] All scenes loaded successfully!");
// 게임 시작 준비 완료
OnAllScenesLoadedCallback?.Invoke();
}
// 콜백 이벤트들
public System.Action<string, int> OnSceneLoadedCallback;
public System.Action OnAllScenesLoadedCallback;
}
public enum GamePhase
{
Initializing,
LoadingScenes,
Ready,
Playing,
GameEnded
}
6. 씬 간 통신 메커니즘
6.1 NetworkObject 관리
public class SceneSpecificNetworkObject : NetworkBehaviour
{
[Networked] public SceneRef OwnerScene { get; set; }
[Networked] public NetworkString<_16> SceneName { get; set; }
public override void Spawned()
{
// 해당 오브젝트가 속한 씬 정보 저장
var currentScene = gameObject.scene;
OwnerScene = SceneRef.FromIndex(currentScene.buildIndex);
SceneName = currentScene.name;
// 씬별 오브젝트 분류 및 등록
RegisterToSceneManager();
Debug.Log($"[NetworkObject] Spawned in scene: {SceneName}");
}
private void RegisterToSceneManager()
{
var sceneManager = FindObjectOfType<MultiSceneNetworkManager>();
sceneManager?.RegisterSceneObject(this);
}
public override void Despawned(NetworkRunner runner, bool hasState)
{
Debug.Log($"[NetworkObject] Despawned from scene: {SceneName}");
base.Despawned(runner, hasState);
}
}
6.2 씬 간 데이터 공유
public class CrossSceneDataManager : NetworkBehaviour
{
[Networked, Capacity(10)]
public NetworkDictionary<int, PlayerData> PlayersData { get; }
[Networked, Capacity(5)]
public NetworkArray<SceneGameState> ScenesState { get; }
[Networked] public GameStats GlobalGameStats { get; set; }
// UI씬에서 게임씬 데이터 접근
public void UpdateUIFromGameScene(int playerId, float health, int score)
{
if (!Object.HasStateAuthority) return;
var playerData = PlayersData.Get(playerId, new PlayerData());
playerData.Health = health;
playerData.Score = score;
playerData.LastUpdated = Runner.Tick;
PlayersData.Set(playerId, playerData);
// UI 업데이트 RPC 호출
RPC_UpdatePlayerUI(playerId, health, score);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_UpdatePlayerUI(int playerId, float health, int score)
{
// UI씬의 플레이어 정보 패널 업데이트
var playerUI = FindObjectOfType<PlayerUIManager>();
playerUI?.UpdatePlayerInfo(playerId, health, score);
}
// 게임씬에서 환경씬 데이터 접근
public void UpdateEnvironmentFromGame(Vector3 explosionPos, float radius)
{
if (!Object.HasStateAuthority) return;
RPC_TriggerEnvironmentEffect(explosionPos, radius);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_TriggerEnvironmentEffect(Vector3 position, float radius)
{
// 환경씬의 이펙트 매니저 호출
var envManager = FindObjectOfType<EnvironmentEffectManager>();
envManager?.TriggerExplosion(position, radius);
}
}
[System.Serializable]
public struct PlayerData : INetworkStruct
{
public float Health;
public int Score;
public Vector3 Position;
public Tick LastUpdated;
}
[System.Serializable]
public struct SceneGameState : INetworkStruct
{
public NetworkString<_16> SceneName;
public bool IsActive;
public int ObjectCount;
public float StateValue;
}
[System.Serializable]
public struct GameStats : INetworkStruct
{
public int TotalKills;
public int TotalDeaths;
public float GameDuration;
public Tick GameStartTick;
}
7. 게임 결과 처리 및 반영
7.1 게임 결과 관리 시스템
public class GameResultManager : NetworkBehaviour
{
[Networked] public GamePhase CurrentPhase { get; set; }
[Networked] public NetworkString<_32> WinnerName { get; set; }
[Networked] public int WinnerScore { get; set; }
[Networked] public Tick GameEndTick { get; set; }
[Header("Game Configuration")]
[SerializeField] private int targetScore = 100;
[SerializeField] private float gameDuration = 300f; // 5분
private CrossSceneDataManager _dataManager;
public override void Spawned()
{
_dataManager = FindObjectOfType<CrossSceneDataManager>();
// 게임 상태 모니터링 시작
if (Object.HasStateAuthority)
{
StartCoroutine(MonitorGameConditions());
}
}
private IEnumerator MonitorGameConditions()
{
while (CurrentPhase == GamePhase.Playing)
{
// 승리 조건 체크
CheckWinConditions();
// 시간 초과 체크
CheckTimeLimit();
yield return new WaitForSeconds(1f);
}
}
private void CheckWinConditions()
{
var playersData = _dataManager.PlayersData;
foreach (var kvp in playersData)
{
if (kvp.Value.Score >= targetScore)
{
EndGame(kvp.Key, kvp.Value.Score, "Score Limit Reached");
return;
}
}
}
private void CheckTimeLimit()
{
var gameStats = _dataManager.GlobalGameStats;
var elapsedTime = (Runner.Tick - gameStats.GameStartTick) * Runner.DeltaTime;
if (elapsedTime >= gameDuration)
{
// 최고 점수 플레이어 찾기
var topPlayer = FindTopScorePlayer();
EndGame(topPlayer.Key, topPlayer.Value, "Time Limit Reached");
}
}
private KeyValuePair<int, int> FindTopScorePlayer()
{
var playersData = _dataManager.PlayersData;
var topScore = -1;
var topPlayerId = -1;
foreach (var kvp in playersData)
{
if (kvp.Value.Score > topScore)
{
topScore = kvp.Value.Score;
topPlayerId = kvp.Key;
}
}
return new KeyValuePair<int, int>(topPlayerId, topScore);
}
public void EndGame(int winnerId, int finalScore, string reason)
{
if (!Object.HasStateAuthority) return;
CurrentPhase = GamePhase.GameEnded;
GameEndTick = Runner.Tick;
// 승자 정보 설정
var winnerName = GetPlayerName(winnerId);
WinnerName = winnerName;
WinnerScore = finalScore;
Debug.Log($"[GameResult] Game ended: {reason} - Winner: {winnerName} (Score: {finalScore})");
// 게임 결과 RPC 전송
RPC_GameEnded(winnerId, finalScore, reason);
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_GameEnded(int winnerId, int finalScore, NetworkString<_64> reason)
{
var winnerName = GetPlayerName(winnerId);
Debug.Log($"[Network] Game ended - Winner: {winnerName}, Score: {finalScore}, Reason: {reason}");
// UI씬의 결과창 업데이트
var resultUI = FindObjectOfType<ResultUI>();
resultUI?.ShowResults(winnerId, winnerName, finalScore, reason.ToString());
// 게임플레이 씬의 오브젝트들 정리
StartCoroutine(CleanupAndPrepareNext());
}
private IEnumerator CleanupAndPrepareNext()
{
// 게임플레이 오브젝트들 비활성화
yield return StartCoroutine(DeactivateGameplayObjects());
// 5초간 결과 화면 표시
yield return new WaitForSeconds(5f);
// 다음 게임 준비
if (Object.HasStateAuthority)
{
PrepareNextGame();
}
}
private IEnumerator DeactivateGameplayObjects()
{
var gameplayScene = SceneManager.GetSceneByName("GameplayScene");
if (gameplayScene.isLoaded)
{
var rootObjects = gameplayScene.GetRootGameObjects();
foreach (var obj in rootObjects)
{
var networkObj = obj.GetComponent<NetworkObject>();
if (networkObj != null && networkObj.IsValid)
{
// NetworkObject는 Runner를 통해 Despawn
Runner.Despawn(networkObj);
}
else
{
// 일반 GameObject는 직접 비활성화
obj.SetActive(false);
}
}
}
yield return null;
}
private void PrepareNextGame()
{
Debug.Log("[GameResult] Preparing next game...");
// 게임 상태 초기화
CurrentPhase = GamePhase.Initializing;
WinnerName = "";
WinnerScore = 0;
// 플레이어 데이터 초기화
var playersData = _dataManager.PlayersData;
var playerIds = new List<int>();
foreach (var kvp in playersData)
{
playerIds.Add(kvp.Key);
}
foreach (var playerId in playerIds)
{
var playerData = new PlayerData
{
Health = 100f,
Score = 0,
Position = Vector3.zero,
LastUpdated = Runner.Tick
};
playersData.Set(playerId, playerData);
}
// 새 게임 시작
RPC_StartNewGame();
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_StartNewGame()
{
Debug.Log("[Network] Starting new game...");
// UI 초기화
var resultUI = FindObjectOfType<ResultUI>();
resultUI?.HideResults();
var gameUI = FindObjectOfType<GameUI>();
gameUI?.ResetUI();
// 게임플레이 오브젝트들 재활성화
StartCoroutine(ReactivateGameplayObjects());
// 게임 시작
CurrentPhase = GamePhase.Playing;
// 게임 시작 시간 기록
var gameStats = _dataManager.GlobalGameStats;
gameStats.GameStartTick = Runner.Tick;
gameStats.TotalKills = 0;
gameStats.TotalDeaths = 0;
_dataManager.GlobalGameStats = gameStats;
}
private IEnumerator ReactivateGameplayObjects()
{
var gameplayScene = SceneManager.GetSceneByName("GameplayScene");
if (gameplayScene.isLoaded)
{
var rootObjects = gameplayScene.GetRootGameObjects();
foreach (var obj in rootObjects)
{
obj.SetActive(true);
// NetworkObject가 있다면 다시 Spawn
var networkObj = obj.GetComponent<NetworkObject>();
if (networkObj != null && !networkObj.IsValid)
{
// 필요시 Runner.Spawn 호출
}
}
}
yield return null;
}
private string GetPlayerName(int playerId)
{
// 실제 구현에서는 플레이어 ID를 이름으로 변환하는 로직 필요
return $"Player_{playerId}";
}
}
7.2 결과 UI 시스템
public class ResultUI : MonoBehaviour
{
[Header("UI Components")]
[SerializeField] private GameObject resultPanel;
[SerializeField] private Text winnerText;
[SerializeField] private Text scoreText;
[SerializeField] private Text reasonText;
[SerializeField] private Button nextGameButton;
private void Start()
{
if (nextGameButton != null)
{
nextGameButton.onClick.AddListener(OnNextGameClicked);
}
HideResults();
}
public void ShowResults(int winnerId, string winnerName, int finalScore, string reason)
{
if (resultPanel != null)
{
resultPanel.SetActive(true);
if (winnerText != null)
winnerText.text = $"Winner: {winnerName}";
if (scoreText != null)
scoreText.text = $"Final Score: {finalScore}";
if (reasonText != null)
reasonText.text = $"Game ended: {reason}";
}
Debug.Log($"[ResultUI] Showing results - Winner: {winnerName}, Score: {finalScore}");
}
public void HideResults()
{
if (resultPanel != null)
{
resultPanel.SetActive(false);
}
}
private void OnNextGameClicked()
{
Debug.Log("[ResultUI] Next game button clicked");
HideResults();
}
}
8. 전체 동작 플로우
8.1 시스템 초기화 단계
- NetworkRunner 생성: 공유모드로 세션 시작
- 기본 씬 로드: 베이스 씬이 먼저 로드됨
- Scene Authority 결정: 첫 번째 클라이언트가 씬 관리 권한 획득
- NetworkSceneManager 초기화: 커스텀 씬 매니저 설정
8.2 멀티씬 로딩 단계
- Authority 체크: Scene Authority를 가진 클라이언트만 씬 로드 수행
- Additive 로딩: UI, 게임플레이, 환경 씬들을 순차적으로 로드
- NetworkObject 등록: 각 씬의 네트워크 오브젝트들을 Runner에 등록
- 동기화 완료: 모든 클라이언트에게 로딩 완료 알림
8.3 게임플레이 단계
- 실시간 동기화: 모든 씬의 NetworkObject들이 자동 동기화
- 씬 간 통신: CrossSceneDataManager를 통한 데이터 공유
- 상태 관리: 각 씬별 게임 상태 실시간 업데이트
- 이벤트 처리: RPC를 통한 씬 간 이벤트 전파
8.4 게임 종료 및 결과 처리
- 승리 조건 감지: 점수 또는 시간 기반 게임 종료 조건 체크
- 결과 계산: 최종 승자 및 점수 결정
- 전체 알림: 모든 클라이언트에게 게임 결과 전송
- 씬 정리: 게임플레이 오브젝트들 정리 및 UI 업데이트
- 다음 게임 준비: 게임 상태 초기화 및 새 게임 시작
9. 주요 고려사항 및 최적화
9.1 성능 최적화
public class NetworkOptimizer : MonoBehaviour
{
[Header("Optimization Settings")]
[SerializeField] private float cullingDistance = 50f;
[SerializeField] private int maxNetworkObjects = 1000;
[SerializeField] private float updateFrequency = 20f; // Hz
[Header("Scene-Specific Settings")]
[SerializeField] private Dictionary<string, OptimizationProfile> sceneProfiles;
private void Start()
{
// 씬별 최적화 프로파일 설정
SetupSceneOptimization();
}
private void SetupSceneOptimization()
{
sceneProfiles = new Dictionary<string, OptimizationProfile>
{
{"UIScene", new OptimizationProfile { UpdateRate = 10f, CullingEnabled = false }},
{"GameplayScene", new OptimizationProfile { UpdateRate = 60f, CullingEnabled = true }},
{"EnvironmentScene", new OptimizationProfile { UpdateRate = 30f, CullingEnabled = true }}
};
}
// 거리 기반 NetworkObject 컬링
public void ApplyDistanceCulling(NetworkObject netObj, Vector3 playerPosition)
{
float distance = Vector3.Distance(netObj.transform.position, playerPosition);
if (distance > cullingDistance)
{
// 원거리 오브젝트는 업데이트 빈도 감소
netObj.GetComponent<NetworkBehaviour>().enabled = false;
}
else
{
netObj.GetComponent<NetworkBehaviour>().enabled = true;
}
}
}
[System.Serializable]
public class OptimizationProfile
{
public float UpdateRate;
public bool CullingEnabled;
public int MaxObjects;
}
9.2 메모리 관리
public class MemoryManager : MonoBehaviour
{
[Header("Memory Settings")]
[SerializeField] private int maxPoolSize = 100;
[SerializeField] private float cleanupInterval = 30f;
private Dictionary<string, Queue<GameObject>> objectPools;
private Dictionary<Scene, List<NetworkObject>> sceneNetworkObjects;
private void Start()
{
objectPools = new Dictionary<string, Queue<GameObject>>();
sceneNetworkObjects = new Dictionary<Scene, List<NetworkObject>>();
// 주기적 메모리 정리
InvokeRepeating(nameof(PerformMemoryCleanup), cleanupInterval, cleanupInterval);
}
public GameObject GetPooledObject(string prefabName)
{
if (!objectPools.ContainsKey(prefabName))
{
objectPools[prefabName] = new Queue<GameObject>();
}
var pool = objectPools[prefabName];
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return null; // 새로 생성 필요
}
public void ReturnToPool(string prefabName, GameObject obj)
{
if (!objectPools.ContainsKey(prefabName))
{
objectPools[prefabName] = new Queue<GameObject>();
}
var pool = objectPools[prefabName];
if (pool.Count < maxPoolSize)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
else
{
Destroy(obj); // 풀이 가득 찬 경우 삭제
}
}
private void PerformMemoryCleanup()
{
// 비활성화된 씬의 오브젝트들 정리
foreach (var kvp in sceneNetworkObjects.ToList())
{
if (!kvp.Key.isLoaded)
{
foreach (var netObj in kvp.Value)
{
if (netObj != null && netObj.IsValid)
{
netObj.Runner.Despawn(netObj);
}
}
sceneNetworkObjects.Remove(kvp.Key);
}
}
// 가비지 컬렉션 요청
System.GC.Collect();
Debug.Log("[MemoryManager] Memory cleanup completed");
}
}
9.3 에러 처리 및 복구
public class NetworkErrorHandler : NetworkBehaviour, INetworkRunnerCallbacks
{
[Header("Error Handling")]
[SerializeField] private int maxRetryAttempts = 3;
[SerializeField] private float retryDelay = 2f;
private int currentRetryCount = 0;
public void OnConnectedToServer(NetworkRunner runner)
{
Debug.Log("[ErrorHandler] Connected to server successfully");
currentRetryCount = 0;
}
public void OnDisconnectedFromServer(NetworkRunner runner)
{
Debug.LogWarning("[ErrorHandler] Disconnected from server");
AttemptReconnection();
}
public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason)
{
Debug.LogError($"[ErrorHandler] Connection failed: {reason}");
AttemptReconnection();
}
private async void AttemptReconnection()
{
if (currentRetryCount >= maxRetryAttempts)
{
Debug.LogError("[ErrorHandler] Max retry attempts reached. Giving up.");
ShowConnectionErrorUI();
return;
}
currentRetryCount++;
Debug.Log($"[ErrorHandler] Attempting reconnection ({currentRetryCount}/{maxRetryAttempts})");
await Task.Delay(Mathf.RoundToInt(retryDelay * 1000));
// 재연결 시도
var gameManager = FindObjectOfType<MultiSceneGameManager>();
await gameManager.InitializeSharedMode();
}
private void ShowConnectionErrorUI()
{
// 연결 오류 UI 표시
var errorUI = FindObjectOfType<ErrorUI>();
errorUI?.ShowConnectionError("서버 연결에 실패했습니다. 네트워크 상태를 확인해주세요.");
}
// Scene 로딩 오류 처리
public void OnSceneLoadDone(NetworkRunner runner)
{
Debug.Log("[ErrorHandler] Scene loading completed");
}
public void OnSceneLoadStart(NetworkRunner runner)
{
Debug.Log("[ErrorHandler] Scene loading started");
}
}
9.4 보안 고려사항
public class SecurityManager : NetworkBehaviour
{
[Header("Security Settings")]
[SerializeField] private bool enableAntiCheat = true;
[SerializeField] private float maxAllowedSpeed = 10f;
[SerializeField] private int maxActionsPerSecond = 10;
private Dictionary<PlayerRef, PlayerSecurityData> playerSecurityData;
public override void Spawned()
{
playerSecurityData = new Dictionary<PlayerRef, PlayerSecurityData>();
}
// 플레이어 행동 검증
public bool ValidatePlayerAction(PlayerRef player, Vector3 newPosition, float deltaTime)
{
if (!enableAntiCheat) return true;
if (!playerSecurityData.ContainsKey(player))
{
playerSecurityData[player] = new PlayerSecurityData();
}
var securityData = playerSecurityData[player];
var distance = Vector3.Distance(securityData.LastPosition, newPosition);
var speed = distance / deltaTime;
// 속도 검증
if (speed > maxAllowedSpeed)
{
Debug.LogWarning($"[Security] Player {player} exceeded max speed: {speed}");
return false;
}
// 액션 빈도 검증
securityData.ActionCount++;
if (Time.time - securityData.LastActionTime >= 1f)
{
if (securityData.ActionCount > maxActionsPerSecond)
{
Debug.LogWarning($"[Security] Player {player} exceeded max actions per second");
return false;
}
securityData.ActionCount = 0;
securityData.LastActionTime = Time.time;
}
securityData.LastPosition = newPosition;
return true;
}
}
[System.Serializable]
public class PlayerSecurityData
{
public Vector3 LastPosition;
public float LastActionTime;
public int ActionCount;
}
10. 문제 해결 가이드
10.1 일반적인 문제들
1. NetworkObject 중복 등록
// 문제: 같은 NetworkId를 가진 오브젝트들이 여러 씬에 존재
// 해결: 씬별로 고유한 NetworkId 범위 할당
public class UniqueNetworkIdManager : MonoBehaviour
{
private static Dictionary<string, int> sceneIdRanges = new Dictionary<string, int>
{
{"UIScene", 1000},
{"GameplayScene", 2000},
{"EnvironmentScene", 3000}
};
public static int GetNextNetworkId(string sceneName)
{
if (sceneIdRanges.ContainsKey(sceneName))
{
return sceneIdRanges[sceneName]++;
}
return UnityEngine.Random.Range(10000, 99999);
}
}
2. 씬 로딩 동기화 문제
// 문제: 클라이언트들 간의 씬 로딩 타이밍 차이
// 해결: 로딩 상태 동기화 및 대기 메커니즘
public class SceneLoadSynchronizer : NetworkBehaviour
{
[Networked, Capacity(8)]
public NetworkDictionary<PlayerRef, bool> PlayerLoadStates { get; }
public void NotifySceneLoaded(PlayerRef player)
{
if (Object.HasStateAuthority)
{
PlayerLoadStates.Set(player, true);
if (AllPlayersLoaded())
{
RPC_AllPlayersReady();
}
}
}
private bool AllPlayersLoaded()
{
foreach (var kvp in PlayerLoadStates)
{
if (!kvp.Value) return false;
}
return true;
}
[Rpc(RpcSources.StateAuthority, RpcTargets.All)]
void RPC_AllPlayersReady()
{
// 모든 플레이어 준비 완료
var gameManager = FindObjectOfType<MultiSceneGameManager>();
gameManager.StartGame();
}
}
10.2 성능 문제 해결
1. 네트워크 트래픽 최적화
public class TrafficOptimizer : NetworkBehaviour
{
[Networked] public Vector3 Position { get; set; }
[Networked] public Quaternion Rotation { get; set; }
private Vector3 lastSentPosition;
private Quaternion lastSentRotation;
private float sendThreshold = 0.1f;
public override void FixedUpdateNetwork()
{
// 변화량이 임계값 이상일 때만 전송
if (Vector3.Distance(transform.position, lastSentPosition) > sendThreshold ||
Quaternion.Angle(transform.rotation, lastSentRotation) > 5f)
{
Position = transform.position;
Rotation = transform.rotation;
lastSentPosition = transform.position;
lastSentRotation = transform.rotation;
}
}
}
2. 메모리 누수 방지
public class ResourceManager : MonoBehaviour
{
private List<IDisposable> disposableResources = new List<IDisposable>();
public void RegisterDisposable(IDisposable resource)
{
disposableResources.Add(resource);
}
private void OnDestroy()
{
foreach (var resource in disposableResources)
{
try
{
resource?.Dispose();
}
catch (System.Exception ex)
{
Debug.LogError($"Error disposing resource: {ex.Message}");
}
}
disposableResources.Clear();
}
}
11. 참고 문서 및 출처
본 가이드는 다음 공식 문서들을 참조하여 작성되었습니다:
주요 참조 문서:
-
**[Fusion 2 - Setting Up A Scene Photon Engine](https://doc.photonengine.com/fusion/current/tutorials/host-mode-basics/2-setting-up-a-scene)** - NetworkRunner 초기화 및 SceneManager 설정 방법
-
**[Fusion 2 - Scene Loading Photon Engine](https://doc.photonengine.com/fusion/current/manual/scene-loading)** - NetworkSceneManagerDefault 구현 및 Version 필드 활용
-
**[Fusion 2 - Network Runner Photon Engine](https://doc.photonengine.com/fusion/current/manual/network-runner)** - StartGame 메서드 및 NetworkSceneInfo 설정
-
**[Fusion 2 - Scene and Player Photon Engine](https://doc.photonengine.com/fusion/current/tutorials/shared-mode-basics/2-scene-and-player)** - 공유모드에서의 씬 설정 및 프로토타입 러너 구성
-
**[Fusion 2 - Network Object Photon Engine](https://doc.photonengine.com/fusion/current/manual/network-object)** - NetworkObject 관리 및 씬 간 동기화
추가 리소스:
- Photon Fusion: NetworkRunner Class Reference
-
**[Fusion 2 - Fusion 2 Introduction Photon Engine](https://doc.photonengine.com/fusion/current/fusion-intro)**
12. 결론
Photon Fusion 2의 공유모드와 멀티씬 로딩을 결합한 네트워킹 시스템은 복잡하지만 매우 강력한 기능을 제공합니다.
핵심 요약:
✅ 주요 장점:
- 고성능: 틱 기반 시뮬레이션으로 정확한 동기화
- 유연성: 씬별 독립적 관리 및 동적 로딩
- 확장성: 대규모 멀티플레이어 게임 지원
- 안정성: 자동 오류 복구 및 재연결 기능
⚠️ 주의사항:
- Scene Authority 관리: 씬 관리 권한의 명확한 분리 필요
- 메모리 최적화: 멀티씬 환경에서의 적절한 리소스 관리
- 네트워크 트래픽: 씬 간 데이터 동기화 최적화 필수
- 동기화 타이밍: 클라이언트별 씬 로딩 완료 시점 조율
🎯 권장 사항:
- 단계적 구현: 기본 기능부터 시작하여 점진적으로 복잡성 추가
- 충분한 테스트: 다양한 네트워크 조건에서의 동작 검증
- 모니터링: 실시간 네트워크 상태 및 성능 모니터링 구현
- 문서화: 팀 내 구현 사항 및 설정값 공유
이 가이드를 통해 안정적이고 효율적인 멀티씬 네트워킹 시스템을 구축하시기 바랍니다.
- 이 글은 ai가 작성했습니다.