Melkam

command module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: MIT Imports: 7 Imported by: 0

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 ground
  • body_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 zone
  • body_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

  1. Understand the architecture: Read the Quick Example above
  2. Run the full example: go run . (see main.go for details)
  3. Build your game: Modify main.go with your own entities and logic
  4. 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

Happy coding! 🎮

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
docs
examples command
examples3d command
include
ecs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL