Guide the creation of new ECS components following established architectural patterns including component class creation, JSON serialization support, editor UI implementation, dependency injection...
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).
Invoke this skill when:
Location: Engine/Scene/Components/
Guidelines:
Naming Convention:
MyNewComponentAudioSourceComponent, TransformComponentExample 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);
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):
TileMapComponent, AnimationComponent)Register Converter (if custom):
Add to SceneSerializer or serialization configuration:
options.Converters.Add(new ParticleEmitterComponentConverter());
Location: Editor/ComponentEditors/
Guidelines:
IComponentEditor interface from Editor.ComponentEditors.CoreComponentEditorRegistry.DrawComponent<T>() helper for consistent UI framingEditorUIConstants for all UI dimensions and spacingExample 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);
});
}
}
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:
dotnet buildcd Editor && dotnet runDrawComponent() implementationIf the component doesn't appear or the UI doesn't render, check:
Program.cs (Step 4a)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:
Engine.Scene.Components namespaceIComponent interface (or be a recognized component type)Location: Engine/Scene/Systems/
When to Create a System:
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);
Avoid these frequent pitfalls when creating components:
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.
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).
[JsonIgnore] on Runtime DataProblem: 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
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 */ }