0

我正在开发一个实体组件系统 (ECS),灵感来自Bitsquid 的博客系列。我的 ECS 由两个主要类组成:系统(负责创建/销毁实体)和属性(负责为给定系统存储组件的 std::vector)。

创建/销毁实体时,系统会触发一些信号以使属性实例了解系统上的状态更改,以保持数据连续和良好映射。

下面是一个汽车系统的简单示例,每辆汽车都有两个属性:point2d 位置,std::string 名称。

System<Car> cars;
Property<Car, point2d> positions(cars);
Property<Car, std::string> names(cars);
auto car0 = cars.add();
auto car1 = cars.add();
positions[car0] = {0.0, 0.0};
names[car0]     = "Car 0";
positions[car1] = {1.0, 2.0};
names[car1]     = "Car 1";

这样我可以将数据保存在单独的“数组”中。

假设我想添加 1000 个实体。我可以调用std::vector::reserve(1000)所有属性,然后push_back对每个属性执行 1000 次。

我已经确定了这种方法的一个问题:reserves如果我有 N 个属性,我将需要 1+N 。我想知道我是否可以使用 astd::vector<tuple<point2d, std::string>>来处理内存分配,但假装管理数据(重新解释强制转换?)我将所有的point2d数据连续存储,然后是字符串。

这样我就可以利用std::vectorapi 来保存通知/保留/调整大小的操作。虽然我必须调整访问方法(如 vector::at、operator[]、begin、end)。

您对如何实现这一目标有任何想法吗?或者如果您认为这不是一个好主意,有什么替代建议?

4

3 回答 3

2

这是不可能的,至少在std::tuple. 您可以将s 模板参数中的成员视为tuple简单的。这意味着它们将在内存中一个接一个地对齐。structtuple

相反,您的所有经理都可以实现一个允许调整大小(保留)的接口,并且您可以在 er.. 中注册所有经理,ManagerOfManagers这将在循环中调整所有经理的大小

于 2017-03-27T15:16:36.937 回答
1

似乎除了它自己的 Ts 数组之外,没有简单的方法可以std::vector<T>对某些数据进行操作。因此,您的选择包括搜索第三方 SoA(阵列结构)库或制作您自己的库。

std::vector这可能是模拟的接口的 SoA 类的起点:

// This snippet uses C++14 features
#include <functional>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

template <typename F, typename... Ts, std::size_t... Is>
void tuple_for_each(std::tuple<Ts...>& tuple, F f, std::index_sequence<Is...>) {
    using expander = int[];
    (void)expander{0, ((void)f(std::get<Is>(tuple)), 0)...};
}

template <typename F, typename... Ts>
void tuple_for_each(std::tuple<Ts...>& tuple, F f) {
    tuple_for_each(tuple, f, std::make_index_sequence<sizeof...(Ts)>());
}

// Missing in this example:
// - full support for std::vector's interface (iterators, exception safety guarantees, etc.);
// - access to individual homogeneous vectors;
// - lots of other things.

template <typename T, typename... Ts>
class soa_vector {
    std::tuple<std::vector<T>, std::vector<Ts>...> data_;

    template <std::size_t>
    void push_back_impl() const {}

    template <std::size_t position, typename Value, typename... Values>
    void push_back_impl(Value&& value, Values&&... values) {
        std::get<position>(data_).push_back(std::forward<Value>(value));
        push_back_impl<position + 1, Values...>(std::forward<Values>(values)...);
    }

    template<std::size_t... Is>
    std::tuple<std::add_lvalue_reference_t<T>, std::add_lvalue_reference_t<Ts>...>
    tuple_at(std::size_t position, std::index_sequence<Is...>) {
        return std::make_tuple(std::ref(std::get<Is>(data_)[position])...);
    }

public:    
    template <typename... Values>
    std::enable_if_t<sizeof...(Values) == sizeof...(Ts) + 1, void>
    push_back(Values&&... values) {
        push_back_impl<0, Values...>(std::forward<Values>(values)...);
    }

    void reserve(std::size_t new_capacity) {
        tuple_for_each(data_, [new_capacity](auto& vec) { vec.reserve(new_capacity); });
    }

    std::size_t size() const { return std::get<0>(data_).size(); }

    std::tuple<std::add_lvalue_reference_t<T>, std::add_lvalue_reference_t<Ts>...>
    operator[](std::size_t position) {
        return tuple_at(position, std::make_index_sequence<sizeof...(Ts) + 1>());
    }
};

On Coliru

于 2017-03-28T09:38:25.770 回答
1

我(几乎)使用std::vector底层数据结构完成了我的 SoA 实现。

假设我们要为 types 创建一个 SoA <int, double, char>。class不是创建三个向量,每个类型一个,而是TupleVector<int, double, char>创建一个std::vector. 对于数据访问/保留/调整大小,它使用重新解释转换、偏移和元组大小计算。

我刚刚创建了一个简单的基准代码,它调用了.resize(.size()+1)10.000.000 次,我的 SoA 实现似乎比使用单独std::vectors的 .

在这里你可以看到基准代码: https ://github.com/csguth/Entity/blob/development/src/Test/TupleVectorBenchmark.cpp

虽然这个实现仍然需要一些迭代器功能(微不足道的)和更多的基准测试。

希望这对某人有用!

于 2017-03-27T11:10:54.407 回答