README
¶
Melkam Game Engine
A minimal game engine in Go built with SDL3 for graphics. Uses an Entity-Component-System (ECS) architecture with event-based communication between systems.
Setup
Requirements
- Go 1.18+ (for generics support)
- SDL3 (installed via purego-sdl3 Go module)
Installation
# Clone or navigate to the project
cd Melkam
# Download dependencies
go mod download
# Run the example game
go run .
# Build as standalone executable
go build -o melkam.exe
How It Works
Three Core Concepts
1. Entities
An entity is a game object like a player, enemy, or projectile. Each entity is just an ID that can hold data.
player := scene.CreateEntity() // Creates a new entity
// Mark entities for 2D or 3D space
ecs.Mark2D(world, player) // 2D entity
ecs.Mark3D(world, player) // 3D entity
2. Components
Components are data containers attached to entities. They hold position, shape, physics properties, etc.
// Attach position and size to player
ecs.Set(world, player, systems.Transform2D{X: 100, Y: 200, W: 32, H: 32})
// Attach physics
ecs.Set(world, player, systems.RigidBody2D{VelocityX: 0, VelocityY: 0, Gravity: true})
// Attach color
ecs.Set(world, player, systems.ColorHex("#F5DC5A"))
3. Systems
Systems are functions that run every frame and operate on entities. Each system looks for entities with specific components and updates them.
// RenderSystem finds all entities with Transform2D + Color, then draws them
renderSystem.Update(world, dt)
// Physics2DSystem finds all entities with RigidBody2D + Collider2D, applies physics
physicsSystem.Update(world, dt)
Game Loop
The engine runs a standard game loop with two separate loops:
┌─ Render Loop (60+ fps)
│ ├─ Update timers
│ ├─ Update camera
│ ├─ Draw everything
│ └─ Flush deferred events
│
└─ Physics Loop (fixed timestep, default 60 fps)
├─ Handle input
├─ Update physics
├─ Detect collisions
└─ Flush deferred events
You provide callbacks for each loop:
win.OnPhysicsProcess(func(dt float32) {
// Handle input, update physics
physicsSystem.Update(world, dt)
})
win.OnProcess(func(dt float32) {
// Update visuals
renderSystem.Update(world, dt)
})
win.Run() // Starts the loop
Events/Signals
Systems communicate via events (signals). When something happens, a system emits an event that other code can listen for.
// Physics system emits "body_grounded" when player lands
// Your code listens for it:
core.ConnectSystemSignal1[ecs.Entity]("Physics2DSystem", "body_grounded",
func(entity ecs.Entity) {
if entity == player {
canJump = true
}
})
This keeps systems decoupled—physics doesn't need to know about your game logic.
3D Game Development
Melkam now supports full 3D rendering with mesh-based entities. Use space tagging to mark entities as 3D:
// Create 3D scene
scene := core.NewScene()
world := scene.World()
// Create 3D camera
cam := scene.CreateEntity()
ecs.Mark3D(world, cam)
ecs.Set(world, cam, components.NewTransform3D())
ecs.Set(world, cam, components.NewCamera3D().WithFov(75).WithClip(0.1, 500))
scene.SetCamera(cam)
// Create 3D cube
cube := scene.CreateEntity()
ecs.Mark3D(world, cube)
ecs.Set(world, cube, components.NewTransform3D())
ecs.Set(world, cube, components.NewCubeMesh(1))
ecs.Set(world, cube, components.NewMaterial3D("material", components.HEX("#FF5500")).Solid())
ecs.Set(world, cube, components.RigidBody3D{Gravity: false})
Example 3D project: Run go run . to see the bundled 3D ECS example.
Asset Management
Use the built-in asset manager for files under assets/:
assets := core.DefaultAssets
iconPath := assets.MustResolve("appicon.png")
data, err := assets.ReadBytes("config.json")
if err != nil {
panic(err)
}
assets.Register("player_icon", "sprites/player.png")
The manager resolves paths, checks file existence, and caches file bytes for repeated reads.
3D Game Development
Melkam supports full 3D development using the ECS architecture. See docs/3D_GUIDE.md for comprehensive guide on:
- 3D scene setup
- 3D components (Transform3D, Camera3D, meshes, materials)
- 3D physics (StaticBody3D, RigidBody3D, CharacterBody3D)
- Space tagging (Mark3D/Mark2D)
- Input handling for 3D cameras
Quick example: go run . runs the bundled 3D ECS example.
Sprite Rendering
Render textured sprites from image files (PNG, JPEG, GIF, WebP) stored in the assets/ folder.
Requirements: Transform2D + Sprite2D components
// Add a sprite to an entity
player := scene.CreateEntity()
ecs.Set(world, player, systems.Transform2D{X: 100, Y: 260, W: 48, H: 48})
ecs.Set(world, player, systems.Sprite2D{
Asset: "appicon.png", // File from assets/ folder
Fit: systems.SpriteFitContain,
})
Fit Modes:
SpriteFitOriginal— Display at original PNG size (ignores Transform2D.W/H)SpriteFitStretch— Stretch to fill Transform2D.W/H (may distort)SpriteFitContain— Scale to fit within Transform2D.W/H while preserving aspect ratio
// Examples with different fit modes
ecs.Set(world, entity1, systems.Sprite2D{Asset: "texture.png", Fit: systems.SpriteFitOriginal})
ecs.Set(world, entity2, systems.Sprite2D{Asset: "background.jpg", Fit: systems.SpriteFitStretch})
ecs.Set(world, entity3, systems.Sprite2D{Asset: "icon.gif", Fit: systems.SpriteFitContain})
ecs.Set(world, entity4, systems.Sprite2D{Asset: "modern.webp", Fit: systems.SpriteFitContain})
Supported Formats:
- PNG — Lossless, supports transparency
- JPEG — Compressed, good for photos/backgrounds
- GIF — Supports animation and lossless compression
- WebP — Modern format with excellent compression
Textures are automatically decoded from Go's image libraries and cached after first load for performance.
Project Structure
Melkam/
├── main.go ← Your game here
├── README.md ← This file
├── GETTING_STARTED.md ← Full tutorial
├── docs/
│ ├── SPRITES.md ← Sprite rendering guide
│ ├── SIGNAL_ARCHITECTURE.md ← Event system details
│ └── SIGNAL_QUICK_REFERENCE.md ← Event examples
├── assets/ ← Game assets (images, etc)
│ └── appicon.png
└── include/
├── core/
│ ├── window.go ← Game window + loop
│ ├── scene.go ← Scene management
│ ├── input.go ← Input handling
│ ├── signal.go ← Event system
│ └── assets.go ← Asset manager
├── ecs/
│ ├── component.go ← Component definitions
│ ├── entity.go ← Entity management
│ ├── query.go ← Component queries
│ ├── system.go ← System base classes
│ └── world.go ← Entity-Component-System
├── systems/
│ ├── render.go ← Drawing system (sprites, shapes, colors)
│ ├── physics.go ← Physics & collision
│ ├── area2d.go ← Trigger zones
│ └── timer.go ← Timer system
└── helpers/
└── signals_helpers.go ← Event utilities
Quick Example
Here's a minimal working game:
package main
import (
"Melkam/internal/core"
"Melkam/internal/ecs"
"Melkam/internal/systems"
"fmt"
"log"
"github.com/jupiterrider/purego-sdl3/sdl"
)
func main() {
// Setup input
core.Input.BindAction("move_right", sdl.ScancodeD)
core.Input.BindAction("move_left", sdl.ScancodeA)
core.Input.BindAction("jump", sdl.ScancodeSpace)
// Create window
win, err := core.New("My Game", 800, 600, sdl.WindowResizable)
if err != nil {
log.Fatal(err)
}
defer win.Destroy()
// Create scene and systems
scene := core.NewScene()
world := scene.World()
scene.SetDefaultCamera()
renderSystem := systems.NewRenderSystem(win)
physicsSystem := systems.NewPhysics2DSystem()
// Create background
bg := scene.CreateEntity()
ecs.Set(world, bg, systems.ColorRectHex("#1a1a1a"))
// Create player
player := scene.CreateEntity()
ecs.Set(world, player, systems.Transform2D{X: 100, Y: 250, W: 32, H: 32})
ecs.Set(world, player, systems.RigidBody2D{Gravity: true, GravityScale: 1})
ecs.Set(world, player, systems.Collider2D{Width: 32, Height: 32, Solid: true})
ecs.Set(world, player, systems.ColorHex("#F5DC5A"))
// Create ground
ground := scene.CreateEntity()
ecs.Set(world, ground, systems.Transform2D{X: 0, Y: 500, W: 800, H: 100})
ecs.Set(world, ground, systems.Collider2D{Width: 800, Height: 100, Solid: true})
ecs.Set(world, ground, systems.ColorHex("#2E3844"))
// Listen for player landing
core.ConnectSystemSignal1[ecs.Entity]("Physics2DSystem", "body_grounded",
func(entity ecs.Entity) {
if entity == player {
fmt.Println("Player landed!")
}
})
// Render loop
win.OnProcess(func(dt float32) {
renderSystem.Update(world, dt)
core.FlushDeferredSignals()
})
// Physics loop
win.OnPhysicsProcess(func(dt float32) {
// Input
if body, ok := ecs.Get[systems.RigidBody2D](world, player); ok {
body.VelocityX = 0
if core.Input.IsActionPressed("move_right") {
body.VelocityX = 180
}
if core.Input.IsActionPressed("move_left") {
body.VelocityX = -180
}
if core.Input.IsActionJustPressed("jump") && body.Grounded {
body.VelocityY = -360
}
}
// Update physics
physicsSystem.Update(world, dt)
core.FlushDeferredSignals()
})
win.Run()
}
Run it:
go run .
Players can move with A/D and jump with Space.
Built-in Systems
RenderSystem
Draws rectangles and sprites for all entities with a position and color/texture.
Solid Color Rendering: Transform2D + Color components
ecs.Set(world, entity, systems.Transform2D{X: 0, Y: 0, W: 100, H: 100})
ecs.Set(world, entity, systems.ColorHex("#FF0000")) // Red rectangle
Sprite Rendering: Transform2D + Sprite2D components
ecs.Set(world, entity, systems.Transform2D{X: 100, Y: 200, W: 64, H: 64})
ecs.Set(world, entity, systems.Sprite2D{Asset: "player.png", Fit: systems.SpriteFitContain})
Mesh Rendering: Transform2D + MeshInstance2D components
ecs.Set(world, entity, systems.Transform2D{X: 50, Y: 50, W: 40, H: 40})
ecs.Set(world, entity, systems.MeshInstance2D{
Shape: systems.MeshShapeCircle,
Fill: systems.Color{R: 255, G: 100, B: 0, A: 255},
Filled: true,
Segments: 32,
})
Supported Shapes: MeshShapeRect, MeshShapeTriangle, MeshShapeCircle
Physics2DSystem
Applies gravity, handles velocity, and detects collisions.
Requirements: RigidBody2D + Collider2D components
Events emitted:
body_grounded— Entity touched solid groundbody_airborne— Entity lost ground contact
Area2DSystem
Detects when entities enter/exit trigger zones (non-solid colliders).
Requirements: Collider2D component with Solid: false
Events emitted:
body_entered— Entity entered zonebody_exited— Entity left zone
TimerSystem
Counts down timers and emits events when they expire.
Usage:
timer := scene.CreateEntity()
ecs.Set(world, timer, systems.NewTimer(3.0, true)) // 3 seconds, repeating
if t, ok := ecs.Get[systems.Timer](world, timer); ok {
t.Start()
}
Events emitted:
timeout— Timer expired
Available Components
| Component | Purpose | Fields |
|---|---|---|
StaticBody2D |
Static body (doesn't move) | — |
RigidBody2D |
Physics-simulated body | VelocityX, VelocityY, Gravity, GravityScale, Grounded |
CharacterBody2D |
Character-controlled body | VelocityX, VelocityY, Gravity, GravityScale, MaxSpeed, Grounded |
Collider2D |
Collision shape | Width, Height, Solid (bool) |
Transform2D |
Position and size | X, Y, W, H, Rotation |
Color |
Render color | R, G, B |
Camera2D |
Camera position | X, Y |
Timer |
Countdown timer | WaitTime, TimeLeft, Running |
Common Tasks
Move an Entity
if body, ok := ecs.Get[systems.RigidBody2D](world, entity); ok {
body.VelocityX = 100 // Pixel per second
}
Draw Something
entity := scene.CreateEntity()
ecs.Set(world, entity, systems.Transform2D{X: 0, Y: 0, W: 32, H: 32})
ecs.Set(world, entity, systems.ColorHex("#FF0000"))
Create Solid Collision
ecs.Set(world, entity, systems.Collider2D{
Width: 32,
Height: 32,
Solid: true, // Can collide with other solid objects
})
Create Trigger Zone
ecs.Set(world, entity, systems.Collider2D{
Width: 100,
Height: 100,
Solid: false, // Trigger, doesn't block movement
})
Listen for Event
id := core.ConnectSystemSignal1[ecs.Entity]("Physics2DSystem", "body_grounded",
func(entity ecs.Entity) {
// Handle event
})
defer core.DisconnectSystemSignal("Physics2DSystem", "body_grounded", id)
Input
Bind keys to actions at startup:
core.Input.BindAction("jump", sdl.ScancodeSpace)
core.Input.BindAction("move_right", sdl.ScancodeD)
Poll in your game loop:
if core.Input.IsActionPressed("jump") {
// Jump is held down
}
if core.Input.IsActionJustPressed("jump") {
// Jump was pressed this frame
}
if core.Input.IsActionJustReleased("jump") {
// Jump was released this frame
}
Next Steps
- Understand the architecture: Read the Quick Example above
- Run the full example:
go run .(see main.go for details) - Build your game: Modify main.go with your own entities and logic
- Learn more: See GETTING_STARTED.md for complete reference
Files You'll Modify
- main.go — Your game code goes here (entity setup, input handling, etc.)
Files You Won't Touch
- internal/core/ — Engine core (window, events, input)
- internal/ecs/ — Entity system
- internal/systems/ — Built-in game systems (physics, rendering)
Troubleshooting
Build fails: Make sure Go 1.18+ is installed (go version)
Game doesn't run: Check that SDL3 is available on your system
Physics feels wrong: Adjust GravityScale on RigidBody2D
Collisions not working: Ensure entities have both RigidBody2D AND Collider2D
API at a Glance
| Task | Code |
|---|---|
| Create entity | entity := scene.CreateEntity() |
| Add component | ecs.Set(world, entity, MyComponent{}) |
| Get component | if comp, ok := ecs.Get[MyComponent](world, entity); ok { ... } |
| Update physics | physicsSystem.Update(world, dt) |
| Draw frame | renderSystem.Update(world, dt) |
| Get input | core.Input.IsActionPressed("key") |
| Emit event | core.EmitSystemSignal(system, name, args...) |
| Listen event | core.ConnectSystemSignal1[Type](system, name, callback) |
| Flush events | core.FlushDeferredSignals() |
Documentation
- GETTING_STARTED.md — Full tutorial and reference
- docs/SIGNAL_QUICK_REFERENCE.md — Event system examples
- docs/SIGNAL_ARCHITECTURE.md — Technical details
Happy coding! 🎮
Documentation
¶
There is no documentation for this package.