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
- Examples
- Installation
- Getting Started
- Ordered Queries
- Features
- Projects Using Donburi
- Architecture
- How to contribute?
- Contributors
- 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
, andNot
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.
To check all examples, visit this page.
The bunnymark example was adapted from mizu's code, which is made by sedyh.
go get github.com/yohamta/donburi
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.
// Component is any struct that holds some kind of data.
type PositionData struct {
X, Y float64
}
type VelocityData struct {
X, Y float64
}
// ComponentType represents kind of component which is used to create or query entities.
var Position = donburi.NewComponentType[PositionData]()
var Velocity = donburi.NewComponentType[VelocityData]()
// Create an entity by specifying components that the entity will have.
// Component data will be initialized by default value of the struct.
entity = world.Create(Position, Velocity)
// We can use entity (it's a wrapper of int64) to get an Entry object from World
// which allows you to access the components that belong to the entity.
entry := world.Entry(entity)
// You can set or get the data via the ComponentType
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.
// Fetch the first entity with PlayerTag component
query := donburi.NewQuery(filter.Contains(PlayerTag))
// Query.First() returns only the first entity that
// matches the query.
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() removes the entity from the world.
world.Remove(someEntity)
// Deleted entities become invalid immediately.
if world.Valid(someEntity) == false {
println("this entity is invalid")
}
}
Entities can be retrieved using the First
and Iter
methods of Components as follows:
// GameState Component
type GameStateData struct {
// .. some data
}
var GameState = donburi.NewComponentType[GameStateData]()
// Bullet Component
type BulletData struct {
// .. some data
}
var Bullet = donburi.NewComponentType[BulletData]()
// Init the world and create entities
world := donburi.NewWorld()
world.Create(GameState)
world.CreateMany(100, Bullet)
// Query the first GameState entity
if entry, ok := GameState.First(world); ok {
gameState := GameState.Get(entry)
// .. do stuff with the gameState entity
}
// Query all Bullet entities
for entry := range Bullet.Iter(world) {
bullet := Bullet.Get(entry)
// .. do stuff with the bullet entity
}
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.
// Define a query by declaring what componet you want to find.
query := donburi.NewQuery(filter.Contains(Position, Velocity))
// Iterate through the entities found in the world
for entry := range query.Iter(world) {
// An entry is an accessor to entity and its components.
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:
// This query retrieves entities that have an NpcTag and no Position component.
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:
// We have a query for all entities that have Position and Size, but also any of Sprite, Text or Shape.
query := donburi.NewQuery(
filter.And(
filter.Contains(Position, Size),
filter.Or(
filter.Contains(Sprite),
filter.Contains(Text),
filter.Contains(Shape),
),
),
)
// In our query we can check if the entity has some of the optional components before attempting to retrieve them
for entry := range query.Iter(world) {
// We'll always be able to access Position and Size
position := Position.Get(entry)
size := Size.Get(entry)
if entry.HasComponent(Sprite) {
sprite := Sprite.Get(entry)
// .. do sprite things
}
if entry.HasComponent(Text) {
text := Text.Get(entry)
// .. do text things
}
if entry.HasComponent(Shape) {
shape := Shape.Get(entry)
// .. do shape things
}
}
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) {
// This will be iterated according to the spatial.TransformComponent's Order() function.
}
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.
// This is the utility function to make 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)
// Search entities with EnemyTag
for entry := range EnemyTag.Iter(world) {
// Perform some operation on the Entities with the EnemyTag component.
}
⚠ 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.
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)
.
// Some System's function
func SomeFunction(ecs *ecs.ECS) {
// ...
}
ecs.AddSystem(SomeFunction)
We can provide Renderer
for certain system.
ecs.AddRenderer(ecs.LayerDefault, DrawBackground)
// Draw all systems
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
// Create an entity on layer0
ecslib.Create(layer0, someComponents...)
// Create a query to iterate entities on layer0
queryForLayer0 := ecslib.NewQuery(layer0, filter.Contains(someComponent))
The debug package provides some debug utilities for World
.
For example:
debug.PrintEntityCounts(world)
// [Example Output]
// Entity Counts:
// Archetype Layout: {TransformData, Size, SpriteData, EffectData } has 61 entities
// Archetype Layout: {TransformData, Size, SpriteData, ColliderData } has 59 entities
// Archetype Layout: {TransformData, Size, SpriteData, WeaponData} has 49 entities
// ...
Under the features directory, we develop common functions for game dev. Any kind of Issues or PRs will be very appreciated.
The math package provides the basic types (Vec2 etc) and helpers.
See the GoDoc for more details.
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()
// setup parent
parent := w.Entry(w.Create(transform.Transform))
// set world position and scale for the parent
transform.SetWorldPosition(parent, dmath.Vec2{X: 1, Y: 2})
transform.SetWorldScale(parent, dmath.Vec2{X: 2, Y: 3})
// setup child
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},
})
// add the child to the parent
transform.AppendChild(parent, child, false)
// get world position of the child with parent's position taken into account
pos := transform.WorldPosition(child)
// roatation
rot := transform.WorldRotation(child)
// scale
scale := transform.WorldScale(child)
How to remove chidren (= destroy entities):
// Remove children
transform.RemoveChildrenRecursive(parent)
// Remove children and the parent
transform.RemoveRecursive(parent)
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"
// Define any data
type EnemyKilled struct {
EnemyID int
}
// Define an EventType with the type of the event data
var EnemyKilledEvent = events.NewEventType[EnemyKilled]()
// Create a world
world := donburi.NewWorld()
// Add handlers for the event
EnemyKilledEvent.Subscribe(world, LevelUp)
EnemyKilledEvent.Subscribe(world, UpdateScore)
// Sending an event
EnemyKilledEvent.Publish(world, EnemyKilled{EnemyID: 1})
// Process specific events
EnemyKilledEvent.ProcessEvents(world)
// Process all events
events.ProcessAllEvents(world)
// Receives the events
func LevelUp(w donburi.World, event EnemyKilled) {
// .. processs the event for levelup
}
func UpdateScore(w donburi.World, event EnemyKilled) {
// .. processs the event for updating the player's score
}
- airplanes - A 2D shoot 'em up game by m110
- goingo - Go game implemented in the Go language by joelschutz
- revdriller - An action puzzle game by yohamta
Feel free to contribute in any way you want. Share ideas, questions, submit issues, and create pull requests. Thanks!
Made with contrib.rocks.