Skip to content

Instantly share code, notes, and snippets.

@JuanDiegoMontoya
Last active February 17, 2025 08:36
Show Gist options
  • Save JuanDiegoMontoya/f6002350a9f5e64c962ee52d7e879922 to your computer and use it in GitHub Desktop.
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.
#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>;
#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);
}
#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