Description
While working on a student game called “Trouble in Toy Town”, I implemented a reflection system in C++ that allows us to know information about types and objects at run-time. Having reflection in our engine provided our team with many benefits, such as generic serialization and deserialization as well as a generic inspection of entities using our editor. The following piece of code shows the implementation of a recursive function that iterates through an entity’s components and exposes every property to the user through the editor. This made it extremely easy for anyone in our team to add variables to the editor for use, which improved our iteration and testing times significantly.
Output
Code Sample
/******************************************************************************/ /* Author: Alejandro Hitti Brief: This recursive function is part of an editor system built for a student game project called "Trouble in Toy Town". The function itself inspects the selected entity and exposes to the editor all its components along with all properties associated with the entity. This functionality is possible to achieve in a single recursive function thanks to the reflection system written in C++ by me that gives us run-time information about any object in our project that is being reflected. It will recursively inspect each member until it reaches a special base case or until it reaches a Plain-Old Data type (POD) member. All content © 2014-2015 DigiPen (USA) Corporation, all rights reserved. */ /******************************************************************************/ /* * Initial call to the recursive inspection function. It will get the current * selected entity and inspect every property recursively using the reflection * system. This function kickstarts the recursive call. */ void InspectorModule::Inspect(Core::Entity* entity) { // Get all components from the entity const std::unordered_map<std::string, Core::Component*>* components = entity->GetAllComponents(); // Checks if component is empty bool isComponentsEmpty = true; // Loops through every component type in the entity for (int i = 0; i < Core::NumOfComponents; ++i) { if (components[i].size() != 0) isComponentsEmpty = false; // Loop through every individual components for (auto iter = components[i].begin(); iter != components[i].end(); ++iter) { auto& componentPair = *iter; // Start the tree node with he component name if (ImGui::TreeNode(componentPair.first.c_str())) { // Get reflection information (Type, data and members) const Reflex::TypeInfo* compInfo = componentPair.second->GetTypeInfo(); Reflex::Variant compVar = componentPair.second; void *compData = compVar.GetData(); std::vector<Reflex::Member> compMembers = compInfo->GetAllMembers(); // For every member in the current component for (unsigned i = 0; i < compMembers.size(); ++i) { // Get reflection information (Type of each member and offset) const Reflex::Member* mem = &compMembers[i]; const Reflex::TypeInfo* memInfo = mem->GetType(); void* offsetData = PTR_ADD(compData, mem->GetOffset()); // Checks if the type is a special base case if (IsBaseCase(memInfo)) InspectPod(Reflex::Variant(memInfo, offsetData), mem, entity); // Recursively inspect every member until it's a base case or a POD else { // Start a tree node with the name of the member if (ImGui::TreeNode(mem->GetName())) { // Kickstart a recursive call to get members of members InspectRec(Reflex::Variant(memInfo, offsetData), entity); // Pop member name tree node ImGui::TreePop(); } SetHelpTooltip("Open to see members."); } } // Creates a button to remove the current component from the entity std::string remCompText = "Remove " + componentPair.first + " Component"; if (ImGui::Button(remCompText.c_str())) { auto otherIter = iter; ++iter; entity->RemoveComponent(entity->GetComponentNamed((*otherIter).second->GetType(), (*otherIter).first)); continue; } SetHelpTooltip("Removes the component from the entity."); // Pop component name tree node ImGui::TreePop(); } SetHelpTooltip("Open to see members."); } } // If the entity has no components, display a message if (isComponentsEmpty) { ImGui::Text("No components found on this entity."); SetHelpTooltip("Open the options menu to add components to the entity."); } } /* * Recursive function called from Inspect. Continually inspects each member in * the entity until it reaches a base case or a POD. */ void InspectorModule::InspectRec(Reflex::Variant var, Core::Entity* entity) { // Get reflection information (Type, members, data) const Reflex::TypeInfo* info = var.GetTypeInfo(); std::vector<Reflex::Member> members = info->GetAllMembers(); void* data = var.GetData(); // For every member in the type for (unsigned i = 0; i < members.size(); ++i) { // Get reflection information (Current member, member type and member data) const Reflex::Member* mem = &members[i]; const Reflex::TypeInfo* memInfo = mem->GetType(); void* offsetData = PTR_ADD(data, mem->GetOffset()); // Checks if the type is a special base case if (IsBaseCase(memInfo)) InspectPod(Reflex::Variant(memInfo, offsetData), mem, entity); // Recursively inspect every member until it's a base case or a POD else { // Start a tree node with the name of the member if (ImGui::TreeNode(mem->GetName())) { // Kickstart a recursive call to get members of members InspectRec(Reflex::Variant(memInfo, offsetData), entity); // Pop member name tree node ImGui::TreePop(); } SetHelpTooltip("Open to see members."); } } } /* * Handles the inspection of members that need to be treated as base cases * to achieve some special behavior that is not standard. Needs to be defined * in the IsBaseCase function below. */ void InspectorModule::InspectPod(Reflex::Variant var, const Reflex::Member* mem, const Core::Entity* entity) { // Get reflection information (Type, data) const Reflex::TypeInfo* info = var.GetTypeInfo(); void* data = var.GetData(); // Deal with all POD cases if (info == TYPE_STR("double")) ImGui::InputDouble(mem->GetName(), (double*)data, 0.1, 0.5); else if (info == TYPE_STR("float")) ImGui::InputFloat(mem->GetName(), (float*)data, 0.1f, 0.5f); else if (info == TYPE_STR("int")) ImGui::InputInt(mem->GetName(), (int*)data); else if (info == TYPE_STR("unsigned")) ImGui::InputInt(mem->GetName(), (int*)data); else if (info == TYPE_STR("bool")) ImGui::Checkbox(mem->GetName(), (bool*)data); else if (info == TYPE_STR("std::string")) { std::strcpy(m_TextBuffer, reinterpret_cast<std::string*>(data)->c_str()); if (ImGui::InputText(mem->GetName(), m_TextBuffer, 256, ImGuiInputTextFlags_::ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_::ImGuiInputTextFlags_EnterReturnsTrue)) { *reinterpret_cast<std::string*>(data) = m_TextBuffer; } } // // Special cases (Specified in the IsBaseCase function below) // // Displays Vector3s in a way that is easier to read else if (info == TYPE_STR("Vector3")) { float* vec = reinterpret_cast<float*>(data); ImGui::InputFloat3(mem->GetName(), vec, 2); } // Special case to display a combo box of textures, read from the file directory else if (info == TYPE_STR("Texture")) { // Gets all texture names std::string* texName = reinterpret_cast<std::string*>(data); std::vector<String> texList = Utilities::Filesystem::GetFilepathsInDirectory("./textures/"); static int index = 0; std::string comboNames = "; for (unsigned i = 0; i < texList.size(); ++i) { // Erases the directory from the name texList[i].erase(0, texList[i].find_last_of("/") + 1); // Makes the string of textures separated by 0 (To comply with ImGUI) comboNames += texList[i] + '\0'; // Starts the combo box at the currently selected texture if (*texName == texList[i]) index = i; } // Sets the texture. Loads it into memory if it's not there yet. if (ImGui::Combo(std::string("Texures##" + std::to_string(*(int*)entity)).c_str(), &index, comboNames.c_str(), 12)) { // Load the texture if it cannot be found if (EDITOR->GetGFX()->GetTexture(texList[index]) == EZ::Handle::Null()) EDITOR->GetGFX()->LoadTexture(std::string("./textures/" + texList[index])); // Set the texture in the graphics component entity->GetComponentAs<Graphics3DComponent>(Core::Graphics3D)->SetTextureName(texList[index]); } } // Special case for Model3D to display a combo box with all models in the file directory else if (info == TYPE_STR("Model3D")) { // Gets all model names std::string* modelName = reinterpret_cast<std::string*>(data); std::vector<std::string> modelList = Utilities::Filesystem::GetFilepathsInDirectory("./models/"); static int index = 0; std::string comboNames = "; for (unsigned i = 0; i < modelList.size(); ++i) { // Erases the directory part modelList[i].erase(0, modelList[i].find_last_of("/") + 1); // Makes the string of models separated by 0 (To comply with ImGUI) comboNames += modelList[i] + '\0'; // Starts the combo box at the currently selected model if (*modelName == modelList[i]) index = i; } // Sets the model. Loads it into memory if it's not there yet. if (ImGui::Combo(std::string("Models##" + std::to_string(*(int*)entity)).c_str(), &index, comboNames.c_str())) { // Load if the model cannot be found if (EDITOR->GetGFX()->GetModel(modelList[index]) == nullptr) EDITOR->GetGFX()->LoadModel(std::string("./models/" + modelList[index])); // Set the new model entity->GetComponentAs<Graphics3DComponent>(Core::Graphics3D)->SetModel(modelList[index]); } } // Position needs to be set after modifying the values so that it updates physics correctly else if (info == TYPE_STR("Position")) { // Gets every value from the data void* float* vec = reinterpret_cast<float*>(data); vec[0] = entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->GetPosition().X; vec[1] = entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->GetPosition().Y; vec[2] = entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->GetPosition().Z; // Show the values and set the new position in the transform component ImGui::InputFloat3(mem->GetName(), vec, 2); Math::Vector3 newPos = Math::Vector3(vec[0], vec[1], vec[2]); entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->SetPosition(newPos); } // Quaternion internals are different than its member variables. The easiest way to se it up // is with Pitch, Yaw and Roll. We get those values from the user and create the quaternion // from it. else if (info == TYPE_STR("Rotation")) { // Gets current rotation static Math::Vector3 eulerAngles = entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->GetOrientation().GetEuler(); ImGui::PushItemWidth(150); // Gets the Pitch euler angle ImGui::PushID(0); ImGui::InputFloat(", &eulerAngles.X, 1, 10, 2); ImGui::PopID(); ImGui::SameLine(); ImGui::PushAllowKeyboardFocus(false); ImGui::SliderFloat("Pitch", &eulerAngles.X, 0.0f, 360.0f, "%.2f"); ImGui::PopAllowKeyboardFocus(); // Gets the Yaw euler angle ImGui::PushID(1); ImGui::InputFloat(", &eulerAngles.Y, 1, 10, 2); ImGui::SameLine(); ImGui::PopID(); ImGui::PushAllowKeyboardFocus(false); ImGui::SliderFloat("Yaw", &eulerAngles.Y, 0.0f, 360.0f, "%.2f"); ImGui::PopAllowKeyboardFocus(); // Gets the Roll euler angle ImGui::PushID(2); ImGui::InputFloat(", &eulerAngles.Z, 1, 10, 2); ImGui::SameLine(); ImGui::PopID(); ImGui::PushAllowKeyboardFocus(false); ImGui::SliderFloat("Roll", &eulerAngles.Z, 0.0f, 360.0f, "%.2f"); ImGui::PopAllowKeyboardFocus(); ImGui::PopItemWidth(); // Sets the rotation by generating a new quaternion and passing it to the transform component Math::Quaternion newQuaternion(eulerAngles.X, eulerAngles.Y, eulerAngles.Z); entity->GetComponentAs<Core::TransformComponent>(Core::Transform)->SetOrientation(newQuaternion); } // Gets color information as RGBA else if (info == TYPE_STR("Color")) { ImGui::ColorEdit4(info->GetName(), reinterpret_cast<float*>(data)); } // Used to determine if a certain POD case hasn't been dealt with else { ImGui::Text(mem->GetName()); ImGui::SameLine(); ImGui::Text(info->GetName()); } } /* * Function that checks if a specific type should be treated as a special base * case to achieve specific behaviour that is not standard. */ bool InspectorModule::IsBaseCase(const Reflex::TypeInfo* info) { if (info->IsPOD() || info == TYPE_STR("Vector3") || info == TYPE_STR("Texture") || info == TYPE_STR("Position") || info == TYPE_STR("Rotation") || info == TYPE_STR("Model3D") || info == TYPE_STR("Color")) return true; else return false; }