서비스
교육 철학

Resource Updating(리소스 업데이트)-Addressable

Abstract

📌
리소스 업데이트는 스토어 심사를 다시 거치지 않고 게임 내용을 바꾸는 방법입니다. 이 에디션에서 그것은 프레임워크의 기능이 아니라 Addressables 의 기능입니다. 에디터에서 번들을 서버에 올려 두고, 게임 시작 시 Procedure 하나가 "새 것이 있는지 확인 → 필요한 만큼 미리 받기"를 한 뒤 본 게임으로 넘깁니다. 그 뒤의 ShowEntity / OpenUIForm 은 평소와 똑같이 쓰면 새 번들에서 로드됩니다.
실무에서 필요한 순간
•
밸런스 표나 이벤트 배너를 빌드 없이 바꾸고 싶을 때
•
앱 설치 용량을 줄이고 나머지를 첫 실행 때 받게 하고 싶을 때
•
시즌 콘텐츠를 날짜에 맞춰 열고 싶을 때
코드(C# 스크립트)는 이 방법으로 바꿀 수 없습니다. 바뀌는 것은 프리팹·텍스처·사운드·데이터 파일 같은 에셋입니다.
ToyboxNightmare 에서는
쓰지 않습니다. 오프라인 싱글플레이 게임이고 25개 에셋이 전부 앱 안(Local 그룹)에 들어 있습니다. 이 페이지의 절차는 라이브 서비스로 확장할 때 필요합니다.

전체 흐름

graph LR
    subgraph 에디터
        A["그룹을 Remote 로"] --> B["빌드"] --> C["번들·카탈로그를<br>서버에 업로드"]
    end
    subgraph 게임 실행
        D["카탈로그 갱신 확인"] --> E["받을 크기 확인"] --> F["미리 다운로드"] --> G["본 게임 진입"]
    end
    C -.-> D
Mermaid
복사
용어
뜻
카탈로그
"이 주소의 에셋은 어느 번들에 있고, 그 번들은 어디서 받는가"를 적은 목록. 패치는 결국 이 목록을 새것으로 바꾸는 일이다
Remote 그룹
번들을 앱에 넣지 않고 서버에서 받게 설정한 그룹
Content State 파일
addressables_content_state.bin. "출시한 빌드에 무엇이 들어 있었나"의 기록. 다음 패치를 만들 때 비교 기준이 된다

에디터에서 준비하기

1.
패치할 에셋을 담을 그룹을 만들고, 그룹 설정의 Build & Load Paths 를 Remote 로 바꾼다.
2.
Window → Asset Management → Addressables → Profiles 에서 Remote.LoadPath 에 CDN 주소를 넣는다. (예: https://cdn.example.com/[BuildTarget])
3.
Addressables Settings 에서 Build Remote Catalog 를 켠다. 카탈로그가 서버에 있어야 앱이 "새 버전이 있다"는 것을 알 수 있다.
4.
Groups 창에서 Build → New Build → Default Build Script. ServerData/<플랫폼>/ 에 번들(.bundle)과 카탈로그(.bin 또는 .json, .hash)가 나온다. 이것을 서버에 올린다.
5.
이때 생긴 addressables_content_state.bin 을 보관한다. 출시한 빌드마다 하나씩이다.
내용을 바꿔 패치를 낼 때
1.
에셋을 고친다.
2.
Build → Update a Previous Build 를 고르고 보관해 둔 addressables_content_state.bin 을 지정한다.
3.
ServerData/<플랫폼>/ 에 새로 생긴 파일을 서버에 올린다. 달라진 번들과 새 카탈로그만 바뀐다.
💡
무엇을 Remote 로 둘 것인가 — 전부 Remote 로 두면 첫 실행에 네트워크가 반드시 필요합니다. 보통 "첫 화면과 튜토리얼에 필요한 것은 Local, 나머지는 Remote" 로 나눕니다. 자주 바뀌는 것(밸런스 데이터, 이벤트 UI)과 거의 안 바뀌는 것(캐릭터 모델)을 다른 그룹에 두면, 패치 때 작은 번들만 다시 받게 됩니다.

게임 흐름에 끼워 넣기

Procedure 컴포넌트의 Entrance Procedure 를 새로 만든 ProcedureCheckUpdate 로 바꾸고, 끝나면 ProcedureMain 으로 넘기는 구조입니다.
graph LR
    A["ProcedureCheckUpdate"] -->|"받을 것 없음 / 다 받음"| B["ProcedureMain"]
    A -->|"실패"| C["재시도 안내"]
Mermaid
복사
미리 받을 에셋에는 Addressables 라벨(아래 예제의 preload)을 붙여 둡니다. 라벨 하나로 "이 묶음을 전부 받아 둬"를 지시할 수 있습니다.
using System.Collections.Generic; using GameFramework.Procedure; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.AsyncOperations; using UnityGameFramework.Runtime; using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>; namespace MyGame { /// <summary>카탈로그 갱신 → 필요한 번들 다운로드 → 본 게임 진입.</summary> public class ProcedureCheckUpdate : ProcedureBase { private const string PreloadLabel = "preload"; // 미리 받을 에셋에 붙인 Addressables 라벨 private enum Step { CheckCatalog, UpdateCatalog, GetSize, Download, Done, Failed } private Step mStep; private AsyncOperationHandle<List<string>> mCheckHandle; private AsyncOperationHandle<List<IResourceLocator>> mUpdateHandle; private AsyncOperationHandle<long> mSizeHandle; private AsyncOperationHandle mDownloadHandle; protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); mCheckHandle = Addressables.CheckForCatalogUpdates(false); mStep = Step.CheckCatalog; } protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) { base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); switch (mStep) { case Step.CheckCatalog: TickCheckCatalog(); break; case Step.UpdateCatalog: TickUpdateCatalog(); break; case Step.GetSize: TickGetSize(); break; case Step.Download: TickDownload(); break; case Step.Done: ChangeState<ProcedureMain>(procedureOwner); break; } } private void TickCheckCatalog() { if (!mCheckHandle.IsDone) return; bool failed = mCheckHandle.Status != AsyncOperationStatus.Succeeded; bool hasNewCatalog = !failed && mCheckHandle.Result.Count > 0; if (failed) { Fail("카탈로그 확인 실패"); } else if (hasNewCatalog) { mUpdateHandle = Addressables.UpdateCatalogs(mCheckHandle.Result, false); mStep = Step.UpdateCatalog; } else { StartGetSize(); } Addressables.Release(mCheckHandle); } private void TickUpdateCatalog() { if (!mUpdateHandle.IsDone) return; bool succeeded = mUpdateHandle.Status == AsyncOperationStatus.Succeeded; Addressables.Release(mUpdateHandle); if (!succeeded) { Fail("카탈로그 갱신 실패"); return; } StartGetSize(); } private void StartGetSize() { mSizeHandle = Addressables.GetDownloadSizeAsync(PreloadLabel); mStep = Step.GetSize; } private void TickGetSize() { if (!mSizeHandle.IsDone) return; long bytesToDownload = mSizeHandle.Status == AsyncOperationStatus.Succeeded ? mSizeHandle.Result : 0L; Addressables.Release(mSizeHandle); if (bytesToDownload <= 0L) { mStep = Step.Done; // 이미 다 받아 둔 상태 return; } Log.Info("패치 다운로드 시작: {0} bytes", bytesToDownload); mDownloadHandle = Addressables.DownloadDependenciesAsync(PreloadLabel, false); mStep = Step.Download; } private void TickDownload() { if (!mDownloadHandle.IsDone) { // 진행률 UI 는 여기서: mDownloadHandle.GetDownloadStatus().Percent (0~1) return; } bool succeeded = mDownloadHandle.Status == AsyncOperationStatus.Succeeded; Addressables.Release(mDownloadHandle); if (!succeeded) { Fail("번들 다운로드 실패"); return; } mStep = Step.Done; } private void Fail(string reason) { Log.Error("ProcedureCheckUpdate: {0}", reason); mStep = Step.Failed; // 여기서 재시도 팝업을 띄운다 } } }
C#
복사
코루틴이나 async 대신 OnUpdate 에서 핸들의 IsDone 을 확인하는 방식으로 썼습니다. Procedure 는 MonoBehaviour 가 아니라 코루틴을 직접 돌릴 수 없고, 이 방식이 프레임워크의 다른 Procedure 와 모양이 같습니다.
💡
이 코드는 이 에디션(Addressables 2.9)에서 컴파일되는 것까지 확인했습니다. 실제 CDN 을 두고 패치를 받아 보는 시험은 하지 않았으므로, 적용할 때는 아래 "에디터에서 시험하기" 절차로 한 번 돌려 보세요.

에디터에서 시험하기

개발용 기본 설정인 Use Asset Database 모드에는 원격 카탈로그가 없어서 이 흐름을 탈 수 없습니다.
1.
Groups 창에서 Play Mode Script 를 Use Existing Build 로 바꾼다.
2.
로컬에 간단한 파일 서버를 띄운다. Addressables 2.x 에는 예전의 Hosting 창이 없으므로, ServerData 폴더에서 python -m http.server 8000 같은 정적 서버를 실행하고 프로필의 Remote.LoadPath 를 http://localhost:8000/[BuildTarget] 로 맞춘다. http 주소를 쓰려면 Player Settings 의 Allow downloads over HTTP 를 허용으로 바꿔야 한다(시험이 끝나면 되돌린다).
3.
번들을 빌드하고 플레이 → 다운로드가 일어나는지 본다.
4.
에셋을 고쳐 Update a Previous Build → 다시 플레이 → 새 카탈로그를 받아 오는지 본다.
5.
시험이 끝나면 Play Mode Script 를 Use Asset Database 로 되돌린다.

자주 걸리는 곳

⚠️
•
핸들은 쓴 뒤 반드시 Release 합니다. 위 코드는 autoReleaseHandle 에 false 를 넘기고 결과를 읽은 다음 직접 풉니다. true 로 두면 완료 직후 핸들이 무효가 되어 Result 를 읽을 수 없습니다.
•
카탈로그 갱신은 에셋을 로드하기 전에 합니다. 이미 로드한 에셋이 있는 상태에서 카탈로그를 바꾸면 옛 번들과 새 번들이 섞입니다. 그래서 Entrance Procedure 에 둡니다.
•
addressables_content_state.bin 을 잃어버리면 그 빌드에 대한 패치를 만들 수 없습니다. 출시 빌드마다 버전 관리에 넣어 두세요.
•
받지 않고 넘어가도 게임은 됩니다. Remote 그룹의 에셋은 처음 요청될 때 자동으로 내려받습니다. 다만 그 순간 ShowEntity 가 다운로드가 끝날 때까지 늦어지므로, 전투 중에 끊기지 않게 하려면 위처럼 미리 받아 둡니다.
•
Local 그룹의 에셋을 고친 경우는 번들을 서버에 올려도 반영되지 않습니다. 앱 안의 번들을 계속 쓰기 때문입니다. Update a Previous Build 전에 Tools → Check for Content Update Restrictions 를 돌리면 그런 에셋을 찾아 Remote 그룹으로 옮겨 줍니다.
•
프레임워크의 IResourceManager 에 있는 CheckResources / UpdateResources / ApplyResources 는 이 에디션에서 빈 구현입니다. 호출해도 패치가 일어나지 않으므로 위처럼 Addressables API 를 직접 씁니다.