В статье описан способ ограничения доступа систем ECS к хранилищу данных компонентов на языке C++. Доступны два режима доступа к компонентам: только чтение и чтение-запись.
Обзор
Допустим, у нас есть очень простая игра, в которой объекты имеют положение и могут двигаться с заданной скоростью.
Это представлено следующими компонентами:
struct Position { int x; int y; }; struct Velocity { int x; int y; };
Хранилище данных для компонентов определено так, чтобы обернуть кортеж векторов (см. Дизайн, ориентированный на данные) каждого типа компонента:
template<typename... TComponents> struct Storage { template<typename TComponent> using Container = std::vector<TComponent>; std::tuple<Container<TComponents>...> containers; template<typename TComponent> Container<TComponent>& Get() { return std::get<Container<TComponent>>(containers); } }; Storage<Position, Velocity> storage;
Компоненты управляются с помощью системы движения, которая считывает скорость каждого компонента и обновляет его положение:
struct Movement { template<typename TData> void Process(TData&& data, const size_t entity_id) { auto& v = data.Read<Velocity>(entity_id); auto& p = data.Write<Position>(entity_id); p.x += v.x; p.y += v.y; } }; Movement movement;
Мы хотим убедиться, что система движения имеет доступ только для чтения к скорости, но доступ для чтения и записи к положению.
Выполнение
Давайте определим методы доступа к хранилищу для чтения и записи.
Метод доступа Reader возвращает константную ссылку, чтобы убедиться, что сохраненные данные недоступны для редактирования:
template<typename TComponent> struct Reader { template<typename TStorage> const auto& Read(TStorage&& storage) const { return storage.template Get<TComponent>(); } };
в то время как Writer позволяет редактировать данные, возвращая неконстантную ссылку:
template<typename TComponent> struct Writer { template<typename TStorage> auto& Write(TStorage&& storage) { return storage.template Get<TComponent>(); } };
Теперь определите шаблон Accessors, который будет обертывать хранилище и предоставлять системам соответствующие методы Read и Write:
template <typename... Ts> struct types { }; template <typename TStorage, typename TRead, typename TWrite> struct Accessors; template < template <typename...> typename TTypes, typename TStorage, typename... TReads, typename... TWrites> struct Accessors< TStorage, TTypes<TReads...>, TTypes<TWrites...> > : private Reader<TReads>..., private Reader<TWrites>..., private Writer<TWrites>... { Accessors(TStorage &storage) : storage_{storage}, Reader<TReads>{}..., Reader<TWrites>{}..., Writer<TWrites>{}... { } template <typename TComponent> auto &Read(const size_t id) const { return Reader<TComponent>::template Read(storage_)[id]; } template <typename TComponent> auto &Write(const size_t id) { return Writer<TComponent>::template Write(storage_)[id]; } private: TStorage &storage_; };
Шаблон Accessor используется для ограничения доступа к хранилищу:
auto accessors = Accessors< Storage<Position, Velocity>, types<Velocity>, // components with read-only access types<Position> // components with read-write access >{storage};
Переменная доступа может использоваться для чтения компонентов Velocity и Read/Write Position:
accessors.Read<Velocity>(entity_id); accessors.Write<Position>(entity_id);
Вызов записи для Velocity приведет к ошибке компиляции. Аксессоры можно передать методу Process системы Movement:
movement.Process(accessors, ...);
Пример кода, который использует обсуждаемые элементы:
int main(int argc, char *argv[]) { using GameStorage = Storage<Position, Velocity>; GameStorage storage; Movement movement; auto accessors = Accessors< GameStorage, types<Velocity>, types<Position>>{storage}; // prepare sample data static constexpr size_t Size = 100; storage.Get<Velocity>().resize(Size); storage.Get<Position>().resize(Size); // main loop while(true) { for (size_t i = 0; i < Size; ++i) movement.Process(accessors, i); } return 0; }
В качестве расширения может быть реализована другая система для обработки гравитации:
struct Gravity { template<typename TData> void Process(TData&& data, const size_t id) { auto& v = data.Write<Velocity>(id); v.y += 1; } };
С аксессором чтения-записи для Velocity:
using GravityAccessor = Accessor< GameStorage, types</* no read-only component*/>, types<Velocity> >;
Спасибо за чтение! Ваши отзывы приветствуются.