Capcom Open-Sourced RE:Dox — Here's the Data Engine Architecture You Can Steal Today
In a nutshell
Dissect Capcom's open-source RE:Dox data engine and learn game data engine architecture patterns used in Resident Evil, Street Fighter 6, and Monster Hunter Rise.
Your entity system loads 14,000 asset references at startup, your designers are begging for faster iteration cycles, and every hot-reload takes 45 seconds because your data pipeline couples serialization to runtime memory layouts. Sound familiar? Capcom just open-sourced the exact piece of their RE Engine that solves this problem — and the architecture is worth stealing.
RE:Dox is Capcom's C# data engine, the middleware responsible for managing how game data flows from editor tools through serialization and into runtime memory for titles like Resident Evil Village, Street Fighter 6, and Monster Hunter Rise. It handles schema definition, binary serialization, asset referencing, and the data pipeline that lets designers iterate without recompiling C++ engine code.
This is not a toy. RE Engine powers games that run at 60fps on Nintendo Switch hardware while managing thousands of unique data types — weapon stats, AI behavior trees, material parameters, animation graphs, quest flags, loot tables. Every one of those data types flows through a system like RE:Dox before it touches gameplay code.
Let's break down what this architecture actually does, why Capcom made these specific design choices, and how you can apply the same patterns to your own engine or project.
Why AAA Studios Build Data Engines (And Why You Should Too)
Most indie and mid-size projects manage game data with one of three approaches:
- ScriptableObjects or Resources (Unity): Designer-editable, but serialization is opaque, versioning is painful, and cross-referencing creates circular dependency nightmares at scale.
- Raw JSON/CSV with custom loaders: Flexible but fragile. No schema validation, no editor integration, manual parsing for every new data type, and zero compile-time safety.
- Hard-coded data tables: Fast at runtime, impossible to maintain. Changing a weapon's damage value requires a recompile.
Each approach breaks at a different scale point. ScriptableObjects start choking around 2,000–5,000 unique assets because Unity's serialization overhead grows linearly with reference depth. JSON loaders break the moment two designers edit the same file and you discover the merge conflict after a four-hour QA session. Hard-coded tables break the moment a designer exists on your team.
A data engine like RE:Dox solves the fundamental problem: decoupling your data schema from your runtime memory layout while keeping both in sync automatically.
Here's what that means in practice. In a typical Unity project, if you want to add a new field to your weapon stats, you:
- Add the field to your C# class
- Hope Unity's serializer picks it up correctly
- Manually update every existing ScriptableObject instance (or write a migration script)
- Rebuild asset bundles
- Test that serialization round-trips don't corrupt existing data
With a data engine like RE:Dox, the workflow is:
- Define the field in a schema definition file
- The engine generates serialization code, editor UI, and migration paths automatically
- Existing data gains the new field with a sensible default
- No rebuild of unrelated assets required
That difference sounds small until you have 847 unique data types and 40,000 asset instances across a 30-person content team.
The Architecture of RE:Dox: A Technical Breakdown
RE:Dox is written entirely in C# and operates as a standalone library that can be integrated into any .NET-based game project. It's not tied to Unity or any specific engine — Capcom designed it to work with their custom RE Engine toolchain, which means the abstractions are engine-agnostic by necessity.
The core architecture has four layers:
Layer 1: Schema Definition
Every data type in RE:Dox starts with a schema definition. Unlike C# classes where your runtime representation and your serialization format are tangled together, schemas in RE:Dox are declared independently.
Think of it like Protocol Buffers or FlatBuffers — you define what your data looks like in a neutral format, and the system generates the code to work with it. Here's what a simplified schema definition might look like for a weapon data type:
// 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);
}
}
Three things to notice here:
Version tracking is built in. Every schema has an integer version. When you change the schema, you write a migration function that transforms old records into the new format. This runs at load time, not at edit time, so existing data files never need manual updating. Capcom's RE Engine has been in production since 2017 — that's eight years of schema evolution across multiple shipped titles. Without automatic migration, the data maintenance cost alone would have consumed an entire team.
Field definitions are declarative. You don't write properties — you describe what the data is and the system generates the accessor code. This means the serialization layer knows exactly how to pack and unpack every field without reflection, which brings us to the next layer.
References are explicit. The Reference field type creates a typed link to another data record. Unlike Unity's SerializeReference or raw object references, this is validated at schema load time. If the target record doesn't exist, you get an error in the editor, not a null reference at runtime in a shipped build.
Layer 2: Code Generation and Accessors
Once schemas are defined, RE:Dox generates strongly-typed accessor structs. This is critical for performance — the generated code avoids boxing, avoids reflection, and can be aggressively optimized by the JIT compiler or burst-compiled in Unity contexts.
Here's what a generated accessor might look like for our weapon schema:
// 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);
}
The key design choice: fields are accessed by index, not by name. The schema maps field names to indices at load time, and all runtime access uses integer indices. This means property access compiles down to a simple offset calculation into a flat byte buffer — no hash lookups, no dictionary probes, no string comparisons.
For a game like Monster Hunter Rise that has hundreds of weapon variants, each accessed dozens of times per frame during damage calculation, this difference between O(1) indexed access and O(log n) dictionary lookup is the gap between hitting your frame budget and missing it.
Layer 3: Binary Serialization
RE:Dox uses a columnar binary format for its serialized data. This is the part that most custom solutions get wrong — they serialize each record independently (row-oriented), which works fine for small datasets but falls apart when you need to load 50,000 records at startup.
A columnar format stores all values of a single field contiguously in memory:
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, ...]
Why does this matter for games? Three reasons:
Cache locality. When your combat system needs to iterate over all weapon damage values to calculate DPS rankings, a columnar layout means those values are packed next to each other in memory. The CPU prefetcher pulls them into L1 cache automatically. With a row-oriented layout, you're jumping over unrelated fields (IDs, names, tags) for every record, causing cache misses. On modern hardware, an L1 cache hit costs ~1ns; a main memory fetch costs ~100ns. Multiply that by 50,000 records and you're looking at the difference between 50µs and 5ms.
Compression. Columnar data compresses dramatically better because adjacent values tend to be similar (delta encoding, run-length encoding). Capcom's RE Engine assets for Resident Evil Village total around 27GB — the data engine's serialization format helps keep loaded memory footprint manageable on hardware with 16GB system RAM.
Selective loading. Need only weapon names and damage values for a UI tooltip? A columnar format lets you skip loading the tags, upgrade paths, and animation references entirely. The data engine can read just the columns you request, reducing both I/O time and memory allocation.
Layer 4: Editor Integration
The fourth layer — and arguably the one that provides the most value to content teams — is the editor integration. Because schemas are declarative, RE:Dox can generate editor UI automatically:
- New data type defined? Editor panels appear without code changes.
- Field has a
defaultValue? The editor pre-fills it and highlights overrides. - Field is a
Referenceto another schema? The editor provides a searchable dropdown populated with valid targets. - Schema version bumped? The editor shows a migration preview before applying changes.
This is the pattern that makes data engines worth building. The upfront cost of defining schemas and writing migration code is repaid hundreds of times over when designers can create, edit, and validate game data without touching code, waiting for recompiles, or pinging engineers to debug serialization errors.
How to Build a Minimal Data Engine in C#
You don't need to replicate RE:Dox's full architecture to get value from these patterns. Here's a stripped-down data engine that implements the three most impactful concepts: declarative schemas, indexed field access, and automatic migration.
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)
}
}
This is roughly 100 lines and it gives you indexed field access, flat binary storage, and versioned migration. It won't replace RE:Dox, but it demonstrates the core pattern that makes data engines work: separate what your data is from how it's stored, and track every change you make to that definition.
Lessons for Your Game's Data Architecture
1. Separate Schema from Representation
The single most impactful pattern from RE:Dox is decoupling your data definition from your runtime classes. Your gameplay code should work with convenient, strongly-typed accessors. Your storage format should be compact, versioned, and independent of those accessors.
If your weapon class changes, your save files shouldn't break. If your save format evolves, your weapon class shouldn't need refactoring. These are two separate concerns connected by an explicit migration layer.
2. Version Everything, Migrate Automatically
Every shipped game accumulates schema debt. RE Engine has been running since 2017 across at least 12 major titles — that's hundreds of schema migrations over eight years. Capcom's approach of embedding migration functions directly in the schema definition means every data file carries its version number, and the engine can upgrade any old file to the current format on load.
Start versioning your data schemas from day one. Even if you never ship a sequel, your Early Access builds will generate save files in old formats that need to survive the 1.0 launch.
3. Use Indexed Access for Hot-Path Data
If a data field is read inside a per-frame loop — damage values, animation timings, AI weights — it should be accessed by index, not by name or hash. The difference is small per access but massive in aggregate. A combat system iterating 200 active weapons at 60fps means 12,000 field reads per second. Indexed access completes in ~1ns per read (direct memory offset); dictionary lookups take ~50-200ns per read.
For data that's read once on load (display names, descriptions, lore text), indexed access doesn't matter. Profile before optimizing, but know that the pattern exists for when you need it.
4. Columnar Storage for Large Datasets
If your game has more than 1,000 records of any single data type, consider a columnar storage layout. The cache locality benefits are real and measurable — benchmarks from similar architectures show 3-8x improvement in iteration speed for aggregate queries (sums, filters, sorts) over row-oriented storage.
This applies to loot tables, inventory catalogs, dialogue trees, quest databases — anything where you might iterate over many records to find or compute something.
5. Generate Editor UI from Schemas
The productivity multiplier of a data engine isn't runtime performance — it's designer iteration speed. If your schemas are declarative, you can auto-generate editor interfaces. This eliminates the most common source of designer-engineer friction: "Can you add a field to the thing?"
With a schema-driven approach, adding a field is a one-line schema change that automatically appears in every editor panel. No C# modifications, no custom property drawers, no inspector scripts. If you're building tools for a team larger than two people, this alone justifies the investment.
Where Backend Systems Fit In
RE:Dox handles the engine-side data pipeline — how data gets defined, serialized, and loaded into runtime memory. But modern games also need to manage player-specific data: save files synced across devices, leaderboards fed by gameplay telemetry, remote configuration that lets you tune drop rates without shipping a patch.
If you're building that backend layer yourself, you're looking at user authentication flows, conflict resolution for concurrent writes, revision tracking for cloud saves, and a dashboard your team can actually use. That's weeks of infrastructure work before you write a single gameplay feature.
Services like horizOn handle these specific backend concerns — account-bound cloud saves with revision-aware reads and writes, leaderboards, remote configuration, and crash reporting — so you can focus your engineering time on the engine-side data architecture that directly affects player experience.
Applying These Patterns to Your Current Project
You don't need to build a complete data engine to benefit from RE:Dox's architecture. Start with these concrete steps:
Audit your current data loading. Profile how long your startup takes and where the time goes. If more than 15% of load time is spent on data deserialization and validation, you have room to optimize.
Identify your hottest data paths. Use your profiler to find which data structures are accessed most frequently during gameplay. Those are candidates for indexed access and columnar storage.
Write a migration framework. Even a simple version number on your save files plus a dictionary of migration functions is 90% of the value. You don't need the full schema system to get migration benefits.
Separate your data definitions. If your weapon stats are defined in the same C# class that Unity serializes to disk, that's your coupling point. Extract the field definitions into a separate, versioned schema — even if it's just a JSON schema file that you validate against.
Benchmark before and after. Measure load times, memory usage, and hot-path iteration speeds before making changes. Data architecture improvements compound over time, but you need a baseline to prove the investment is paying off.
The Bigger Picture: Why Open Source Matters Here
Capcom open-sourcing RE:Dox is significant beyond the code itself. AAA studios historically treat their engine technology as proprietary advantage. When Capcom shares a component like this, they're signaling that the data engine layer is not their competitive moat — the content, game design, and art direction are.
For indie and mid-size developers, this means the patterns and architecture decisions behind one of the most successful game engines in the industry are now directly inspectable. You can read how Capcom handles schema migration, study their serialization choices, and apply those lessons to your own projects.
The code itself might not drop directly into your Unity or Godot project, but the architectural patterns — declarative schemas, indexed access, columnar storage, automatic migration, generated editor UI — are universal. They work regardless of engine, language, or scale.
Start with one pattern. Version your data. Build from there. Your future self — and your designers — will thank you.