feat: add reflection

This commit is contained in:
2026-10-30 17:09:53 +01:00
parent 0fbc023018
commit 08acd84404
10 changed files with 763 additions and 110 deletions
+23
View File
@@ -4,6 +4,7 @@
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/Reflect.h>
Component::Component(GameObject& pParent, const std::string& name)
: Object(name),
@@ -50,6 +51,28 @@ void Component::FixedUpdate(float fixedDt) {
}
void Component::ImGuiInspector() {
InspectorDrawer drawer(m_ParentGameObjectPtr != nullptr ? m_ParentGameObjectPtr->GetScene() : nullptr);
Reflect(drawer);
if (drawer.Changed()) {
OnFieldsEdited();
}
}
// Reflect also loads, so it can't be const, but the writer only reads the fields
nlohmann::json Component::Serialize() const {
JsonWriter writer;
const_cast<Component*>(this)->Reflect(writer);
return std::move(writer.Result());
}
void Component::Deserialize(const nlohmann::json& data) {
JsonReader reader(data);
Reflect(reader);
}
void Component::ResolveReferences(const ObjectMap& objects) {
ReferenceResolver resolver(*this, objects);
Reflect(resolver);
}
void Component::ImGuiRender() {
+292
View File
@@ -0,0 +1,292 @@
#include <destrum/Serialization/Reflect.h>
#include <algorithm>
#include <cctype>
#include <imgui.h>
#include <spdlog/spdlog.h>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] ImGuiDataType ToImGui(reflect_detail::Scalar type)
{
switch (type) {
case reflect_detail::Scalar::S8: return ImGuiDataType_S8;
case reflect_detail::Scalar::U8: return ImGuiDataType_U8;
case reflect_detail::Scalar::S16: return ImGuiDataType_S16;
case reflect_detail::Scalar::U16: return ImGuiDataType_U16;
case reflect_detail::Scalar::S32: return ImGuiDataType_S32;
case reflect_detail::Scalar::U32: return ImGuiDataType_U32;
case reflect_detail::Scalar::S64: return ImGuiDataType_S64;
case reflect_detail::Scalar::U64: return ImGuiDataType_U64;
case reflect_detail::Scalar::Float: return ImGuiDataType_Float;
case reflect_detail::Scalar::Double: return ImGuiDataType_Double;
}
return ImGuiDataType_Float;
}
[[nodiscard]] ImGuiSliderFlags ClampFlags(const FieldOptions& options)
{
return options.min || options.max ? ImGuiSliderFlags_AlwaysClamp : ImGuiSliderFlags_None;
}
template <int N>
bool DrawFloats(const char* label, float* value, const FieldOptions& options)
{
if constexpr (N == 3) {
if (options.color) {
return ImGui::ColorEdit3(label, value);
}
} else if constexpr (N == 4) {
if (options.color) {
return ImGui::ColorEdit4(label, value);
}
}
const float min = options.min ? static_cast<float>(*options.min) : 0.0f;
const float max = options.max ? static_cast<float>(*options.max) : 0.0f;
return reflect_detail::DrawScalar(label, reflect_detail::Scalar::Float, value, N,
options.min ? &min : nullptr, options.max ? &max : nullptr, options);
}
[[nodiscard]] std::string ObjectLabel(const ObjectRef& ref, const Scene& scene)
{
const GameObject* object = ref.Get(scene);
return object != nullptr ? object->GetName() : "Missing object " + std::to_string(ref.GetId());
}
}
namespace reflect_detail {
bool DrawScalar(const char* label, Scalar type, void* value, int components, const void* min, const void* max, const FieldOptions& options)
{
return ImGui::DragScalarN(label, ToImGui(type), value, components, options.speed, min, max, options.format, ClampFlags(options));
}
bool DrawEnum(const char* label, std::int64_t& value, const FieldOptions& options)
{
if (options.enumNames.empty()) {
return ImGui::InputScalar(label, ImGuiDataType_S64, &value);
}
const auto count = static_cast<std::int64_t>(options.enumNames.size());
const char* preview = value >= 0 && value < count ? options.enumNames[static_cast<std::size_t>(value)] : "?";
bool changed = false;
if (ImGui::BeginCombo(label, preview)) {
for (std::int64_t i = 0; i < count; ++i) {
if (ImGui::Selectable(options.enumNames[static_cast<std::size_t>(i)], i == value)) {
value = i;
changed = true;
}
}
ImGui::EndCombo();
}
return changed;
}
void BeginList(const char* label)
{
ImGui::SeparatorText(label);
}
bool ListRemoveButton(std::size_t index)
{
ImGui::PushID(static_cast<int>(index));
const bool clicked = ImGui::SmallButton("x");
ImGui::SameLine();
return clicked;
}
void EndListRow()
{
ImGui::PopID();
}
bool ListAddButton()
{
return ImGui::SmallButton("+");
}
std::string PrettifyFieldName(std::string_view key)
{
std::string label;
label.reserve(key.size() + 4);
for (std::size_t i = 0; i < key.size(); ++i) {
const auto c = static_cast<unsigned char>(key[i]);
if (c == '_') {
label += ' ';
continue;
}
const bool wordStart = i > 0 && std::isupper(c) && !std::isupper(static_cast<unsigned char>(key[i - 1])) && key[i - 1] != '_';
if (wordStart) {
label += ' ';
}
label += static_cast<char>(label.empty() || label.back() == ' ' ? std::toupper(c) : c);
}
return label;
}
}
bool DrawField(const char* label, bool& value, const FieldOptions&, const Scene*)
{
return ImGui::Checkbox(label, &value);
}
bool DrawField(const char* label, std::string& value, const FieldOptions&, const Scene*)
{
return ImGuiUtils::InputString(label, value);
}
bool DrawField(const char* label, glm::vec2& value, const FieldOptions& options, const Scene*)
{
return DrawFloats<2>(label, &value.x, options);
}
bool DrawField(const char* label, glm::vec3& value, const FieldOptions& options, const Scene*)
{
return DrawFloats<3>(label, &value.x, options);
}
bool DrawField(const char* label, glm::vec4& value, const FieldOptions& options, const Scene*)
{
return DrawFloats<4>(label, &value.x, options);
}
bool DrawField(const char* label, ObjectRef& value, const FieldOptions& options, const Scene* scene)
{
if (scene == nullptr) {
ImGui::TextDisabled("%s: not in a scene", label);
return false;
}
return ImGuiUtils::DrawObjectRefPicker(label, value, *scene, options.filter);
}
bool DrawField(const char* label, std::vector<ObjectRef>& value, const FieldOptions& options, const Scene* scene)
{
reflect_detail::BeginList(label);
if (scene == nullptr) {
ImGui::TextDisabled("Not in a scene");
return false;
}
bool changed = false;
std::optional<std::size_t> removeIndex;
for (std::size_t i = 0; i < value.size(); ++i) {
if (reflect_detail::ListRemoveButton(i)) {
removeIndex = i;
}
if (value[i].Get(*scene) != nullptr) {
ImGui::TextUnformatted(ObjectLabel(value[i], *scene).c_str());
} else {
ImGui::TextDisabled("%s", ObjectLabel(value[i], *scene).c_str());
}
reflect_detail::EndListRow();
}
if (removeIndex) {
value.erase(value.begin() + static_cast<std::ptrdiff_t>(*removeIndex));
changed = true;
}
const auto canAdd = [&](const GameObject& object) {
return !object.IsBeingDestroyed() && std::ranges::find(value, ObjectRef(object)) == value.end() &&
(!options.filter || options.filter(object));
};
if (ImGui::BeginCombo("##Add", "Add...")) {
for (const auto& object : scene->GetObjects()) {
if (object == nullptr || !canAdd(*object)) {
continue;
}
ImGui::PushID(static_cast<int>(object->GetId()));
if (ImGui::Selectable(object->GetName().c_str())) {
value.emplace_back(*object);
changed = true;
}
ImGui::PopID();
}
ImGui::EndCombo();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
const GameObject* dropped = scene->FindGameObject(*static_cast<const ObjectId*>(payload->Data));
if (dropped != nullptr && canAdd(*dropped)) {
value.emplace_back(*dropped);
changed = true;
}
}
ImGui::EndDragDropTarget();
}
return changed;
}
void ResolveField(ObjectRef& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
{
const ObjectId fileId = value.GetId();
if (!value.Resolve(objects)) {
dropped.push_back(fileId);
}
}
void ResolveField(std::vector<ObjectRef>& value, const ObjectRef::ObjectMap& objects, std::vector<ObjectId>& dropped)
{
std::erase_if(value, [&](ObjectRef& ref) {
const ObjectId fileId = ref.GetId();
if (!ref.Resolve(objects)) {
dropped.push_back(fileId);
return true;
}
return !ref.IsSet();
});
}
void JsonWriter::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
{
if (!options.transient) {
ops.save(value, m_Result[std::string{key}], options);
}
}
void JsonReader::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
{
if (options.transient || !m_Data.is_object()) {
return;
}
const auto it = m_Data.find(std::string{key});
if (it == m_Data.end()) {
return;
}
try {
ops.load(value, *it, options);
} catch (const std::exception& exception) {
throw std::runtime_error("\"" + std::string{key} + "\": " + exception.what());
}
}
void ReferenceResolver::Visit(std::string_view key, void* value, const FieldOptions&, const FieldOps& ops)
{
if (ops.resolve == nullptr) {
return;
}
std::vector<ObjectId> dropped;
ops.resolve(value, m_Objects, dropped);
const GameObject* owner = m_Owner.GetGameObject();
for (const ObjectId id : dropped) {
spdlog::warn("{} on {}: {} references object {} which is not in the scene file, dropping it",
m_Owner.GetTypeName(), owner != nullptr ? owner->GetName() : "?", key, id);
}
}
void InspectorDrawer::Visit(std::string_view key, void* value, const FieldOptions& options, const FieldOps& ops)
{
if (options.hidden) {
return;
}
const std::string label = options.label != nullptr ? options.label : reflect_detail::PrettifyFieldName(key);
ImGui::PushID(key.data(), key.data() + key.size());
ImGui::BeginDisabled(options.readOnly);
m_Changed |= ops.draw(label.c_str(), value, options, m_Scene);
ImGui::EndDisabled();
ImGui::PopID();
}