ecs
Build your own Game-Engine based on the Entity Component System concept in Golang
About ecs
ECS - Entity Component System
Build a game engine in Go out of entities, components and systems. It is for people who want that core and nothing else: no rendering, no input, no physics, and no dependencies — you bring the game library you like.
- Data and behaviour stay apart. A component is data. A system is behaviour. An entity is just an id and a bag of components.
- Nothing else is in the box. The module has zero dependencies, so it adds none to your game.
- Small enough to read in one sitting. About 200 lines of code.
Install
go get github.com/andygeiss/ecs
30 seconds
package main
import (
"context"
"fmt"
"github.com/andygeiss/ecs"
)
const maskPosition = uint64(1 << 0)
type position struct{ X, Y int }
func (p *position) Mask() uint64 { return maskPosition }
// stepSystem moves every position by one, three times, then stops the engine.
type stepSystem struct{ steps int }
func (s *stepSystem) Setup() { s.steps = 3 }
func (s *stepSystem) Process(em ecs.EntityManager) (state int) {
for _, e := range em.FilterByMask(maskPosition) {
e.Get(maskPosition).(*position).X++
}
s.steps--
if s.steps == 0 {
return ecs.StateEngineStop
}
return ecs.StateEngineContinue
}
func (s *stepSystem) Teardown() {}
func main() {
em := ecs.NewEntityManager()
em.Add(ecs.NewEntity("player", []ecs.Component{&position{}}))
sm := ecs.NewSystemManager()
sm.Add(&stepSystem{})
engine := ecs.NewDefaultEngine(em, sm)
engine.Setup()
defer engine.Teardown()
engine.Run(context.Background())
fmt.Println(em.Get("player").Get(maskPosition).(*position).X) // 3
}
Example engine
See engine-example for a real implementation using raylib.
Walkthrough
Project layout
At first we create a basic project layout:
mkdir ecs-example
cd ecs-example
go mod init example
mkdir components systems
Next we create a main.go with the following content:
package main
import (
"context"
"os"
"os/signal"
"github.com/andygeiss/ecs"
)
func main() {
// Ctrl-C cancels the context, and the engine stops after the current pass.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
em := ecs.NewEntityManager()
sm := ecs.NewSystemManager()
de := ecs.NewDefaultEngine(em, sm)
de.Setup()
defer de.Teardown()
de.Run(ctx)
}
The program loops until you press Ctrl-C, because no system tells it to stop yet.
It spins a core flat out while it does: Run never sleeps, so pacing the loop is a
system's job — a renderer waiting for vsync, say.
The movement system
A system needs to implement the methods defined by the interface
System.
So we create a new file locally at systems/movement.go:
package systems
import (
"github.com/andygeiss/ecs"
)
type movementSystem struct{}
func (a *movementSystem) Process(em ecs.EntityManager) (state int) {
// This state simply tells the engine to stop after the first call.
return ecs.StateEngineStop
}
func (a *movementSystem) Setup() {}
func (a *movementSystem) Teardown() {}
func NewMovementSystem() ecs.System {
return &movementSystem{}
}
Now we can add the following lines to main.go:
sm := ecs.NewSystemManager()
sm.Add(systems.NewMovementSystem()) // <--
de := ecs.NewDefaultEngine(em, sm)
If we start our program now, it returns immediately without looping forever.
The player entity
A game engine usually processes different types of components that represent information about the game world itself. A component only represents the data, and the systems are there to implement the behavior or game logic and change these components. Entities are simply a composition of components that provide a scalable data-oriented architecture.
A component needs to implement the methods defined by the interface
Component.
Let's define our Player components by first creating a mask at
components/components.go:
package components
const (
MaskPosition = uint64(1 << 0)
MaskVelocity = uint64(1 << 1)
)
Then create a component for Position and Velocity by creating
corresponding files such as components/position.go:
package components
type Position struct {
X float32 `json:"x"`
Y float32 `json:"y"`
}
func (a *Position) Mask() uint64 {
return MaskPosition
}
func (a *Position) WithX(x float32) *Position {
a.X = x
return a
}
func (a *Position) WithY(y float32) *Position {
a.Y = y
return a
}
func NewPosition() *Position {
return &Position{}
}
Now we can add the following lines to main.go:
em := ecs.NewEntityManager()
em.Add(ecs.NewEntity("player", []ecs.Component{ // <--
components.NewPosition().
WithX(10).
WithY(10),
components.NewVelocity().
WithX(100).
WithY(100),
})) // -->
Extend the movement system
Our final step is to add behavior to our movement system:
func (a *movementSystem) Process(em ecs.EntityManager) (state int) {
for _, e := range em.FilterByMask(components.MaskPosition | components.MaskVelocity) {
position := e.Get(components.MaskPosition).(*components.Position)
velocity := e.Get(components.MaskVelocity).(*components.Velocity)
position.X += velocity.X * rl.GetFrameTime()
position.Y += velocity.Y * rl.GetFrameTime()
}
return ecs.StateEngineStop
}
The movement system now moves every entity which has a position and velocity component.
We can replace ecs.StateEngineStop with ecs.StateEngineContinue later if we add
another system to handle user input.
A rendering system is also essential for a game, so you can use game libraries such as raylib or SDL. This system could look like this with raylib:
// ...
func (a *renderingSystem) Setup() {
rl.InitWindow(a.width, a.height, a.title)
}
func (a *renderingSystem) Process(em ecs.EntityManager) (state int) {
// First check if app should stop.
if rl.WindowShouldClose() {
return ecs.StateEngineStop
}
// Clear the screen
if rl.IsWindowReady() {
rl.BeginDrawing()
rl.ClearBackground(rl.Black)
rl.DrawFPS(10, 10)
rl.EndDrawing()
}
return ecs.StateEngineContinue
}
func (a *renderingSystem) Teardown() {
rl.CloseWindow()
}
Upgrading to v0.4.0
Two changes, and only the first one touches your code.
Runtakes acontext.Context.de.Run()becomesde.Run(ctx), and the engine now stops when that context is cancelled — checked once per pass over the systems. Passcontext.Background()if you do not want that.System.Processis unchanged, so your systems stay as they are.- The constructors return structs rather than interfaces:
*DefaultEngine,*DefaultEntityManager,*DefaultSystemManager. Existing call sites keep compiling — the interfacesEngine,EntityManagerandSystemManagerare still there.
One bug went with them: FilterByNames counted a name once per matching component, so an
entity carrying two components with the same name matched a name it did not carry at all.
Working on this repository
make # every gate against the working tree; run before a commit
make ci # the same gates against the commit; run before a push
make fmt # apply goimports and go fix
make test # the inner loop
Built to the engineering baseline — its
checklists/library.md is the definition of done here. No rule is waived.
Frequently Asked Questions
What is ecs?
ecs is a Game Development library for the Go programming language. Build your own Game-Engine based on the Entity Component System concept in Golang
How do I install ecs?
Install ecs with the Go module system using `go get andygeiss/ecs`. Check the repository for the current installation instructions.
What category does ecs belong to?
ecs is listed under Game Development, specifically Game Development.