Skip to content

Latest commit

 

History

History
313 lines (238 loc) · 9.29 KB

File metadata and controls

313 lines (238 loc) · 9.29 KB

Agent Guidelines for Game Engine Development

This document provides guidelines for AI agents working on this raylib-based Go game engine.

Core Principles

When Facing Issues

  1. Read raylib source code for C API patterns
  2. Read raylib-go source code for Go bindings
  3. Try to reproduce minimal versions of the issue
  4. Check for dependency updates that may help

Communication Style

  • Be concise - fit answers in 1-4 sentences
  • Never use emojis unless specifically requested
  • Add only necessary comments to generated code
  • Never create/update documentation unless requested

Repository Structure

engine-workspace/
├── go.work                  # Workspace file (for local development)
├── engine/                  # Engine module (github.com/TheLazyLemur/engine)
│   ├── component/           # Core components (Transform, Camera, RenderMesh, Name)
│   ├── core/                # Engine, systems, registries
│   ├── editor/              # Editor UI and state
│   ├── scene/               # Scene API and serialization
│   ├── system/              # System interface
│   ├── math/                # 3D math library
│   ├── viewport/            # Viewport rendering
│   └── config/              # Engine configuration
├── game/                    # Game module (imports engine)
│   ├── cmd/game/main.go     # Game entry point
│   ├── component/           # Game components
│   ├── system/              # Game systems
│   ├── systems.go           # RegisterSystems() integration point
│   └── assets/              # Game assets
└── rtsgame/                 # Another game module using the engine

ECS Architecture

Components

Components are data containers attached to entities. They should NOT contain logic.

Pattern: Define data struct → Create ComponentType variable → Register with engine → Use in systems

// component/selectable.go
package component

type SelectableData struct {
    Selectable      bool    `inspector:"Selectable"`
    MinSelectRadius float32 `inspector:"Min Selection Radius,min:0.1,max:5,step:0.1"`
}

func NewSelectable() SelectableData {
    return SelectableData{
        Selectable:      true,
        MinSelectRadius: 0.5,
    }
}

// component/components.go
var Selectable = donburi.NewComponentType[SelectableData]()

Serialization Rules:

  • Use json:"-" to exclude from serialization (runtime-only state)
  • Inspector tags control editor UI display
  • Components with json:"-" fields are not persisted

Transforms

Mutating Transforms:

transform, _ := scene.GetTransform(entityId)
transform.Position = transform.Position.Add(delta)
scene.SetTransform(entityId, *transform)  // CRITICAL: triggers dirty flag

Read-only access (distance checks, etc.) does NOT need SetTransform.

Matrix Rules (Unity-style)

Critical: Raylib's MatrixMultiply(left, right) swaps parameters and returns right × left. Code order is reversed from math order!

  1. Local matrix = T × R × S (Scale first, Translation last, never scaled)

    scaleMatrix.Multiply(rotateMatrix).Multiply(translateMatrix)  // Code order reversed!

    This produces T × R × S mathematically.

  2. World matrix = Parent × Local

    transform.LocalMatrix.Multiply(parentWorldMatrix)  // Code order reversed!

    Never swap these - breaks parent/child movement.

Hierarchy

  • Scene.SetParent(child, parent) - pass parent=0 to detach
  • Scene.DestroyEntity(id) - recursively destroys children

Systems

Systems query for entities with specific components and update them each frame.

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

System Modes:

  • SystemModeEdit - Editor edit mode only
  • SystemModePlay - Editor play mode only
  • SystemModeGame - Standalone game only
  • SystemModeRuntime - Play + Game (most common)
  • SystemModeAlways - All modes

Entity Creation

Critical: world.Create() requires at least one component type argument.

// 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)

Rendering

Five-Pass Pipeline

  1. Shadow Pass - Opaque entities only, depth from light
  2. Opaque Geometry - With shadows applied
  3. Gizmos - Debug visualization (3D)
  4. Transparent - Sorted back-to-front
  5. Overlays - 2D UI, selection boxes

Transparent entities: Set RenderMesh.IsTransparent = true and they won't cast shadows.

Material Registry

Maps string IDs to rl.Material (shaders + textures). Materials are GPU resources:

  • Missing mesh/material IDs fall back to __error_mesh (magenta cube) and __error_material
  • Auto-loaded from assets/models/*.obj indexed as filename.obj:0, filename.obj:1

System Render Hooks

Implement SystemWithRender to inject rendering at specific phases:

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

Shadow System

Two-pass rendering:

shadowSystem.BeginShadowPass()  // Renders depth from light
// ... render opaque geometry
shadowSystem.EndShadowPass()
shadowSystem.BeginScenePass()   // Applies shadows
// ... render rest of scene
shadowSystem.EndScenePass()

Render Phases

Phase Use Case Inside 3D?
RenderPhaseShadow Custom shadow casters Yes
RenderPhaseGeometry Custom 3D rendering Yes
RenderPhaseGizmos Debug visualization (lines, spheres) Yes
RenderPhaseOverlay 2D UI, selection boxes, HUD No
RenderPhaseEditorUI ImGui panels (editor only) No

Gizmos vs Overlay

  • Gizmos (3D): Draw inside the 3D camera view. Use for debug spheres, lines, wireframes.
  • Overlay (2D): Draw to viewport texture. Use for UI, selection boxes, HUD.

Viewport-Aware 2D Rendering

In editor mode, the viewport is offset by panels. Use the callback approach:

// 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 {
        startX := (float32(box.StartX) - s.viewportX) * (1920.0 / s.viewportWidth)
        startY := (float32(box.StartY) - s.viewportY) * (1080.0 / s.viewportHeight)
        // ... draw at startX, startY
    }
}

Alternative: Run with --mode=game where viewport fills the screen.

Build And Run

make                    # Build game
./bin/run.sh --assets ./game/assets  # Run
./bin/rtsgame --mode=editor --assets ./rtsgame/assets  # Run RTS game
./bin/rtsgame --mode=game --assets ./rtsgame/assets    # Run RTS game (no editor)

Skills Available

The following skills are available for this project:

Skill Description
engine-component Creating ECS components
engine-system Creating ECS systems
engine-shader Creating custom shaders
engine-collision Collision detection
engine-rendering Rendering customization
engine-math 3D math utilities
engine-scene Scene API and serialization
engine-inspector Editor UI customization

Common Patterns

Input Handling

if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
    // Handle click
}
if rl.IsKeyDown(rl.KeyW) {
    // Handle key hold
}

World-to-Screen Conversion

Use raylib's GetWorldToScreen with a Camera3D:

camera := rl.Camera3D{
    Position:   transform.Position.ToRaylib(),
    Target:     cameraData.Target.ToRaylib(),
    Up:         math.Vector3Up().ToRaylib(),
    Fovy:       cameraData.Fov,
    Projection: rl.CameraPerspective,
}
screenPos := rl.GetWorldToScreen(pos.ToRaylib(), camera)

Querying Entities

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

for entry := range query.Iter(world) {
    transform := engineComponent.Transform.Get(entry)
    selectable := gameComponent.Selectable.Get(entry)
    // Process entity
}

Module Boundaries

The engine and games are separate Go modules. For cross-module features:

  1. Engine provides callbacks/interfaces for games to implement
  2. Games register callbacks during initialization
  3. Engine invokes callbacks at appropriate times

Example: SetViewportRectCallback in engine allows games to receive viewport bounds for overlay rendering.

Best Practices

  1. Use existing patterns: Check similar code in the codebase first
  2. Check imports: Use absolute paths for engine imports
  3. Run tests: Use go test ./... to verify changes
  4. Build before commit: Run make to ensure everything compiles
  5. Read raylib docs: The C API docs apply to raylib-go
  6. Serialize properly: Mark runtime-only fields with json:"-"