#pragma once // ----------------------------------------------------------------------------- // Минимальная ECS: сущность — это идентификатор, данные лежат в типизированных // плотных пулах, системы — свободные функции над пулами. // // Зачем она здесь, если прототип и без неё работал: // * оружие стало отдельной СУЩНОСТЬЮ со своим набором статов (пистолет, // карабин, клинок). Боец ссылается на неё, а не носит числа в себе — // значит ствол можно подменить, не трогая ни бойца, ни правила боя; // * мишени, пули и бойцы перестали быть тремя разными контейнерами в Game // и стали тремя пулами одного мира. Добавить четвёртый вид сущности — // это новый компонент, а не новое поле в Game и новый цикл в Step. // // Чего здесь НЕТ и почему: // * архетипов и запросов вида View. Сущностей в прототипе десятки, // а не десятки тысяч; выигрыш от архетипов ушёл бы в сложность, а цена // промаха кеша на 5 агентах равна нулю; // * реакции на изменение компонентов, планировщика систем, рефлексии. // // Гранулярность компонентов сознательно КРУПНАЯ (Agent, Target, Bullet целиком, // а не Position+Velocity+Body по отдельности). Причина в docs/12-ecs.md: // правила боя из спека читают состояние агента целиком и в одном порядке, // и дробление их только запутало бы, не дав ни строчки полезного обобщения. // // Детерминизм: удаление — swap-remove, порядок пулов зависит только от истории // вызовов. Одинаковая последовательность операций даёт одинаковый мир, поэтому // --headless по-прежнему воспроизводим. // ----------------------------------------------------------------------------- #include #include #include namespace ecs { // Идентификатор: младшие 24 бита — индекс слота, старшие 8 — поколение. // Поколение отличает «ту же ячейку, но уже другую сущность» от живой ссылки. using Entity = uint32_t; constexpr Entity NO_ENTITY = 0xFFFFFFFFu; constexpr uint32_t ENTITY_INDEX_BITS = 24; constexpr uint32_t ENTITY_INDEX_MASK = (1u << ENTITY_INDEX_BITS) - 1u; inline uint32_t IndexOf(Entity e) { return e & ENTITY_INDEX_MASK; } inline uint32_t GenOf(Entity e) { return e >> ENTITY_INDEX_BITS; } inline Entity MakeEntity(uint32_t index, uint32_t gen) { return (index & ENTITY_INDEX_MASK) | (gen << ENTITY_INDEX_BITS); } // --- пулы -------------------------------------------------------------------- struct IPool { virtual ~IPool() = default; virtual void Remove(Entity e) = 0; virtual void Clear() = 0; }; // Плотный массив компонентов + разрежённая таблица «индекс сущности -> место». // data остаётся сплошным, поэтому его можно отдавать наружу как обычный массив // (именно так SolverContext получает Agent* и ничего не знает про ECS). template struct Pool final : IPool { std::vector data; std::vector owner; // кому принадлежит data[i] std::vector sparse; // индекс сущности -> позиция в data, -1 = нет bool Has(Entity e) const { const uint32_t i = IndexOf(e); return i < sparse.size() && sparse[i] >= 0; } T* Find(Entity e) { const uint32_t i = IndexOf(e); if (i >= sparse.size() || sparse[i] < 0) return nullptr; return &data[size_t(sparse[i])]; } const T* Find(Entity e) const { const uint32_t i = IndexOf(e); if (i >= sparse.size() || sparse[i] < 0) return nullptr; return &data[size_t(sparse[i])]; } T& Add(Entity e, const T& value) { const uint32_t i = IndexOf(e); if (i >= sparse.size()) sparse.resize(size_t(i) + 1, -1); if (sparse[i] >= 0) { data[size_t(sparse[i])] = value; return data[size_t(sparse[i])]; } sparse[i] = int32_t(data.size()); data.push_back(value); owner.push_back(e); return data.back(); } void Remove(Entity e) override { const uint32_t i = IndexOf(e); if (i >= sparse.size() || sparse[i] < 0) return; const size_t slot = size_t(sparse[i]); const size_t last = data.size() - 1; if (slot != last) { data[slot] = data[last]; owner[slot] = owner[last]; sparse[IndexOf(owner[slot])] = int32_t(slot); } data.pop_back(); owner.pop_back(); sparse[i] = -1; } void Clear() override { data.clear(); owner.clear(); for (int32_t& s : sparse) s = -1; } int Count() const { return int(data.size()); } }; // --- идентификаторы типов ---------------------------------------------------- inline uint32_t NextTypeId() { static uint32_t next = 0; return next++; } template inline uint32_t TypeId() { static const uint32_t id = NextTypeId(); return id; } // --- мир --------------------------------------------------------------------- struct World { // Создание/уничтожение сущностей. Entity Create() { if (!freeSlots.empty()) { const uint32_t i = freeSlots.back(); freeSlots.pop_back(); alive[i] = 1; return MakeEntity(i, generation[i]); } const uint32_t i = uint32_t(alive.size()); alive.push_back(1); generation.push_back(0); return MakeEntity(i, 0); } bool Alive(Entity e) const { const uint32_t i = IndexOf(e); return i < alive.size() && alive[i] != 0 && generation[i] == GenOf(e); } void Destroy(Entity e) { if (!Alive(e)) return; const uint32_t i = IndexOf(e); for (auto& p : pools) if (p) p->Remove(e); alive[i] = 0; generation[i] = uint8_t((generation[i] + 1u) & 0xFFu); freeSlots.push_back(i); } // Пул компонента. Создаётся при первом обращении и живёт до смерти мира: // Clear() чистит СОДЕРЖИМОЕ, но не сами пулы, поэтому ссылки на data // (например Game::targets) переживают Reset уровня. template Pool& Store() { const uint32_t id = TypeId(); if (id >= pools.size()) pools.resize(size_t(id) + 1); if (!pools[id]) pools[id] = std::make_unique>(); return *static_cast*>(pools[id].get()); } template const Pool& Store() const { return const_cast(this)->Store(); } template T& Add(Entity e, const T& value) { return Store().Add(e, value); } template T* Get(Entity e) { return Store().Find(e); } template const T* Get(Entity e) const { return Store().Find(e); } template std::vector& All() { return Store().data; } void Clear() { for (auto& p : pools) if (p) p->Clear(); alive.clear(); generation.clear(); freeSlots.clear(); } private: std::vector alive; std::vector generation; std::vector freeSlots; std::vector> pools; }; } // namespace ecs