ECS#
metatron的ecs系统中, entity/component由entt管理, system则需要实现ecs::Daemon接口.
ecs::Hierarchy用于维护世界树, 树节点采用Unix路径命名并分配对应的ecs::Entity. hierarchy会包含多个ecs::Stage, stage中则包含多个ecs::Daemon, stage会按序更新, 但内部的daemon可能会由于多线程不能保证有序, 若需有序需位于不同的stage.
// ...
resource_stage->daemons = {
&transform_daemon,
&shape_daemon,
&medium_daemon,
&texture_daemon,
};
// ...
hierarchy.stages = {
spectrum_stage.get(),
resource_stage.get(),
material_stage.get(),
camera_stage.get(),
render_stage.get(),
};
各个daemon会有init与update接口, init用于提前挂载预定义的components(例如金属的IOR光谱)以及注册serde, update则执行前述的更新操作, 负载重的任务可通过stl::scheduler实现多线程.
struct Daemon final: pro::facade_builder
::add_convention<daemon_init, auto () noexcept -> void>
::add_convention<daemon_update, auto () noexcept -> void>
::add_skill<pro::skills::as_view>
::build {};
entt不会记录component是否被修改, metatron通过在世界树节点上挂载额外的ecs::Dirty_Mark<T>实现, 各个daemon更新完毕后需要清除.
template<typename T>
auto attach(Entity entity, T&& component = {}) noexcept -> void {
registry.emplace<Dirty_Mark<T>>(entity);
registry.emplace<T>(entity, std::forward<T>(component));
}
当前活跃的hierarchy可以通过global static ecs::Hierarchy::instance访问, 执行activate()来修改instance. 这主要用于方便entity与路径的转换, 例如可以通过字面量_et执行转换.
namespace mtt::ecs {
auto to_path(Entity entity) -> std::string {
return Hierarchy::instance->path(entity);
}
auto to_entity(std::string const& path) -> Entity {
return Hierarchy::instance->create(path);
}
}
namespace mtt {
auto operator"" _et(view<char> path, usize size) -> ecs::Entity {
return ecs::to_entity(path);
}
}
SERDE#
Glaze库通过调用编译器的internal函数实现反射功能, 对于aggregate的反射无需手动注册, 对部分容器也有支持, 在c++26的静态反射实装前是很好用的替代品.
对于ecs::Entity, 我们通过全局的ecs::Hierarchy::instance调用ecs::to_path与ecs::to_entity即可. 对于metatron中最基础的math::Matrix类型, 注册为反射内部实际存储数据的std::array<Element, first_dim>, Glaze会处理Element的递归. math::Quaternion的反射同理. 除这三个类型外别的都可以交给Glaze处理.
template<typename T, mtt::usize first_dim, mtt::usize... rest_dims>
struct from<JSON, mtt::math::Matrix<T, first_dim, rest_dims...>> {
template<auto Opts>
auto static op(mtt::math::Matrix<T, first_dim, rest_dims...>& v, auto&&... args) noexcept -> void {
using M = mtt::math::Matrix<T, first_dim, rest_dims...>;
using E = M::Element;
auto data = std::array<E, first_dim>{};
parse<JSON>::op<Opts>(data, args...);
v = M{std::span<E const>{data}};
}
};
template<typename T, mtt::usize first_dim, mtt::usize... rest_dims>
struct to<JSON, mtt::math::Matrix<T, first_dim, rest_dims...>> {
template<auto Opts>
auto static op(mtt::math::Matrix<T, first_dim, rest_dims...> const& v, auto&&... args) noexcept -> void {
using E = mtt::math::Matrix<T, first_dim, rest_dims...>::Element;
auto const& data = std::array<E, first_dim>(v);
serialize<JSON>::op<Opts>(data, args...);
}
};
对于enum, Glaze需要手动注册才能反射出各项名称, 如果后续添加c++26支持的话理论上是不需要的, 或者说实现类似magic_enum的功能, 两种方法都会方便很多.
template<>
struct glz::meta<mtt::color::Color_Space::Spectrum_Type> {
using enum mtt::color::Color_Space::Spectrum_Type;
auto constexpr static value = glz::enumerate(
albedo,
unbounded,
illuminant
);
};
对于需要多态的components, 例如不同的光源, metatron通过std::variant实现, 避免同时存储各个子component的数据. 每个子component最后的i32用于Glaze序列化时标记, 否则需要为所有类型手动注册glz::meta::value, 并为std::variant<Ts...>注册glz::meta::tag与glz::meta::ids.
struct Parallel_Light final {
ecs::Entity spectrum;
i32 parallel{0};
};
struct Point_Light final {
ecs::Entity spectrum;
i32 point{0};
};
struct Spot_Light final {
ecs::Entity spectrum;
f32 falloff_start_theta;
f32 falloff_end_theta;
i32 spot{0};
};
struct Environment_Light final {
ecs::Entity env_map;
i32 environment{0};
};
using Light = std::variant<
Parallel_Light,
Point_Light,
Spot_Light,
Environment_Light
>;
需要注意的是对于多重std::variant(std::variant<std::variant<Ts...>, std::varaint<Us...>>)Glaze是无法正确反序列化的, 原本metatron中有这种写法, 因为texture需要分为spectrum_texture与vector_texture, 后面还是展开到单个std::variant中, 通过实现stl::is_variant_alternative判断是否位于原本的内部std::variant中, 不再走std::visit来判断.
template<typename T, typename Variant>
struct is_variant_alternative : std::false_type {};
template<typename T, typename... Types>
struct is_variant_alternative<T, std::variant<Types...>>
: std::disjunction<std::is_same<T, Types>...> {};
template<typename T, typename Variant>
auto constexpr is_variant_alternative_v = is_variant_alternative<T, Variant>::value;
json序列化结构如下, 每个daemon会注册处理序列化与反序列化的函数, 通过type索引到对应的std::function.
{
"entity": "/divider/cloud",
"type": "divider",
"serialized": {
"shape": "/hierarchy/shape/bound",
"medium": "/hierarchy/medium/cloud",
"material": "/material/cloud"
}
}
注册的函数在ecs::Hierarchy中通过模板实现, 各个daemon提供类型即可, 类型名会通过宏MTT_SERDE(T)获取.
template<typename T>
auto static serde(std::string const& type) noexcept -> void {
auto sanitized_type = type;
std::ranges::transform(
sanitized_type,
sanitized_type.begin(),
::tolower
);
auto fr = [](ecs::Entity e, glz::raw_json const& s) -> void {
auto d = T{};
if (auto er = glz::read_json<T>(d, s.str); er) {
std::println("desrialize {} with glaze error: {}", s.str, glz::format_error(er));
std::abort();
} else {
ecs::Hierarchy::instance->attach(e, std::move(d));
}
};
auto fw = [sanitized_type]() -> std::vector<serde::json> {
auto v = std::vector<serde::json>{};
auto& r = ecs::Hierarchy::instance->registry;
for (auto e: r.view<T>()) {
auto s = glz::write_json(r.get<T>(e));
if (!s) {
std::println(
"failed to serialize component {} on {}",
sanitized_type, ecs::Hierarchy::instance->path(e)
);
std::abort();
}
v.emplace_back(e, sanitized_type, s.value());
}
return v;
};
ecs::Hierarchy::instance->enable(sanitized_type, fr, fw);
}