Entity Inspection

Page
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

Inspector

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

[top]

Advertisements