2025. 5. 9. 18:53ㆍ[Unity] Game Programing
Addressables와 FishNet으로 구현하는 비동기 씬 로딩 흐름
Unity의 Addressables 시스템과 FishNet의 네트워크 기능을 결합하여 씬 전환을 구현할 때, Addressables를 활용한 리소스 로딩과 FishNet의 LoadGlobalScenes/LoadConnectionScenes 호출을 연계할 수 있습니다. Addressables는 비동기 방식으로 에셋을 로드하여 런타임 메모리 사용량을 줄이고 빌드 관리를 단순화해 주는 시스템입니다. 이 문서에서는 로비 씬에서 시작 버튼을 눌러 로딩 UI를 띄우고, Addressables로 타겟 씬의 데이터들을 미리 로드한 뒤 FishNet으로 씬을 전환하는 전체 흐름과 유의 사항을 코드 예제와 함께 상세히 설명합니다.
Addressables 기본 개념 요약
Addressables는 Unity 에디터와 런타임에서 주소(address) 기반으로 에셋을 로드할 수 있게 해주는 자산 관리 시스템입니다. 에셋을 Addressable로 설정하면, 해당 에셋은 Addressables Group에 포함되고, 고유한 주소나 라벨(label)을 할당받게 됩니다. 런타임에는 Addressables.LoadAssetAsync<T>("주소")나 Addressables.LoadAssetsAsync<T>("라벨", callback) 등의 API로 에셋을 비동기 로드합니다. Addressables는 비동기 로딩 방식을 사용하여 필요한 에셋만 가져오므로 초기 로딩 시간을 줄이고 메모리 관리를 효율화할 수 있습니다. 또한 로컬/원격 서버 상의 에셋 위치와 상관없이 동일한 API로 로드가 가능합니다.
Addressables 사용 시 주요 절차는 다음과 같습니다:
- Addressable 설정: 에셋을 선택하여 Addressable로 표시(마킹)하고, 적절한 그룹과 라벨을 할당합니다. 필요에 따라 빌드 프로파일을 구성하여 개발용/배포용 경로를 분리할 수 있습니다.
- 빌드: Addressables Groups 창에서 빌드를 수행하여 에셋 번들과 카탈로그를 생성합니다.
- 런타임 로딩: Addressables API(LoadAssetAsync, LoadAssetsAsync, LoadSceneAsync 등)로 에셋을 로드합니다. 비동기 작업의 진행상태는 AsyncOperationHandle<T>을 통해 확인할 수 있습니다. 로딩 완료 후에는 Addressables.Release로 메모리를 해제해야 합니다.
실습 예제로, 씬 전환 전에 필요한 데이터(모델, 프리팹 등)를 Addressables로 로드하려면 다음과 같이 합니다:
// 예: 라벨 "GameData"로 묶인 여러 에셋을 로드하고 콜백에서 네트워크 객체를 캐싱
AsyncOperationHandle<IList<GameObject>> handle =
Addressables.LoadAssetsAsync<GameObject>("GameDataLabel", asset => {
NetworkObject nob = asset.GetComponent<NetworkObject>();
if (nob != null) cache.Add(nob);
});
yield return handle;
// 로드 성공 여부 확인 및 사용 준비
if (handle.Status == AsyncOperationStatus.Succeeded) {
var assets = handle.Result;
// 에셋 사용 후 필요 시 Release
Addressables.Release(handle);
}
Addressables 로딩 상태(Progress) 추적
Addressables의 로딩 진행률은 AsyncOperationHandle.PercentComplete 프로퍼티로 확인할 수 있습니다. 이 값은 하위 비동기 작업 전체에 대한 누적 진행률(0~1)로 계산되며, 실제 다운로드된 바이트 비율이 아닌 작업의 단계별 완료율을 나타냅니다. 즉, 여러 개의 하위 작업이 있을 때 어느 정도 진행되었는지를 보여주며, 실제 다운로드 퍼센트만 추적하지는 않습니다. (실제 다운로드 진행도를 알고 싶다면 Addressables.GetDownloadStatus()를 사용해야 합니다.)
예를 들어, Addressables로 에셋 그룹을 로드하면서 UI에 퍼센티지를 띄우려면, 코루틴에서 매 프레임 handle.PercentComplete를 읽어 UI를 갱신합니다:
while (!handle.IsDone) {
float progress = handle.PercentComplete * 100f; // 0~100%
loadingUI.SetDataLoadingProgress(progress); // UI에 퍼센트 표시
yield return null;
}
// 로드 완료 시 진행률 100%
loadingUI.SetDataLoadingProgress(100f);
로딩 도중 예외나 실패가 발생할 수 있으므로, handle.Status를 확인하여 Succeeded인지 Failed인지 구분하고 실패 시 적절히 처리해야 합니다. 예를 들어 실패하면 경고 메시지 표시나 재시도 로직을 구현할 수 있습니다.
FishNet의 글로벌 객체(IsGlobal)와 DontDestroyOnLoad
FishNet에서는 네트워크 객체를 영구히 유지하기 위해 NetworkObject의 IsGlobal 플래그를 사용합니다. IsGlobal = true로 설정된 객체는 Unity의 DontDestroyOnLoad(DDOL) 씬으로 이동하여 씬 전환 시에도 파괴되지 않고 유지됩니다. 이를 통해 게임매니저나 오디오매니저 같은 싱글톤형 네트워크 객체를 씬 사이에 유지할 수 있습니다. FishNet 문서에 따르면, 글로벌 객체는 서버와 클라이언트 모두 DDOL 씬에 속하게 되므로 상태 동기화가 자동으로 유지됩니다.
단, 주의할 점이 있습니다. 사전에 씬에 배치된(Networked Scene Object) 형태의 네트워크 객체는 기술적 한계로 DontDestroyOnLoad나 IsGlobal을 적용할 수 없습니다. Unity는 씬 객체의 세부 정보를 알 수 없어, 로딩이 되지 않은 상태에서 해당 객체를 다른 씬으로 이동시킬 수 없습니다. 따라서 네트워크 동기화가 필요한 씬 전용 객체는 글로벌로 설정하면 오류가 발생합니다. 요약하면, 운영 중에 스폰(Spawn)한 네트워크 객체만 IsGlobal로 설정하여 씬 간에 유지할 수 있으며, 빌트인 씬 객체는 글로벌 처리가 불가능합니다.
예: 서버에서 다음과 같이 네트워크 매니저 객체를 생성한 뒤 글로벌로 표시할 수 있습니다.
NetworkObject manager = Instantiate(managerPrefab).GetComponent<NetworkObject>();
manager.IsGlobal = true;
base.ServerManager.Spawn(manager);
그러면 이 매니저는 씬이 전환되어도 파괴되지 않습니다. 반면 씬에 미리 배치된 네트워크 오브젝트는 글로벌 처리가 불가능하므로 주의해야 합니다.
LoadingUI와 NetworkObject 구분
로딩 화면용 UI(LoadingUI)는 클라이언트 로컬에서만 사용되므로 NetworkObject를 붙일 필요가 없습니다. 로드 진행률 표시나 간단한 애니메이션 등을 위한 UI는 서버-클라이언트 동기화 대상이 아니며, 모든 클라이언트에서 각각 활성화됩니다. 만약 LoadingUI에 NetworkObject를 붙이면 FishNet이 이를 네트워크 스폰 대상으로 취급하여 의도치 않게 모든 클라이언트에 복제되거나 관리가 복잡해질 수 있습니다. 또한 Unity의 DontDestroyOnLoad를 사용하여 같은 UI를 여러 씬에서 유지하려는 경우, FishNet의 글로벌 객체 모델(IsGlobal)과 혼동되지 않도록 주의합니다. 일반적으로 로컬 UI는 DontDestroyOnLoad(loadingUI.gameObject)로 유지한 뒤 필요 시 활성화/비활성화하면 충분합니다.
FishNet 씬 로딩 (LoadGlobalScenes / LoadConnectionScenes)
FishNet의 SceneManager를 사용하면 **서버 권한 기반(scene authority)**으로 씬 로딩을 제어할 수 있습니다. 씬 로드 방식은 크게 두 가지입니다:
- 글로벌 씬 로드 (LoadGlobalScenes): 현재 접속 중인 모든 클라이언트가 해당 씬을 로드합니다. 이후 접속하는 클라이언트도 자동으로 로드됩니다. 이를 위해 SceneLoadData에 씬 이름이나 핸들을 지정하고 InstanceFinder.SceneManager.LoadGlobalScenes(sld)를 호출합니다.
- 연결별 씬 로드 (LoadConnectionScenes): 특정 클라이언트(들)만 씬을 로드하도록 할 수 있습니다. 서버만 미리 씬을 로드해 둘 수도 있습니다. 예를 들어 base.SceneManager.LoadConnectionScenes(conn, sld)로 특정 네트워크 연결에만 씬 로드를 지시할 수 있습니다.
// 예: "GameScene" 씬을 모든 클라이언트에 로드 (글로벌)
SceneLoadData sld = new SceneLoadData("GameScene");
InstanceFinder.SceneManager.LoadGlobalScenes(sld);
// 예: 특정 클라이언트에게 "GameScene"만 로드
SceneLoadData sld2 = new SceneLoadData("GameScene");
NetworkConnection targetConn = base.Owner; // 소유자 클라이언트
InstanceFinder.SceneManager.LoadConnectionScenes(targetConn, sld2);
// 예: 서버에만 로드 (추후 접속 대기용)
SceneLoadData sld3 = new SceneLoadData("Arena");
InstanceFinder.SceneManager.LoadConnectionScenes(sld3);
씬 언로드(ReplaceScenes) 옵션
기본적으로 LoadGlobalScenes/LoadConnectionScenes는 기존 로드된 씬을 자동 언로드하지 않습니다. 따라서 씬 전환 시 이전 씬을 제거하려면 SceneLoadData.ReplaceScenes 옵션을 설정해야 합니다. FishNet은 ReplaceOption 열거형을 제공하며, 대표적으로 다음과 같은 옵션이 있습니다:
- None (기본값): 아무 것도 언로드하지 않고 지정된 씬만 추가 로드합니다.
- All: 현재 모든 씬을 언로드한 뒤 새 씬을 로드합니다. 이때 FishNet이 로드하지 않은 일반 Unity 씬도 모두 언로드됩니다. 예를 들어 다중 씬 모드에서 완전 전환하려면 사용합니다.
- OnlineOnly: FishNet SceneManager로 로드한 씬들만 언로드하고, 그렇지 않은 씬(예: 추가로 로드된 Unity 씬)은 유지합니다.
// 예: 이전 씬 모두를 언로드하고 "GameScene"을 로드
SceneLoadData sld = new SceneLoadData("GameScene");
sld.ReplaceScenes = ReplaceOption.All;
InstanceFinder.SceneManager.LoadGlobalScenes(sld);
// 예: FishNet이 로드한 씬만 언로드하고 "GameScene"을 로드
SceneLoadData sld = new SceneLoadData("GameScene");
sld.ReplaceScenes = ReplaceOption.OnlineOnly;
InstanceFinder.SceneManager.LoadGlobalScenes(sld);
Replace 옵션을 적절히 설정하여 원치 않는 씬 잔존 문제를 방지할 수 있습니다. 예를 들어 로비 씬을 완전히 언로드하려면 All 옵션을 사용하고, 추가 씬을 그대로 두고 전환하려면 OnlineOnly를 사용합니다.
씬 로딩 진행도(OnLoadPercentChange) 추적
FishNet의 SceneManager에는 씬 로딩 큐가 진행되는 동안 퍼센트 변화를 통지하는 이벤트가 있습니다. 바로 OnLoadPercentChange 이벤트입니다. 이 이벤트는 현재 큐에서 로드 중인 씬들의 총 진행률을 float Percent 값으로 제공합니다. 개발자는 이 이벤트에 핸들러를 등록하여 씬 로딩 진행 상황을 UI에 표시할 수 있습니다. 예를 들어 InstanceFinder.SceneManager.OnLoadPercentChange += OnSceneLoadProgress; 형태로 구독한 뒤, 콜백에서 args.Percent를 이용해 프로그레스바를 갱신합니다. 씬 로딩이 완전히 끝난 후에는 OnLoadEnd 이벤트가 호출되며, 이때 로딩 UI를 해제하거나 필요한 후처리를 하면 됩니다.
private void OnEnable() {
InstanceFinder.SceneManager.OnLoadPercentChange += OnSceneLoadProgress;
InstanceFinder.SceneManager.OnLoadEnd += OnSceneLoadComplete;
}
private void OnDisable() {
InstanceFinder.SceneManager.OnLoadPercentChange -= OnSceneLoadProgress;
InstanceFinder.SceneManager.OnLoadEnd -= OnSceneLoadComplete;
}
private void OnSceneLoadProgress(SceneLoadPercentChangeEventArgs args) {
float scenePercent = args.Percent * 100f; // 0~100%
loadingUI.SetSceneLoadingProgress(scenePercent);
loadingUI.SetMessage($"게임 씬 로딩 중... {scenePercent:F0}%");
}
private void OnSceneLoadComplete(SceneLoadEndEventArgs args) {
// 씬 로드 완료 시 UI 제거
loadingUI.SetActive(false);
}
이렇게 하면 Addressables 로딩과 씬 로딩을 순차적으로 UI에 반영할 수 있습니다. FishNet 문서에서는 OnLoadPercentChange 이벤트를 통해 로드 중인 씬의 총 진행률을 얻을 수 있다고 명시하고 있습니다.
전체 흐름 예제 및 구현 구조
- 로비 씬에서 게임 시작 버튼 클릭: UI 버튼의 OnClick에 이벤트 함수를 연결하여 로딩 과정을 시작합니다. 예를 들어 StartGame() 함수를 호출합니다.
- LoadingUI 활성화: 로딩 화면 UI는 미리 DontDestroyOnLoad로 설정하여 로드 사이에도 유지해두고, 활성화합니다. 사용자에게 "게임 데이터 불러오는 중..." 메시지와 진행도를 표시하기 위해 UI 상태를 초기화합니다.
- Addressables 비동기 로드: 코루틴을 통해 타겟 씬에서 사용할 리소스를 Addressables로 로드합니다. 로딩 중 AsyncOperationHandle.PercentComplete를 읽어 UI에 %를 업데이트합니다. 예를 들어:
- IEnumerator LoadGameData() { loadingUI.SetMessage("게임 데이터 불러오는 중..."); AsyncOperationHandle<IList<GameObject>> dataHandle = Addressables.LoadAssetsAsync<GameObject>("GameAssets", obj => { /* 콜백 */ }); while (!dataHandle.IsDone) { float percent = dataHandle.PercentComplete * 100f; loadingUI.SetDataLoadingProgress(percent); yield return null; } if (dataHandle.Status != AsyncOperationStatus.Succeeded) { Debug.LogError("Addressables 로딩 실패"); yield break; } // 필요한 데이터 처리 완료 (예: 네트워크 객체 등록 등) }
- FishNet 씬 로드 호출: Addressables 로드가 완료되면, 서버에서 SceneLoadData를 구성하여 FishNet 씬 로드를 호출합니다. 예를 들어 모든 클라이언트를 전환할 경우:이를 NetworkBehaviour 내부라면 base.SceneManager.LoadGlobalScenes(sld)로 호출합니다. 클라이언트에서는 서버가 호출한 씬 로드 명령에 따라 씬이 전환됩니다.
- SceneLoadData sld = new SceneLoadData("TargetGameScene"); sld.ReplaceScenes = ReplaceOption.All; // 이전 씬 언로드 옵션 InstanceFinder.SceneManager.LoadGlobalScenes(sld);
- 씬 로딩 퍼센트 표시: 위에서 구독한 OnLoadPercentChange 콜백이 호출되면서 "게임 씬 불러오는 중..." 메시지와 함께 실시간으로 씬 로딩 퍼센티지를 UI에 표시합니다. 이때 Addressables 데이터 로딩 단계가 아니라, 실제 씬 전환 로딩 단계입니다.
- 로딩 완료 후 UI 제거: 씬 로딩이 끝나면 OnLoadEnd 이벤트가 발생합니다. 이 시점에 LoadingUI를 비활성화하거나 파괴하여 로딩 화면을 종료합니다.
이 과정을 종합하면 다음과 같은 C# 구조가 될 수 있습니다:
public class SceneLoadingController : MonoBehaviour {
public LoadingUI loadingUI; // Canvas 기반 UI (DontDestroyOnLoad)
private bool isLoading = false;
// 버튼 이벤트 핸들러
public void StartGame() {
if (isLoading) return; // 중복 클릭 방지
isLoading = true;
loadingUI.SetActive(true);
StartCoroutine(LoadDataAndScene());
}
private IEnumerator LoadDataAndScene() {
// Addressables로 게임 데이터 로드
loadingUI.SetMessage("게임 데이터 불러오는 중...");
AsyncOperationHandle<IList<GameObject>> handle =
Addressables.LoadAssetsAsync<GameObject>("GameDataLabel", obj => { /* 필요시 처리 */ });
while (!handle.IsDone) {
loadingUI.SetDataLoadingProgress(handle.PercentComplete * 100f);
yield return null;
}
if (handle.Status != AsyncOperationStatus.Succeeded) {
Debug.LogError("데이터 로드 실패");
loadingUI.SetActive(false);
yield break;
}
// 데이터 로드 완료
// 씬 로드 준비
loadingUI.SetDataLoadingProgress(100f);
// FishNet 씬 로드 호출 (서버 권한)
SceneLoadData sld = new SceneLoadData("GameScene");
sld.ReplaceScenes = ReplaceOption.All;
InstanceFinder.SceneManager.LoadGlobalScenes(sld);
// 씬 로딩 진행은 OnLoadPercentChange 이벤트에서 처리
}
private void OnEnable() {
InstanceFinder.SceneManager.OnLoadPercentChange += OnPercentChange;
InstanceFinder.SceneManager.OnLoadEnd += OnLoadEnd;
}
private void OnDisable() {
InstanceFinder.SceneManager.OnLoadPercentChange -= OnPercentChange;
InstanceFinder.SceneManager.OnLoadEnd -= OnLoadEnd;
}
private void OnPercentChange(SceneLoadPercentChangeEventArgs args) {
float percent = args.Percent * 100f;
loadingUI.SetMessage($"게임 씬 불러오는 중... {percent:F0}%");
loadingUI.SetSceneLoadingProgress(percent);
}
private void OnLoadEnd(SceneLoadEndEventArgs args) {
// 씬 로딩 끝나면 UI 해제
loadingUI.SetActive(false);
isLoading = false;
}
}
위 예제에서는 Addressables 로드 단계와 씬 로드 단계를 명확히 분리하여 처리합니다. isLoading 플래그를 사용해 중복 호출을 방지하고, 로딩 도중 에러 발생 시 UI를 닫도록 예외 처리를 넣었습니다. 또한 OnEnable/OnDisable에서 SceneManager 이벤트를 구독/해제하여 메모리 누수를 예방합니다.
FishNet SceneManager 대신 RPC로 씬 전환하기 (대안)
FishNet은 권장 방식으로 SceneManager.LoadGlobalScenes 등을 제공하지만, 개발자에 따라 직접 RPC를 이용해 씬 전환 로직을 구현할 수도 있습니다. 예를 들어 서버에서 [ObserversRpc]를 사용하여 모든 클라이언트에게 Unity의 SceneManager.LoadScene 또는 Addressables 기반 로딩을 지시할 수 있습니다.
[ObserversRpc]
private void RPC_LoadScene(string sceneName) {
// 클라이언트가 Addressables로 씬 로드
Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
}
그러나 이 방식은 주의가 필요합니다. Unity의 기본 LoadScene나 Addressables 호출은 FishNet의 씬 로딩 파이프라인을 거치지 않으므로, 네트워크 객체의 자동 동기화나 이벤트가 처리되지 않을 수 있습니다. 즉, 로비 씬에서 스폰된 글로벌 NetworkObject 등이 새 씬으로 이동되지 않거나, 클라이언트마다 로드 타이밍이 달라질 위험이 있습니다. RPC 기반 씬 로딩은 완전한 수동 동기화가 필요하므로 복잡도가 높아집니다. 따라서 가능하면 FishNet의 SceneManager를 이용하는 것이 안정적입니다.
확장성과 안정성 고려 사항
실제 프로젝트에 적용할 때 다음과 같은 점들을 고려하여 설계하는 것이 좋습니다:
- 예외 및 에러 핸들링: Addressables 로드나 씬 로드 중 오류가 발생할 수 있습니다. AsyncOperationHandle의 Status를 확인하고 Failed일 경우 경고 로그를 남기거나 로비로 복귀하는 로직을 구현해야 합니다. 또한 네트워크 연결 끊김 등으로 씬 전환 실패 시 대체 시나리오(재시도, 사용자 안내)를 마련해야 합니다.
- 중복 호출 방지: 위 예제처럼 isLoading 플래그를 두거나 버튼을 비활성화하여 사용자가 로딩 과정 중에 다시 버튼을 누르는 것을 막습니다. FishNet의 씬 로드 큐에 동일 명령이 중복 쌓이지 않도록 해야 합니다.
- 타임아웃 처리: Addressables나 씬 로딩이 예상보다 오래 걸릴 경우, 타임아웃 제한을 두고 중간에 취소하거나 재시도하는 전략을 둘 수 있습니다. 예를 들어 별도의 WaitForSeconds(maxTime)를 두고 끝나지 않으면 실패로 처리하게 합니다.
- 메모리 관리: Addressables로 로드한 에셋은 사용 후 Addressables.Release로 반드시 해제하여 메모리 누수를 방지합니다. 로딩 UI도 필요 없을 때 즉시 파괴하거나 SetActive(false)로 관리해야 합니다.
- 확장성: 로드할 에셋이 많아지면 복수의 Handle을 관리하는 로직이 필요합니다. 예를 들어 여러 그룹의 라벨을 순차 로드하거나, 로드 우선순위 큐를 구현할 수 있습니다. 또한 네트워크 플레이어 로드, 로딩 화면 애니메이션, 리소스 동기화 등이 추가될 수 있습니다.
이러한 사항들을 고려하면 팀 내에서 유지보수하기 쉬운 구조를 갖춘 씬 로딩 시스템을 구축할 수 있습니다. Addressables와 FishNet을 결합한 위 구조는 유지 보수성과 확장성을 고려한 설계로, 대규모 콘텐츠 로딩과 네트워크 동기화가 필요한 프로젝트에 유용합니다.
참고: FishNet 공식 문서에서는 Addressables를 네트워크 씬 로딩에 통합하는 방법도 안내하고 있으며, SceneManager의 다양한 이벤트와 옵션을 활용한 고급 씬 관리 방법을 제공하고 있습니다.�
'[Unity] Game Programing' 카테고리의 다른 글
| 🎓 Unity CustomEditor TIL (Today I Learned) (0) | 2025.05.14 |
|---|---|
| TIL(Today I Learned) 행동 트리(Behaviour Tree) (3) | 2025.05.08 |
| TIL(Today I Learned) 병행 상태 머신 (2) | 2025.05.02 |
| TIL: Unity Addressable Asset System 정복하기 (0) | 2025.04.30 |
| TIL : 유니티 게임 시스템 구현 (1) | 2025.04.29 |