This document provides guidelines for AI agents working on this raylib-based Go game engine.
- Read raylib source code for C API patterns
- Read raylib-go source code for Go bindings
- Try to reproduce minimal versions of the issue
- Check for dependency updates that may help
- 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
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
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
Mutating Transforms:
transform, _ := scene.GetTransform(entityId)
transform.Position = transform.Position.Add(delta)
scene.SetTransform(entityId, *transform) // CRITICAL: triggers dirty flagRead-only access (distance checks, etc.) does NOT need SetTransform.
Critical: Raylib's MatrixMultiply(left, right) swaps parameters and returns right × left. Code order is reversed from math order!
-
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.
-
World matrix = Parent × Local
transform.LocalMatrix.Multiply(parentWorldMatrix) // Code order reversed!
Never swap these - breaks parent/child movement.
Scene.SetParent(child, parent)- pass parent=0 to detachScene.DestroyEntity(id)- recursively destroys children
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 onlySystemModePlay- Editor play mode onlySystemModeGame- Standalone game onlySystemModeRuntime- Play + Game (most common)SystemModeAlways- All modes
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)- Shadow Pass - Opaque entities only, depth from light
- Opaque Geometry - With shadows applied
- Gizmos - Debug visualization (3D)
- Transparent - Sorted back-to-front
- Overlays - 2D UI, selection boxes
Transparent entities: Set RenderMesh.IsTransparent = true and they won't cast shadows.
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/*.objindexed asfilename.obj:0,filename.obj:1
Implement SystemWithRender to inject rendering at specific phases:
type SystemWithRender interface {
Render(phase RenderPhase, world donburi.World, scene *scene.Scene)
}Two-pass rendering:
shadowSystem.BeginShadowPass() // Renders depth from light
// ... render opaque geometry
shadowSystem.EndShadowPass()
shadowSystem.BeginScenePass() // Applies shadows
// ... render rest of scene
shadowSystem.EndScenePass()| 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 (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.
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.
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)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 |
if rl.IsMouseButtonPressed(rl.MouseLeftButton) {
// Handle click
}
if rl.IsKeyDown(rl.KeyW) {
// Handle key hold
}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)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
}The engine and games are separate Go modules. For cross-module features:
- Engine provides callbacks/interfaces for games to implement
- Games register callbacks during initialization
- Engine invokes callbacks at appropriate times
Example: SetViewportRectCallback in engine allows games to receive viewport bounds for overlay rendering.
- Use existing patterns: Check similar code in the codebase first
- Check imports: Use absolute paths for engine imports
- Run tests: Use
go test ./...to verify changes - Build before commit: Run
maketo ensure everything compiles - Read raylib docs: The C API docs apply to raylib-go
- Serialize properly: Mark runtime-only fields with
json:"-"