ecs

package module
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2025 License: MIT Imports: 2 Imported by: 0

README

Simple-ECS

dead-simple library for writing game systems in Go

Install

go get github.com/BrownNPC/simple-ecs

Documentation

GoDoc can be found here

Jump to Example

Simple-ECS Features:
  • Easy syntax / api
  • Good perfomance!
  • Low level (implement what you need)
  • No Dependencies on other libraries
What is ECS? (and why you should use it)

I recommend you watch this video. feel free to skip around or watch at 2x speed

ECS is an alternative to inheritance.

Instead of creating game objects using Object Oriented Design where things inherit from each other eg. Player inherits from Actor, Actor inherits From Entity, we think about the Data. The goal is to seperate the logic from the data. This is known as Data oriented design.

In this pattern, we have entities which have components. Components are pure-data for example a position component might look like this:

type Position struct{
  X,Y float64
}

A health component might just be an integer.

Using components you can make systems. Systems are just normal functions that modify components.

For example, you may have a movement system that loops over all the entities that have a Position and a Velocity component, and then adds the Velocity to the Position of the entity

func MovementSystem(entities []entity){
	for _ ent := range entities{
		ent.Position.X += ent.Velocity.X
		ent.Position.Y += ent.Velocity.Y
	}
}
Why use ECS for writing game systems?

Because Go does not have inheritance. The language prefers seperating data from logic.

How to use Simple ECS for writing systems

Before we jump into the example, understanding how this library is implemented will help us learn it easily.

The heart of this ECS is the memory pool Think of the pool like a database or a spreadsheet. On the Y axis (columns) there are arrays of components

We use a struct called storage to hold the components arrays.

components can be any data type

These arrays are pre-allocated to a fixed size provided by the user

An entity is just an index into these arrays

So on the X axis there are entities which are just indexes

// stores slice of components
type Storage[Component any] struct {
	// slice of components
	components     []Component
	// a bitset is used to store which
	//indexes are occupied by entities
	b   bitset.BitSet
}

The storage struct also has a bitset (like an array of boleans)

Each bit in the bitset corresponds to an entity. By setting the bit on the bitset, we can keep a record of whether an entity has the component added to it.

The pool also has its own bitset that tracks which entities are alive you dont need to worry about how the pool works, just know that the pool is responsible for creating and deleting entities.

Now here is an example:

package main

import (
	ecs "github.com/BrownNPC/simple-ecs"
	"math/rand"
)

// Define component types
type Vec2 struct {
	X, Y float64
}

// components need to be concrete types
type Position Vec2
type Velocity Vec2

func main() {
	// create a memory pool of component arrays
	// the pool can hold a maximum of 1000 alive entities
	var pool = ecs.New(1000)
	// create 1000 entities
	for range 1000 {
		// entities (which are just ids)
		// should only be created using the pool
		var ent = ecs.NewEntity(pool)
		// add position and
		// velocity components to the entity
		ecs.Add2(pool, ent,
			Position{},
			Velocity{
				X: rand.Float64(),
				Y: rand.Float64(),
			})
	}
	// run movement system 60 times
	for range 60 {
		MovementSystem(pool, 1.0/60)
	}
}

// a system is a regular function that
// operates on the components
func MovementSystem(p *ecs.Pool, deltaTime float64) {
	// a storage holds a slice (array) of components
	POSITION, VELOCITY :=
		ecs.GetStorage2[ // helper function so you dont have to call GetStorage twice
			Position,
			Velocity,
		](p)
	// get entities (id/index) that have
	// a position and velocity component
	for _, ent := range POSITION.And(VELOCITY) {
		// use the entity to index the
		// position and velocity slices
		pos, vel :=
			POSITION.Get(ent),
			VELOCITY.Get(ent)
		pos.X += vel.X * deltaTime
		pos.Y += vel.Y * deltaTime
		// update position of entity
		POSITION.Update(ent, pos)
	}
}
When to not use an ECS

You dont need ECS if your game is going to be very simple like pong or flappy bird. But if you are making eg. "flappy bird with guns" then ECS makes sense. But even if you are using ECS;

Everything in your game does not need to be an entity. For example. If you are making "Chess with magic spells", you might want to represent the board state using a Chess board struct (object) and the pieces would probably be entities that have components. and you would probably have systems for animations, the timer, magic spells, and maybe checking if a piece can move to a square etc.

Your user interface (UI) would probably also not benefit from being entities.

Motivation:

The other ECS libraries seem to focus on having the best possible performance, sometimes sacrificing a simpler syntax. They also provide features I dont need. And these libraries had many ways to do the same thing. (eg. Arche has 2 apis)

I made this library to have less features,
and sacrifice a little performance
for more simplicity.
Note: if you care about every nanosecond of performance, dont use my library.
Acknowledgements

Donburi is another library that implements ECS with a simple API.

Running tests

go test -count 10 -race ./...

Documentation

Overview

entity component system that is performant an easy to use.

The heart of this ECS is the memory pool
Think of the pool like a database.
On the Y axis (columns) there are arrays of components

We use a struct called storage to hold the components arrays
 components can be any data type, but they cannot be interfaces
These arrays are pre-allocated to a fixed size provided by the user

an entity is just an index into these arrays
So on the X axis there are entities which are just indexes

The storage struct also has a bitset.

each bit in the bitset corresponds to an entity
 the bitset is used for maintaining
a record of which entity has the component the storage is storing

The pool also has its own bitset that tracks which entities are alive

	there is also a map from entities to a slice of component storages

	we update this map when an entity has a component added to it

	we use this map to go into every storage and zero out the component
	when an entity is killed

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Add

func Add[Component any](p *Pool, e Entity, c Component)

Add a component to an entity.

adding to a dead entity is a no-op. but note that this does not validate generations

func Add2

func Add2[A any, B any](p *Pool, e Entity,
	c1 A, c2 B,
)

add 2 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add3

func Add3[A any, B any, C any](p *Pool, e Entity,
	c1 A, c2 B, c3 C,
)

add 3 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add4 added in v1.3.1

func Add4[A any, B any, C any, D any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D,
)

add 4 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add5 added in v1.3.1

func Add5[A any, B any, C any, D any, E any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D, c5 E,
)

add 5 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add6 added in v1.3.1

func Add6[A any, B any, C any, D any, E any, F any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D, c5 E, c6 F,
)

add 6 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add7 added in v1.3.1

func Add7[A any, B any, C any, D any, E any, F any, G any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D, c5 E, c6 F, c7 G,
)

add 7 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add8 added in v1.3.1

func Add8[A any, B any, C any, D any, E any, F any, G any, H any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D, c5 E, c6 F, c7 G, c8 H,
)

add 8 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func Add9 added in v1.3.1

func Add9[A any, B any, C any, D any, E any, F any, G any, H any, I any](p *Pool, e Entity,
	c1 A, c2 B, c3 C, c4 D, c5 E, c6 F, c7 G, c8 H, c9 I,
)

add 9 components to an entity automatically register component if ecs.AutoRegisterComponents is true (default) This is just a wrapper arround calling ecs.Add multiple times

func GetGeneration added in v1.2.5

func GetGeneration(p *Pool, e Entity) uint32

You only need this if you are storing Entities within components

func GetStorage2

func GetStorage2[A any, B any](p *Pool) (*Storage[A], *Storage[B])

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage3

func GetStorage3[A any, B any, C any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage4

func GetStorage4[A any, B any,
	C any, D any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage5

func GetStorage5[A any, B any, C any,
	D any, E any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
	*Storage[E],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage6

func GetStorage6[A any, B any, C any,
	D any, E any, F any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
	*Storage[E], *Storage[F])

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage7

func GetStorage7[A any, B any, C any,
	D any, E any, F any, G any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
	*Storage[E], *Storage[F],
	*Storage[G],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage8

func GetStorage8[A any, B any, C any,
	D any, E any, F any, G any, H any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
	*Storage[E], *Storage[F],
	*Storage[G], *Storage[H],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func GetStorage9

func GetStorage9[A any, B any, C any,
	D any, E any, F any, G any, H any, I any](p *Pool) (
	*Storage[A], *Storage[B],
	*Storage[C], *Storage[D],
	*Storage[E], *Storage[F],
	*Storage[G], *Storage[H],
	*Storage[I],
)

storage contains all components of a type This is just a wrapper arround calling ecs.GetStorage multiple times

func IsAlive added in v1.2.4

func IsAlive(p *Pool, e Entity) bool

Check if an entity is alive.

This is not enough if you are storing entities within components

func IsAliveWithGeneration added in v1.2.7

func IsAliveWithGeneration(p *Pool, e Entity, generation Generation) bool

Check if internal generation for this Entity matches the generation you are storing

You only need this if you are storing entities

func Kill

func Kill(p *Pool, entities ...Entity)

Give an entity back to the pool, allowing recycling

func Remove

func Remove[Component any](p *Pool, e Entity)

Remove a component from an entity

removing from a dead entity is a no-op. but note that this does not validate generations

Types

type Entity

type Entity = uint32

An entity is just an integer. But if you are storing entities within components, then do not forget to also store their generation, and verify them using IsAliveWithGeneration before looping over said components

func NewEntity

func NewEntity(p *Pool) Entity

recycle a dead entity id, or create a new one

type Generation added in v1.4.0

type Generation = uint32

type Pool

type Pool struct {
	TotalEntities uint32
	// contains filtered or unexported fields
}

The pool holds Component slices within storages and tracks entity lifetimes

func New

func New(capacity uint32) (p *Pool)

type Storage

type Storage[Component any] struct {
	ID int
	// contains filtered or unexported fields
}

A storage holds a slice of components

func GetStorage

func GetStorage[Component any](p *Pool) *Storage[Component]

Get a component storage, allocate it if not already

func (*Storage[Component]) All added in v1.4.0

func (s *Storage[Component]) All() []Entity

All entities that have this component

func (*Storage[Component]) And added in v1.2.0

func (s *Storage[Component]) And(others ...storage) []Entity

All entities that have this component and the other components

func (*Storage[Component]) ButNot added in v1.2.0

func (s *Storage[Component]) ButNot(others ...storage) []Entity

All entities that have this component but not the other components

func (*Storage[Component]) EntityHasComponent

func (s *Storage[Component]) EntityHasComponent(e Entity) bool

func (*Storage[Component]) Get

func (s *Storage[Component]) Get(e Entity) Component

get a copy of a component this does not check if the entity is alive

func (*Storage[Component]) Or added in v1.4.0

func (s *Storage[Component]) Or(others ...storage) []Entity

All entities that have either components

func (*Storage[Component]) Update

func (s *Storage[Component]) Update(e Entity, c Component)

update the component of an entity. this does not check if the entity is alive

Jump to

Keyboard shortcuts

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