返回 Skill 列表
extension
分类: 开发与工程无需 API Key

component-workflow

根据已建立的架构模式指导新ECS组件的创建,包括组件类创建、JSON序列化支持、编辑器UI实现、依赖注入注册以及文档编写。在向引擎中添加新的组件类型或扩展现有组件功能时使用。

person作者: jakexiaohubgithub

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

  1. Component Creation Workflow
  2. Common Mistakes
  3. Performance Considerations

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 IComponentEditor interface from Editor.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 EditorUIConstants for 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:

  1. Build the project: dotnet build
  2. Launch the editor: cd Editor && dotnet run
  3. Create or open a scene
  4. Select an entity
  5. Click "Add Component" in the Properties panel
  6. Search for your component name - it should appear in the list
  7. 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:

  1. Component must be in Engine.Scene.Components namespace
  2. Component must implement IComponent interface (or be a recognized component type)
  3. Editor will automatically show "Add Component" button in Properties Panel
  4. 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