Назад к блогу

Capcom открыла исходный код RE:Dox — архитектура data engine, которую можно позаимствовать уже сегодня

Опубликовано 13 октября 2026 г.
Capcom открыла исходный код RE:Dox — архитектура data engine, которую можно позаимствовать уже сегодня Создано с помощью ИИ

Коротко о главном

Изучите архитектуру открытого data engine Capcom RE:Dox: декларативные схемы, индексированный доступ, колоночное хранение и миграция данных.

Ваша система сущностей загружает 14 000 ссылок на ассеты при старте, дизайнеры умоляют ускорить цикл итераций, а каждый hot-reload занимает 45 секунд, потому что ваш конвейер данных связывает сериализацию с компоновкой памяти в рантайме. Знакомо? Capcom только что открыла исходный код именно того компонента своего RE Engine, который решает эту проблему, — и эту архитектуру стоит позаимствовать.

RE:Dox — это C# data engine от Capcom, middleware, отвечающий за то, как игровые данные переходят из редакторских инструментов через сериализацию в память рантайма для таких игр, как Resident Evil Village, Street Fighter 6 и Monster Hunter Rise. Он управляет определением схем, бинарной сериализацией, ссылками на ассеты и конвейером данных, который позволяет дизайнерам вносить изменения без перекомпиляции C++ кода движка.

Это не игрушка. RE Engine обеспечивает работу игр, которые держат 60 кадров/с на Nintendo Switch, управляя тысячами уникальных типов данных — характеристики оружия, деревья поведения ИИ, параметры материалов, графы анимаций, флаги квестов, таблицы лута. Каждый из этих типов данных проходит через систему вроде RE:Dox, прежде чем попасть в игровой код.

Почему AAA-студии создают data engines (и почему вам тоже стоит)

Большинство инди- и проектов среднего размера управляют игровыми данными одним из трёх способов:

  1. ScriptableObjects или Resources (Unity): редактируются дизайнерами, но сериализация непрозрачна, версионирование болезненно, а перекрёстные ссылки в масштабе создают кошмары с циклическими зависимостями.
  2. Сырые JSON/CSV с собственными загрузчиками: гибко, но хрупко. Нет валидации схем, нет интеграции с редактором, ручной парсинг для каждого нового типа данных и ноль безопасности на этапе компиляции.
  3. Жёстко закодированные таблицы данных: быстро в рантайме, невозможно поддерживать. Изменение значения урона оружия требует перекомпиляции.

Каждый подход ломается при своём масштабе. ScriptableObjects начинают задыхаться примерно на 2 000–5 000 уникальных ассетов, потому что накладные расходы Unity на сериализацию растут линейно с глубиной ссылок. JSON-загрузчики ломаются в тот момент, когда два дизайнера редактируют один файл, и вы обнаруживаете конфликт слияния после четырёхчасовой QA-сессии. Жёстко закодированные таблицы ломаются, как только в команде появляется дизайнер.

Data engine вроде RE:Dox решает фундаментальную проблему: разделение схемы данных и компоновки памяти в рантайме с автоматической синхронизацией обоих.

Вот что это значит на практике. В типичном проекте на Unity, если вы хотите добавить новое поле в характеристики оружия, вы:

  1. Добавляете поле в C# класс
  2. Надеетесь, что сериализатор Unity подхватит его корректно
  3. Вручную обновляете каждый существующий экземпляр ScriptableObject (или пишете миграционный скрипт)
  4. Пересобираете asset bundles
  5. Проверяете, что циклы сериализации не портят существующие данные

С data engine вроде RE:Dox рабочий процесс выглядит так:

  1. Определяете поле в файле определения схемы
  2. Движок автоматически генерирует код сериализации, UI редактора и пути миграции
  3. Существующие данные получают новое поле с разумным значением по умолчанию
  4. Пересборка несвязанных ассетов не требуется

Эта разница кажется небольшой, пока у вас нет 847 уникальных типов данных и 40 000 экземпляров ассетов в контент-команде из 30 человек.

Архитектура 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);
    }
}

Здесь стоит обратить внимание на три вещи:

Встроенное отслеживание версий. У каждой схемы есть целочисленная версия. Когда вы меняете схему, вы пишете функцию миграции, которая преобразует старые записи в новый формат. Она выполняется в момент загрузки, а не редактирования, поэтому существующие файлы данных никогда не требуют ручного обновления. RE Engine от Capcom используется в продакшене с 2017 года — это восемь лет эволюции схем в нескольких выпущенных тайтлах. Без автоматической миграции одни только затраты на сопровождение данных съели бы целую команду.

Определения полей декларативны. Вы не пишете свойства — вы описываете, чем являются данные, а система генерирует код доступа. Это означает, что слой сериализации точно знает, как упаковать и распаковать каждое поле без рефлексии, что подводит нас к следующему уровню.

Ссылки явные. Тип поля Reference создаёт типизированную ссылку на другую запись данных. В отличие от Unity SerializeReference или сырых объектных ссылок, такая ссылка валидируется при загрузке схемы. Если целевой записи не существует, вы получаете ошибку в редакторе, а не null reference в рантайме в выпущенной сборке.

Уровень 2: Генерация кода и аксессоры

Как только схемы определены, RE:Dox генерирует строго типизированные структуры-аксессоры. Это критически важно для производительности — сгенерированный код избегает boxing, избегает рефлексии и может агрессивно оптимизироваться JIT-компилятором или burst-компиляцией в контексте Unity.

Вот как может выглядеть сгенерированный аксессор для нашей схемы оружия:

// 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 использует колоночный бинарный формат для сериализованных данных. Это та часть, в которой ошибаются большинство кастомных решений — они сериализуют каждую запись независимо (строчная ориентация), что отлично работает для небольших наборов данных, но разваливается, когда нужно загрузить 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, колоночная компоновка означает, что эти значения расположены в памяти рядом друг с другом. Процессорный префетчер автоматически подтягивает их в L1-кэш. При строчной ориентации вы перескакиваете через несвязанные поля (ID, имена, теги) для каждой записи, что вызывает промахи кэша. На современном железе попадание в L1-кэш стоит ~1 нс; обращение к основной памяти — ~100 нс. Умножьте на 50 000 записей — и речь идёт о разнице между 50 мкс и 5 мс.

Сжатие. Колоночные данные сжимаются значительно лучше, поскольку соседние значения обычно похожи (дельта-кодирование, кодирование длин серий). Ассеты RE Engine для Resident Evil Village в сумме составляют около 27 ГБ — формат сериализации data engine помогает держать загружаемый объём памяти в разумных пределах на железе с 16 ГБ оперативной памяти.

Выборочная загрузка. Нужны только имена оружия и значения урона для UI-тултипа? Колоночный формат позволяет вообще не загружать теги, пути улучшения и ссылки на анимации. Data engine может читать только запрашиваемые колонки, сокращая и время I/O, и выделение памяти.

Уровень 4: Интеграция с редактором

Четвёртый уровень — и, возможно, самый ценный для контент-команд — интеграция с редактором. Поскольку схемы декларативны, RE:Dox может автоматически генерировать UI редактора:

  • Добавлен новый тип данных? Панели в редакторе появляются без изменения кода.
  • У поля есть defaultValue? Редактор подставляет его и подсвечивает переопределения.
  • Поле является Reference на другую схему? Редактор даёт поисковый выпадающий список с допустимыми целями.
  • Версия схемы повышена? Редактор показывает предпросмотр миграции перед применением изменений.

Именно этот паттерн делает data engines стоящими разработки. Первоначальные затраты на определение схем и написание кода миграции окупаются сотни раз, когда дизайнеры могут создавать, редактировать и валидировать игровые данные, не прикасаясь к коду, не дожидаясь перекомпиляции и не дёргая инженеров для отладки ошибок сериализации.

Как создать минимальный data engine на C#

Вам не нужно повторять всю архитектуру RE:Dox, чтобы получить пользу от этих паттернов. Вот урезанный data engine, реализующий три самых значимых концепции: декларативные схемы, индексированный доступ к полям и автоматическая миграция.

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, но демонстрирует ключевой паттерн, который делает data engines рабочими: отделяйте, чем являются ваши данные, от того, как они хранятся, и отслеживайте каждое изменение этого определения.

Уроки для архитектуры данных вашей игры

1. Отделяйте схему от представления

Самый значимый паттерн из RE:Dox — разделение определения данных и рантайм-классов. Ваш игровой код должен работать с удобными строго типизированными аксессорами. Формат хранения должен быть компактным, версионированным и независимым от этих аксессоров.

Если ваш класс оружия изменится, файлы сохранений не должны ломаться. Если формат сохранений эволюционирует, класс оружия не должен требовать рефакторинга. Это две разные задачи, соединённые явным слоем миграции.

2. Версионируйте всё и мигрируйте автоматически

Каждая выпущенная игра накапливает схема-долг. RE Engine работает с 2017 года как минимум на 12 крупных тайтлах — это сотни миграций схем за восемь лет. Подход Capcom, при котором функции миграции встраиваются прямо в определение схемы, означает, что каждый файл данных несёт свой номер версии, и движок может обновить любой старый файл до текущего формата при загрузке.

Начните версионировать схемы данных с первого дня. Даже если вы никогда не выпустите сиквел, ваши Early Access-сборки создадут файлы сохранений в старых форматах, которые должны пережить запуск 1.0.

3. Используйте индексированный доступ для hot-path данных

Если поле данных читается внутри цикла, выполняемого каждый кадр — значения урона, тайминги анимаций, веса ИИ — доступ к нему должен быть по индексу, а не по имени или хэшу. Разница мала на один доступ, но огромна в сумме. Боевая система, перебирающая 200 активных единиц оружия при 60 кадрах/с, означает 12 000 чтений полей в секунду. Индексированный доступ занимает ~1 нс на чтение (прямое смещение в памяти); поиск по словарю — ~50–200 нс на чтение.

Для данных, которые читаются один раз при загрузке (отображаемые имена, описания, лор-текст), индексированный доступ не важен. Профилируйте перед оптимизацией, но знайте, что этот паттерн существует на случай, когда он понадобится.

4. Колоночное хранение для больших наборов данных

Если в вашей игре более 1 000 записей любого отдельного типа данных, рассмотрите колоночную компоновку хранения. Преимущества кэш-локальности реальны и измеримы — бенчмарки схожих архитектур показывают улучшение скорости итераций в 3–8 раз для агрегирующих запросов (суммы, фильтры, сортировки) по сравнению со строчным хранением.

Это применимо к таблицам лута, каталогам инвентаря, деревьям диалогов, базам квестов — ко всему, где может понадобиться перебирать много записей, чтобы что-то найти или вычислить.

5. Генерируйте UI редактора из схем

Мультипликатор продуктивности data engine — это не производительность рантайма, а скорость итераций дизайнера. Если ваши схемы декларативны, вы можете автоматически генерировать интерфейсы редактора. Это устраняет самый частый источник трения между дизайнерами и инженерами: «А можно добавить поле в эту штуку?»

При подходе, управляемом схемами, добавление поля — это изменение одной строки схемы, которое автоматически появляется во всех панелях редактора. Никаких изменений C#, никаких кастомных property drawers, никаких inspector-скриптов. Если вы делаете инструменты для команды больше двух человек, одного этого достаточно, чтобы оправдать вложения.

Куда вписываются бэкенд-системы

RE:Dox отвечает за конвейер данных на стороне движка — как данные определяются, сериализуются и загружаются в память рантайма. Но современным играм также нужно управлять данными конкретного игрока: файлы сохранений, синхронизируемые между устройствами, таблицы лидеров, питающиеся телеметрией геймплея, удалённая конфигурация, позволяющая менять шансы выпадения без выпуска патча.

Если вы строите этот бэкенд-слой сами, вас ждут потоки аутентификации пользователей, разрешение конфликтов при конкурентных записях, отслеживание ревизий для облачных сохранений и дашборд, которым команда реально сможет пользоваться. Это недели инфраструктурной работы до того, как вы напишете хотя бы одну геймплейную фичу.

Сервисы вроде horizOn закрывают эти специфические бэкенд-задачи: облачные сохранения, привязанные к аккаунту, с чтением и записью с учётом ревизий, таблицы лидеров, удалённую конфигурацию и репортинг крашей — так вы можете сосредоточить инженерное время на архитектуре данных на стороне движка, которая напрямую влияет на игровой опыт.

Применяем эти паттерны в вашем текущем проекте

Вам не нужно создавать полный data engine, чтобы получить пользу от архитектуры RE:Dox. Начните с конкретных шагов:

  1. Проведите аудит текущей загрузки данных. Профилируйте, сколько занимает запуск и куда уходит время. Если больше 15% времени загрузки тратится на десериализацию и валидацию данных, у вас есть пространство для оптимизации.

  2. Определите самые горячие пути данных. С помощью профилировщика найдите, какие структуры данных чаще всего используются во время геймплея. Они кандидаты на индексированный доступ и колоночное хранение.

  3. Напишите каркас миграции. Даже простой номер версии в файлах сохранений плюс словарь функций миграции дают 90% ценности. Вам не нужна полная система схем, чтобы получить преимущества миграции.

  4. Разделите определения данных. Если характеристики оружия определены в том же C# классе, который Unity сериализует на диск, это ваша точка связности. Вынесите определения полей в отдельную версионированную схему — даже если это просто JSON-схема, по которой вы валидируете данные.

  5. Замеряйте до и после. Измеряйте время загрузки, использование памяти и скорость итераций на горячих путях до внесения изменений. Улучшения архитектуры данных накапливаются со временем, но вам нужен базовый замер, чтобы доказать, что вложения окупаются.

Более широкая картина: почему открытый исходный код важен

Открытие исходного кода RE:Dox компанией Capcom важно не только самим кодом. AAA-студии исторически относятся к своим движковым технологиям как к конкурентному преимуществу. Когда Capcom делится таким компонентом, она сигнализирует, что слой data engine — не их главный конкурентный барьер: контент, геймдизайн и арт-дирекшн — вот что важно.

Для инди- и средних разработчиков это означает, что паттерны и архитектурные решения одного из самых успешных игровых движков в индустрии теперь можно напрямую изучить. Вы можете прочитать, как Capcom обрабатывает миграцию схем, изучить решения по сериализации и применить эти уроки в своих проектах.

Сам код, возможно, не ляжет напрямую в ваш проект на Unity или Godot, но архитектурные паттерны — декларативные схемы, индексированный доступ, колоночное хранение, автоматическая миграция, генерируемый UI редактора — универсальны. Они работают независимо от движка, языка и масштаба.

Начните с одного паттерна. Версионируйте данные. Отстраивайтесь дальше. Ваше будущее «я» — и ваши дизайнеры — скажут вам спасибо.