Last active
February 17, 2025 08:36
-
-
Save JuanDiegoMontoya/f6002350a9f5e64c962ee52d7e879922 to your computer and use it in GitHub Desktop.
Shrimple example of reflection with entt meta, plus a lil' ImGui thingamabob to draw all components.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "glm/glm.hpp" | |
#include "entt/meta/meta.hpp" | |
#include <unordered_map> | |
// Components that we want to reflect for a hypothetical game. | |
struct LocalTransform | |
{ | |
glm::vec3 position; | |
glm::quat rotation; | |
float scale; | |
}; | |
struct Health | |
{ | |
float hp; | |
float maxHp; | |
}; | |
// Traits for reflection. | |
// entt only supports up to 16 bits of trait storage. | |
// entt can also reflect base classes, so empty base classes could be used as traits instead of these. | |
enum Traits : uint16_t | |
{ | |
EDITOR = 1 << 0, // Draw this component/data member in the editor | |
SERIALIZE = 1 << 1, // Does not contain derived data | |
NETWORKED = 1 << 2, // idk, just an idea | |
}; | |
// The data structure we'll use to store additional properties about our types and their members. | |
// entt::hashed_string implicitly converts to entt::id_type and entt::meta_any and store anything, making this very flexible. | |
using PropertiesMap = std::unordered_map<entt::id_type, entt::meta_any>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "a_components_and_reflection.h" | |
#include "entt/meta/factory.hpp" | |
#include "imgui.h" | |
// `properties` contains the flexible properties of this data member or component. | |
// Ideally this would go in c_gui.cpp, but I wanted all the reflection registration in one place for this example. | |
static bool DrawEditorFloat(float& f, const PropertiesMap& properties) | |
{ | |
const char* label = "float"; | |
float min = 0; | |
float max = 1; | |
// This is kinda hideous, but it works. | |
if (auto it = properties.find("name"_hs); it != properties.end()) | |
{ | |
// Note that these casts will return nullptr if you supply the wrong type in the property map. | |
label = *it->second.try_cast<const char*>(); | |
} | |
if (auto it = properties.find("min"_hs); it != properties.end()) | |
{ | |
min = *it->second.try_cast<float>(); | |
} | |
if (auto it = properties.find("max"_hs); it != properties.end()) | |
{ | |
max = *it->second.try_cast<float>(); | |
} | |
return ImGui::SliderFloat(label, &f, min, max); | |
} | |
// These look very similar to the above function. I'm not writing them all out to save space. | |
static bool DrawEditorVec3(glm::vec3& v, const PropertiesMap& properties); | |
static bool DrawEditorQuat(glm::quat& q, const PropertiesMap& properties); | |
void InitializeReflectionInfo() | |
{ | |
// Reset existing reflection info. | |
entt::meta_reset(); | |
// Register 'leaf' types that we are using and inform the reflection system that we have a function that we want to use for drawing them in the editor. | |
entt::meta<float>().func<&DrawEditorFloat>("DrawEditor"_hs); | |
entt::meta<glm::vec3>().func<&DrawEditorVec3>("DrawEditor"_hs); | |
entt::meta<glm::quat>().func<&DrawEditorQuat>("DrawEditor"_hs); | |
// Register components. | |
entt::meta<LocalTransform>() | |
// Hashed strings are used to give them unique identifiers. A macro could be used... | |
// entt::as_ref_t means meta_any objects will not copy the data, which allows our DrawEditor* functions to work as they take a reference and modify it. | |
.data<&LocalTransform::position, entt::as_ref_t>("position"_hs) | |
// The properties map is used to register the label name of the field in the editor as well as things like the min and max for a slider. | |
.custom<PropertiesMap>(PropertiesMap{{"name"_hs, "position"}}) | |
.traits(Traits::EDITOR) | |
.data<&LocalTransform::rotation, entt::as_ref_t>("rotation"_hs) | |
.custom<PropertiesMap>(PropertiesMap{{"name"_hs, "rotation"}}) | |
.traits(Traits::EDITOR) | |
.data<&LocalTransform::scale, entt::as_ref_t>("scale"_hs) | |
.custom<PropertiesMap>(PropertiesMap{{"name"_hs, "scale"}}) | |
.traits(Traits::EDITOR); | |
entt::meta<Health>() | |
.data<&Health::hp, entt::as_ref_t>("hp"_hs) | |
.custom<PropertiesMap>(PropertiesMap{{"name"_hs, "hp"}, {"min"_hs, 0.0f}, {"max"_hs, 100.0f}}) | |
.traits(Traits::EDITOR) | |
.data<&Health::maxHp, entt::as_ref_t>("maxHp"_hs) | |
.custom<PropertiesMap>(PropertiesMap{{"name"_hs, "maxHp"}, {"min"_hs, 0.0f}, {"max"_hs, 100.0f}}) | |
.traits(Traits::EDITOR); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "a_components_and_reflection.h" | |
#include "entt/meta/factory.hpp" | |
#include "entt/entity/registry.hpp" | |
#include "imgui.h" | |
static void DrawComponentHelper(entt::meta_any instance, entt::meta_custom custom, int& guiId); | |
void DrawEntityEditor(entt::registry registry) | |
{ | |
if (ImGui::Begin("Entities")) | |
{ | |
// Iterate over all entities. | |
for (auto entity : registry.view<entt::entity>()) | |
{ | |
if (ImGui::TreeNode("entity", "%u (v%u)", entt::to_entity(e), entt::to_version(e))) | |
{ | |
ImGui::PushID((int)entity); // Don't worry about the potential UB.. | |
// Iterate over all components in the registry. | |
for (int i = 0; auto&& [id, storage] : registry.storage()) | |
{ | |
if (!storage.contains(e)) | |
{ | |
continue; | |
} | |
// The name of the component is helpfully already stored in the registry without our intervention. | |
ImGui::SeparatorText(std::string(storage.type().name()).c_str()); | |
if (auto meta = entt::resolve(id)) | |
{ | |
DrawComponentHelper(meta.from_void(storage.value(e)), meta.custom(), i); | |
} | |
} | |
ImGui::PopID(); | |
ImGui::TreePop(); | |
} | |
} | |
} | |
ImGui::End(); | |
} | |
static void DrawComponentHelper(entt::meta_any instance, entt::meta_custom custom, int& guiId) | |
{ | |
auto meta = instance.type(); | |
// If the type has a bespoke DrawEditor function, use that. Otherwise, recurse over data members. | |
// Currently, there is no behavior if the type/member has no DrawEditor function or any registered data members. | |
if (auto func = meta.func("DrawEditor"_hs)) | |
{ | |
PropertiesMap map = {}; | |
if (auto* mp = static_cast<const PropertiesMap*>(custom)) | |
{ | |
map = *mp; | |
} | |
func.invoke(instance, map); | |
} | |
else | |
{ | |
for (auto [id, data] : meta.data()) | |
{ | |
if (data.traits<Traits>() & Traits::EDITOR) | |
{ | |
ImGui::PushID(guiId++); | |
DrawComponentHelper(data.get(instance), data.custom(), guiId); | |
ImGui::PopID(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment