Тактический отряд с автоматическим огнём в изометрии, собственный софтверный растеризатор (raylib только окно, ввод и финальный блит). Ядро (спек, раздел 6): - LaneClear: проверка линии огня по своим, стенам и дальности; - решатель огневых позиций с гистерезисом и коммитом; - резервирование линий огня и расступание из чужих секторов; - пули-снаряды, дружественный урон физически возможен. Хоррор-слой (render-only): свет и туман войны с памятью карты, светящиеся трассеры, кровь, виньетка, зерно, напряжение. Ближний бой: сектор удара, три фазы, связка из трёх ударов, своя дисциплина «не бить сквозь своего». Проверка: squad_proto.exe --accept прогоняет критерии приёмки раздела 11 плюс блок M7 по ближнему бою — все PASS, FRIENDLY_HITS = 0 за 3 минуты боя. Сборка: cmake -S . -B build && cmake --build build --config Release raylib 6.0 подтягивается через FetchContent. Документация — CLAUDE.md как оглавление, содержание в docs/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
|