AgentSkillsCN

engine-system

为这款引擎开发全新的游戏系统。当用户提出添加游戏逻辑、行为机制、AI 算法、移动系统,或需要编写任何用于处理实体的代码时,可选用此技能。系统承载核心逻辑,而组件则负责存储和管理各类数据。

SKILL.md
--- frontmatter
name: engine-system
description: Create new game systems for this engine. Use when the user asks to add gameplay logic, behaviors, AI, movement systems, or any code that processes entities. Systems contain logic; components store data.

Creating Engine Systems

This skill guides creation of ECS systems that process entities with specific components.

System Architecture

Systems query for entities with required components and update them each frame. They implement the System interface and optionally SystemWithRender for custom rendering.

System Interface

go
type System interface {
    Update(world donburi.World, scene *scene.Scene, dt float32)
    Mode() SystemMode
    Name() string
}

// Optional interfaces:
type SystemWithInit interface {
    System
    Init(world donburi.World)
}

type SystemWithRender interface {
    System
    Render(phase RenderPhase, world donburi.World, scene *scene.Scene)
}

type SystemWithCleanup interface {
    System
    Cleanup(world donburi.World)
}

System Modes

ModeWhen it runs
SystemModeEditEditor edit mode only
SystemModePlayEditor play mode only
SystemModeGameStandalone game only
SystemModeAlwaysAll modes
SystemModeRuntimePlay + Game (most common)

Basic System Template

Location: game/system/<name>.go

go
package system

import (
    gameComponent "game/component"
    engineComponent "github.com/TheLazyLemur/engine/component"
    engineScene "github.com/TheLazyLemur/engine/scene"
    sys "github.com/TheLazyLemur/engine/system"

    "github.com/yohamta/donburi"
    "github.com/yohamta/donburi/filter"
)

type <Name>System struct{}

func (s *<Name>System) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    query := donburi.NewQuery(
        filter.Contains(
            engineComponent.Transform,
            gameComponent.<ComponentName>,
        ),
    )

    for entry := range query.Iter(world) {
        transform := engineComponent.Transform.Get(entry)
        myComp := gameComponent.<ComponentName>.Get(entry)

        // Your logic here...

        // CRITICAL: Mark dirty after modifying transform
        transform.MarkDirty()
    }
}

func (s *<Name>System) Mode() sys.SystemMode {
    return sys.SystemModeRuntime
}

func (s *<Name>System) Name() string {
    return "<Name>"
}

Register the System

In game/systems.go:

go
func RegisterSystems(e *engine.Engine) {
    // ... existing systems ...
    e.RegisterSystem(&gameSystem.<Name>System{})
}

Common Patterns

Input Handling System

go
func (s *PlayerSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    query := donburi.NewQuery(filter.Contains(
        engineComponent.Transform,
        gameComponent.Player,
    ))

    for entry := range query.Iter(world) {
        transform := engineComponent.Transform.Get(entry)
        player := gameComponent.Player.Get(entry)

        moveDir := math.Vector3Zero()

        if rl.IsKeyDown(rl.KeyW) {
            moveDir.Z -= 1
        }
        if rl.IsKeyDown(rl.KeyS) {
            moveDir.Z += 1
        }
        if rl.IsKeyDown(rl.KeyA) {
            moveDir.X -= 1
        }
        if rl.IsKeyDown(rl.KeyD) {
            moveDir.X += 1
        }

        if moveDir.LengthSquared() > 0 {
            moveDir = moveDir.Normalize()
            transform.Position = transform.Position.Add(
                moveDir.Scale(player.MoveSpeed * dt),
            )
            transform.MarkDirty()
        }
    }
}

System with Initialization

go
type SpawnerSystem struct {
    spawnTimer float32
}

func (s *SpawnerSystem) Init(world donburi.World) {
    s.spawnTimer = 0
}

func (s *SpawnerSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    s.spawnTimer += dt
    if s.spawnTimer >= 2.0 {
        s.spawnTimer = 0
        // Spawn logic...
    }
}

System with Gizmo Rendering

go
func (s *DebugSystem) Render(phase sys.RenderPhase, world donburi.World, scene *engineScene.Scene) {
    if phase != sys.RenderPhaseGizmos {
        return
    }

    query := donburi.NewQuery(filter.Contains(
        engineComponent.Transform,
        gameComponent.Waypoint,
    ))

    for entry := range query.Iter(world) {
        transform := engineComponent.Transform.Get(entry)
        pos := transform.WorldMatrix.ExtractPosition()

        // Draw debug sphere at waypoint
        rl.DrawSphereWires(pos.ToRaylib(), 0.5, 8, 8, rl.Green)
    }
}

Entity Creation in System

go
func (s *BulletSpawnerSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    if rl.IsKeyPressed(rl.KeySpace) {
        // Use scene builder API
        bulletId := scene.Spawn("Bullet").
            At(startPos).
            Scale(math.NewVector3(0.1, 0.1, 0.5)).
            Mesh("cube", "yellow").
            With(gameComponent.Bullet, gameComponent.BulletData{
                Speed:     50.0,
                Direction: forward,
                Lifetime:  3.0,
            }).
            Build()
    }
}

Collision Response System

go
func (s *DamageSystem) Update(world donburi.World, scene *engineScene.Scene, dt float32) {
    query := donburi.NewQuery(filter.Contains(
        engineComponent.Transform,
        engineComponent.Colliding,  // Has collision data
        gameComponent.Health,
    ))

    for entry := range query.Iter(world) {
        colliding := engineComponent.Colliding.Get(entry)
        health := gameComponent.Health.Get(entry)

        for _, contact := range colliding.Contacts {
            if contact.State == component.CollisionStateEnter {
                otherEntry := world.Entry(donburi.Entity(contact.OtherEntity))
                if otherEntry.HasComponent(gameComponent.Damage) {
                    dmg := gameComponent.Damage.Get(otherEntry)
                    health.Current -= dmg.Amount
                }
            }
        }
    }
}

Query Patterns

Multiple Required Components

go
query := donburi.NewQuery(filter.Contains(
    engineComponent.Transform,
    gameComponent.Movement,
    gameComponent.Health,
))

Optional Components

go
for entry := range query.Iter(world) {
    // Required
    transform := engineComponent.Transform.Get(entry)

    // Optional check
    if entry.HasComponent(gameComponent.Boost) {
        boost := gameComponent.Boost.Get(entry)
        // Apply boost...
    }
}

Exclude Filter

go
query := donburi.NewQuery(filter.And(
    filter.Contains(engineComponent.Transform),
    filter.Not(filter.Contains(gameComponent.Dead)),
))

Update Order

Engine runs systems in this order:

  1. Your registered systems (in registration order)
  2. CollisionSystem (detects collisions, creates Colliding components)
  3. TransformSystem (recomputes world matrices from dirty flags)

Important Rules

  1. Mark Dirty: Call transform.MarkDirty() after modifying position/rotation/scale
  2. Query Once: Create query at start of Update, iterate once
  3. System Order: Systems that create entities should register before systems that query them
  4. Collision Timing: Colliding component data reflects LAST frame's collisions
  5. Render Phases: Check phase in Render() before drawing
  6. Donburi Entity Creation: world.Create() requires at least one component type argument
    go
    // WRONG - panics: "entity must have at least one component"
    entity := world.Create()
    
    // CORRECT
    entity := world.Create(engineComponent.Name)
    entry := world.Entry(entity)
    entry.AddComponent(MyComponent)
    

Render Phases

PhaseUse CaseInside 3D?
RenderPhaseShadowCustom shadow castersYes
RenderPhaseGeometryCustom 3D renderingYes
RenderPhaseGizmosDebug visualization (lines, spheres)Yes
RenderPhaseOverlay2D UI, selection boxes, HUDNo (screen space)
RenderPhaseEditorUIImGui panels (editor only)No (ImGui)

Gizmos vs Overlay Phase

go
func (s *MySystem) Render(phase sys.RenderPhase, world donburi.World, scene *engineScene.Scene) {
    // 3D debug visualization - inside 3D camera view
    if phase == sys.RenderPhaseGizmos {
        query := donburi.NewQuery(filter.Contains(
            engineComponent.Transform,
            gameComponent.MyComponent,
        ))
        for entry := range query.Iter(world) {
            transform := engineComponent.Transform.Get(entry)
            pos := transform.WorldMatrix.ExtractPosition()
            rl.DrawSphereWires(pos.ToRaylib(), 1.0, 8, 8, rl.Green)
        }
    }

    // 2D overlay - draws to viewport texture (not screen)
    if phase == sys.RenderPhaseOverlay {
        // IMPORTANT: Overlay draws to render texture coordinates (1920x1080)
        // In editor mode, viewport may be offset by panels
        // Use SetViewportRectCallback to get viewport bounds
    }
}

Viewport-Aware 2D Rendering (Overlay Phase)

In editor mode, the viewport is offset by panels. To draw correctly:

  1. Engine callback approach (recommended for games):
go
// In main.go
selSys := &SelectionSystem{}
eng.RegisterSystem(selSys)
eng.SetViewportRectCallback(selSys.SetViewportRect)

// In SelectionSystem
type SelectionSystem struct {
    viewportX, viewportY, viewportWidth, viewportHeight float32
}

func (s *SelectionSystem) SetViewportRect(x, y, width, height float32) {
    s.viewportX = x
    s.viewportY = y
    s.viewportWidth = width
    s.viewportHeight = height
}

func (s *SelectionSystem) Render(phase sys.RenderPhase, ...) {
    if phase == sys.RenderPhaseOverlay && box.IsActive {
        // Convert screen coordinates to render texture coordinates
        startX := (float32(box.StartX) - s.viewportX) * (1920.0 / s.viewportWidth)
        startY := (float32(box.StartY) - s.viewportY) * (1080.0 / s.viewportHeight)
        // ... draw at startX, startY
    }
}
  1. Alternative: game mode only - Run with --mode=game where viewport fills screen

Example: Complete Rotation System

See existing implementation at:

  • System: game/system/rotation.go
  • Registration: game/systems.go