Capcom a open-sourcé RE:Dox — Voici l'architecture de moteur de données que vous pouvez réutiliser dès aujourd'hui
En bref
Découvrez l'architecture du moteur de données RE:Dox de Capcom et apprenez à appliquer ses patterns de conception à vos propres projets de jeux.
Votre système d'entités charge 14 000 références d'assets au démarrage, vos designers supplient pour des cycles d'itération plus rapides, et chaque hot-reload prend 45 secondes parce que votre pipeline de données couple la sérialisation aux layouts mémoire runtime. Ça vous dit quelque chose ? Capcom vient d'open-sourcer la pièce exacte de son RE Engine qui résout ce problème — et cette architecture vaut la peine d'être réutilisée.
RE:Dox est le moteur de données C# de Capcom, le middleware chargé de gérer la façon dont les données de jeu circulent des outils d'édition à la sérialisation, puis dans la mémoire runtime, pour des titres comme Resident Evil Village, Street Fighter 6 et Monster Hunter Rise. Il gère la définition de schémas, la sérialisation binaire, les références d'assets et le pipeline de données qui permet aux designers d'itérer sans recompiler le code C++ du moteur.
Ce n'est pas un jouet. RE Engine fait tourner des jeux en 60 fps sur Nintendo Switch tout en gérant des milliers de types de données uniques — statistiques d'armes, arbres de comportement IA, paramètres de matériaux, graphes d'animation, flags de quêtes, tables de butin. Chacun de ces types de données passe par un système comme RE:Dox avant de toucher au code de gameplay.
Décomposons ce que fait réellement cette architecture, pourquoi Capcom a fait ces choix de conception spécifiques, et comment vous pouvez appliquer les mêmes patterns à votre propre moteur ou projet.
Pourquoi les studios AAA construisent des moteurs de données (et pourquoi vous devriez le faire aussi)
La plupart des projets indépendants et de taille moyenne gèrent les données de jeu avec l'une des trois approches suivantes :
- ScriptableObjects ou Resources (Unity) : modifiables par les designers, mais la sérialisation est opaque, le versioning est pénible, et les références croisées créent des cauchemars de dépendances circulaires à grande échelle.
- JSON/CSV bruts avec chargeurs personnalisés : flexibles mais fragiles. Pas de validation de schéma, pas d'intégration éditeur, parsing manuel pour chaque nouveau type de données, et zéro sécurité à la compilation.
- Tables de données en dur : rapides à l'exécution, impossibles à maintenir. Modifier la valeur de dégâts d'une arme nécessite une recompilation.
Chaque approche casse à un niveau d'échelle différent. Les ScriptableObjects commencent à saturer autour de 2 000 à 5 000 assets uniques car le surcoût de sérialisation d'Unity croît linéairement avec la profondeur de référence. Les chargeurs JSON cassent dès que deux designers modifient le même fichier et que vous découvrez le conflit de fusion après une session QA de quatre heures. Les tables en dur cassent dès qu'un designer existe dans votre équipe.
Un moteur de données comme RE:Dox résout le problème fondamental : découpler votre schéma de données de votre layout mémoire runtime tout en les gardant automatiquement synchronisés.
Voici ce que cela signifie en pratique. Dans un projet Unity typique, si vous voulez ajouter un nouveau champ à vos statistiques d'armes, vous :
- Ajoutez le champ à votre classe C#
- Espérez que le sérialiseur d'Unity le détecte correctement
- Mettez à jour manuellement chaque instance ScriptableObject existante (ou écrivez un script de migration)
- Reconstruisez les asset bundles
- Testez que les allers-retours de sérialisation ne corrompent pas les données existantes
Avec un moteur de données comme RE:Dox, le workflow est :
- Définissez le champ dans un fichier de définition de schéma
- Le moteur génère automatiquement le code de sérialisation, l'UI éditeur et les chemins de migration
- Les données existantes obtiennent le nouveau champ avec une valeur par défaut pertinente
- Aucune reconstruction d'assets non liés n'est requise
Cette différence semble minime jusqu'à ce que vous ayez 847 types de données uniques et 40 000 instances d'assets réparties sur une équipe de contenu de 30 personnes.
L'architecture de RE:Dox : une analyse technique
RE:Dox est écrit entièrement en C# et fonctionne comme une bibliothèque autonome qui peut être intégrée à tout projet de jeu basé sur .NET. Il n'est lié ni à Unity ni à un moteur spécifique — Capcom l'a conçu pour fonctionner avec sa chaîne d'outils RE Engine personnalisée, ce qui signifie que les abstractions sont indépendantes du moteur par nécessité.
L'architecture de base comporte quatre couches :
Couche 1 : Définition de schéma
Chaque type de données dans RE:Dox commence par une définition de schéma. Contrairement aux classes C# où votre représentation runtime et votre format de sérialisation sont entremêlés, les schémas dans RE:Dox sont déclarés indépendamment.
Pensez-y comme Protocol Buffers ou FlatBuffers — vous définissez à quoi ressemblent vos données dans un format neutre, et le système génère le code pour les manipuler. Voici à quoi pourrait ressembler une définition de schéma simplifiée pour un type de données d'arme :
// 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);
}
}
Trois choses à remarquer ici :
Le suivi de version est intégré. Chaque schéma a une version entière. Lorsque vous modifiez le schéma, vous écrivez une fonction de migration qui transforme les anciens enregistrements dans le nouveau format. Cela s'exécute au chargement, pas à l'édition, donc les fichiers de données existants n'ont jamais besoin de mise à jour manuelle. Le RE Engine de Capcom est en production depuis 2017 — cela fait huit ans d'évolution de schémas à travers plusieurs titres commercialisés. Sans migration automatique, le coût de maintenance des données à lui seul aurait consommé une équipe entière.
Les définitions de champs sont déclaratives. Vous n'écrivez pas de propriétés — vous décrivez ce que la donnée est et le système génère le code d'accès. Cela signifie que la couche de sérialisation sait exactement comment empaqueter et dépaqueter chaque champ sans réflexion, ce qui nous amène à la couche suivante.
Les références sont explicites. Le type de champ Reference crée un lien typé vers un autre enregistrement de données. Contrairement à SerializeReference d'Unity ou aux références d'objets brutes, cela est validé au moment du chargement du schéma. Si l'enregistrement cible n'existe pas, vous obtenez une erreur dans l'éditeur, pas une référence nulle au runtime dans une build livrée.
Couche 2 : Génération de code et accesseurs
Une fois les schémas définis, RE:Dox génère des structures d'accesseurs fortement typées. C'est crucial pour les performances — le code généré évite le boxing, évite la réflexion, et peut être agressivement optimisé par le compilateur JIT ou compilé en burst dans les contextes Unity.
Voici à quoi pourrait ressembler un accesseur généré pour notre schéma d'arme :
// 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);
}
Le choix de conception clé : les champs sont accédés par index, pas par nom. Le schéma mappe les noms de champs à des indices au chargement, et tout accès runtime utilise des indices entiers. Cela signifie que l'accès aux propriétés se compile en un simple calcul d'offset dans un tampon d'octets plat — pas de recherches de hachage, pas de sondages de dictionnaire, pas de comparaisons de chaînes.
Pour un jeu comme Monster Hunter Rise qui a des centaines de variantes d'armes, chacune accédée des dizaines de fois par frame pendant le calcul des dégâts, cette différence entre l'accès indexé O(1) et la recherche dans un dictionnaire O(log n) est l'écart entre atteindre votre budget de frame et le rater.
Couche 3 : Sérialisation binaire
RE:Dox utilise un format binaire columnar pour ses données sérialisées. C'est la partie que la plupart des solutions personnalisées ratent — elles sérialisent chaque enregistrement indépendamment (orienté ligne), ce qui fonctionne bien pour les petits ensembles de données mais s'effondre lorsque vous devez charger 50 000 enregistrements au démarrage.
Un format columnar stocke toutes les valeurs d'un seul champ de manière contiguë en mémoire :
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, ...]
Pourquoi cela importe-t-il pour les jeux ? Trois raisons :
Localité de cache. Lorsque votre système de combat doit itérer sur toutes les valeurs de dégâts d'armes pour calculer les classements DPS, un layout columnar signifie que ces valeurs sont regroupées les unes à côté des autres en mémoire. Le préfetcheur du CPU les charge automatiquement dans le cache L1. Avec un layout orienté ligne, vous sautez par-dessus des champs non liés (IDs, noms, tags) pour chaque enregistrement, provoquant des cache misses. Sur le matériel moderne, un cache hit L1 coûte ~1 ns ; un accès mémoire principale coûte ~100 ns. Multipliez cela par 50 000 enregistrements et vous avez la différence entre 50 µs et 5 ms.
Compression. Les données columnar se compressent nettement mieux car les valeurs adjacentes ont tendance à être similaires (delta encoding, run-length encoding). Les assets RE Engine de Capcom pour Resident Evil Village totalisent environ 27 Go — le format de sérialisation du moteur de données aide à maintenir une empreinte mémoire chargée gérable sur du matériel avec 16 Go de RAM système.
Chargement sélectif. Besoin uniquement des noms d'armes et des valeurs de dégâts pour une infobulle UI ? Un format columnar vous permet d'ignorer complètement le chargement des tags, des chemins d'amélioration et des références d'animation. Le moteur de données peut lire uniquement les colonnes que vous demandez, réduisant à la fois le temps d'E/S et l'allocation mémoire.
Couche 4 : Intégration éditeur
La quatrième couche — et sans doute celle qui apporte le plus de valeur aux équipes de contenu — est l'intégration éditeur. Parce que les schémas sont déclaratifs, RE:Dox peut générer automatiquement l'UI éditeur :
- Nouveau type de données défini ? Les panneaux éditeur apparaissent sans modification de code.
- Un champ a un
defaultValue? L'éditeur le pré-remplit et met en évidence les overrides. - Un champ est une
Referencevers un autre schéma ? L'éditeur fournit une liste déroulante recherchable remplie de cibles valides. - Version de schéma incrémentée ? L'éditeur affiche un aperçu de migration avant d'appliquer les changements.
C'est ce pattern qui rend les moteurs de données dignes d'être construits. Le coût initial de définition des schémas et d'écriture du code de migration est remboursé des centaines de fois lorsque les designers peuvent créer, modifier et valider des données de jeu sans toucher au code, attendre des recompilations, ou solliciter les ingénieurs pour déboguer des erreurs de sérialisation.
Comment construire un moteur de données minimal en C#
Vous n'avez pas besoin de répliquer l'architecture complète de RE:Dox pour tirer de la valeur de ces patterns. Voici un moteur de données allégé qui implémente les trois concepts les plus impactants : schémas déclaratifs, accès aux champs indexé et migration automatique.
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)
}
}
C'est environ 100 lignes et cela vous donne un accès aux champs indexé, un stockage binaire plat et une migration versionnée. Cela ne remplacera pas RE:Dox, mais cela démontre le pattern central qui fait fonctionner les moteurs de données : séparez ce que sont vos données de la façon dont elles sont stockées, et suivez chaque modification que vous apportez à cette définition.
Leçons pour l'architecture de données de votre jeu
1. Séparez le schéma de la représentation
Le pattern le plus impactant de RE:Dox est le découplage de votre définition de données de vos classes runtime. Votre code de gameplay devrait fonctionner avec des accesseurs pratiques et fortement typés. Votre format de stockage devrait être compact, versionné et indépendant de ces accesseurs.
Si votre classe d'arme change, vos fichiers de sauvegarde ne devraient pas casser. Si votre format de sauvegarde évolue, votre classe d'arme ne devrait pas nécessiter de refactoring. Ce sont deux préoccupations distinctes reliées par une couche de migration explicite.
2. Versionez tout, migrez automatiquement
Chaque jeu livré accumule une dette de schémas. RE Engine fonctionne depuis 2017 à travers au moins 12 titres majeurs — cela représente des centaines de migrations de schémas sur huit ans. L'approche de Capcom consistant à intégrer les fonctions de migration directement dans la définition de schéma signifie que chaque fichier de données porte son numéro de version, et que le moteur peut mettre à niveau tout ancien fichier vers le format actuel au chargement.
Commencez à versionner vos schémas de données dès le premier jour. Même si vous ne livrez jamais de suite, vos builds Early Access généreront des fichiers de sauvegarde dans d'anciens formats qui devront survivre au lancement 1.0.
3. Utilisez l'accès indexé pour les données de chemin critique
Si un champ de données est lu dans une boucle par frame — valeurs de dégâts, timings d'animation, poids IA — il devrait être accédé par index, pas par nom ou hachage. La différence est faible par accès mais massive en agrégat. Un système de combat itérant sur 200 armes actives à 60 fps signifie 12 000 lectures de champs par seconde. L'accès indexé s'exécute en ~1 ns par lecture (offset mémoire direct) ; les recherches dans un dictionnaire prennent ~50-200 ns par lecture.
Pour les données lues une seule fois au chargement (noms affichés, descriptions, textes de lore), l'accès indexé n'a pas d'importance. Profilez avant d'optimiser, mais sachez que ce pattern existe pour quand vous en aurez besoin.
4. Stockage columnar pour les grands ensembles de données
Si votre jeu a plus de 1 000 enregistrements d'un même type de données, envisagez un layout de stockage columnar. Les bénéfices de localité de cache sont réels et mesurables — des benchmarks d'architectures similaires montrent une amélioration de 3 à 8 fois de la vitesse d'itération pour les requêtes agrégées (sommes, filtres, tris) par rapport au stockage orienté ligne.
Cela s'applique aux tables de butin, catalogues d'inventaire, arbres de dialogue, bases de données de quêtes — tout ce où vous pourriez itérer sur de nombreux enregistrements pour trouver ou calculer quelque chose.
5. Générez l'UI éditeur à partir des schémas
Le multiplicateur de productivité d'un moteur de données n'est pas la performance runtime — c'est la vitesse d'itération des designers. Si vos schémas sont déclaratifs, vous pouvez auto-générer des interfaces éditeur. Cela élimine la source la plus courante de friction designer-ingénieur : « Peux-tu ajouter un champ à ce truc ? »
Avec une approche pilotée par schémas, ajouter un champ est une modification de schéma d'une ligne qui apparaît automatiquement dans chaque panneau éditeur. Pas de modifications C#, pas de property drawers personnalisés, pas de scripts d'inspecteur. Si vous construisez des outils pour une équipe de plus de deux personnes, cela justifie à lui seul l'investissement.
Où les systèmes backend s'intègrent
RE:Dox gère le pipeline de données côté moteur — comment les données sont définies, sérialisées et chargées en mémoire runtime. Mais les jeux modernes doivent aussi gérer des données spécifiques au joueur : fichiers de sauvegarde synchronisés entre appareils, classements alimentés par la télémétrie de gameplay, configuration à distance qui permet d'ajuster les taux de drop sans livrer de patch.
Si vous construisez cette couche backend vous-même, vous avez devant vous les flux d'authentification utilisateur, la résolution de conflits pour les écritures concurrentes, le suivi de révision pour les sauvegardes cloud, et un tableau de bord que votre équipe peut réellement utiliser. C'est des semaines de travail d'infrastructure avant d'écrire une seule fonctionnalité de gameplay.
Des services comme horizOn gèrent ces préoccupations backend spécifiques — sauvegardes cloud liées au compte avec lectures et écritures tenant compte des révisions, classements, configuration à distance et rapport de crashs — afin que vous puissiez concentrer votre temps d'ingénierie sur l'architecture de données côté moteur qui affecte directement l'expérience du joueur.
Appliquer ces patterns à votre projet actuel
Vous n'avez pas besoin de construire un moteur de données complet pour bénéficier de l'architecture de RE:Dox. Commencez par ces étapes concrètes :
Auditez votre chargement de données actuel. Profilez combien de temps prend votre démarrage et où va ce temps. Si plus de 15 % du temps de chargement est consacré à la désérialisation et à la validation des données, vous avez de la marge pour optimiser.
Identifiez vos chemins de données les plus chauds. Utilisez votre profileur pour trouver quelles structures de données sont accédées le plus fréquemment pendant le gameplay. Ce sont des candidates pour l'accès indexé et le stockage columnar.
Écrivez un framework de migration. Même un simple numéro de version sur vos fichiers de sauvegarde plus un dictionnaire de fonctions de migration représente 90 % de la valeur. Vous n'avez pas besoin du système de schémas complet pour bénéficier de la migration.
Séparez vos définitions de données. Si vos statistiques d'armes sont définies dans la même classe C# qu'Unity sérialise sur disque, c'est votre point de couplage. Extrayez les définitions de champs dans un schéma séparé et versionné — même si ce n'est qu'un fichier de schéma JSON que vous validez.
Faites des benchmarks avant et après. Mesurez les temps de chargement, l'utilisation mémoire et les vitesses d'itération des chemins critiques avant de faire des changements. Les améliorations d'architecture de données se cumulent avec le temps, mais vous avez besoin d'une base de référence pour prouver que l'investissement porte ses fruits.
La vue d'ensemble : pourquoi l'open source compte ici
Le fait que Capcom open-source RE:Dox est significatif au-delà du code lui-même. Les studios AAA traitent historiquement leur technologie de moteur comme un avantage propriétaire. Lorsque Capcom partage un composant comme celui-ci, ils signalent que la couche moteur de données n'est pas leur fossé concurrentiel — le contenu, le game design et la direction artistique le sont.
Pour les développeurs indépendants et de taille moyenne, cela signifie que les patterns et les décisions d'architecture derrière l'un des moteurs de jeu les plus réussis de l'industrie sont désormais directement inspectables. Vous pouvez lire comment Capcom gère la migration de schémas, étudier leurs choix de sérialisation et appliquer ces leçons à vos propres projets.
Le code lui-même ne s'intégrera peut-être pas directement dans votre projet Unity ou Godot, mais les patterns architecturaux — schémas déclaratifs, accès indexé, stockage columnar, migration automatique, UI éditeur générée — sont universels. Ils fonctionnent quel que soit le moteur, le langage ou l'échelle.
Commencez avec un seul pattern. Versionez vos données. Construisez à partir de là. Votre futur vous — et vos designers — vous remercieront.