惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
博客园 - 【当耐特】
月光博客
月光博客
Vercel News
Vercel News
D
Docker
I
InfoQ
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
GbyAI
GbyAI
有赞技术团队
有赞技术团队
雷峰网
雷峰网
博客园 - 聂微东
小众软件
小众软件
Y
Y Combinator Blog
腾讯CDC
L
LangChain Blog
The GitHub Blog
The GitHub Blog
宝玉的分享
宝玉的分享
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
T
The Blog of Author Tim Ferriss

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Статический архетип в ECS: локальность данных без неявных...
Tidehunter · 2026-05-04 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели1.3K

Обзор

У архетипов в ECS есть неотъемлимое преимущество перед обычными sparse set'ами - локальность данных. Но есть и существенная проблема - хаотическое копирование данных при миграции из одного архетипа в другой. Что если попробовать избавиться от этого недостатка, оставив локальность данных? Предлагаю рассмотреть статический архетип, который требует явного вызова метода для миграции данных.

Проблема

Рассмотрим проблему на конкретном примере: на игровой механике стрельбы, которую реализуем с помощью ECS.

Есть снаряд, выпущенный из оружия. Он летит в определенном направлении и попадает в цель. В момент попадания снаряд взрывается. Сущность снаряда перейдет в сущность взрыва. Ненужные компоненты будут удалены, новые добавлены.

Понятно, что есть множество вариантов это реализовать как-то по другому, но рассмотрим именно этот вариант для наглядности.

Итак, для полета снаряда нам понадобятся компоненты:

  • Position - текущая позиция,

  • Velocity - скорость и направление полета,

  • Damage - урон.

А попадание будет представлено:

  • Position - местом попадания,

  • ExplosionRadius - радиусом поражения,

  • Damage - уроном.

Сами компоненты будут иметь вот такой вид:

public struct Position        : IComponent { public Vector3 Vector; }

public struct Velocity        : IComponent { public Vector3 Vector; }

public struct ExplosionRadius : IComponent { public float Value; }

public struct Damage          : IComponent { public int Value; }

При переходе сущности из снаряда во взрыв, т.е. при переходе из одного архетипа в другой система вынуждена:

  • скопировать все компоненты сущности из старого архетипа,

  • вставить в подходящие массивы нового архетипа,

  • удалить запись в старом архетипе.

Чем больше компонентов, тем дороже каждая миграция. В сценариях с частыми переходами между состояниями (снаряды, бафы, фазы атак) это становится заметными накладными расходами.

Вопрос напрашивается сам собой: можно ли сохранить локальность данных и при этом взять контроль над моментом копирования в свои руки?

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

Статический архетип

Идея проста: набор компонентов архетипа фиксируется на этапе инициализации. После этого сущность попадает в архетип и находится там до тех пор, пока мы сами не решим её переместить или удалить. Никакого неявного перемещения, только мы контролируем этот процесс.

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

Рассмотрим механизм работы таких статических архетипов. В этом нам поможет библиотека PewPew.Ecs. Там как раз реализованы статические архетипы.

Нам понадобится nuget пакет PewPew.Ecs, который включает в себя всё, что нужно.

Объявление статических архетипов

Из примера выше объявим 2 архетипа: Projectile, будет отвечать за снаряд, и Explosion, будет отвечать за взрыв, с нужным набором компонент.

Нам понадобится атрибут [Archetype]. Он нужен для SourceGenerator, который добавит нужные методы и структуры.

// объявляем архетип снаряда
[Archetype]
public ref struct Projectile
{
    // добавляем нужные компоненты как ref поля
    public ref Position Position;
    public ref Velocity Velocity;
    public ref Damage Damage;

    // объявляем конструктор
    public Projectile(ref Position position, ref Velocity velocity, ref Damage damage)
    {
        Position = ref position;
        Velocity = ref velocity;
        Damage = ref damage;
    }
}

// по аналогии объявляем архетип взрыва с нужным набором полей
[Archetype]
public ref struct Explosion
{
    public ref Position Position;
    public ref ExplosionRadius ExplosionRadius;
    public ref Damage Damage;

    public Explosion(ref Position position, ref ExplosionRadius explosionRadius, ref Damage damage)
    {
        Position = ref position;
        ExplosionRadius = ref explosionRadius;
        Damage = ref damage;
    }
}

Инициализация

Для работы со статическими архетипами нам понадобится HybridWorld - мир, который умеет хранить сущности как в обычных sparse set'ах, так и в статических архетипах.

// создаем HybridWorld
HybridWorld world = WorldFactory.Shared.CreateHybridWorld();

// объявляем архетип летящего снаряда Projectile с компонентами Position, Velocity, Damage
world.InitProjectileArchetype();

// объявляем архетип взрывающегося снаряда Explosion с компонентами Position, ExplosionRadius, Damage
world.InitExplosionArchetype();

Явная инициализация имеет большое преимущество перед неявной. Процесс прогрева всех внутренних коллекций происходит в понятное для разработчика время.

Добавление сущности

После инициализации можно добавлять сущности в архетип:

// получаем сгенерированный архетип для Projectile
ProjectileArchetype projectileArchetype = world.GetProjectileArchetype();

// создаем идентификатор сущности
EntityId entityId = world.CreateEntityId();

// добавляем в архетип новую сущность
Projectile projectile = projectileArchetype.Add(entityId);

// назначаем компонентам нужные значения
projectile.Position.Vector = spawnPoint;
projectile.Velocity.Vector = direction;
projectile.Damage.Value    = 50;

Метод Add возвращает refструктуру Projectile с refссылками на компоненты прямо в массивах архетипа. Никаких промежуточных копий.

Явная миграция

Перейдём к ключевой части. Рассмотрим сценарий: в каждом тике мы проверяем летящие снаряды и переводим те, что во что-то попали, в архетип взрыва.

// получаем сгенерированный архетип для Projectile
ProjectileArchetype projectileArchetype = world.GetProjectileArchetype();

// получаем сгенерированный архетип для Explosion
ExplosionArchetype explosionArchetype   = world.GetExplosionArchetype();

// получаем все сущности хранящиеся в архетипе
Span<EntityId> entities = projectileArchetype.Entities;

// пример итерации с перемещением сущности
for (var i = 0; i < entities.Length; i++)
{
    EntityId entityId = entities[i];

    // проверка столкновений
    if (HitSomething(entityId))
        // копирование компонент из старого архетипа в новый
        projectileArchetype.MoveEntityTo(entityId, explosionArchetype);
}

Метод MoveEntityTo копирует общие компонентыPosition и Damage из старого архетипа в новый, а компонент Velocity просто отбрасывается, т.к. в новом архетипе взрыва его нет.

Также в новом архетипе есть компонент ExplosionRadius , который будет содержать значения по умолчанию. Этот компонент мы можем инициализировать строкой ниже чуть позже.

Именно метод MoveEntityTo выполняет копирование компонентов. Но копирование происходит только тогда, когда мы сами этого захотели. Никакой скрытой работы за кадром.

Локальность данных

Осталось продемонстрировать, что итерация по сущностям работает также, как и для классического архетипа. Заодно продемострируем использование SIMD инструкций.

Поскольку компоненты одного архетипа хранятся в плотных массивах, мы можем обрабатывать их пачками. Для этого воспользуемся HybridWorld , который поддерживает батч-запросы через IBatchQuery:

// создаем фильтр для сущностей
FilterDefinition flyingDefinition = new FilterDefinition()
    .With<Position>()
    .With<Velocity>();

// создаем запрос, который будет выполняться для сущностей, удовлетворяющих фильтру
FlightBatchQuery flightBatchQuery = new FlightBatchQuery();

// запускаем запрос
world.ExecuteBatchQuery<FlightBatchQuery, Position, Velocity>(flyingDefinition, flightBatchQuery);

// именно структуры позволяют инлайнить методы, что позволяет убрать накладные расходы на вызовы метода
public readonly struct FlightBatchQuery : IBatchQuery<Position, Velocity>
{
    // метод обработает сущности пачкой, когда сущности хранятся в статическом архетипе,
    // тут содержится SIMD специфичная логика сложения значений,
    // метод SparseUpdate ниже делает тоже самое, только поштучно для каждой сущности
    public void BatchUpdate(Span<Position> positions, Span<Velocity> velocities)
    {
        int vectorCount = Vector<float>.Count;
        int length = positions.Length - positions.Length % vectorCount;
        var posVectors = MemoryMarshal.Cast<Position,  Vector<float>>(positions[..length]);
        var velVectors = MemoryMarshal.Cast<Velocity, Vector<float>>(velocities[..length]);
        for (int i = 0; i < posVectors.Length; i++)
            posVectors[i] += velVectors[i];

        for (int i = length; i < positions.Length; i++)
            positions[i].Vector += velocities[i].Vector;
    }

    // поштучно обрабатывает сущности, когда сущности хранятся в sparse set коллекциях
    public void SparseUpdate(ref Position position, ref Velocity velocity)
        => position.Vector += velocity.Vector;
}

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

Ограничения

Статический архетип, в свою очередь, тоже имеет свои минусы:

  • Сущность не может иметь компоненты, кроме тех, что объявлены в статическом архетипе. Но сущность может существовать отдельно от архетипа и хранить компоненты в sparse set'ах.

  • Сущность может находиться только в одном архетипе одновременно.

Эти ограничения обусловленны сложностями, возникающими в случае итерации по таким сущностям. Какие именно там возникают сложности, мы можем обсудить в комментариях.

Когда использовать?

Статические архетипы хорошо работают в следующих сценариях:

  • количество сущностей достаточно большое (более 10000 сущностей в одном мире), чтобы реализовать преимущество локальности данных, как и в случае классического архетипа,

  • структура сущностей известна заранее и меняется дискретно: снаряд летит / взрывается, враг жив / мёртв, юнит стоит / бежит,

  • важна предсказуемость момента копирования данных,

  • нужна SIMD-обработка или локальность данных.

Если же структура данных непредсказуема, то, вероятно, стоит рассмотреть обычные sparse set хранилища.

Заключение

Статический архетип это компромисс между классическим архетипом и sparse set хранилищем. Он позволяет сохранить локальность данных и получить SIMD-дружественный доступ к компонентам, но взамен требует явно вызывать копирование данных с помощью метода MoveEntityTo. Копированием управляет разработчик, а не ECS.

P.S. Буду рад обсудить статические и классические архетипы в комментариях!