Entity Component System

github.com/mechanical-lich/mlge/ecs

A classic Entity Component System with blueprint-based entity creation, component maps, and a system manager.

Core Concepts

  • Entity — A container that holds a map of components
  • Component — A data struct implementing the Component interface
  • System — Logic that operates on entities with specific component requirements
  • Blueprint — A text-based definition for creating entities with pre-configured components

Types

ComponentType

type ComponentType string

String identifier for component types. Each component defines its own type constant.

Component

type Component interface {
    GetType() ComponentType
}

All components must implement this interface.

Entity

type Entity struct {
    Components map[ComponentType]Component
    Blueprint  string
}

Methods:

Method Signature Description
AddComponent (component Component) Adds a component to the entity
HasComponent (componentType ComponentType) bool Checks if entity has a component
HasComponents (componentTypes ...ComponentType) bool Checks for multiple components
HasComponentsSlice (componentTypes []ComponentType) bool Checks for components from a slice
GetComponent (componentType ComponentType) Component Retrieves a component by type
RemoveComponent (componentType ComponentType) Removes a component

SystemInterface

type SystemInterface interface {
    UpdateSystem(params any) error
    UpdateEntity(params any, entity *Entity) error
    Requires() []ComponentType
}

Systems are called by the SystemManager for each entity that has the required components.

SystemManager

type SystemManager struct{}

Methods:

Method Signature Description
AddSystem (system SystemInterface) Registers a system
UpdateSystems (params any) error Runs all systems (calls UpdateSystem)
UpdateSystemsForEntity (params any, entity *Entity) error Runs systems for a single entity
UpdateSystemsForEntities (params any, entities []*Entity) error Runs systems for a slice of entities

Blueprints

Blueprints allow you to define entity templates and create entities from them at runtime.

Text-Based Factory (Legacy)

The original factory uses a line-based text format with string parameters.

Registering Component Factories

type ComponentAddFunction func([]string) (Component, error)

ecs.RegisterComponentAddFunction("position2d", func(args []string) (ecs.Component, error) {
    x, _ := strconv.ParseFloat(args[0], 64)
    y, _ := strconv.ParseFloat(args[1], 64)
    return &basecomponents.Position2dComponent{X: x, Y: y}, nil
})

Blueprint Text Format

Each entity starts with its name on a new line, followed by component definitions in the format componentType:param1,param2,.... Entities are separated by blank lines.

player
position2d:100,200
health:100
sprite:player_idle

enemy
position2d:0,0
health:50
sprite:enemy_idle
ai:

Loading and Creating

// Load blueprints from file
err := ecs.LoadBlueprintsFromFile("data/blueprints.txt")

// Or from an io.Reader
err := ecs.LoadFactoryFromStream(reader)

// Create an entity from a blueprint
entity, err := ecs.Create("player")

The JSONFactory uses JSON blueprints and populates components via JSON unmarshalling, making it more flexible for complex component data.

Creating a Factory

factory := ecs.NewJSONFactory()

Registering Components

Register component constructors that return zero-value component pointers:

factory.RegisterComponent("HealthComponent", func() ecs.Component {
    return &components.HealthComponent{}
})
factory.RegisterComponent("AppearanceComponent", func() ecs.Component {
    return &components.AppearanceComponent{}
})

Blueprint JSON Format

Each JSON file contains a map of blueprint names to component definitions:

{
    "player": {
        "HealthComponent": {"MaxHealth": 100, "Health": 100},
        "AppearanceComponent": {"SpriteName": "player_idle"}
    },
    "enemy": {
        "HealthComponent": {"MaxHealth": 50},
        "AppearanceComponent": {"SpriteName": "enemy_idle"},
        "HostileAIComponent": {}
    }
}

Loading Blueprints

// Load from a single file
err := factory.LoadBlueprintsFromFile("data/units.json")

// Load all JSON files from a directory
err := factory.LoadBlueprintsFromDir("data/blueprints")

Creating Entities

// Create an entity from a blueprint
entity, err := factory.Create("player")

// Create with a callback for custom initialization
entity, err := factory.CreateWithCallback("player", func(comp ecs.Component) error {
    // Auto-initialize health if not set
    if hc, ok := comp.(*HealthComponent); ok {
        if hc.Health == 0 {
            hc.Health = hc.MaxHealth
        }
    }
    return nil
})

Additional Methods

Method Signature Description
BlueprintExists (name string) bool Check if a blueprint is registered
GetBlueprintNames () []string Get all registered blueprint names

Built-in Components

The ecs/basecomponents package provides common position components:

import "github.com/mechanical-lich/mlge/ecs/basecomponents"

Position2dComponent

type Position2dComponent struct {
    X, Y float64
}

Type: basecomponents.Position2d

Methods: GetX(), GetY(), SetPosition(x, y float64)

Position3dComponent

type Position3dComponent struct {
    X, Y, Z float64
}

Type: basecomponents.Position3d

Methods: GetX(), GetY(), GetZ(), SetPosition(x, y, z float64)

Inanimate Entities

Set ecs.InanimateComponentType to a component type to skip entities with that component during system updates. This is useful for static objects that don’t need per-frame processing.

ecs.InanimateComponentType = "inanimate"

Example: Custom Component and System

// Define a component
const HealthType ecs.ComponentType = "health"

type HealthComponent struct {
    Current int
    Max     int
}

func (h *HealthComponent) GetType() ecs.ComponentType {
    return HealthType
}

// Define a system
type RegenSystem struct{}

func (s *RegenSystem) Requires() []ecs.ComponentType {
    return []ecs.ComponentType{HealthType}
}

func (s *RegenSystem) UpdateSystem(params any) error {
    return nil
}

func (s *RegenSystem) UpdateEntity(params any, entity *ecs.Entity) error {
    health := entity.GetComponent(HealthType).(*HealthComponent)
    if health.Current < health.Max {
        health.Current++
    }
    return nil
}

// Register and use
sm := &ecs.SystemManager{}
sm.AddSystem(&RegenSystem{})
sm.UpdateSystemsForEntities(nil, entities)

Back to top

Copyright © 2026. Distributed under the MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.