В статье описан способ ограничения доступа систем 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>
>;

Спасибо за чтение! Ваши отзывы приветствуются.