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 -.-> DMermaid
복사
용어 | 뜻 |
카탈로그 | "이 주소의 에셋은 어느 번들에 있고, 그 번들은 어디서 받는가"를 적은 목록. 패치는 결국 이 목록을 새것으로 바꾸는 일이다 |
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 를 직접 씁니다.





