Capcom이 RE:Dox를 오픈소스로 공개했습니다 — 오늘 바로 적용할 수 있는 데이터 엔진 아키텍처
핵심 요약
확인하세요: Capcom이 공개한 RE:Dox 데이터 엔진 아키텍처 — 선언적 스키마, 인덱스 접근, 컬럼형 직렬화, 자동 마이그레이션으로 파이프라인을 개선하고 Unity나 Godot 프로젝트에 적용하는 구체적인 실전 팁을 코드 예제로 지금 배워보세요.
엔티티 시스템이 시작 시 14,000개의 에셋 참조를 로드하고, 디자이너들은 더 빠른 반복 주기를 요구하며, 데이터 파이프라인이 직렬화를 런타임 메모리 레이아웃과 결합해 놓아서 모든 핫 리로드에 45초가 걸리고 있습니다. 익숙한 상황인가요? Capcom이 바로 이 문제를 해결하는 RE Engine의 핵심 부분을 오픈소스로 공개했습니다. 그리고 그 아키텍처는 가져다 쓸 만한 가치가 있습니다.
RE:Dox는 Capcom의 C# 데이터 엔진으로, Resident Evil Village, Street Fighter 6, Monster Hunter Rise 같은 타이틀에서 게임 데이터가 에디터 도구에서 직렬화를 거쳐 런타임 메모리로 흘러들어가는 과정을 관리하는 미들웨어입니다. 스키마 정의, 바이너리 직렬화, 에셋 참조, 그리고 디자이너가 C++ 엔진 코드를 다시 컴파일하지 않고도 반복 작업할 수 있게 해주는 데이터 파이프라인을 처리합니다.
이것은 장난감이 아닙니다. RE Engine은 Nintendo Switch 하드웨어에서 60fps로 실행되는 게임을 구동하면서 수천 개의 고유 데이터 타입 — 무기 스탯, AI 행동 트리, 머티리얼 파라미터, 애니메이션 그래프, 퀘스트 플래그, 전리품 테이블 — 을 관리합니다. 그 데이터 타입들은 모두 게임플레이 코드에 닿기 전에 RE:Dox 같은 시스템을 통과합니다.
이 아키텍처가 실제로 무엇을 하는지, Capcom이 왜 이런 특정 설계 선택을 했는지, 그리고 동일한 패턴을 자신의 엔진이나 프로젝트에 어떻게 적용할 수 있는지 하나씩 살펴보겠습니다.
AAA 스튜디오가 데이터 엔진을 만드는 이유 (그리고 당신도 그래야 하는 이유)
대부분의 인디 및 중형 프로젝트는 세 가지 방식 중 하나로 게임 데이터를 관리합니다.
- ScriptableObjects 또는 Resources(Unity): 디자이너가 편집할 수 있지만, 직렬화가 불투명하고 버전 관리가 고통스러우며, 대규모에서 교차 참조가 순환 의존성 악몽을 만듭니다.
- 커스텀 로더가 있는 JSON/CSV: 유연하지만 취약합니다. 스키마 검증도, 에디터 통합도 없고, 새 데이터 타입마다 수동 파싱을 해야 하며 컴파일 타임 안전성도 전무합니다.
- 하드코딩된 데이터 테이블: 런타임에는 빠르지만 유지보수가 불가능합니다. 무기의 데미지 값을 바꾸려면 리컴파일이 필요합니다.
각 방식은 서로 다른 규모에서 한계에 부딪힙니다. ScriptableObjects는 Unity의 직렬화 오버헤드가 참조 깊이에 따라 선형적으로 증가하기 때문에 고유 에셋 약 2,000~5,000개에서부터 문제를 일으키기 시작합니다. JSON 로더는 두 디자이너가 같은 파일을 수정하고, 4시간 동안의 QA 세션 후에 병합 충돌을 발견하는 순간 무너집니다. 하드코딩된 테이블은 디자이너가 팀에 합류하는 순간 무너집니다.
RE:Dox 같은 데이터 엔진은 근본적인 문제를 해결합니다. 데이터 스키마와 런타임 메모리 레이아웃을 분리하고, 동시에 양쪽을 자동으로 동기화하는 것.
이것이 실제로 의미하는 바는 다음과 같습니다. 일반적인 Unity 프로젝트에서 무기 스탯에 새 필드를 추가하려면 다음과 같은 과정을 거칩니다:
- C# 클래스에 필드를 추가한다
- Unity의 직렬화기가 이를 올바르게 처리해 주기를 기대한다
- 기존 모든 ScriptableObject 인스턴스를 수동으로 업데이트한다 (또는 마이그레이션 스크립트를 작성한다)
- 에셋 번들을 다시 빌드한다
- 직렬화 라운드트립이 기존 데이터를 손상시키지 않는지 테스트한다
RE:Dox 같은 데이터 엔진을 사용하면 작업 흐름은 다음과 같습니다:
- 스키마 정의 파일에 필드를 정의한다
- 엔진이 직렬화 코드, 에디터 UI, 마이그레이션 경로를 자동으로 생성한다
- 기존 데이터가 합리적인 기본값으로 새 필드를 얻게 된다
- 관련 없는 에셋을 다시 빌드할 필요가 없다
이 차이는 30명 규모의 콘텐츠 팀에서 847개의 고유 데이터 타입과 40,000개의 에셋 인스턴스를 관리하게 되면 결코 작아 보이지 않습니다.
RE:Dox 아키텍처: 기술적 분석
RE:Dox는 전적으로 C#으로 작성되었으며 모든 .NET 기반 게임 프로젝트에 통합할 수 있는 독립형 라이브러리로 동작합니다. Unity나 특정 엔진에 묶여 있지 않습니다. Capcom이 자체 RE Engine 툴체인과 함께 동작하도록 설계했기 때문에, 추상화는 필연적으로 엔진에 종속되지 않습니다.
핵심 아키텍처는 네 개의 레이어로 구성됩니다:
레이어 1: 스키마 정의
RE:Dox의 모든 데이터 타입은 스키마 정의에서 시작합니다. 런타임 표현과 직렬화 포맷이 서로 얽혀 있는 C# 클래스와 달리, RE:Dox의 스키마는 독립적으로 선언됩니다.
Protocol Buffers나 FlatBuffers와 비슷하다고 생각하면 됩니다. 중립적인 포맷으로 데이터의 형태를 정의하면, 시스템이 그 데이터를 다루는 코드를 생성합니다. 다음은 무기 데이터 타입을 위한 단순화된 스키마 정의 예시입니다:
// Schema definition — this is NOT your runtime class
// RE:Dox uses a declarative format to describe data structures
public class WeaponSchema : DataSchema
{
public override string TypeName => "WeaponData";
public override int Version => 3; // bump when fields change
protected override void DefineFields(SchemaBuilder builder)
{
builder.String("Id", flags: FieldFlags.PrimaryKey);
builder.String("DisplayName");
builder.Int32("BaseDamage", defaultValue: 10);
builder.Float("AttackSpeed", defaultValue: 1.0f);
builder.Float("CritMultiplier", defaultValue: 1.5f);
builder.Int32("MaxDurability", defaultValue: 100);
builder.Enum<DamageType>("DamageType");
builder.Array<string>("Tags");
builder.Reference("UpgradePath", "WeaponUpgradeTree");
}
// Migration from version 2 → 3: added CritMultiplier
protected override void MigrateV2ToV3(DataRecord record)
{
record.SetFloat("CritMultiplier", 1.5f);
}
}
여기서 주목할 점은 세 가지입니다:
버전 추적이 내장되어 있습니다. 모든 스키마에는 정수 버전이 있습니다. 스키마를 변경하면 이전 레코드를 새 포맷으로 변환하는 마이그레이션 함수를 작성합니다. 이것은 편집 시간이 아니라 로드 시간에 실행되므로 기존 데이터 파일을 수동으로 업데이트할 필요가 없습니다. Capcom의 RE Engine은 2017년부터 프로덕션에서 사용되어 왔습니다. 즉, 여러 출시 타이틀에 걸쳐 8년간의 스키마 진화가 있었습니다. 자동 마이그레이션이 없었다면 데이터 유지보수 비용만으로도 팀 하나를 통째로 소모했을 것입니다.
필드 정의는 선언적입니다. 속성을 작성하는 것이 아니라 데이터가 무엇인지 기술하고 시스템이 접근자 코드를 생성합니다. 이는 직렬화 레이어가 리플렉션 없이도 모든 필드를 정확하게 패킹/언패킹할 수 있다는 뜻이며, 여기서 다음 레이어로 이어집니다.
참조는 명시적입니다. Reference 필드 타입은 다른 데이터 레코드에 대한 타입이 지정된 링크를 만듭니다. Unity의 SerializeReference 또는 원시 객체 참조와 달리, 이는 스키마 로드 시간에 검증됩니다. 대상 레코드가 없으면 출시 빌드의 런타임에서 null 참조가 발생하는 대신 에디터에서 오류가 발생합니다.
레이어 2: 코드 생성 및 접근자
스키마가 정의되면 RE:Dox는 강력한 형식의 접근자 구조체(struct)를 생성합니다. 이는 성능에 매우 중요합니다. 생성된 코드는 박싱을 피하고, 리플렉션을 피하며, JIT 컴파일러가 적극적으로 최적화하거나 Unity 환경에서 Burst 컴파일할 수 있습니다.
다음은 무기 스키마에 대해 생성된 접근자 예시입니다:
// Generated by RE:Dox — do not edit manually
public readonly ref struct WeaponDataAccessor
{
private readonly DataRecord _record;
public WeaponDataAccessor(DataRecord record)
{
_record = record;
}
public string Id => _record.GetString(0); // field index 0
public string DisplayName => _record.GetString(1); // field index 1
public int BaseDamage => _record.GetInt32(2); // field index 2
public float AttackSpeed => _record.GetFloat(3); // field index 3
public float CritMultiplier => _record.GetFloat(4); // field index 4
public int MaxDurability => _record.GetInt32(5); // field index 5
public DamageType DamageType => (DamageType)_record.GetEnum(6);
public ArraySegment<string> Tags => _record.GetStringArray(7);
// Reference returns a lazy-loaded record handle
public DataHandle UpgradePath => _record.GetReference(8);
}
핵심 설계 선택은 필드를 이름이 아니라 인덱스로 접근한다는 것입니다. 스키마는 필드 이름을 로드 시간에 인덱스로 매핑하며, 모든 런타임 접근은 정수 인덱스를 사용합니다. 즉, 속성 접근은 플랫 바이트 버퍼에 대한 단순한 오프셋 계산으로 컴파일됩니다. 해시 조회도, 딕셔너리 탐색도, 문자열 비교도 없습니다.
Monster Hunter Rise 같은 게임은 수백 개의 무기 변형이 있고, 데미지 계산 중 매 프레임마다 각각 수십 번씩 접근됩니다. O(1) 인덱스 접근과 O(log n) 딕셔너리 조회의 차이는 프레임 예산을 맞추느냐 못 맞추느냐의 차이입니다.
레이어 3: 바이너리 직렬화
RE:Dox는 직렬화된 데이터에 컬럼형 바이너리 포맷을 사용합니다. 대부분의 커스텀 솔루션이 틀리는 부분이 바로 여기입니다. 각 레코드를 독립적으로 직렬화하는 행 중심(row-oriented) 방식은 작은 데이터셋에서는 잘 동작하지만, 시작 시 50,000개 레코드를 로드해야 할 때 무너집니다.
Row-oriented: [id_1, dmg_1, spd_1, crit_1] [id_2, dmg_2, spd_2, crit_2] ...
Columnar: [id_1, id_2, id_3, ...] [dmg_1, dmg_2, dmg_3, ...] [spd_1, spd_2, spd_3, ...]
이것이 게임에서 왜 중요할까요? 세 가지 이유입니다:
캐시 지역성. 전투 시스템이 모든 무기 데미지 값을 순회하며 DPS 순위를 계산해야 할 때, 컬럼형 레이아웃은 해당 값들이 메모리에서 서로 인접하게 배치된다는 뜻입니다. CPU 프리페처가 그 값들을 L1 캐시로 자동으로 가져옵니다. 행 중심 레이아웃에서는 레코드마다 관련 없는 필드(ID, 이름, 태그)를 건너뛰어야 하므로 캐시 미스가 발생합니다. 최신 하드웨어에서 L1 캐시 히트는 약 1ns, 메인 메모리 페치는 약 100ns가 소요됩니다. 여기에 50,000개 레코드를 곱하면 50µs와 5ms의 차이가 됩니다.
압축. 컬럼형 데이터는 인접한 값들이 비슷한 경향이 있기 때문에(델타 인코딩, 런 길이 인코딩) 압축률이 훨씬 좋습니다. Capcom의 Resident Evil Village용 RE Engine 에셋은 총 약 27GB입니다. 데이터 엔진의 직렬화 포맷은 16GB 시스템 RAM을 갖춘 하드웨어에서 로드되는 메모리 사용량을 관리 가능한 수준으로 유지하는 데 도움을 줍니다.
선택적 로딩. UI 툴팁에 무기 이름과 데미지 값만 필요하다면, 컬럼형 포맷에서는 태그, 업그레이드 경로, 애니메이션 참조를 전혀 로드하지 않을 수 있습니다. 데이터 엔진이 요청한 컬럼만 읽으므로 I/O 시간과 메모리 할당을 모두 줄일 수 있습니다.
레이어 4: 에디터 통합
네 번째 레이어는 — 틀림없이 콘텐츠 팀에 가장 큰 가치를 제공하는 부분 — 에디터 통합입니다. 스키마가 선언적이기 때문에 RE:Dox는 에디터 UI를 자동으로 생성할 수 있습니다:
- 새 데이터 타입이 정의되면? 코드 변경 없이 에디터 패널이 나타납니다.
- 필드에
defaultValue가 있으면? 에디터가 기본값을 미리 채우고 오버라이드를 강조합니다. - 필드가 다른 스키마에 대한
Reference인가요? 에디터가 유효한 대상으로 채워진 검색 가능한 드롭다운을 제공합니다. - 스키마 버전이 올라갔나요? 에디터가 변경 사항을 적용하기 전에 마이그레이션 미리보기를 보여줍니다.
이것이 데이터 엔진을 만들 가치가 있게 만드는 패턴입니다. 스키마를 정의하고 마이그레이션 코드를 작성하는 초기 비용은, 디자이너가 코드를 건드리지 않고, 리컴파일을 기다리지 않고, 직렬화 오류를 디버깅해 달라고 엔지니어를 찾지 않아도 게임 데이터를 만들고 편집하고 검증할 수 있게 되는 순간 수백 배로 보상받습니다.
C#으로 최소 데이터 엔진 구축하기
이 패턴들의 가치를 얻기 위해 RE:Dox의 전체 아키텍처를 복제할 필요는 없습니다. 다음은 가장 영향력 있는 세 가지 개념, 즉 선언적 스키마, 인덱스 필드 접근, 자동 마이그레이션을 구현한 축소판 데이터 엔진입니다.
using System;
using System.Collections.Generic;
// === Core Data Record (flat byte storage) ===
public class DataRecord
{
private readonly byte[] _data;
private readonly Schema _schema;
public DataRecord(Schema schema, byte[] data = null)
{
_schema = schema;
_data = data ?? new byte[schema.TotalSize];
}
public int GetInt32(string fieldName)
{
int offset = _schema.GetOffset(fieldName);
return BitConverter.ToInt32(_data, offset);
}
public void SetInt32(string fieldName, int value)
{
int offset = _schema.GetOffset(fieldName);
BitConverter.TryWriteBytes(_data.AsSpan(offset), value);
}
public float GetFloat(string fieldName)
{
int offset = _schema.GetOffset(fieldName);
return BitConverter.ToSingle(_data, offset);
}
public void SetFloat(string fieldName, float value)
{
int offset = _schema.GetOffset(fieldName);
BitConverter.TryWriteBytes(_data.AsSpan(offset), value);
}
public byte[] Serialize() => _data;
}
// === Schema Definition ===
public class Schema
{
private readonly Dictionary<string, FieldInfo> _fields = new();
private int _currentOffset = 0;
public int TotalSize => _currentOffset;
public int Version { get; set; } = 1;
public void Int32(string name, int defaultValue = 0)
{
_fields[name] = new FieldInfo(_currentOffset, defaultValue);
_currentOffset += 4;
}
public void Float(string name, float defaultValue = 0f)
{
_fields[name] = new FieldInfo(_currentOffset, defaultValue);
_currentOffset += 4;
}
public int GetOffset(string fieldName) =>
_fields.TryGetValue(fieldName, out var fi)
? fi.Offset
: throw new KeyNotFoundException($"Field '{fieldName}' not in schema.");
public bool HasField(string name) => _fields.ContainsKey(name);
private readonly record struct FieldInfo(int Offset, object DefaultValue);
}
// === Migratable Data Store ===
public class DataStore
{
private readonly Dictionary<string, DataRecord> _records = new();
private readonly Schema _schema;
private readonly Dictionary<int, Action<DataRecord>> _migrations = new();
public DataStore(Schema schema)
{
_schema = schema;
}
public void RegisterMigration(int fromVersion, Action<DataRecord> migrate)
{
_migrations[fromVersion] = migrate;
}
public void Insert(string id, DataRecord record, int recordVersion)
{
// Apply migrations in order
for (int v = recordVersion; v < _schema.Version; v++)
{
if (_migrations.TryGetValue(v, out var migrate))
migrate(record);
}
_records[id] = record;
}
public DataRecord Get(string id) => _records.GetValueOrDefault(id);
}
// === Usage Example ===
public static class WeaponDataExample
{
public static void Run()
{
// Define schema
var schema = new Schema();
schema.Version = 2;
schema.Int32("BaseDamage", defaultValue: 10);
schema.Float("AttackSpeed", defaultValue: 1.0f);
schema.Int32("MaxDurability", defaultValue: 100);
// Create store with migration from v1 → v2
var store = new DataStore(schema);
store.RegisterMigration(fromVersion: 1, record =>
{
// v1 records had no MaxDurability; set default
record.SetInt32("MaxDurability", 100);
});
// Insert a record saved under the old v1 schema
var sword = new DataRecord(schema);
sword.SetInt32("BaseDamage", 25);
sword.SetFloat("AttackSpeed", 1.3f);
store.Insert("iron_sword", sword, recordVersion: 1);
// Migration applied: MaxDurability now defaults to 100
var loaded = store.Get("iron_sword");
Console.WriteLine($"Damage: {loaded.GetInt32("BaseDamage")}"); // 25
Console.WriteLine($"Durability: {loaded.GetInt32("MaxDurability")}"); // 100 (migrated)
}
}
이 코드는 대략 100줄이며 인덱스 필드 접근, 플랫 바이너리 저장, 버전 마이그레이션을 제공합니다. RE:Dox를 대체하지는 못하지만 데이터 엔진이 동작하게 만드는 핵심 패턴을 보여줍니다. 데이터가 무엇인지와 어떻게 저장되는지를 분리하고, 그 정의에 가하는 모든 변경을 추적하십시오.
게임 데이터 아키텍처를 위한 교훈
1. 스키마와 표현을 분리하라
RE:Dox에서 가장 영향력 있는 패턴은 데이터 정의를 런타임 클래스에서 분리하는 것입니다. 게임플레이 코드는 편리하고 강력한 타입의 접근자로 작업해야 합니다. 저장 포맷은 컴팩트하고 버전이 관리되며 그러한 접근자와 독립적이어야 합니다.
무기 클래스가 변경되어도 세이브 파일이 깨지지 않아야 합니다. 세이브 포맷이 발전해도 무기 클래스를 리팩터링할 필요가 없어야 합니다. 이 둘은 명시적인 마이그레이션 레이어로 연결된 별개의 관심사입니다.
2. 모든 것을 버전 관리하고 자동으로 마이그레이션하라
출시된 게임은 모두 스키마 부채를 쌓게 됩니다. RE Engine은 2017년부터 최소 12개 주요 타이틀에서 운영되어 왔습니다. 즉, 8년 동안 수백 건의 스키마 마이그레이션이 있었다는 뜻입니다. Capcom이 마이그레이션 함수를 스키마 정의에 직접 내장하는 방식은 모든 데이터 파일이 자기 버전 번호를 갖고 있음을 의미하며, 엔진이 로드 시 어떤 오래된 파일이든 현재 포맷으로 업그레이드할 수 있습니다.
데이터 스키마 버전 관리는 첫날부터 시작하세요. 시퀄을 출시하지 않더라도 얼리 액세스 빌드는 1.0 출시 때까지 살아남아야 하는 구식 포맷의 세이브 파일을 만들게 됩니다.
3. 핫 패스 데이터에는 인덱스 접근을 사용하라
데이터 필드가 프레임당 반복 루프 안에서 읽히는 경우 — 데미지 값, 애니메이션 타이밍, AI 가중치 — 이름이나 해시가 아니라 인덱스로 접근해야 합니다. 접근당 차이는 작지만 총합은 어마어마합니다. 60fps에서 200개의 활성 무기를 순회하는 전투 시스템은 초당 12,000회의 필드 읽기를 수행합니다. 인덱스 접근은 읽기당 약 1ns(직접 메모리 오프셋)로 완료되지만, 딕셔너리 조회는 읽기당 약 50~200ns가 걸립니다.
로드 시 한 번만 읽는 데이터(표시 이름, 설명, 로어 텍스트)에는 인덱스 접근이 중요하지 않습니다. 최적화 전에 프로파일링을 하세요. 다만 필요할 때 쓸 수 있는 패턴이 있다는 것을 알아두세요.
4. 대규모 데이터셋에는 컬럼형 저장을 사용하라
게임에서 특정 데이터 타입의 레코드가 1,000개를 넘는다면 컬럼형 저장 레이아웃을 고려하세요. 캐시 지역성 이점은 실재하며 측정 가능합니다. 유사한 아키텍처의 벤치마크에 따르면 집계 쿼리(합계, 필터, 정렬)의 반복 속도가 행 중심 저장보다 3~8배 향상됩니다.
이것은 전리품 테이블, 인벤토리 카탈로그, 대화 트리, 퀘스트 데이터베이스 등 무언가를 찾거나 계산하기 위해 많은 레코드를 순회해야 하는 모든 곳에 적용됩니다.
5. 스키마에서 에디터 UI를 생성하라
데이터 엔진의 생산성 배수 효과는 런타임 성능이 아니라 디자이너의 반복 속도에서 나옵니다. 스키마가 선언적이라면 에디터 인터페이스를 자동으로 생성할 수 있습니다. 이는 디자이너와 엔지니어 사이에서 가장 흔한 마찰 원인인 "이거에 필드 하나 추가해 줄 수 있어?"를 제거합니다.
스키마 기반 접근에서는 필드 추가가 한 줄짜리 스키마 변경일 뿐이며, 그 변경은 모든 에디터 패널에 자동으로 나타납니다. C# 수정도, 커스텀 프로퍼티 드로어도, 인스펙터 스크립트도 필요 없습니다. 두 명보다 큰 팀을 위한 툴을 만든다면 이 이유 하나만으로 투자가 정당화됩니다.
Backend 시스템의 위치
RE:Dox는 엔진 측 데이터 파이프라인, 즉 데이터가 어떻게 정의되고 직렬화되며 런타임 메모리에 로드되는지를 처리합니다. 하지만 현대 게임은 플레이어별 데이터도 관리해야 합니다. 기기 간 동기화되는 세이브 파일, 게임플레이 텔레메트리로 채워지는 리더보드, 패치를 배포하지 않고 드랍률을 튜닝할 수 있는 원격 구성(Remote Configuration) 같은 것들입니다.
이 Backend 레이어를 직접 구축한다면 사용자 인증 흐름, 동시 쓰기에 대한 충돌 해결, 클라우드 세이브의 리비전 추적, 팀이 실제로 쓸 수 있는 대시보드까지 고려해야 합니다. 이것은 게임플레이 기능 하나를 작성하기 전에 몇 주간의 인프라 작업입니다.
horizOn 같은 서비스는 이러한 특정 Backend 문제들을 처리합니다. 계정 연동 클라우드 세이브(리비전 인식 읽기/쓰기), 리더보드, 원격 구성, 크래시 리포팅 등을 제공하므로, 플레이어 경험에 직접 영향을 주는 엔진 측 데이터 아키텍처에 개발 시간을 집중할 수 있습니다.
현재 프로젝트에 이 패턴들 적용하기
RE:Dox 아키텍처의 이점을 얻기 위해 완전한 데이터 엔진을 구축할 필요는 없습니다. 다음의 구체적인 단계로 시작하세요:
- 현재 데이터 로딩을 감사하세요. 시작 시간이 얼마나 걸리는지, 시간이 어디에 소비되는지 프로파일링하세요. 로드 시간의 15% 이상이 데이터 역직렬화와 검증에 쓰인다면 최적화 여지가 있습니다.
- 가장 핫한 데이터 경로를 식별하세요. 프로파일러를 사용해 게임플레이 중 가장 자주 접근되는 데이터 구조를 찾으세요. 그것들이 인덱스 접근과 컬럼형 저장의 후보입니다.
- 마이그레이션 프레임워크를 작성하세요. 세이브 파일에 간단한 버전 번호를 넣고 마이그레이션 함수 딕셔너리를 만드는 것만으로도 가치의 90%를 얻을 수 있습니다. 전체 스키마 시스템이 없어도 마이그레이션 이점을 얻을 수 있습니다.
- 데이터 정의를 분리하세요. 무기 스탯이 Unity가 디스크에 직렬화하는 C# 클래스에 정의되어 있다면, 그것이 결합 지점입니다. 필드 정의를 별도의 버전 관리되는 스키마로 추출하세요. 단지 검증 대상 JSON 스키마 파일일지라도 말입니다.
- 변경 전후를 벤치마킹하세요. 변경 전에 로드 시간, 메모리 사용량, 핫 패스 반복 속도를 측정하세요. 데이터 아키텍처 개선은 시간이 지날수록 복리 효과가 있지만, 투자가 효과가 있다는 것을 증명하려면 기준점이 필요합니다.
더 큰 그림: 오픈소스가 왜 중요한가
Capcom이 RE:Dox를 오픈소스로 공개한 것은 코드 자체 이상으로 의미가 있습니다. AAA 스튜디오는 역사적으로 엔진 기술을 독점적 우위로 취급해 왔습니다. Capcom이 이런 구성 요소를 공개하는 것은 데이터 엔진 레이어가 자신들의 경쟁 해자가 아니라는 신호를 보내는 것입니다. 콘텐츠, 게임 디자인, 아트 디렉션이야말로 경쟁 우위라는 뜻입니다.
인디 및 중형 개발자에게 이것은 업계에서 가장 성공한 게임 엔진 중 하나의 패턴과 아키텍처 결정을 직접 들여다볼 수 있게 되었음을 의미합니다. Capcom이 스키마 마이그레이션을 어떻게 처리하는지 읽고, 직렬화 선택을 연구하고, 그 교훈을 자신의 프로젝트에 적용할 수 있습니다.
코드 자체가 Unity나 Godot 프로젝트에 바로 들어맞지는 않을 수 있습니다. 그러나 선언적 스키마, 인덱스 접근, 컬럼형 저장, 자동 마이그레이션, 생성형 에디터 UI 같은 아키텍처 패턴은 보편적입니다. 엔진, 언어, 규모와 관계없이 동작합니다.
하나의 패턴으로 시작하세요. 데이터에 버전을 매기세요. 거기서부터 확장해 나가세요. 미래의 당신과 디자이너가 고마워할 것입니다.