donburi

package module
v0.0.0-...-07b684a Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2024 License: CC0-1.0, MIT Imports: 12 Imported by: 13

README

donburi

Donburi

Go Reference

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.

Installation

go get github.com/bitver/donburi

Getting Started

Worlds

import "github.com/bitver/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

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
  }
}

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) {
  // This will be iterated according to the spatial.TransformComponent's Order() function.
}

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.

// 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.
}

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/bitver/donburi"
  ecslib "github.com/bitver/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))

Debug

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
// ...

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()

// 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)

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/bitver/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
}

Projects Using Donburi

Games

Libraries

  • necs - Networked Entity Component System; a networking layer for donburi by gin

Architecture

arch

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.

Documentation

Overview

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.

Index

Constants

This section is empty.

Variables

View Source
var Null = storage.Null

Null represents a invalid entity which is zero.

Functions

func Add

func Add[T any](e *Entry, c component.IComponentType, component *T)

Add adds the component to the entry.

func Get

func Get[T any](e *Entry, c component.IComponentType) *T

Get returns the component from the entry

func GetComponents

func GetComponents(e *Entry) []any

GetComponents uses reflection to convert the unsafe.Pointers of an entry into its component data instances. Note that this is likely to be slow and should not be used in hot paths or if not necessary.

func GetValue

func GetValue[T any](e *Entry, c component.IComponentType) T

GetValue gets the value of the component.

func RegisterInitializer

func RegisterInitializer(initializer initializer)

RegisterInitializer registers an initializer for a world.

func Remove

func Remove[T any](e *Entry, ctype component.IComponentType)

Remove removes the component from the entry.

func Set

func Set[T any](e *Entry, c component.IComponentType, component *T)

Set sets the comopnent of the entry.

func SetValue

func SetValue[T any](e *Entry, c component.IComponentType, value T)

SetValue sets the value of the component.

func Valid

func Valid(e *Entry) bool

Valid returns true if the entry is valid.

Types

type ComponentType

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

CompnentType represents a type of component. It is used to identify a component when getting or setting components of an entity.

func NewComponentType

func NewComponentType[T any](opts ...T) *ComponentType[T]

NewComponentType creates a new component type. The function is used to create a new component of the type. It receives a function that returns a pointer to a new component. The first argument is a default value of the component.

func NewTag

func NewTag(opts ...any) *ComponentType[Tag]

NewTag is an utility to create a tag component. Which is just an component that contains no data. Specify a string as the first and only parameter if you wish to name the component.

func (*ComponentType[T]) Each

func (c *ComponentType[T]) Each(w World, callback func(*Entry))

Each iterates over the entities that have the component.

func (*ComponentType[T]) EachEntity

func (c *ComponentType[T]) EachEntity(w World, callback func(*Entry))

deprecated: use Each instead

func (*ComponentType[T]) First

func (c *ComponentType[T]) First(w World) (*Entry, bool)

First returns the first entity that has the component.

func (*ComponentType[T]) FirstEntity

func (c *ComponentType[T]) FirstEntity(w World) (*Entry, bool)

deprecated: use First instead

func (*ComponentType[T]) Get

func (c *ComponentType[T]) Get(entry *Entry) *T

Get returns component data from the entry.

func (*ComponentType[T]) GetValue

func (c *ComponentType[T]) GetValue(entry *Entry) T

GetValue returns value of the component from the entry.

func (*ComponentType[T]) Id

Id returns the component type id.

func (*ComponentType[T]) Iter

func (c *ComponentType[T]) Iter(w World) iter.Seq[*Entry]

Iter returns an iterator for the entities that have the component.

func (*ComponentType[T]) MustFirst

func (c *ComponentType[T]) MustFirst(w World) *Entry

MustFirst returns the first entity that has the component or panics.

func (*ComponentType[T]) MustFirstEntity

func (c *ComponentType[T]) MustFirstEntity(w World) *Entry

deprecated: use MustFirst instead

func (*ComponentType[T]) Name

func (c *ComponentType[T]) Name() string

Name returns the component type name.

func (*ComponentType[T]) New

func (c *ComponentType[T]) New() unsafe.Pointer

func (*ComponentType[T]) Set

func (c *ComponentType[T]) Set(entry *Entry, component *T)

Set sets component data to the entry.

func (*ComponentType[T]) SetName

func (c *ComponentType[T]) SetName(name string) *ComponentType[T]

SetName sets the component type name.

func (*ComponentType[T]) SetValue

func (c *ComponentType[T]) SetValue(entry *Entry, value T)

SetValue sets the value of the component.

func (*ComponentType[T]) String

func (c *ComponentType[T]) String() string

String returns the component type name.

func (*ComponentType[T]) Typ

func (c *ComponentType[T]) Typ() reflect.Type

Typ returns the reflect.Type of the ComponentType.

type Entity

type Entity = storage.Entity

Entity is identifier of an entity. Entity is just a wrapper of uint64.

type Entry

type Entry struct {
	World *world
	// contains filtered or unexported fields
}

Entry is a struct that contains an entity and a location in an archetype.

func (*Entry) AddComponent

func (e *Entry) AddComponent(c component.IComponentType, components ...unsafe.Pointer)

AddComponent adds the component to the entity.

func (*Entry) Archetype

func (e *Entry) Archetype() *storage.Archetype

Archetype returns the archetype.

func (*Entry) Component

func (e *Entry) Component(c component.IComponentType) unsafe.Pointer

Component returns the component.

func (*Entry) Entity

func (e *Entry) Entity() Entity

Entity returns the entity.

func (*Entry) HasComponent

func (e *Entry) HasComponent(c component.IComponentType) bool

HasComponent returns true if the entity has the given component type.

func (*Entry) Id

func (e *Entry) Id() storage.EntityId

Id returns the entity id.

func (*Entry) Remove

func (e *Entry) Remove()

Remove removes the entity from the world.

func (*Entry) RemoveComponent

func (e *Entry) RemoveComponent(c component.IComponentType)

RemoveComponent removes the component from the entity.

func (*Entry) SetComponent

func (e *Entry) SetComponent(c component.IComponentType, component unsafe.Pointer)

SetComponent sets the component.

func (*Entry) String

func (e *Entry) String() string

func (*Entry) Valid

func (e *Entry) Valid() bool

Valid returns true if the entry is valid.

type IComponentType

type IComponentType = component.IComponentType

IComponentType is an interface for component types.

func AllComponentTypes

func AllComponentTypes() []IComponentType

AllComponentTypes returns all IComponentTypes created at that point in time. All types created using `donburi.NewComponentType()` will be in this list. This is useful if you want to use introspection on the ECS, such as when doing (de)serialization. This includes components which are not in any archetypes or on any entity.

type IOrderable

type IOrderable interface {
	Order() int
}

type OrderedEntryIterator

type OrderedEntryIterator[T IOrderable] struct {
	// contains filtered or unexported fields
}

OrderedEntryIterator is an iterator for entries from a list of `[]Entity`.

func NewOrderedEntryIterator

func NewOrderedEntryIterator[T IOrderable](current int, w World, entities []Entity, orderedBy *ComponentType[T]) OrderedEntryIterator[T]

OrderedEntryIterator is an iterator for entries based on a list of `[]Entity`.

func (*OrderedEntryIterator[T]) HasNext

func (it *OrderedEntryIterator[T]) HasNext() bool

HasNext returns true if there are more entries to iterate over.

func (*OrderedEntryIterator[T]) Next

func (it *OrderedEntryIterator[T]) Next() *Entry

Next returns the next entry.

type OrderedQuery

type OrderedQuery[T IOrderable] struct {
	Query
	// contains filtered or unexported fields
}

OrderedQuery is a special extension of Query which has a type parameter used when running ordered queries using `EachOrdered`.

func NewOrderedQuery

func NewOrderedQuery[T IOrderable](filter filter.LayoutFilter) *OrderedQuery[T]

NewOrderedQuery creates a new ordered query. It takes a filter parameter that is used when evaluating the query. Use `OrderedQuery.EachOrdered` to run a Each query in ordered mode.

func (*OrderedQuery[T]) EachOrdered

func (q *OrderedQuery[T]) EachOrdered(w World, orderBy *ComponentType[T], callback func(*Entry))

EachOrdered iterates over all entities within the query filter, and uses the `orderBy` parameter to figure out which property to order using. `T` must implement `IOrderable`

func (*OrderedQuery[T]) IterOrdered

func (q *OrderedQuery[T]) IterOrdered(w World, orderBy *ComponentType[T]) iter.Seq[*Entry]

IterOrdered returns an iterator over all entities within the query filter, ordered by the specified component.

type Query

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

Query represents a query for entities. It is used to filter entities based on their components. It receives arbitrary filters that are used to filter entities. It contains a cache that is used to avoid re-evaluating the query. So it is not recommended to create a new query every time you want to filter entities with the same query.

func NewQuery

func NewQuery(filter filter.LayoutFilter) *Query

NewQuery creates a new query. It receives arbitrary filters that are used to filter entities.

func (*Query) Count

func (q *Query) Count(w World) int

Count returns the number of entities that match the query.

func (*Query) Each

func (q *Query) Each(w World, callback func(*Entry))

Each iterates over all entities that match the query.

func (*Query) EachEntity

func (q *Query) EachEntity(w World, callback func(*Entry))

deprecated: use Each instead

func (*Query) First

func (q *Query) First(w World) (entry *Entry, ok bool)

First returns the first entity that matches the query.

func (*Query) FirstEntity

func (q *Query) FirstEntity(w World) (entry *Entry, ok bool)

deprecated: use First instead

func (*Query) Iter

func (q *Query) Iter(w World) iter.Seq[*Entry]

type StorageAccessor

type StorageAccessor struct {
	// Index is the search index for the world.
	Index *storage.Index
	// Components is the component storage for the world.
	Components *storage.Components
	// Archetypes is the archetype storage for the world.
	Archetypes []*storage.Archetype
}

StorageAccessor is an accessor for the world's storage.

type Tag

type Tag string

type World

type World interface {
	// Id returns the unique identifier for the world.
	Id() WorldId
	// Create creates a new entity with the specified components.
	Create(components ...component.IComponentType) Entity
	// CreateMany creates a new entity with the specified components.
	CreateMany(n int, components ...component.IComponentType) []Entity
	// Entry returns an entry for the specified entity.
	Entry(entity Entity) *Entry
	// Remove removes the specified entity.
	Remove(entity Entity)
	// Valid returns true if the specified entity is valid.
	Valid(e Entity) bool
	// Len returns the number of entities in the world.
	Len() int
	// StorageAccessor returns an accessor for the world's storage.
	// It is used to access components and archetypes by queries.
	StorageAccessor() StorageAccessor
	// ArcheTypes returns the archetypes in the world.
	Archetypes() []*storage.Archetype

	// OnCreate registers a callback function that gets triggered when an entity is created.
	OnCreate(callback func(world World, entity Entity))

	// OnRemove registers a callback function that gets triggered when an entity is removed.
	// Note that it is called before the entity is removed from the ECS.
	OnRemove(callback func(world World, entity Entity))
}

World is a collection of entities and components.

func NewWorld

func NewWorld() World

NewWorld creates a new world.

type WorldId

type WorldId int

WorldId is a unique identifier for a world.

Directories

Path Synopsis
examples
bunnymark module
bunnymark_ecs module
platformer module
features
transform
This code is adapted from https://github.com/m110/airplanes (author: m110)
This code is adapted from https://github.com/m110/airplanes (author: m110)
internal

Jump to

Keyboard shortcuts

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