232 lines
8.4 KiB
C
232 lines
8.4 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Минимальная ECS: сущность — это идентификатор, данные лежат в типизированных
|
|||
|
|
// плотных пулах, системы — свободные функции над пулами.
|
|||
|
|
//
|
|||
|
|
// Зачем она здесь, если прототип и без неё работал:
|
|||
|
|
// * оружие стало отдельной СУЩНОСТЬЮ со своим набором статов (пистолет,
|
|||
|
|
// карабин, клинок). Боец ссылается на неё, а не носит числа в себе —
|
|||
|
|
// значит ствол можно подменить, не трогая ни бойца, ни правила боя;
|
|||
|
|
// * мишени, пули и бойцы перестали быть тремя разными контейнерами в Game
|
|||
|
|
// и стали тремя пулами одного мира. Добавить четвёртый вид сущности —
|
|||
|
|
// это новый компонент, а не новое поле в Game и новый цикл в Step.
|
|||
|
|
//
|
|||
|
|
// Чего здесь НЕТ и почему:
|
|||
|
|
// * архетипов и запросов вида View<A, B, C>. Сущностей в прототипе десятки,
|
|||
|
|
// а не десятки тысяч; выигрыш от архетипов ушёл бы в сложность, а цена
|
|||
|
|
// промаха кеша на 5 агентах равна нулю;
|
|||
|
|
// * реакции на изменение компонентов, планировщика систем, рефлексии.
|
|||
|
|
//
|
|||
|
|
// Гранулярность компонентов сознательно КРУПНАЯ (Agent, Target, Bullet целиком,
|
|||
|
|
// а не Position+Velocity+Body по отдельности). Причина в docs/12-ecs.md:
|
|||
|
|
// правила боя из спека читают состояние агента целиком и в одном порядке,
|
|||
|
|
// и дробление их только запутало бы, не дав ни строчки полезного обобщения.
|
|||
|
|
//
|
|||
|
|
// Детерминизм: удаление — swap-remove, порядок пулов зависит только от истории
|
|||
|
|
// вызовов. Одинаковая последовательность операций даёт одинаковый мир, поэтому
|
|||
|
|
// --headless по-прежнему воспроизводим.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <cstdint>
|
|||
|
|
#include <memory>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
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 <class T>
|
|||
|
|
struct Pool final : IPool
|
|||
|
|
{
|
|||
|
|
std::vector<T> data;
|
|||
|
|
std::vector<Entity> owner; // кому принадлежит data[i]
|
|||
|
|
std::vector<int32_t> 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 <class T>
|
|||
|
|
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 <class T>
|
|||
|
|
Pool<T>& Store()
|
|||
|
|
{
|
|||
|
|
const uint32_t id = TypeId<T>();
|
|||
|
|
if (id >= pools.size()) pools.resize(size_t(id) + 1);
|
|||
|
|
if (!pools[id]) pools[id] = std::make_unique<Pool<T>>();
|
|||
|
|
return *static_cast<Pool<T>*>(pools[id].get());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
template <class T>
|
|||
|
|
const Pool<T>& Store() const
|
|||
|
|
{
|
|||
|
|
return const_cast<World*>(this)->Store<T>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
template <class T>
|
|||
|
|
T& Add(Entity e, const T& value) { return Store<T>().Add(e, value); }
|
|||
|
|
|
|||
|
|
template <class T>
|
|||
|
|
T* Get(Entity e) { return Store<T>().Find(e); }
|
|||
|
|
|
|||
|
|
template <class T>
|
|||
|
|
const T* Get(Entity e) const { return Store<T>().Find(e); }
|
|||
|
|
|
|||
|
|
template <class T>
|
|||
|
|
std::vector<T>& All() { return Store<T>().data; }
|
|||
|
|
|
|||
|
|
void Clear()
|
|||
|
|
{
|
|||
|
|
for (auto& p : pools)
|
|||
|
|
if (p) p->Clear();
|
|||
|
|
alive.clear();
|
|||
|
|
generation.clear();
|
|||
|
|
freeSlots.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::vector<uint8_t> alive;
|
|||
|
|
std::vector<uint8_t> generation;
|
|||
|
|
std::vector<uint32_t> freeSlots;
|
|||
|
|
std::vector<std::unique_ptr<IPool>> pools;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} // namespace ecs
|