From 6affc10cbd2e4f9dc043c3af3e1f94da7ba3cc85 Mon Sep 17 00:00:00 2001 From: Alex Hirsch Date: Sun, 25 Jun 2023 00:38:40 +0200 Subject: [PATCH] Add C++ static reflection (part 2) --- _posts/2023-11-10-CPP-Static-Reflection-1.md | 2 + _posts/2023-11-11-CPP-Static-Reflection-2.md | 296 ++++++++++++++++++ .../attributes.png | Bin 0 -> 5824 bytes 3 files changed, 298 insertions(+) create mode 100644 _posts/2023-11-11-CPP-Static-Reflection-2.md create mode 100644 assets/2023-11-11-CPP-Static-Reflection-2/attributes.png diff --git a/_posts/2023-11-10-CPP-Static-Reflection-1.md b/_posts/2023-11-10-CPP-Static-Reflection-1.md index 71debd1..7998dfa 100644 --- a/_posts/2023-11-10-CPP-Static-Reflection-1.md +++ b/_posts/2023-11-10-CPP-Static-Reflection-1.md @@ -359,3 +359,5 @@ We then followed that up with the first iteration of the ECS editor feature. In the upcoming parts, we'll introduce the **component registry** which will resolve the outstanding issue of having to list all components explicitly in `EcsEditor` — and everywhere else where we would need to iterate over all components. Furthermore, we will see how additional information can be attached to reflected fields using **attributes**. + +[Go to Part 2](../CPP-Static-Reflection-2) diff --git a/_posts/2023-11-11-CPP-Static-Reflection-2.md b/_posts/2023-11-11-CPP-Static-Reflection-2.md new file mode 100644 index 0000000..7a5cb3c --- /dev/null +++ b/_posts/2023-11-11-CPP-Static-Reflection-2.md @@ -0,0 +1,296 @@ +--- +title: C++ Static Reflection — Part 2 +date: 2023-11-11 +categories: [Programming] +tags: [programming,c++,reflection,ecs,ikaros] +author: alex +img_path: /assets/2023-11-11-CPP-Static-Reflection-2/ +--- + +In [part 1](../CPP-Static-Reflection-1) of this series we investigated how [refl-cpp](https://github.com/veselink1/refl-cpp) can be used to enable some form of static reflection in modern C++. +Along the way, we established a running example of integrating this reflection mechanism into a tiny game engine prototype. + +Now, in part 2, we will extend this integration even further. +Specifically, we will introduce the **component registry** and cover **attributes**. + +## Where We Left Off + +While putting together the first version of the EcsEditor, we discovered that [EnTT](https://github.com/skypjack/entt) (the entity component framework of the engine) does not allow us to simply iterate over all components associated with an entity. +Instead we have to iterate over all component types and check whether an entity possesses an instance of it. + +```c++ +ImGui::BeginChild("Entity", {windowSize.x * 0.7f, windowSize.y}); +if (m_selectedEntity) { + drawComponentEditor("Transform", scene, *m_selectedEntity); + drawComponentEditor("Model", scene, *m_selectedEntity); + drawComponentEditor("Spinner", scene, *m_selectedEntity); +} +ImGui::EndChild(); +``` + +Listing all components this way is undesirable as other subsystems utilizing our reflection mechanism would have to do the same, effectively violating the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). + +## The Component Registry + +We therefore establish a dedicated place, where all component types are registered. +This is referred to as the **component registry**, not to be confused with EnTT's registry (seen previously as part of the `Scene`). + +Let us first establish what data we want to store for each component type. + +```c++ +struct ComponentInfo { + etl::string<64> name; + int sortOrder = 0; + entt::id_type id; + bool hideInEditor = false; + + std::function addTo; + std::function removeFrom; + std::function isPresentIn; + std::function drawEditWidget; +}; +``` + +For each component type we store its name, id (using `entt::type_hash`), and some other metadata. +We also store operators for adding / removing the component to / from an entity, checking whether an entity has a component of this type, and drawing an edit widget. + +`entt::handle` combines an `entt::entity` (which is effectively just an id) with the corresponding `entt::registry`. +Components can be managed through this handle with ease. + +The `ComponentRegistry` itself is rather simple. +It stores instances of `ComponentInfo` for us to easily retrieve and use them. + +```c++ +class ComponentRegistry { + public: + ComponentRegistry(); + ComponentRegistry(const ComponentRegistry&) = delete; + ComponentRegistry& operator=(const ComponentRegistry&) = delete; + ComponentRegistry(ComponentRegistry&&) noexcept = delete; + ComponentRegistry& operator=(ComponentRegistry&&) noexcept = delete; + + template + void registerComponent(std::string_view name, int sortOrder = 0) + { + ComponentInfo info{ + .name = {name.data(), name.size()}, + .sortOrder = sortOrder, + .id = entt::type_hash(), + + .addTo = [](entt::handle entity) { entity.emplace_or_replace(); }, + .removeFrom = [](entt::handle entity) { entity.remove(); }, + .isPresentIn = [](entt::const_handle entity) { return entity.try_get(); }, + .drawEditWidget = [](editor::EditWidgetDrawer& draw, entt::handle entity) { + if (auto* component = entity.try_get()) { + draw(*component); + } + }, + }; + + if constexpr (refl::is_reflectable()) { + info.hideInEditor = has_attribute(refl::reflect()); + } + + addComponentInfo(info); + } + + const auto& components() const { return m_sortedInfos; } + const ComponentInfo* componentByID(entt::id_type) const; + const ComponentInfo* componentByName(std::string_view) const; + + private: + void addComponentInfo(const ComponentInfo&); + + static constexpr size_t MaxComponents = 32; + + etl::vector m_infos; + etl::vector m_sortedInfos; + etl::unordered_map m_lookupByID; + etl::unordered_map m_lookupByName; +}; +``` +{: file="ikaros_component_registry.hpp"} + +Upon registering a component, we fill in the fields for the corresponding `ComponentInfo` and store it. +In the code above we see a new thing we haven't looked at yet: `has_attribute`. +But more about this in a moment. + +> Since `etl::vector` is a fixed-sized array which doesn't use heap allocation, pointers / references to elements won't be invalidated upon adding elements. +{: .prompt-info } + +Registering a component is straightforward, we just have to call the `registerComponent` member function during engine initialization. +We commonly do this in the constructor of the corresponding system. +For instance, the `SpinnerSystem` registers the `SpinnerComponent` upon construction. + +```c++ +SpinnerSystem::SpinnerSystem(ComponentRegistry& cr) +{ + cr.registerComponent("Spinner"); +} +``` + +Given what we can already achieve during compile-time using modern C++ and libraries like refl-cpp, we could probably implement the component registry in a `constexpr` way, where all components are registered during compile time. +Even further, we might be able to iterate over them the same way we can iterate over refl-cpp `FieldDescriptor`s, effectively eliminating any runtime overhead. +However, there is no practical benefit to this at the moment, and the code would likely be more complex. + +### Where We Left Off, Again + +With the component registry established, the undesired code piece can now be replaced. + +```c++ +class EcsEditor { + public: + // ... + + void tick(Scene& scene) + { + // ... + ImGui::BeginChild("Entity", {windowSize.x * 0.7f, windowSize.y}); + if (m_selectedEntity) { + drawComponentEditor(scene.entityHandle(*m_selectedEntity)); + } + ImGui::EndChild(); + // ... + } + + private: + void drawComponentEditor(entt::entity_handle entity) const + { + EditWidgetDrawer drawer; + + for (const auto* component : componentRegistry.components()) { + if (component->hideInEditor || !component->isPresentIn(entity)) { + continue; + } + + if (ImGui::TreeNode(component->name.c_str())) { + if (component->drawEditWidget) { + component->drawEditWidget(drawer, entity); + } else { + ImGui::TextDisabled("No editWidget defined"); + } + ImGui::TreePop(); + } + } + } + + ComponentRegistry& m_componentRegistry; + + std::optional m_selectedEntity; +``` + +Using `componentRegistry.components()`, we can now iterate over all (registered) component types and check whether the given entity possesses such a component. +If so, we invoke the `drawEditWidget` operator with the `EditWidgetDrawer` instance. + +No more explicitly listing all components in various places! + +## Attributes + +Attributes offer a way of attaching additional information to a `FieldDescriptor`. + +```c++ +namespace ikaros::editor::attr { + +// Prevents the type, field, or property to show up in the editor. +struct Hidden : refl::attr::usage::type, + refl::attr::usage::field, + refl::attr::usage::function {}; + +// Uses a slider widget instead of the regular drag widget. +template +struct Slider : refl::attr::usage::field, + refl::attr::usage::function { + constexpr Slider(T min, T max) : min(min), max(max) {} + T min; + T max; +}; + +} // namespace ikaros::editor::attr +``` + +An attribute is just a type that may or may not contain some data. +By inheriting from types located in the `refl::attr::usage` namespace we define what it can be attached to. + +For instance, drawing `exposure` and `gamma` as sliders while hiding `effectIndex`. + +![Attributes Example](attributes.png) + +```c++ +REFL_TYPE(ikaros::PostProcessParams) +REFL_FIELD(exposure, ikaros::editor::attr::Slider(0.0f, 3.0f)) +REFL_FIELD(gamma, ikaros::editor::attr::Slider(0.0f, 5.0f)) +REFL_FIELD(effectIndex, ikaros::editor::attr::Hidden()) +REFL_END +``` + +Using relf-cpp's `has_attribute` and `get_attribute`, we can check whether the attribute is attached and retrieve it in order to access the attached data. +However, we need refl-cpp's descriptor for this. +Here's the corresponding code in the `EditWidgetDrawer`: + +```c++ +class EditWidgetDrawer { + public: + bool field(const char* name, bool& value) { return ImGui::Checkbox(name, &value); } + + template + bool field(ReflDescriptor member, const char* name, float& value) + { + if constexpr (has_attribute>(member)) { + auto attr = get_attribute>(member); + return ImGui::SliderFloat(name, &value, attr.min, attr.max); + } else { + return field(name, value); + } + } + + // ... + + template + bool operator()(T& object) + { + bool changed = false; + + if constexpr (refl::is_reflectable()) { + // Only consider members without the Hidden attribute. + auto members = filter(refl::reflect().members, [](auto member) { // + return !has_attribute(member); + }); + + auto fields = filter(members, [](auto member) { return is_field(member); }); + for_each(fields, [&](auto member) { + changed |= field(member, member.name.c_str(), member(object)); + // ↑ + // Passing the descriptor along to the field member function. + }); + } + + return changed; + } + + private: + // Fallback to silently accept all types that are not drawable. + template + bool field(const char*, T&) { return false; } + + // Fallback for fields that do not take advantage of reflection attributes. + template + bool field(ReflDescriptor, const char* label, T& object) + { + return field(label, object); + } +} +``` + +## What's Next? + +In this part we've extended our infrastructure by introducing the `ComponentRegistry`. +Thanks to this element, we now have a dedicated utility for managing meta information on components. +Iterating over all components attached to a given entity is still its primary purpose. + +We then looked into **attributes**, by which we can attach meta information to a type or to a specific member of a type. +Through this mechanism, semantic information is injected into the system, which allows for finer control in components that utilize the reflection mechanism. +For instance, drawing a slider widget with meaningful lower and upper bounds compared to just a plain numeric input field. + +Next, we augment the `EditWidgetDrawer` to be more robust in what objects are accepted / rejected. +Furthermore, we add the ability to customize how certain objects (commonly components) are drawn. +We may also look into how enums can be supported in a user-friendly manner, so look forward to part 3! diff --git a/assets/2023-11-11-CPP-Static-Reflection-2/attributes.png b/assets/2023-11-11-CPP-Static-Reflection-2/attributes.png new file mode 100644 index 0000000000000000000000000000000000000000..2779b545163b2b7ba0cd7d3d85f707ebe4ec85e2 GIT binary patch literal 5824 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 literal 0 HcmV?d00001