Create ECS component editors using the IComponentEditor interface, the ComponentEditorRegistry.DrawComponent wrapper, VectorPanel for vector properties, and UIPropertyRenderer for simple properties. Covers registering editors in a DI container and manual change-detection patterns to mark components dirty, trigger undo/redo, and minimize unnecessary updates.
Component editors render ECS component properties in the editor's Properties panel. They use the IComponentEditor interface with static utility methods for consistent UI styling.
// Editor/ComponentEditors/Core/IComponentEditor.cs
public interface IComponentEditor
{
void DrawComponent(Entity entity);
}
Key Points:
Entity, not the component directlyentity.GetComponent<T>()Every component editor uses this static wrapper method for consistent UI:
ComponentEditorRegistry.DrawComponent<ComponentType>("Display Name", entity, entity =>
{
var component = entity.GetComponent<ComponentType>();
// Draw property editors here
});
What it provides:
Implementation (ComponentEditorRegistry.cs:60-87):
ImGuiTreeNodeFlags for stylingBest for: Simple primitive properties (int, float, bool, string)
UIPropertyRenderer.DrawPropertyField("Label", currentValue,
newValue => component.Property = (TypeCast)newValue);
Features:
FieldEditorRegistryint, float, double, bool, string, Vector2, Vector3, Vector4Example (CameraComponentEditor.cs:22-23):
UIPropertyRenderer.DrawPropertyField("Primary", cameraComponent.Primary,
newValue => cameraComponent.Primary = (bool)newValue);
Best for: Vector properties needing axis color coding or reset buttons
var newPosition = component.Position;
VectorPanel.DrawVec3Control("Position", ref newPosition);
if (newPosition != component.Position)
component.Position = newPosition;
Features:
With Reset Value (TransformComponentEditor.cs:32):
var newScale = component.Scale;
VectorPanel.DrawVec3Control("Scale", ref newScale, resetValue: 1.0f);
if (newScale != component.Scale)
component.Scale = newScale;
var newSize = component.Size;
VectorPanel.DrawVec2Control("Size", ref newSize);
if (newSize != component.Size)
component.Size = newSize;
VectorPanel.cs methods:
DrawVec3Control(string label, ref Vector3 values, float resetValue = 0.0f)DrawVec2Control(string label, ref Vector2 values, float resetValue = 0.0f)Best for: Enum or string selection dropdowns
private static readonly string[] ProjectionTypeStrings = { "Perspective", "Orthographic" };
LayoutDrawer.DrawComboBox("Projection",
ProjectionTypeStrings[(int)camera.ProjectionType],
ProjectionTypeStrings,
selectedType =>
{
camera.ProjectionType = selectedType switch
{
"Perspective" => ProjectionType.Perspective,
"Orthographic" => ProjectionType.Orthographic,
_ => camera.ProjectionType
};
});
// CameraComponentEditor.cs (simplified)
using ECS;
using Editor.ComponentEditors.Core;
using Editor.UI.Drawers;
using Editor.UI.Elements;
using Engine.Scene.Components;
namespace Editor.ComponentEditors;
public class CameraComponentEditor : IComponentEditor
{
private static readonly string[] ProjectionTypeStrings = { "Perspective", "Orthographic" };
public void DrawComponent(Entity e)
{
ComponentEditorRegistry.DrawComponent<CameraComponent>("Camera", e, entity =>
{
var camera = entity.GetComponent<CameraComponent>().Camera;
UIPropertyRenderer.DrawPropertyField("Size", camera.OrthographicSize,
newValue => camera.OrthographicSize = (float)newValue);
UIPropertyRenderer.DrawPropertyField("Near", camera.OrthographicNear,
newValue => camera.OrthographicNear = (float)newValue);
UIPropertyRenderer.DrawPropertyField("Far", camera.OrthographicFar,
newValue => camera.OrthographicFar = (float)newValue);
});
}
}
// TransformComponentEditor.cs (actual implementation)
using ECS;
using Editor.ComponentEditors.Core;
using Engine.Math;
using Engine.Scene.Components;
namespace Editor.ComponentEditors;
public class TransformComponentEditor : IComponentEditor
{
public void DrawComponent(Entity e)
{
ComponentEditorRegistry.DrawComponent<TransformComponent>("Transform", e, entity =>
{
var tc = entity.GetComponent<TransformComponent>();
// Translation
var newTranslation = tc.Translation;
VectorPanel.DrawVec3Control("Translation", ref newTranslation);
if (newTranslation != tc.Translation)
tc.Translation = newTranslation;
// Rotation (convert radians to degrees for UI)
var rotationRadians = tc.Rotation;
Vector3 rotationDegrees = MathHelpers.ToDegrees(rotationRadians);
VectorPanel.DrawVec3Control("Rotation", ref rotationDegrees);
var newRotationRadians = MathHelpers.ToRadians(rotationDegrees);
if (newRotationRadians != tc.Rotation)
tc.Rotation = newRotationRadians;
// Scale (reset to 1.0 instead of 0.0)
var newScale = tc.Scale;
VectorPanel.DrawVec3Control("Scale", ref newScale, resetValue: 1.0f);
if (newScale != tc.Scale)
tc.Scale = newScale;
});
}
}
Key Pattern: Copy to temp variable → modify → check if changed → assign back
public class MyComponentEditor : IComponentEditor
{
public void DrawComponent(Entity e)
{
ComponentEditorRegistry.DrawComponent<MyComponent>("My Component", e, entity =>
{
var component = entity.GetComponent<MyComponent>();
// Simple properties with UIPropertyRenderer
UIPropertyRenderer.DrawPropertyField("Enabled", component.IsEnabled,
newValue => component.IsEnabled = (bool)newValue);
UIPropertyRenderer.DrawPropertyField("Speed", component.Speed,
newValue => component.Speed = (float)newValue);
// Vector with axis controls
var newPosition = component.Position;
VectorPanel.DrawVec3Control("Position", ref newPosition);
if (newPosition != component.Position)
component.Position = newPosition;
// Dropdown selection
string[] options = { "Option1", "Option2", "Option3" };
LayoutDrawer.DrawComboBox("Mode", options[component.ModeIndex], options,
selected =>
{
component.ModeIndex = Array.IndexOf(options, selected);
});
// Custom UI elements with Drawers
if (ButtonDrawer.DrawButton("Reset"))
{
component.Reset();
}
});
}
}
Component editors are registered in the DI container and injected into ComponentEditorRegistry.
// Register individual component editors
container.Register<TransformComponentEditor>(Reuse.Singleton);
container.Register<CameraComponentEditor>(Reuse.Singleton);
container.Register<MyComponentEditor>(Reuse.Singleton);
// ComponentEditorRegistry constructor receives all editors
container.Register<ComponentEditorRegistry>(Reuse.Singleton);
public class ComponentEditorRegistry(
TransformComponentEditor transformComponentEditor,
CameraComponentEditor cameraComponentEditor,
MyComponentEditor myComponentEditor) : IComponentEditorRegistry // Add your editor here
{
private readonly Dictionary<Type, IComponentEditor> _editors = new()
{
{ typeof(TransformComponent), transformComponentEditor },
{ typeof(CameraComponent), cameraComponentEditor },
{ typeof(MyComponent), myComponentEditor } // Register component type
};
public void DrawAllComponents(Entity entity)
{
foreach (var (componentType, editor) in _editors)
{
editor.DrawComponent(entity);
}
}
}
var oldValue = component.Position;
VectorPanel.DrawVec3Control("Position", ref oldValue);
if (oldValue != component.Position) // Value comparison
component.Position = oldValue;
Why: VectorPanel modifies the ref parameter directly, so we need manual change detection.
UIPropertyRenderer.DrawPropertyField("Speed", component.Speed,
newValue => component.Speed = (float)newValue);
Why: UIPropertyRenderer only calls callback if value changed. No manual check needed.
// ❌ WRONG - Manual tree node management
public void DrawComponent(Entity e)
{
if (ImGui.TreeNode("My Component"))
{
var component = e.GetComponent<MyComponent>();
// ... draw properties
ImGui.TreePop();
}
}
// ✅ CORRECT - Use DrawComponent wrapper
public void DrawComponent(Entity e)
{
ComponentEditorRegistry.DrawComponent<MyComponent>("My Component", e, entity =>
{
var component = entity.GetComponent<MyComponent>();
// ... draw properties
});
}
Why: DrawComponent provides consistent styling, remove button, and safety checks.
// ❌ WRONG - Raw ImGui calls
ImGui.DragFloat3("Position", ref component.Position);
// ✅ CORRECT - Use VectorPanel for axis colors and reset buttons
var newPosition = component.Position;
VectorPanel.DrawVec3Control("Position", ref newPosition);
if (newPosition != component.Position)
component.Position = newPosition;
Why: VectorPanel provides axis color coding, reset buttons, and consistent styling.
// ❌ WRONG - Modifying component property directly
VectorPanel.DrawVec3Control("Position", ref component.Position); // May not work as expected
// ✅ CORRECT - Copy to temp variable first
var newPosition = component.Position;
VectorPanel.DrawVec3Control("Position", ref newPosition);
if (newPosition != component.Position)
component.Position = newPosition;
Why: Component properties may be getters with backing fields or have change tracking.
// ❌ WRONG - Editor won't be found at runtime
public class MyComponentEditor : IComponentEditor { ... }
// (not registered in Program.cs)
// ✅ CORRECT - Register in DI container
container.Register<MyComponentEditor>(Reuse.Singleton);
// AND add to ComponentEditorRegistry constructor + dictionary
// Editor/ComponentEditors/MyComponentEditor.cs
using ECS;
using Editor.ComponentEditors.Core;
using Editor.Panels;
using Editor.UI.Drawers;
using Editor.UI.Elements;
using Engine.Scene.Components;
namespace Editor.ComponentEditors;
public class MyComponentEditor : IComponentEditor
{
public void DrawComponent(Entity e)
{
ComponentEditorRegistry.DrawComponent<MyComponent>("My Component", e, entity =>
{
var component = entity.GetComponent<MyComponent>();
// TODO: Add property editors here
});
}
}
// In editor startup
container.Register<MyComponentEditor>(Reuse.Singleton);
// ComponentEditorRegistry.cs - use primary constructor
public class ComponentEditorRegistry(
// ... existing editors
MyComponentEditor myComponentEditor) // Add parameter
{
private readonly Dictionary<Type, IComponentEditor> _editors = new()
{
// ... existing registrations
{ typeof(MyComponent), myComponentEditor } // Add to dictionary
};
}
Choose the appropriate method for each property type:
// Primitives: Use UIPropertyRenderer
UIPropertyRenderer.DrawPropertyField("Health", component.Health,
newValue => component.Health = (int)newValue);
// Vectors: Use VectorPanel
var newPos = component.Position;
VectorPanel.DrawVec3Control("Position", ref newPos);
if (newPos != component.Position)
component.Position = newPos;
// Enums: Use LayoutDrawer
string[] options = Enum.GetNames<MyEnum>();
LayoutDrawer.DrawComboBox("Mode", options[(int)component.Mode], options,
selected => component.Mode = (MyEnum)Array.IndexOf(options, selected));
DrawPropertyField(string label, object value, Action<object> onValueChanged)
DrawVec3Control(string label, ref Vector3 values, float resetValue = 0.0f)DrawVec2Control(string label, ref Vector2 values, float resetValue = 0.0f)DrawComboBox(string label, string current, string[] options, Action<string> onSelected)DrawButton(string label, Action? onClick = null, bool disabled = false) → returns boolDrawButton(string label, float width, float height, Action? onClick = null)DrawColoredButton(string label, MessageType type) → returns bool (use for Error/Warning/Success)DrawErrorText(string text)DrawWarningText(string text)DrawSuccessText(string text)RenderConfirmationModal(string id, ref bool show, string message, Action onConfirm)Component Editor Checklist:
IComponentEditor interfaceComponentEditorRegistry.DrawComponent<T>() wrapperVectorPanel for vectors (axis colors, reset buttons)UIPropertyRenderer for simple primitivesLayoutDrawer for dropdownsProgram.cs)ComponentEditorRegistry constructor + dictionaryKey Files:
Editor/ComponentEditors/Core/IComponentEditor.cs - InterfaceEditor/ComponentEditors/Core/ComponentEditorRegistry.cs - Registry and wrapperEditor/Panels/VectorPanel.cs - Vector controlsEditor/UI/Elements/UIPropertyRenderer.cs - Property field wrapperEditor/UI/Drawers/LayoutDrawer.cs - Combo boxesEditor/ComponentEditors/TransformComponentEditor.cs - Vector exampleEditor/ComponentEditors/CameraComponentEditor.cs - Mixed properties exampleSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer