Example: Custom Debug Window
A native addon that creates a debug window showing scene objects, selected object transform data, with a configurable menu path.
Overview
This example demonstrates: - Creating dockable ImGui windows - Accessing scene data in real-time - Getting the currently selected object - Displaying transform data (position, rotation, scale) - Custom menu path configuration - Refreshing data live during gameplay
Files
package.json
{
"name": "Custom Debug Window",
"author": "Polyphase Examples",
"description": "Debug window showing scene hierarchy and selected object transforms.",
"version": "1.0.0",
"tags": ["editor", "debug", "tools"],
"native": {
"target": "editor",
"sourceDir": "Source",
"binaryName": "debugwindow",
"apiVersion": 2
}
}
Source/CustomDebugWindow.cpp
/**
* @file CustomDebugWindow.cpp
* @brief Debug window for viewing scene objects and transform data.
*
* This example shows:
* - Creating custom ImGui windows in the editor
* - Using direct engine API calls (GetWorld, GetNumWorlds, etc.)
* - Accessing time via GetDeltaTime/GetElapsedTime
* - Proper include paths for all dependencies
*/
#include "Plugins/PolyphasePluginAPI.h"
#include "Plugins/PolyphaseEngineAPI.h"
#if EDITOR
#include "Plugins/EditorUIHooks.h"
// ImGui is in External/Imgui
#include "imgui.h"
#endif
// GLM is in External/glm
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include <vector>
#include <string>
static PolyphaseEngineAPI* sEngineAPI = nullptr;
static uint64_t sHookId = 0;
#if EDITOR
//=============================================================================
// Configuration
//=============================================================================
// Configurable menu path
static char sMenuPath[64] = "Tools";
static char sMenuItemName[64] = "Scene Debug Window";
static bool sWindowOpen = false;
// Display options
static bool sShowPosition = true;
static bool sShowRotation = true;
static bool sShowScale = true;
static bool sShowWorldTransform = false;
static bool sAutoRefresh = true;
static float sRefreshRate = 0.1f; // Seconds between refreshes
static float sRefreshTimer = 0.0f;
//=============================================================================
// Mock Data Structures (Replace with actual engine types)
//=============================================================================
struct Vec3
{
float x, y, z;
};
struct Transform
{
Vec3 position;
Vec3 rotation; // Euler angles in degrees
Vec3 scale;
};
struct SceneObject
{
std::string name;
int id;
int parentId;
bool isSelected;
Transform localTransform;
Transform worldTransform;
std::vector<int> childIds;
};
// Mock scene data (in a real implementation, query the engine)
static std::vector<SceneObject> sSceneObjects;
static int sSelectedObjectId = -1;
//=============================================================================
// Scene Data Access (Replace with actual engine calls)
//=============================================================================
static void RefreshSceneData()
{
// In a real implementation, you would query the engine:
// World* world = Engine::GetWorld(0);
// Node* root = world->GetRootNode();
// Traverse and populate sSceneObjects
// For this example, we'll use mock data
sSceneObjects.clear();
// Create some mock objects
SceneObject root;
root.name = "World Root";
root.id = 0;
root.parentId = -1;
root.isSelected = (sSelectedObjectId == 0);
root.localTransform = {{0, 0, 0}, {0, 0, 0}, {1, 1, 1}};
root.worldTransform = root.localTransform;
root.childIds = {1, 2, 3};
sSceneObjects.push_back(root);
SceneObject player;
player.name = "Player";
player.id = 1;
player.parentId = 0;
player.isSelected = (sSelectedObjectId == 1);
player.localTransform = {{5.5f, 0.0f, -3.2f}, {0, 45, 0}, {1, 1, 1}};
player.worldTransform = player.localTransform;
player.childIds = {4, 5};
sSceneObjects.push_back(player);
SceneObject camera;
camera.name = "Main Camera";
camera.id = 2;
camera.parentId = 0;
camera.isSelected = (sSelectedObjectId == 2);
camera.localTransform = {{0, 10, -15}, {30, 0, 0}, {1, 1, 1}};
camera.worldTransform = camera.localTransform;
sSceneObjects.push_back(camera);
SceneObject light;
light.name = "Directional Light";
light.id = 3;
light.parentId = 0;
light.isSelected = (sSelectedObjectId == 3);
light.localTransform = {{0, 20, 0}, {50, -30, 0}, {1, 1, 1}};
light.worldTransform = light.localTransform;
sSceneObjects.push_back(light);
SceneObject playerModel;
playerModel.name = "PlayerModel";
playerModel.id = 4;
playerModel.parentId = 1;
playerModel.isSelected = (sSelectedObjectId == 4);
playerModel.localTransform = {{0, 0, 0}, {0, 0, 0}, {1, 1, 1}};
playerModel.worldTransform = {{5.5f, 0.0f, -3.2f}, {0, 45, 0}, {1, 1, 1}};
sSceneObjects.push_back(playerModel);
SceneObject playerWeapon;
playerWeapon.name = "Weapon";
playerWeapon.id = 5;
playerWeapon.parentId = 1;
playerWeapon.isSelected = (sSelectedObjectId == 5);
playerWeapon.localTransform = {{0.5f, 0.8f, 0.2f}, {0, 0, -15}, {0.5f, 0.5f, 0.5f}};
playerWeapon.worldTransform = {{6.0f, 0.8f, -3.0f}, {0, 45, -15}, {0.5f, 0.5f, 0.5f}};
sSceneObjects.push_back(playerWeapon);
}
static SceneObject* GetObjectById(int id)
{
for (auto& obj : sSceneObjects)
{
if (obj.id == id) return &obj;
}
return nullptr;
}
static SceneObject* GetSelectedObject()
{
return GetObjectById(sSelectedObjectId);
}
//=============================================================================
// UI Drawing
//=============================================================================
static void DrawVec3(const char* label, const Vec3& v, ImVec4 color = ImVec4(1,1,1,1))
{
ImGui::TextColored(color, "%s: (%.2f, %.2f, %.2f)", label, v.x, v.y, v.z);
}
static void DrawTransform(const char* header, const Transform& t, bool isWorld = false)
{
ImVec4 headerColor = isWorld ? ImVec4(0.4f, 0.8f, 0.4f, 1.0f) : ImVec4(0.4f, 0.6f, 1.0f, 1.0f);
if (ImGui::CollapsingHeader(header, ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::Indent();
if (sShowPosition)
{
DrawVec3("Position", t.position, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
}
if (sShowRotation)
{
DrawVec3("Rotation", t.rotation, ImVec4(0.4f, 1.0f, 0.4f, 1.0f));
}
if (sShowScale)
{
DrawVec3("Scale", t.scale, ImVec4(0.4f, 0.4f, 1.0f, 1.0f));
}
ImGui::Unindent();
}
}
static void DrawSceneHierarchy(int objectId, int depth = 0)
{
SceneObject* obj = GetObjectById(objectId);
if (!obj) return;
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (obj->childIds.empty())
{
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (obj->isSelected)
{
flags |= ImGuiTreeNodeFlags_Selected;
}
// Indent based on depth
bool isOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj->id, flags, "%s", obj->name.c_str());
// Handle selection
if (ImGui::IsItemClicked())
{
sSelectedObjectId = obj->id;
// In real implementation: EditorState::SetSelectedNode(obj->node);
}
if (isOpen)
{
for (int childId : obj->childIds)
{
DrawSceneHierarchy(childId, depth + 1);
}
ImGui::TreePop();
}
}
static void DrawDebugWindow(void* userData)
{
// Update timer for auto-refresh
if (sAutoRefresh)
{
sRefreshTimer -= ImGui::GetIO().DeltaTime;
if (sRefreshTimer <= 0.0f)
{
RefreshSceneData();
sRefreshTimer = sRefreshRate;
}
}
// Toolbar
if (ImGui::Button("Refresh"))
{
RefreshSceneData();
}
ImGui::SameLine();
ImGui::Checkbox("Auto", &sAutoRefresh);
if (sAutoRefresh)
{
ImGui::SameLine();
ImGui::SetNextItemWidth(60);
ImGui::DragFloat("##Rate", &sRefreshRate, 0.01f, 0.01f, 1.0f, "%.2fs");
}
ImGui::SameLine();
ImGui::TextDisabled("(?)");
if (ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::Text("Scene Debug Window");
ImGui::Text("- Shows all objects in the current scene");
ImGui::Text("- Displays transform data for selected object");
ImGui::Text("- Updates in real-time during gameplay");
ImGui::EndTooltip();
}
ImGui::Separator();
// Split view
float windowHeight = ImGui::GetContentRegionAvail().y;
// Top: Scene Hierarchy
ImGui::BeginChild("SceneHierarchy", ImVec2(0, windowHeight * 0.5f), true);
ImGui::Text("Scene Hierarchy (%zu objects)", sSceneObjects.size());
ImGui::Separator();
// Draw root objects
for (const auto& obj : sSceneObjects)
{
if (obj.parentId == -1)
{
DrawSceneHierarchy(obj.id);
}
}
ImGui::EndChild();
// Bottom: Selected Object Details
ImGui::BeginChild("SelectedDetails", ImVec2(0, 0), true);
SceneObject* selected = GetSelectedObject();
if (selected)
{
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Selected: %s", selected->name.c_str());
ImGui::Text("ID: %d | Parent ID: %d", selected->id, selected->parentId);
ImGui::Separator();
// Display options
if (ImGui::CollapsingHeader("Display Options"))
{
ImGui::Checkbox("Show Position", &sShowPosition);
ImGui::SameLine();
ImGui::Checkbox("Show Rotation", &sShowRotation);
ImGui::SameLine();
ImGui::Checkbox("Show Scale", &sShowScale);
ImGui::Checkbox("Show World Transform", &sShowWorldTransform);
}
// Local Transform
DrawTransform("Local Transform", selected->localTransform, false);
// World Transform
if (sShowWorldTransform)
{
DrawTransform("World Transform", selected->worldTransform, true);
}
// Quick actions
ImGui::Spacing();
if (ImGui::Button("Reset Position"))
{
// selected->localTransform.position = {0, 0, 0};
if (sEngineAPI) sEngineAPI->LogDebug("Reset position");
}
ImGui::SameLine();
if (ImGui::Button("Reset Rotation"))
{
// selected->localTransform.rotation = {0, 0, 0};
if (sEngineAPI) sEngineAPI->LogDebug("Reset rotation");
}
ImGui::SameLine();
if (ImGui::Button("Reset Scale"))
{
// selected->localTransform.scale = {1, 1, 1};
if (sEngineAPI) sEngineAPI->LogDebug("Reset scale");
}
// Copy to clipboard
ImGui::Spacing();
if (ImGui::Button("Copy Transform to Clipboard"))
{
char buf[256];
snprintf(buf, sizeof(buf),
"Position: (%.3f, %.3f, %.3f)\nRotation: (%.3f, %.3f, %.3f)\nScale: (%.3f, %.3f, %.3f)",
selected->localTransform.position.x, selected->localTransform.position.y, selected->localTransform.position.z,
selected->localTransform.rotation.x, selected->localTransform.rotation.y, selected->localTransform.rotation.z,
selected->localTransform.scale.x, selected->localTransform.scale.y, selected->localTransform.scale.z);
ImGui::SetClipboardText(buf);
if (sEngineAPI) sEngineAPI->LogDebug("Transform copied to clipboard");
}
}
else
{
ImGui::TextDisabled("No object selected");
ImGui::TextDisabled("Click an object in the hierarchy above");
}
ImGui::EndChild();
}
//=============================================================================
// Settings Window
//=============================================================================
static bool sSettingsOpen = false;
static void DrawSettingsWindow(void* userData)
{
ImGui::Text("Debug Window Settings");
ImGui::Separator();
ImGui::Text("Menu Configuration");
ImGui::InputText("Menu Path", sMenuPath, sizeof(sMenuPath));
ImGui::InputText("Menu Item Name", sMenuItemName, sizeof(sMenuItemName));
ImGui::TextDisabled("Note: Restart required for menu changes");
ImGui::Spacing();
ImGui::Separator();
ImGui::Text("Default Display Options");
ImGui::Checkbox("Show Position by Default", &sShowPosition);
ImGui::Checkbox("Show Rotation by Default", &sShowRotation);
ImGui::Checkbox("Show Scale by Default", &sShowScale);
ImGui::Checkbox("Show World Transform by Default", &sShowWorldTransform);
ImGui::Spacing();
ImGui::Text("Refresh Settings");
ImGui::Checkbox("Auto Refresh by Default", &sAutoRefresh);
ImGui::DragFloat("Default Refresh Rate", &sRefreshRate, 0.01f, 0.01f, 1.0f, "%.2f seconds");
}
//=============================================================================
// Menu Callbacks
//=============================================================================
static void OnOpenDebugWindow(void* userData)
{
sWindowOpen = true;
RefreshSceneData();
}
static void OnOpenSettings(void* userData)
{
sSettingsOpen = true;
}
//=============================================================================
// Plugin Callbacks
//=============================================================================
static void RegisterEditorUI(EditorUIHooks* hooks, uint64_t hookId)
{
sHookId = hookId;
// Register the main debug window
hooks->RegisterWindow(hookId, "Scene Debug", "debugwindow_main", DrawDebugWindow, nullptr);
// Register the settings window
hooks->RegisterWindow(hookId, "Debug Window Settings", "debugwindow_settings", DrawSettingsWindow, nullptr);
// Add menu items (using configurable path)
hooks->AddMenuItem(hookId, sMenuPath, sMenuItemName, OnOpenDebugWindow, nullptr, "Ctrl+Shift+D");
hooks->AddMenuItem(hookId, sMenuPath, "Debug Window Settings...", OnOpenSettings, nullptr, nullptr);
// Initialize scene data
RefreshSceneData();
if (sEngineAPI)
{
sEngineAPI->LogDebug("CustomDebugWindow: Registered UI hooks");
}
}
#endif // EDITOR
static int OnLoad(PolyphaseEngineAPI* api)
{
sEngineAPI = api;
api->LogDebug("CustomDebugWindow addon loaded!");
return 0;
}
static void OnUnload()
{
if (sEngineAPI)
{
sEngineAPI->LogDebug("CustomDebugWindow addon unloaded.");
}
sEngineAPI = nullptr;
}
//=============================================================================
// Plugin Entry Point
//=============================================================================
extern "C" OCTAVE_PLUGIN_API int PolyphasePlugin_GetDesc(PolyphasePluginDesc* desc)
{
desc->apiVersion = OCTAVE_PLUGIN_API_VERSION;
desc->pluginName = "Custom Debug Window";
desc->pluginVersion = "1.0.0";
desc->OnLoad = OnLoad;
desc->OnUnload = OnUnload;
desc->Tick = nullptr; // Editor-only addon, no gameplay tick
desc->TickEditor = nullptr; // Window updates via ImGui draw callback
desc->RegisterTypes = nullptr;
desc->RegisterScriptFuncs = nullptr;
#if EDITOR
desc->RegisterEditorUI = RegisterEditorUI;
#else
desc->RegisterEditorUI = nullptr;
#endif
return 0;
}
Features
Scene Hierarchy View
- Tree view of all objects in the scene
- Click to select objects
- Expand/collapse to see child objects
- Selected object is highlighted
Transform Display
- Position (X, Y, Z) in red
- Rotation (Euler angles) in green
- Scale (X, Y, Z) in blue
- Toggle between local and world space
Auto-Refresh
- Configurable refresh rate
- Real-time updates during Play mode
- Manual refresh button available
Quick Actions
- Reset position/rotation/scale
- Copy transform to clipboard
- Configurable menu location
Usage
- Open the debug window via Tools > Scene Debug Window (or Ctrl+Shift+D)
- Click objects in the hierarchy to select them
- View transform data in the bottom panel
- Enable "Auto" for real-time updates during gameplay
- Configure settings via Tools > Debug Window Settings...
Customization
Changing the Menu Location
Edit the sMenuPath variable to change where the menu item appears:
static char sMenuPath[64] = "Tools"; // Default
// Change to:
static char sMenuPath[64] = "View"; // Appears under View menu
static char sMenuPath[64] = "My Tools"; // Custom top-level menu
Adding More Data
Extend the DrawDebugWindow function to show additional information:
// Show node type
ImGui::Text("Type: %s", selected->typeName.c_str());
// Show component count
ImGui::Text("Components: %d", selected->componentCount);
// Show visibility
ImGui::Checkbox("Visible", &selected->isVisible);