Help us shape the future of Go! Take the 2026 Go Developer Survey

akara

package module
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 11 Imported by: 49

README

Akara

Akara is a small, typed entity-component-system runtime for Go. Components are ordinary Go data, component stores are generic, subscriptions update immediately, and systems run deterministically in registration order.

Akara deliberately does not require component interfaces, constructor methods, factory wrappers, runtime casts, or public bit masks.

It also supports validated runtime-defined component schemas. This lets script runtimes and data-driven applications participate in the same entities, archetypes, filters, and deterministic systems without making Akara depend on a particular scripting language.

Components

Define components as plain data:

type Position struct {
	X, Y float64
}

type Velocity struct {
	X, Y float64
}

Register each type once during application composition:

world := akara.NewWorld()
defer world.Close()

positions := akara.Register[Position](world)
velocities := akara.Register[Velocity](world)

Register[T] is idempotent. Every call for the same type and world returns the same *Store[T]. Code that does not participate in registration can look up an existing handle without registering implicitly:

positions, found := akara.GetStore[Position](world)
if !found {
	return errors.New("Position was not registered")
}

Entities and stores

entity, err := world.CreateEntity()
if err != nil {
	return err
}

position, err := positions.Add(entity)
if err != nil {
	return err
}
position.X = 10

velocity, err := velocities.Set(entity, Velocity{X: 4, Y: -2})
if err != nil {
	return err
}

position, found := positions.Get(entity)
removed := velocities.Remove(entity)
destroyed := world.DestroyEntity(entity)

Add creates the component's zero value or returns the existing pointer. Set replaces it with a copied value. Adding a component to an unknown entity returns ErrEntityNotFound; orphan components cannot be created.

DestroyEntity immediately removes every component and subscription membership. Entity IDs are world-local, begin at one, and are never reused.

Stores and ECS indexes are safe for concurrent use. A component pointer returned by Add, Set, or Get is ordinary application data: callers must coordinate concurrent field mutation themselves.

Sharing store handles

Prefer explicit dependency injection. A small application registry makes system dependencies visible and avoids service-location during updates:

type Components struct {
	Positions  *akara.Store[Position]
	Velocities *akara.Store[Velocity]
}

func RegisterComponents(world *akara.World) Components {
	return Components{
		Positions:  akara.Register[Position](world),
		Velocities: akara.Register[Velocity](world),
	}
}

Pass Components or the individual stores to system constructors. Use GetStore[T] for occasional lookup across package boundaries.

Runtime-defined components

Applications that do not know every component type at Go compile time can register a named schema:

velocity, err := akara.RegisterSchema(world, akara.Schema{
	Name:    "movement.velocity",
	Version: 1,
	Fields: []akara.Field{
		{Name: "x", Kind: akara.FieldFloat64},
		{Name: "y", Kind: akara.FieldFloat64},
	},
})
if err != nil {
	return err
}

component, err := velocity.Set(entity, map[string]any{
	"x": float64(4),
	"y": float64(-2),
})
if err != nil {
	return err
}

x, err := component.Get("x")

Runtime stores implement ComponentType, so filters may freely combine typed and runtime-defined components. Values are validated against their schemas, and component references are generation checked: removing or replacing an instance invalidates earlier references instead of silently targeting unrelated storage.

Runtime adapters should translate their own values into Akara's deliberately small field vocabulary. Akara does not import Lua, JavaScript, serialization, or application-specific schema packages.

Subscriptions

Subscriptions accept typed store handles directly:

moving, err := world.Subscribe(
	akara.All(positions, velocities),
	akara.Any(playerControlled, aiControlled),
	akara.None(frozen),
)
if err != nil {
	return err
}
defer moving.Close()

for _, entity := range moving.Entities() {
	position, _ := positions.Get(entity)
	velocity, _ := velocities.Get(entity)
	// Update component data.
}
  • All requires every supplied component.
  • Any requires at least one supplied component.
  • None forbids every supplied component.
  • An empty filter matches every entity.

Membership changes immediately when components are added or removed. Entities returns a sorted snapshot, making iteration stable even if the world changes later. A filter cannot accidentally use a store from another world; Subscribe returns ErrForeignComponent.

Subscriptions are independent. Ignore and Include affect only that subscription. Call Close when a subscription is no longer needed.

When a system needs to add or remove components while iterating a query, queue those structural mutations in a CommandBuffer and apply it at the system or phase barrier. Ordinary component field updates can remain immediate.

Deterministic systems

Akara does not start background tick goroutines. The application supplies delta time, and systems run sequentially in registration order:

type MovementSystem struct {
	positions *akara.Store[Position]
	velocities *akara.Store[Velocity]
	moving *akara.Subscription
}

func (system *MovementSystem) Update(_ *akara.World, delta time.Duration) error {
	seconds := delta.Seconds()
	for _, entity := range system.moving.Entities() {
		position, _ := system.positions.Get(entity)
		velocity, _ := system.velocities.Get(entity)
		position.X += velocity.X * seconds
		position.Y += velocity.Y * seconds
	}
	return nil
}

movement := &MovementSystem{
	positions: positions,
	velocities: velocities,
	moving: moving,
}

systemID, err := world.AddSystem(movement)
if err != nil {
	return err
}

if err := world.Update(frameDelta); err != nil {
	return err
}

SystemFunc adapts functions to System. RemoveSystem uses the SystemID returned by registration. An update stops and wraps the first system error. Concurrent calls to Update are serialized.

Lifecycle

NewWorldWithContext binds world shutdown to context cancellation:

world := akara.NewWorldWithContext(gameContext)
defer world.Close()

Close is idempotent. It closes Done and rejects subsequent mutations, subscriptions, system registration, and updates with ErrWorldClosed. Existing component values remain readable for diagnostics or shutdown persistence.

Application integration

Create one world and one typed component registry in the application's composition root. Construct systems with explicit store dependencies, register them in the desired simulation order, and call world.Update(delta) from the main loop. This produces deterministic component ownership without scheduler goroutines or hidden registration.

Entity IDs and component handles are runtime-local. Save games and network protocols should use stable application IDs and schema names rather than Akara internals.

Internal representation

Akara interns each distinct component combination as an archetype. Every entity holds one archetype pointer; entities with the same component types share one dense component mask. Adding or removing a component moves the entity along a cached archetype transition edge.

Subscriptions cache matching archetypes rather than duplicating an entity-membership map. A filter is evaluated once when a new archetype appears, after which all entities in that archetype match implicitly.

This design supports large component registries without allocating a mask per entity. For example, a 10,000-type registry needs roughly 1.25 KB for a fully expanded mask, but that mask is paid once per used component combination rather than once per entity. Unused registered types consume no entity-mask memory. ArchetypeCount is available for diagnostics.

The mask and archetype graph are private implementation details. Akara has no dependency on the standalone BitSet module and can change representations without affecting application code.

Development

go test ./...
go test -race ./...
go vet ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 run ./...

Documentation

Overview

Package akara provides a typed, deterministic entity-component-system runtime.

Components are arbitrary Go data types registered through generic Store values. Subscriptions use opaque store handles and cache shared entity archetypes. Systems execute sequentially in a caller-driven update loop. Component masks and archetype transitions are private; the package does not expose or depend on a bit-set API.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidSchema      = errors.New("akara: invalid component schema")
	ErrSchemaConflict     = errors.New("akara: component schema conflicts with existing registration")
	ErrFieldNotFound      = errors.New("akara: component field does not exist")
	ErrInvalidFieldValue  = errors.New("akara: invalid component field value")
	ErrComponentNotFound  = errors.New("akara: entity does not have component")
	ErrComponentReference = errors.New("akara: component reference is stale")
	ErrSchemaVersion      = errors.New("akara: schema version must increase")
)
View Source
var (
	ErrWorldClosed       = errors.New("akara: world is closed")
	ErrEntityNotFound    = errors.New("akara: entity does not exist")
	ErrEntityExists      = errors.New("akara: entity already exists")
	ErrForeignComponent  = errors.New("akara: component belongs to another world")
	ErrNilComponent      = errors.New("akara: component handle is nil")
	ErrNilSystem         = errors.New("akara: system is nil")
	ErrEntityIDExhausted = errors.New("akara: entity ID space exhausted")
	ErrSystemIDExhausted = errors.New("akara: system ID space exhausted")
)
View Source
var ErrCommandBufferApplied = errors.New("akara: command buffer already applied")

Functions

This section is empty.

Types

type CommandBuffer

type CommandBuffer struct {
	// contains filtered or unexported fields
}

CommandBuffer defers structural world mutations until a safe barrier. It is safe to populate concurrently. Apply executes commands once in submission order.

func NewCommandBuffer

func NewCommandBuffer() *CommandBuffer

NewCommandBuffer creates an empty structural command buffer.

func (*CommandBuffer) AddDynamic

func (buffer *CommandBuffer) AddDynamic(store *DynamicStore, entity Entity, values map[string]any)

AddDynamic queues addition or replacement of a runtime-defined component. The values map is copied at submission time.

func (*CommandBuffer) Apply

func (buffer *CommandBuffer) Apply() error

Apply executes every queued mutation once. It stops at the first error.

func (*CommandBuffer) CreateDynamic

func (buffer *CommandBuffer) CreateDynamic(world *World, components map[*DynamicStore]map[string]any)

CreateDynamic queues creation of an entity with runtime-defined components. The new entity becomes visible after Apply; systems that need to communicate its identity should assign a stable application identifier component.

func (*CommandBuffer) Destroy

func (buffer *CommandBuffer) Destroy(world *World, entity Entity)

Destroy queues entity destruction.

func (*CommandBuffer) Remove

func (buffer *CommandBuffer) Remove(component ComponentType, entity Entity)

Remove queues removal of a component type from an entity.

type ComponentKey

type ComponentKey struct {
	// contains filtered or unexported fields
}

ComponentKey is an opaque, world-local component type identifier. Keys are stable for the lifetime of a World but must not be persisted.

type ComponentType

type ComponentType interface {
	// contains filtered or unexported methods
}

ComponentType is implemented by Store[T] and accepted by filter builders. Its private method prevents forged component keys.

type DynamicComponent

type DynamicComponent struct {
	// contains filtered or unexported fields
}

DynamicComponent is a checked reference to one runtime component instance. Replacing or removing the component invalidates prior references.

func (*DynamicComponent) Get

func (c *DynamicComponent) Get(name string) (any, error)

func (*DynamicComponent) Set

func (c *DynamicComponent) Set(name string, value any) error

func (*DynamicComponent) Snapshot

func (c *DynamicComponent) Snapshot() (map[string]any, error)

Snapshot returns a defensive field-value map.

type DynamicStore

type DynamicStore struct {
	// contains filtered or unexported fields
}

DynamicStore owns instances of one runtime-defined schema.

func DynamicStores added in v0.1.2

func DynamicStores(world *World) []*DynamicStore

DynamicStores returns the world's runtime-defined component stores ordered by stable schema name. The returned slice is detached from the registry; stores remain owned by the world.

func GetDynamicStore

func GetDynamicStore(world *World, name string) (*DynamicStore, bool)

GetDynamicStore looks up an already registered runtime component by name.

func RegisterSchema

func RegisterSchema(world *World, schema Schema) (*DynamicStore, error)

RegisterSchema returns the canonical runtime-defined store for schema.Name. Re-registering an identical schema is idempotent; incompatible declarations return ErrSchemaConflict.

func (*DynamicStore) Add

func (s *DynamicStore) Add(entity Entity) (*DynamicComponent, error)

Add creates a component with schema defaults or returns the existing reference.

func (*DynamicStore) Entities

func (s *DynamicStore) Entities() []Entity

func (*DynamicStore) Get

func (s *DynamicStore) Get(entity Entity) (*DynamicComponent, bool)

Get returns a generation-checked reference to entity's component.

func (*DynamicStore) Has

func (s *DynamicStore) Has(entity Entity) bool

func (*DynamicStore) Key

func (s *DynamicStore) Key() ComponentKey

func (*DynamicStore) Len

func (s *DynamicStore) Len() int

func (*DynamicStore) Migrate

func (s *DynamicStore) Migrate(next Schema, migrate Migration) error

Migrate transactionally replaces this store's schema and all of its component instances. The name must remain stable and the version must increase. If any conversion or validation fails, neither schema nor instances are changed.

func (*DynamicStore) Remove

func (s *DynamicStore) Remove(entity Entity) bool

func (*DynamicStore) Schema

func (s *DynamicStore) Schema() Schema

Schema returns a defensive copy of the registered schema.

func (*DynamicStore) Set

func (s *DynamicStore) Set(entity Entity, values map[string]any) (*DynamicComponent, error)

Set replaces a component using schema defaults overridden by values.

type Entity

type Entity uint64

Entity is a world-local entity identifier. Zero is never a valid entity.

type Field

type Field struct {
	Name    string
	Kind    FieldKind
	Default any
}

Field defines one named field and its optional default value. A nil default selects the zero value for Kind.

type FieldKind

type FieldKind uint8

FieldKind is the storage and validation type of a runtime-defined field.

const (
	FieldBool FieldKind = iota + 1
	FieldInt64
	FieldUint64
	FieldFloat64
	FieldString
	FieldEntity
)

type FilterOption

type FilterOption interface {
	// contains filtered or unexported methods
}

FilterOption contributes one component constraint to a subscription filter.

func All

func All(components ...ComponentType) FilterOption

All requires every supplied component type.

func Any

func Any(components ...ComponentType) FilterOption

Any requires at least one supplied component type. Any() with no arguments adds no constraint.

func None

func None(components ...ComponentType) FilterOption

None forbids every supplied component type.

type Migration

type Migration func(entity Entity, previous map[string]any) (map[string]any, error)

Migration converts one component snapshot to values for a replacement schema.

type Schema

type Schema struct {
	Name    string
	Version uint32
	Fields  []Field
}

Schema defines a runtime component type. Name is the stable application-level identity; Version is available to adapters implementing schema migration.

type Store

type Store[T any] struct {
	// contains filtered or unexported fields
}

Store owns the components of one Go type in one World. Register returns the canonical Store[T] for that type and world.

func GetStore

func GetStore[T any](world *World) (*Store[T], bool)

GetStore returns the already registered store for T.

func Register

func Register[T any](world *World) *Store[T]

Register returns the world's canonical store for T. T does not implement an interface and its zero value is used when Add creates a component. Register panics if world is nil or closed.

Example
package main

import (
	"fmt"

	"github.com/gravestench/akara"
)

type examplePosition struct{ X, Y float64 }

func main() {
	world := akara.NewWorld()
	defer func() { _ = world.Close() }()
	positions := akara.Register[examplePosition](world)
	entity := world.MustCreateEntity()
	position, _ := positions.Set(entity, examplePosition{X: 12, Y: 4})
	fmt.Println(position.X, position.Y)
}
Output:
12 4

func (*Store[T]) Add

func (s *Store[T]) Add(entity Entity) (*T, error)

Add returns the entity's existing component or creates its zero value.

func (*Store[T]) Entities

func (s *Store[T]) Entities() []Entity

Entities returns sorted IDs for entities with this component.

func (*Store[T]) Get

func (s *Store[T]) Get(entity Entity) (*T, bool)

Get returns entity's component. The returned pointer remains valid until the component is removed or replaced by Set; callers synchronize concurrent field access.

func (*Store[T]) Has

func (s *Store[T]) Has(entity Entity) bool

Has reports whether entity has this component.

func (*Store[T]) Key

func (s *Store[T]) Key() ComponentKey

Key returns the opaque component key used by filters and diagnostics.

func (*Store[T]) Len

func (s *Store[T]) Len() int

Len returns the number of stored components.

func (*Store[T]) Remove

func (s *Store[T]) Remove(entity Entity) bool

Remove deletes entity's component and reports whether it existed.

func (*Store[T]) Set

func (s *Store[T]) Set(entity Entity, value T) (*T, error)

Set replaces entity's component with a copy of value and returns its address.

func (*Store[T]) TypeName

func (s *Store[T]) TypeName() string

TypeName returns the registered Go type's name.

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

Subscription caches the archetypes matching an immutable component filter. Entity membership follows archetype transitions without per-entity filter work.

func (*Subscription) Close

func (subscription *Subscription) Close() error

Close unregisters the subscription. It is safe to call repeatedly.

func (*Subscription) Entities

func (subscription *Subscription) Entities() []Entity

Entities returns matching entity IDs in ascending order.

func (*Subscription) Ignore

func (subscription *Subscription) Ignore(entity Entity)

Ignore excludes entity from this subscription until Include is called.

func (*Subscription) Include

func (subscription *Subscription) Include(entity Entity)

Include removes an explicit exclusion.

func (*Subscription) Len

func (subscription *Subscription) Len() int

Len returns the current number of matching entities.

type System

type System interface {
	Update(*World, time.Duration) error
}

System advances one part of a simulation. World.Update invokes systems sequentially in registration order.

type SystemFunc

type SystemFunc func(*World, time.Duration) error

SystemFunc adapts a function to System.

func (SystemFunc) Update

func (function SystemFunc) Update(world *World, delta time.Duration) error

type SystemID

type SystemID uint64

SystemID identifies one system registration in a world.

type World

type World struct {
	// contains filtered or unexported fields
}

World owns entities, component stores, subscriptions, and deterministic systems.

func NewWorld

func NewWorld() *World

NewWorld creates an empty world.

func NewWorldWithContext

func NewWorldWithContext(ctx context.Context) *World

NewWorldWithContext closes its world when ctx is canceled.

func (*World) AddSystem

func (world *World) AddSystem(system System) (SystemID, error)

AddSystem appends system to the deterministic update order.

func (*World) ArchetypeCount

func (world *World) ArchetypeCount() int

ArchetypeCount returns the number of component combinations interned by the world, including the empty archetype.

func (*World) Close

func (world *World) Close() error

Close permanently closes the world. Existing component data remains readable, while mutation, subscription creation, and updates are rejected.

func (*World) Closed

func (world *World) Closed() bool

Closed reports whether Close has been called.

func (*World) CreateEntity

func (world *World) CreateEntity() (Entity, error)

CreateEntity creates an entity with no components.

func (*World) CreateEntityWithID added in v0.1.4

func (world *World) CreateEntityWithID(entity Entity) error

CreateEntityWithID creates an entity with an explicit non-zero identity. This is intended for trusted snapshot restoration. Normal simulation should use CreateEntity so identity allocation remains encapsulated by the world.

func (*World) DestroyEntity

func (world *World) DestroyEntity(entity Entity) bool

DestroyEntity immediately removes an entity, all its components, and all its subscription memberships.

func (*World) Done

func (world *World) Done() <-chan struct{}

Done closes when the world closes.

func (*World) Entities

func (world *World) Entities() []Entity

Entities returns all live entity IDs in ascending order.

func (*World) EntityExists

func (world *World) EntityExists(entity Entity) bool

EntityExists reports whether entity is live.

func (*World) MustCreateEntity

func (world *World) MustCreateEntity() Entity

MustCreateEntity creates an entity or panics. It is convenient during trusted world construction; runtime code should generally use CreateEntity.

func (*World) RemoveSystem

func (world *World) RemoveSystem(id SystemID) bool

RemoveSystem removes a registration and reports whether it existed. A system removed during Update will not run on the next update.

func (*World) Subscribe

func (world *World) Subscribe(options ...FilterOption) (*Subscription, error)

Subscribe creates an independent subscription. An empty filter matches every live entity. Component handles from another world return ErrForeignComponent.

Example
package main

import (
	"fmt"

	"github.com/gravestench/akara"
)

type examplePosition struct{ X, Y float64 }
type exampleVelocity struct{ X, Y float64 }

func main() {
	world := akara.NewWorld()
	defer func() { _ = world.Close() }()
	positions := akara.Register[examplePosition](world)
	velocities := akara.Register[exampleVelocity](world)
	moving, _ := world.Subscribe(akara.All(positions, velocities))
	defer func() { _ = moving.Close() }()

	entity := world.MustCreateEntity()
	_, _ = positions.Add(entity)
	_, _ = velocities.Set(entity, exampleVelocity{X: 2})
	fmt.Println(moving.Entities())
}
Output:
[1]

func (*World) Systems

func (world *World) Systems() []SystemID

Systems returns the registered IDs in update order.

func (*World) Update

func (world *World) Update(delta time.Duration) error

Update runs a stable snapshot of systems sequentially. Concurrent Update calls are serialized.

Example
package main

import (
	"fmt"
	"time"

	"github.com/gravestench/akara"
)

func main() {
	world := akara.NewWorld()
	defer func() { _ = world.Close() }()
	_, _ = world.AddSystem(akara.SystemFunc(func(_ *akara.World, delta time.Duration) error {
		fmt.Println(delta)
		return nil
	}))
	_ = world.Update(16 * time.Millisecond)
}
Output:
16ms

Jump to

Keyboard shortcuts

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