xiaohongshu
xiaohongshu
editor-component-editors
Create ECS component editors using IComponentEditor interface, ComponentEditorRegistry.DrawComponent wrapper, VectorPanel for vectors, and UIPropertyRenderer for simple properties. Covers registration in DI container and manual change detection patterns.
Full skill instructions
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", ButtonDrawer.ButtonType.Primary))
{
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, ButtonType type = ButtonType.Default) → returns boolDrawButton(string label, float width, float height, Action? onClick = null)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 examplexiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience