Donburi
![Go Reference](https://pkg.go.dev/badge/github.com/yohamta/donburi.svg)
Donburi is an Entity Component System library for Go / Ebitengine inspired by legion.
It aims to be a feature rich and high-performance ECS Library.
Contents
Summary
- It introduces the concept of Archetype, which allows us to query entities very efficiently based on the components layout.
- It is possible to combine
And
, Or
, and Not
conditions to perform complex queries for components. - It avoids reflection for performance.
- Ability to dynamically add or remove components from an entity.
- Type-safe APIs powered by Generics
- Zero dependencies
- Provides Features that are common in game dev (e.g.,
math
, transform
, hieralchy
, events
, etc) built on top of the ECS architecture.
Examples
To check all examples, visit this page.
The bunnymark example was adapted from mizu's code, which is made by sedyh.
![](https://github.com/yohamta/donburi/raw/HEAD/./examples/platformer/assets/images/example.gif)
Installation
go get github.com/yohamta/donburi
Getting Started
Worlds
import "github.com/yohamta/donburi"
world := donburi.NewWorld()
Entities can be created via either Create
(for a single entity) or CreateMany
(for a collection of entities with the same component types). The world will create a unique ID for each entity upon insertion that we can use to refer to that entity later.
type PositionData struct {
X, Y float64
}
type VelocityData struct {
X, Y float64
}
var Position = donburi.NewComponentType[PositionData]()
var Velocity = donburi.NewComponentType[VelocityData]()
entity = world.Create(Position, Velocity)
entry := world.Entry(entity)
Position.SetValue(entry, math.Vec2{X: 10, Y: 20})
Velocity.SetValue(entry, math.Vec2{X: 1, Y: 2})
position := Position.Get(entry)
velocity := Velocity.Get(entry)
position.X += velocity.X
position.Y += velocity.y
Components can be added and removed through Entry
objects.
query := donburi.NewQuery(filter.Contains(PlayerTag))
if entry, ok := query.First(world); ok {
donburi.Add(entry, Position, &PositionData{
X: 100,
Y: 100,
})
donburi.Remove(entry, Velocity)
}
Entities can be removed from World with the World.Remove() as follows:
if SomeLogic.IsDead(world, someEntity) {
world.Remove(someEntity)
if world.Valid(someEntity) == false {
println("this entity is invalid")
}
}
Entities can be retrieved using the First
and Iter
methods of Components as follows:
type GameStateData struct {
}
var GameState = donburi.NewComponentType[GameStateData]()
type BulletData struct {
}
var Bullet = donburi.NewComponentType[BulletData]()
world := donburi.NewWorld()
world.Create(GameState)
world.CreateMany(100, Bullet)
if entry, ok := GameState.First(world); ok {
gameState := GameState.Get(entry)
}
for entry := range Bullet.Iter(world) {
bullet := Bullet.Get(entry)
}
Queries
Queries allow for high performance and expressive iteration through the entities in a world, to get component references, test if an entity has a component or to add and remove components.
query := donburi.NewQuery(filter.Contains(Position, Velocity))
for entry := range query.Iter(world) {
position := Position.Get(entry)
velocity := Velocity.Get(entry)
position.X += velocity.X
position.Y += velocity.Y
}
There are other types of filters such as And
, Or
, Exact
and Not
. Filters can be combined wth to find the target entities.
For example:
query := donburi.NewQuery(filter.And(
filter.Contains(NpcTag),
filter.Not(filter.Contains(Position))))
If you need to determine if an entity has a component, there is entry.HasComponent
For example:
query := donburi.NewQuery(
filter.And(
filter.Contains(Position, Size),
filter.Or(
filter.Contains(Sprite),
filter.Contains(Text),
filter.Contains(Shape),
),
),
)
for entry := range query.Iter(world) {
position := Position.Get(entry)
size := Size.Get(entry)
if entry.HasComponent(Sprite) {
sprite := Sprite.Get(entry)
}
if entry.HasComponent(Text) {
text := Text.Get(entry)
}
if entry.HasComponent(Shape) {
shape := Shape.Get(entry)
}
}
Ordered Queries
Sometimes you may need to iterate a query in a specific order. Donburi supports this through the OrderedQuery[T]
type.
In order to use this, the component must implement the IOrderable interface:
type IOrderable interface {
Order() int
}
Example:
Here we assume the spatial.TransformComponent
implements Order()
.
q := donburi.NewOrderedQuery[spatial.Transform](
filter.Contains(sprite.Component, spatial.TransformComponent))
for entry := range q.IterOrdered(w) {
}
Tags
One or multiple "Tag" components can be attached to an entity. "Tag"s are just components with a single name string as data.
Here is the utility function to create a tag component.
func NewTag(name string) *ComponentType {
return NewComponentType(Tag(name))
}
Since "Tags" are components, they can be used in queries in the same way as components as follows:
var EnemyTag = donburi.NewTag("Enemy")
world.CreateMany(100, EnemyTag, Position, Velocity)
for entry := range EnemyTag.Iter(world) {
}
Systems (Experimental)
⚠ this feature is currently experimental, the API can be changed in the future.
The ECS package provides so-called System feature in ECS which can be used together with a World
instance.
See the GoDoc and Example.
How to create an ECS instance:
import (
"github.com/yohamta/donburi"
ecslib "github.com/yohamta/donburi/ecs"
)
world := donburi.NewWorld()
ecs := ecslib.NewECS(world)
A System
is created from just a function that receives an argument (ecs *ecs.ECS)
.
func SomeFunction(ecs *ecs.ECS) {
}
ecs.AddSystem(SomeFunction)
We can provide Renderer
for certain system.
ecs.AddRenderer(ecs.LayerDefault, DrawBackground)
ecs.Draw(screen)
The Layer
parameter allows us to control the order of rendering systems and to which screen to render. A Layer
is just an int
value. The default value is just 0
.
For example:
const (
LayerBackground ecslib.LayerID = iota
LayerActors
)
ecs.
AddSystem(UpdateBackground).
AddSystem(UpdateActors).
AddRenderer(LayerBackground, DrawBackground).
AddRenderer(LayerActors, DrawActors)
func (g *Game) Draw(screen *ebiten.Image) {
screen.Clear()
g.ecs.DrawLayer(LayerBackground, screen)
g.ecs.DrawLayer(LayerActors, screen)
}
The ecs.Create()
and ecs.NewQuery()
wrapper-functions allow to create and query entities on a certain Layer
:
For example:
var layer0 ecs.LayerID = 0
ecslib.Create(layer0, someComponents...)
queryForLayer0 := ecslib.NewQuery(layer0, filter.Contains(someComponent))
Debug
The debug package provides some debug utilities for World
.
For example:
debug.PrintEntityCounts(world)
Features
Under the features directory, we develop common functions for game dev. Any kind of Issues or PRs will be very appreciated.
Math
The math package provides the basic types (Vec2 etc) and helpers.
See the GoDoc for more details.
Transform
The transform package provides the Tranform
Component and helpers.
It allows us to handle position
, rotation
, scale
data relative to the parent.
This package was adapted from ariplane's code, which is created by m110.
For example:
w := donburi.NewWorld()
parent := w.Entry(w.Create(transform.Transform))
transform.SetWorldPosition(parent, dmath.Vec2{X: 1, Y: 2})
transform.SetWorldScale(parent, dmath.Vec2{X: 2, Y: 3})
child := w.Entry(w.Create(transform.Transform))
transform.Transform.SetValue(child, transform.TransformData{
LocalPosition: dmath.Vec2{X: 1, Y: 2},
LocalRotation: 90,
LocalScale: dmath.Vec2{X: 2, Y: 3},
})
transform.AppendChild(parent, child, false)
pos := transform.WorldPosition(child)
rot := transform.WorldRotation(child)
scale := transform.WorldScale(child)
How to remove chidren (= destroy entities):
transform.RemoveChildrenRecursive(parent)
transform.RemoveRecursive(parent)
Events
The events package allows us to send arbitrary data between systems in a Type-safe manner.
This package was adapted from ariplane's code, which is created by m110.
For example:
import "github.com/yohamta/donburi/features/events"
type EnemyKilled struct {
EnemyID int
}
var EnemyKilledEvent = events.NewEventType[EnemyKilled]()
world := donburi.NewWorld()
EnemyKilledEvent.Subscribe(world, LevelUp)
EnemyKilledEvent.Subscribe(world, UpdateScore)
EnemyKilledEvent.Publish(world, EnemyKilled{EnemyID: 1})
EnemyKilledEvent.ProcessEvents(world)
events.ProcessAllEvents(world)
func LevelUp(w donburi.World, event EnemyKilled) {
}
func UpdateScore(w donburi.World, event EnemyKilled) {
}
Projects Using Donburi
Games
Libraries
- necs - Networked Entity Component System; a networking layer for donburi by gin
Architecture
![arch](https://github.com/yohamta/donburi/raw/HEAD/assets/architecture.png)
How to contribute?
Feel free to contribute in any way you want. Share ideas, questions, submit issues, and create pull requests. Thanks!
Contributors
Made with contrib.rocks.