Component Workflow
Overview
This skill provides step-by-step guidance for adding new ECS components to the game engine, ensuring consistency with architectural patterns.
Current Architecture: Instance-based IComponentEditor with constructor injection (no static methods). Uses ComponentEditorRegistry.DrawComponent<T>() for UI framing and leverages UI infrastructure (UIPropertyRenderer, VectorPanel, LayoutDrawer, drag-drop targets).
When to Use
Invoke this skill when:
- Adding or extending ECS component types
- Implementing component serialization or custom JSON converters
- Creating component editors for the Properties panel
- Understanding ComponentEditorRegistry integration or UI infrastructure patterns
Table of Contents
Component Creation Workflow
Step 1: Create Component Class
Location: Engine/Scene/Components/
Guidelines:
- Components should be data-only classes (without logic, use systems for logic)
- Use properties for data fields
- Provide sensible defaults
- Keep components small and focused
- Prefer value types (structs/record structs) for small components
- Use reference types (classes/records) for larger components
- Matrix/transform calculations are acceptable in components
Naming Convention:
- Suffix with "Component":
MyNewComponent - Use PascalCase:
AudioSourceComponent,TransformComponent
Example Component:
namespace Engine.Scene.Components;
public class ParticleEmitterComponent
{
public int MaxParticles { get; set; } = 100;
public float EmissionRate { get; set; } = 10.0f;
public float ParticleLifetime { get; set; } = 2.0f;
public Vector4 StartColor { get; set; } = Vector4.One;
public Vector4 EndColor { get; set; } = new Vector4(1, 1, 1, 0);
public bool IsActive { get; set; } = true;
}
For Small Components (use record struct):
namespace Engine.Scene.Components;
public record struct VelocityComponent(Vector2 Velocity);
Step 2: Implement Serialization Support
Location: Engine/Scene/Serializer/ (if custom converter needed)
Standard Serialization (automatic):
Most components work with default JSON serialization. The SceneSerializer handles standard properties automatically.
Custom Serialization (when needed):
- Complex types (e.g.,
TileMapComponent,AnimationComponent) - Resource references (textures, audio clips)
- Specialized data structures
Register Converter (if custom):
Add to SceneSerializer or serialization configuration:
options.Converters.Add(new ParticleEmitterComponentConverter());
Step 3: Create Component Editor
Location: Editor/ComponentEditors/
Guidelines:
- Implement
IComponentEditorinterface fromEditor.ComponentEditors.Core - Use constructor injection for dependencies (services, UI elements)
- Use
ComponentEditorRegistry.DrawComponent<T>()helper for consistent UI framing - Leverage UI infrastructure for common patterns:
- UIPropertyRenderer - Automatic type-based rendering for primitives
- VectorPanel - Vector2/Vector3/Vector4 controls with X/Y/Z labels
- LayoutDrawer - Indentation, separators, spacing utilities
- ButtonDrawer - Consistent button styling (Primary, Secondary, Danger)
- Drag-Drop Targets - TextureDropTarget, AudioDropTarget, MeshDropTarget, ModelDropTarget, PrefabDropTarget
- Use
EditorUIConstantsfor all UI dimensions and spacing - Validate input ranges where appropriate
Example Component Editor (Basic):
namespace Editor.ComponentEditors;
using ECS;
using Editor.ComponentEditors.Core;
using Editor.UI.Drawers;
using Editor.UI.Elements;
using Engine.Scene.Components;
public class ParticleEmitterComponentEditor : IComponentEditor
{
public void DrawComponent(Entity entity)
{
ComponentEditorRegistry.DrawComponent<ParticleEmitterComponent>("Particle Emitter", entity, e =>
{
var component = e.GetComponent<ParticleEmitterComponent>();
// Use UIPropertyRenderer for automatic type-based rendering
UIPropertyRenderer.DrawPropertyField("Max Particles", component.MaxParticles,
newValue => component.MaxParticles = Math.Max(1, (int)newValue));
// Vector controls for colors
VectorPanel.DrawVec4Control("Start Color", ref component.StartColor);
VectorPanel.DrawVec4Control("End Color", ref component.EndColor);
});
}
}
Step 4: Register Editor in Dependency Injection
Location: Editor/Program.cs and Editor/ComponentEditors/Core/ComponentEditorRegistry.cs
All component editors must be registered to work with the ComponentEditorRegistry system.
Step 4a: Register Editor in Program.cs:
// In ConfigureServices method
container.Register<ParticleEmitterComponentEditor>(Reuse.Singleton);
If editor has dependencies, register those too:
// Dependencies are usually already registered, but verify:
container.Register<TextureDropTarget>(Reuse.Singleton);
container.Register<AudioDropTarget>(Reuse.Singleton);
// ... etc
Step 4b: Add Editor to ComponentEditorRegistry:
// In ComponentEditorRegistry.cs - use primary constructor
public class ComponentEditorRegistry(
// ... other existing editors
ParticleEmitterComponentEditor particleEmitterComponentEditor) // Add parameter
{
private readonly Dictionary<Type, IComponentEditor> _editors = new()
{
// ... other mappings
{ typeof(ParticleEmitterComponent), particleEmitterComponentEditor } // Add mapping
};
}
Verify Registration: After completing Steps 4a and 4b, verify your component editor is properly registered:
- Build the project:
dotnet build - Launch the editor:
cd Editor && dotnet run - Create or open a scene
- Select an entity
- Click "Add Component" in the Properties panel
- Search for your component name - it should appear in the list
- Add the component - the editor UI should render using your
DrawComponent()implementation
If the component doesn't appear or the UI doesn't render, check:
- DI registration in
Program.cs(Step 4a) - ComponentEditorRegistry constructor parameter and dictionary entry (Step 4b)
- Build succeeded without errors
Step 5: Component Addition via ComponentSelector
Location: Editor/UI/Elements/ComponentSelector.cs (automatic)
How Component Addition Works:
The ComponentSelector UI element automatically provides a searchable list of all available components. It's already integrated into the Properties Panel and Entity Context Menu.
No manual integration needed - components are added via reflection-based component discovery in the ComponentSelector.
To verify component is discoverable:
- Component must be in
Engine.Scene.Componentsnamespace - Component must implement
IComponentinterface (or be a recognized component type) - Editor will automatically show "Add Component" button in Properties Panel
- ComponentSelector will list your new component
Step 6: Create System (if needed)
Location: Engine/Scene/Systems/
When to Create a System:
- Component requires per-frame updates
- Component has logic that operates on entities
- Component needs to interact with rendering, physics, or other systems
Example System:
namespace Engine.Scene.Systems;
public class ParticleSystem : ISystem
{
// Priority ranges: 0-99 = early (input, physics), 100-199 = game logic,
// 200+ = rendering/post-processing. Lower values execute first.
// Common values: Physics=100, Game Logic=150, Rendering=200, UI=300
public int Priority => 150; // Execute before rendering
public void OnAttach(Scene scene) { }
public void OnDetach(Scene scene) { }
public void OnUpdate(Scene scene, TimeSpan deltaTime)
{
// Update particle logic here
}
public void OnEvent(Scene scene, Event e) { }
}
Register System:
// In SceneSystemRegistry.cs
public static void RegisterDefaultSystems(SystemManager systemManager, IServiceProvider services)
{
// ... existing systems
systemManager.AddSystem(services.GetRequiredService<ParticleSystem>());
}
// In Program.cs (Engine or Editor)
container.Register<ParticleSystem>(Reuse.Singleton);
Common Mistakes
Avoid these frequent pitfalls when creating components:
❌ Forgetting DI Registration
Problem: Component editor doesn't appear in Properties panel after adding component to entity.
Cause: Missing registration in Program.cs (Step 4a) or missing entry in ComponentEditorRegistry constructor (Step 4b).
Solution: Follow the verification checklist in Step 4. Check both registration points.
❌ Using Static Methods Instead of Instance Methods
Problem: Cannot inject dependencies, breaks DI pattern, makes testing difficult.
Solution: Use instance methods with constructor injection. Component editors must implement IComponentEditor interface with injected dependencies (e.g., TextureDropTarget, AudioDropTarget).
❌ Skipping [JsonIgnore] on Runtime Data
Problem: Runtime data (cached objects, computed values) gets serialized, bloating save files.
Solution: Mark runtime-only properties with [JsonIgnore] attribute.
public string AudioClipPath { get; set; } = string.Empty; // Serialized
[JsonIgnore] public AudioClip? LoadedClip { get; set; } // Runtime only
❌ Putting Game Logic in Components
Problem: Violates ECS architecture. Components are data-only. Solution: Move logic to Systems. Components store data, Systems process behavior.
// Component: Data only (properties + defaults)
public class HealthComponent { public float Health { get; set; } = 100f; }
// System: Logic (damage processing, death handling, etc.)
public class HealthSystem : ISystem { /* OnUpdate handles logic */ }
Performance Considerations
- Avoid allocations: Don't create objects in component properties
- Keep components small: Large components hurt cache coherency
- Use value types: When component is small (<= 16 bytes)
- Minimize references: Each reference is a pointer chase
- Group related data: Components accessed together should be similar size
Scan to join WeChat group