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

推荐订阅源

D
Docker
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
爱范儿
爱范儿
罗磊的独立博客
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
U
Unit 42
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
H
Help Net Security
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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. Буду рад обсудить статические и классические архетипы в комментариях!