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;
}
