Capcom ha open-sourcato RE:Dox — Ecco l'architettura del data engine che puoi rubare oggi
In breve
Scopri come l'architettura del data engine RE:Dox di Capcom disaccoppia schemi e memoria runtime per iterare più velocemente nei tuoi giochi.
Il tuo sistema di entità carica 14.000 riferimenti ad asset all'avvio, i tuoi designer implorano cicli di iterazione più rapidi, e ogni hot-reload richiede 45 secondi perché la tua pipeline di dati accoppia la serializzazione ai layout di memoria runtime. Ti sembra familiare? Capcom ha appena open-sourcato il pezzo esatto del suo RE Engine che risolve questo problema — e l'architettura merita di essere rubata.
RE:Dox è il data engine C# di Capcom, il middleware responsabile della gestione del flusso dei dati di gioco dagli strumenti dell'editor attraverso la serializzazione fino alla memoria runtime per titoli come Resident Evil Village, Street Fighter 6 e Monster Hunter Rise. Gestisce la definizione degli schemi, la serializzazione binaria, i riferimenti agli asset e la pipeline di dati che consente ai designer di iterare senza ricompilare il codice C++ del motore.
Non è un giocattolo. RE Engine alimenta giochi che girano a 60fps su hardware Nintendo Switch gestendo migliaia di tipi di dati unici — statistiche delle armi, alberi comportamentali dell'IA, parametri dei materiali, grafi di animazione, flag delle missioni, tabelle del bottino. Ognuno di questi tipi di dati passa attraverso un sistema come RE:Dox prima di toccare il codice di gameplay.
Vediamo nel dettaglio cosa fa davvero questa architettura, perché Capcom ha fatto queste scelte progettuali specifiche e come puoi applicare gli stessi pattern al tuo motore o progetto.
Perché gli studi AAA costruiscono data engine (e perché dovresti farlo anche tu)
La maggior parte dei progetti indie e di medie dimensioni gestisce i dati di gioco con uno di tre approcci:
- ScriptableObjects o Resources (Unity): modificabili dai designer, ma la serializzazione è opaca, il versioning è doloroso e i riferimenti incrociati creano incubi di dipendenze circolari su larga scala.
- JSON/CSV grezzi con loader personalizzati: flessibili ma fragili. Nessuna validazione dello schema, nessuna integrazione con l'editor, parsing manuale per ogni nuovo tipo di dati e zero sicurezza a livello di compilazione.
- Tabelle dati hard-coded: veloci a runtime, impossibili da mantenere. Cambiare il valore del danno di un'arma richiede una ricompilazione.
Ogni approccio si rompe a un diverso punto di scala. Gli ScriptableObjects iniziano a soffocare intorno ai 2.000–5.000 asset unici perché l'overhead di serializzazione di Unity cresce linearmente con la profondità dei riferimenti. I loader JSON si rompono nel momento in cui due designer modificano lo stesso file e scopri il conflitto di merge dopo una sessione di QA di quattro ore. Le tabelle hard-coded si rompono nel momento in cui un designer entra nel tuo team.
Un data engine come RE:Dox risolve il problema fondamentale: disaccoppiare lo schema dei dati dal layout di memoria runtime mantenendo entrambi sincronizzati automaticamente.
Ecco cosa significa in pratica. In un tipico progetto Unity, se vuoi aggiungere un nuovo campo alle statistiche delle armi, devi:
- Aggiungere il campo alla tua classe C#
- Sperare che il serializzatore di Unity lo rilevi correttamente
- Aggiornare manualmente ogni istanza ScriptableObject esistente (o scrivere uno script di migrazione)
- Ricostruire gli asset bundle
- Verificare che i round-trip di serializzazione non corrompano i dati esistenti
Con un data engine come RE:Dox, il flusso di lavoro è:
- Definire il campo in un file di definizione dello schema
- Il motore genera automaticamente codice di serializzazione, UI dell'editor e percorsi di migrazione
- I dati esistenti ricevono il nuovo campo con un default sensato
- Nessuna ricostruzione di asset non correlati richiesta
Questa differenza sembra piccola finché non hai 847 tipi di dati unici e 40.000 istanze di asset in un team di contenuti di 30 persone.
L'architettura di RE:Dox: analisi tecnica
RE:Dox è scritto interamente in C# e funziona come libreria standalone integrabile in qualsiasi progetto di gioco basato su .NET. Non è legato a Unity né a un motore specifico — Capcom l'ha progettato per funzionare con la propria toolchain RE Engine, il che significa che le astrazioni sono engine-agnostic per necessità.
L'architettura core ha quattro livelli:
Livello 1: Definizione dello schema
Ogni tipo di dati in RE:Dox inizia con una definizione di schema. A differenza delle classi C# in cui la rappresentazione runtime e il formato di serializzazione sono intrecciati, gli schemi in RE:Dox sono dichiarati in modo indipendente.
Pensalo come Protocol Buffers o FlatBuffers — definisci come appaiono i tuoi dati in un formato neutro e il sistema genera il codice per lavorarci. Ecco come potrebbe apparire una definizione di schema semplificata per un tipo di dati arma:
// 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);
}
}
Tre cose da notare qui:
Il version tracking è integrato. Ogni schema ha una versione intera. Quando modifichi lo schema, scrivi una funzione di migrazione che trasforma i vecchi record nel nuovo formato. Questo avviene al momento del caricamento, non della modifica, quindi i file di dati esistenti non richiedono mai aggiornamenti manuali. Il RE Engine di Capcom è in produzione dal 2017 — sono otto anni di evoluzione degli schemi attraverso numerosi titoli pubblicati. Senza migrazione automatica, il solo costo di manutenzione dei dati avrebbe consumato un intero team.
Le definizioni dei campi sono dichiarative. Non scrivi proprietà — descrivi cosa è il dato e il sistema genera il codice di accesso. Questo significa che il livello di serializzazione sa esattamente come impacchettare e spacchettare ogni campo senza reflection, il che ci porta al livello successivo.
I riferimenti sono espliciti. Il tipo di campo Reference crea un collegamento tipizzato a un altro record di dati. A differenza di SerializeReference di Unity o dei riferimenti a oggetti grezzi, questo viene validato al caricamento dello schema. Se il record di destinazione non esiste, ottieni un errore nell'editor, non un riferimento nullo a runtime in una build pubblicata.
Livello 2: Generazione del codice e accessor
Una volta definiti gli schemi, RE:Dox genera struct di accessor fortemente tipizzati. Questo è fondamentale per le prestazioni — il codice generato evita il boxing, evita la reflection e può essere ottimizzato aggressivamente dal compilatore JIT o compilato burst nei contesti Unity.
Ecco come potrebbe apparire un accessor generato per il nostro schema arma:
// 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);
}
La scelta progettuale chiave: i campi sono accessibili per indice, non per nome. Lo schema mappa i nomi dei campi agli indici al momento del caricamento e tutto l'accesso runtime usa indici interi. Questo significa che l'accesso alle proprietà si compila in un semplice calcolo di offset in un buffer di byte piatto — niente hash lookup, niente probe su dizionari, niente confronti di stringhe.
Per un gioco come Monster Hunter Rise che ha centinaia di varianti di armi, ognuna accessa dozzine di volte per frame durante il calcolo del danno, questa differenza tra accesso indicizzato O(1) e lookup su dizionario O(log n) è il divario tra rispettare il frame budget e mancarlo.
Livello 3: Serializzazione binaria
RE:Dox usa un formato binario colonnare per i dati serializzati. Questa è la parte che la maggior parte delle soluzioni personalizzate sbaglia — serializzano ogni record in modo indipendente (row-oriented), che funziona bene per dataset piccoli ma crolla quando devi caricare 50.000 record all'avvio.
Un formato colonnare memorizza tutti i valori di un singolo campo in modo contiguo in memoria:
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, ...]
Perché è importante per i giochi? Tre motivi:
Cache locality. Quando il tuo sistema di combattimento deve iterare su tutti i valori di danno delle armi per calcolare le classifiche DPS, un layout colonnare significa che quei valori sono impacchettati uno accanto all'altro in memoria. Il prefetcher della CPU li porta automaticamente nella cache L1. Con un layout row-oriented, salti campi non correlati (ID, nomi, tag) per ogni record, causando cache miss. Su hardware moderno, un cache hit L1 costa ~1ns; un fetch dalla memoria principale costa ~100ns. Moltiplica per 50.000 record e stai guardando la differenza tra 50µs e 5ms.
Compressione. I dati colonnari si comprimono molto meglio perché i valori adiacenti tendono a essere simili (delta encoding, run-length encoding). Gli asset RE Engine di Capcom per Resident Evil Village ammontano a circa 27GB — il formato di serializzazione del data engine aiuta a mantenere gestibile l'ingombro di memoria caricata su hardware con 16GB di RAM di sistema.
Caricamento selettivo. Ti servono solo nomi delle armi e valori di danno per un tooltip UI? Un formato colonnare ti permette di saltare del tutto il caricamento di tag, percorsi di potenziamento e riferimenti alle animazioni. Il data engine può leggere solo le colonne richieste, riducendo sia il tempo di I/O sia l'allocazione di memoria.
Livello 4: Integrazione con l'editor
Il quarto livello — e probabilmente quello che offre il maggior valore ai team di contenuti — è l'integrazione con l'editor. Poiché gli schemi sono dichiarativi, RE:Dox può generare automaticamente la UI dell'editor:
- Nuovo tipo di dati definito? I pannelli dell'editor appaiono senza modifiche al codice.
- Il campo ha un
defaultValue? L'editor lo precompila ed evidenzia le override. - Il campo è un
Referencea un altro schema? L'editor fornisce un dropdown ricercabile popolato con i target validi. - Versione dello schema aumentata? L'editor mostra un'anteprima della migrazione prima di applicare le modifiche.
Questo è il pattern che rende i data engine degni di essere costruiti. Il costo iniziale di definire gli schemi e scrivere codice di migrazione viene ripagato centinaia di volte quando i designer possono creare, modificare e validare i dati di gioco senza toccare il codice, aspettare ricompilazioni o contattare gli ingegneri per fare debug degli errori di serializzazione.
Come costruire un data engine minimale in C#
Non devi replicare l'intera architettura di RE:Dox per trarre valore da questi pattern. Ecco un data engine ridotto all'osso che implementa i tre concetti di maggiore impatto: schemi dichiarativi, accesso indicizzato ai campi e migrazione automatica.
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)
}
}
Sono circa 100 righe e ti danno accesso indicizzato ai campi, storage binario piatto e migrazione versionata. Non sostituirà RE:Dox, ma dimostra il pattern core che fa funzionare i data engine: separa ciò che sono i tuoi dati da come vengono memorizzati e traccia ogni modifica che fai a quella definizione.
Lezioni per l'architettura dei dati del tuo gioco
1. Separa lo schema dalla rappresentazione
Il pattern di maggiore impatto di RE:Dox è il disaccoppiamento della definizione dei dati dalle classi runtime. Il tuo codice di gameplay dovrebbe lavorare con accessor convenienti e fortemente tipizzati. Il tuo formato di storage dovrebbe essere compatto, versionato e indipendente da quegli accessor.
Se la tua classe arma cambia, i tuoi file di salvataggio non dovrebbero rompersi. Se il tuo formato di salvataggio evolve, la tua classe arma non dovrebbe richiedere refactoring. Sono due preoccupazioni separate collegate da un livello di migrazione esplicito.
2. Versiona tutto, migra automaticamente
Ogni gioco pubblicato accumula debito di schema. RE Engine è in esecuzione dal 2017 su almeno 12 titoli principali — sono centinaia di migrazioni di schema in otto anni. L'approccio di Capcom di incorporare le funzioni di migrazione direttamente nella definizione dello schema significa che ogni file di dati porta con sé il proprio numero di versione e il motore può aggiornare qualsiasi file vecchio al formato corrente al caricamento.
Inizia a versionare i tuoi schemi di dati dal primo giorno. Anche se non pubblichi mai un sequel, le tue build Early Access genereranno file di salvataggio in vecchi formati che dovranno sopravvivere al lancio 1.0.
3. Usa l'accesso indicizzato per i dati hot-path
Se un campo dati viene letto all'interno di un loop per-frame — valori di danno, timing delle animazioni, pesi IA — dovrebbe essere accesso per indice, non per nome o hash. La differenza è piccola per accesso ma enorme in aggregato. Un sistema di combattimento che itera 200 armi attive a 60fps significa 12.000 letture di campo al secondo. L'accesso indicizzato si completa in ~1ns per lettura (offset di memoria diretto); i lookup su dizionario richiedono ~50-200ns per lettura.
Per i dati letti una volta al caricamento (nomi visualizzati, descrizioni, testo lore), l'accesso indicizzato non ha importanza. Fai profiling prima di ottimizzare, ma sappi che il pattern esiste per quando ti serve.
4. Storage colonnare per dataset di grandi dimensioni
Se il tuo gioco ha più di 1.000 record di un singolo tipo di dati, considera un layout di storage colonnare. I benefici di cache locality sono reali e misurabili — i benchmark di architetture simili mostrano un miglioramento di 3-8x nella velocità di iterazione per query aggregate (somme, filtri, ordinamenti) rispetto allo storage row-oriented.
Questo vale per tabelle del bottino, cataloghi di inventario, alberi di dialogo, database di missioni — qualsiasi cosa in cui potresti iterare su molti record per trovare o calcolare qualcosa.
5. Genera la UI dell'editor dagli schemi
Il moltiplicatore di produttività di un data engine non sono le prestazioni runtime — è la velocità di iterazione dei designer. Se i tuoi schemi sono dichiarativi, puoi auto-generare le interfacce dell'editor. Questo elimina la fonte più comune di attrito designer-ingegnere: «Puoi aggiungere un campo alla cosa?»
Con un approccio schema-driven, aggiungere un campo è una modifica di una riga allo schema che appare automaticamente in ogni pannello dell'editor. Niente modifiche C#, niente property drawer personalizzati, niente script inspector. Se stai costruendo strumenti per un team più grande di due persone, questo da solo giustifica l'investimento.
Dove si inseriscono i sistemi backend
RE:Dox gestisce la pipeline di dati lato engine — come i dati vengono definiti, serializzati e caricati in memoria runtime. Ma i giochi moderni devono anche gestire dati specifici del giocatore: file di salvataggio sincronizzati tra dispositivi, classifiche alimentate dalla telemetria di gameplay, configurazione remota che ti permette di regolare i tassi di drop senza pubblicare una patch.
Se stai costruendo quel livello backend da solo, ti trovi di fronte a flussi di autenticazione utente, risoluzione dei conflitti per scritture concorrenti, tracciamento delle revisioni per i salvataggi cloud e una dashboard che il tuo team possa davvero usare. Sono settimane di lavoro infrastrutturale prima di scrivere una singola funzionalità di gameplay.
Servizi come horizOn gestiscono queste specifiche problematiche backend — salvataggi cloud legati all'account con letture e scritture che tengono conto delle revisioni, classifiche, configurazione remota e crash reporting — così puoi concentrare il tuo tempo di ingegneria sull'architettura dei dati lato engine che influisce direttamente sull'esperienza del giocatore.
Applicare questi pattern al tuo progetto attuale
Non devi costruire un data engine completo per trarre beneficio dall'architettura di RE:Dox. Inizia con questi passi concreti:
Fai un audit del caricamento dati attuale. Profila quanto tempo richiede l'avvio e dove va il tempo. Se più del 15% del tempo di caricamento è speso in deserializzazione e validazione dei dati, hai margine per ottimizzare.
Identifica i tuoi percorsi dati più caldi. Usa il profiler per scoprire quali strutture dati vengono accesse più frequentemente durante il gameplay. Quelle sono candidate per l'accesso indicizzato e lo storage colonnare.
Scrivi un framework di migrazione. Anche un semplice numero di versione sui file di salvataggio più un dizionario di funzioni di migrazione è il 90% del valore. Non ti serve il sistema di schemi completo per ottenere i benefici della migrazione.
Separa le definizioni dei dati. Se le statistiche delle armi sono definite nella stessa classe C# che Unity serializza su disco, quello è il tuo punto di accoppiamento. Estrai le definizioni dei campi in uno schema separato e versionato — anche se è solo un file JSON schema contro cui validare.
Fai benchmark prima e dopo. Misura tempi di caricamento, utilizzo della memoria e velocità di iterazione hot-path prima di fare modifiche. I miglioramenti all'architettura dei dati si accumulano nel tempo, ma ti serve una baseline per dimostrare che l'investimento sta ripagando.
Il quadro più ampio: perché l'open source conta qui
Il fatto che Capcom abbia open-sourcato RE:Dox è significativo al di là del codice stesso. Gli studi AAA hanno storicamente trattato la loro tecnologia engine come vantaggio proprietario. Quando Capcom condivide un componente come questo, sta segnalando che il livello del data engine non è il loro fossato competitivo — lo sono i contenuti, il game design e la direzione artistica.
Per gli sviluppatori indie e di medie dimensioni, questo significa che i pattern e le decisioni architetturali dietro uno dei motori di gioco di maggior successo del settore sono ora direttamente ispezionabili. Puoi leggere come Capcom gestisce la migrazione degli schemi, studiare le loro scelte di serializzazione e applicare quelle lezioni ai tuoi progetti.
Il codice stesso potrebbe non essere integrabile direttamente nel tuo progetto Unity o Godot, ma i pattern architetturali — schemi dichiarativi, accesso indicizzato, storage colonnare, migrazione automatica, UI dell'editor generata — sono universali. Funzionano indipendentemente da motore, linguaggio o scala.
Inizia con un pattern. Versiona i tuoi dati. Costruisci da lì. Il tuo io futuro — e i tuoi designer — ti ringrazieranno.