akara

package module
v0.0.0-...-01aaae5 Latest Latest
Warning

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

Go to latest
Published: Feb 24, 2023 License: MIT Imports: 7 Imported by: 49

README

A Golang Entity Component System

A Golang Entity Component System implementation

tl;dr

If you really want to just see how to use this package, skip ahead to the Examples section.

This ECS implementation provides at-runtime entityy/component association without heavy use of reflection.

What is ECS?

Entity Component System is a design pattern.

In ECS, an entity represents anything, and is defined through composition
as opposed to inheritance. What this means is that an entity is composed of pieces, instead of defined with a class hierarchy. More concretely, this means that an entity ends up being an identifier that points to various "aspects" about the entity. All "aspects" of an entity are expressed as components.

In practice, components store data, and hardly ever any logic. Components express the various aspects of an entity; position, velocity, rotation, scale, etc.

Systems end up being where most logic is located. Most systems will iterate over entities and use the entity's components for performing whatever logic the system requires. Consider a MovementSystem, which will operate upon all entities that have a Position and Velocity component.

Peculiarities about Akara

There are several parts of Akara's API that are peculiar only to Akara.

These things include the following:

  • The ECS World
  • Component registration
  • Component Factories
  • Component Subscription & Component Filters
  • System tick rates
  • there's likely a lot more to list here, this doc needs editing...

Here are some big-picture ideas to keep in mind when working with Akara:

  • Entities are literally just numbers (uint64's)
  • Components must be registered in the ECS world
  • Every entity has a BitSet that describes which components it has
  • There is only one component factory for any given component type
  • Systems are in charge of their own tick rate
The ECS World

The World is the single place where systems, components, and entities are stored and managed. The world is in charge of authoring entity ID's, registering components, creating and updating component subscriptions, etc.

Here is a very simple example of creating a World and invoking the update loop:

package main

import (
	"github.com/gravestench/akara"

	"github.com/example/project/systems/magick"
	"github.com/example/project/systems/monster"
	"github.com/example/project/systems/itemspawn"
)

func main() {
	cfg := akara.NewWorldConfig()

	cfg.With(&monster.System{}).
		With(&itemspawn.System{}).
		With(&magick.System{})

	world := akara.NewWorld(cfg)

	for {
		if err := world.Update(); err != nil {
			panic(err)
		}
	}
}
Component registration

Creating components in Akara requires registering the component withing a World. The World will only ever have a single component factory for any given component.

Registering a component will do two things:

  1. associate a unique ID for the component type
  2. create an abstract component factory

Here's an example Velocity component:

type Velocity struct {
	x, y float64
}

To make this implement the akara.Component interface, we need a New method:

func (*Velocity) New() akara.Component {
	return &Velocity{}
}

Now we can register the component:

// this only yields the component ID
velocityID := world.RegisterComponent(&Velocity{})
Component Factories

We could use the component ID to grab the abstract component factory:

factory := world.GetComponentFactory(velocityID)

Component factories are used to create, retrieve, and remove components from entities:

e := world.NewEntity()

// returns a akara.Component interface!
c := factory.Add(e) 

// we must cast the interface to our concrete component type
v := c.(*Velocity) 

Notice how we always have to cast the returned interface back to our concrete component implementation? We can get around this annoyance by making a concrete component factory:

type VelocityFactory struct {
	*akara.ComponentFactory
}

func (m *VelocityFactory) Add(id akara.EID) *Velocity {
	return m.ComponentFactory.Add(id).(*Velocity)
}

func (m *VelocityFactory) Get(id akara.EID) (*Velocity, bool) {
	component, found := m.ComponentFactory.Get(id)
	if !found {
		return nil, found
	}

	return component.(*Velocity), found
}

this allows us to just use Add and Get without having to cast the returned value.

It's worth mentioning that each distinct component type that is registered will only have one component factory and one component ID.

Here, we try to register the same component twice, but nothing bad happens:

id1 := world.RegisterComponent(&Velocity{})
id2 := world.RegisterComponent(&Velocity{})

isSame := id1 == id2 // true 
Entities

An Entity is just a unique uint64, nothing more.

Systems

Systems are fairly simple in that they need only implement this interface:

type System interface {
	Active() bool
	SetActive(bool)
	Update()
}

However, there are a couple of concrete system types provided which you can use to create your own systems.

The first is BaseSystem, and it has its own Active and SetActive methods.

type BaseSystem struct {
   *World
   active bool
}

func (s *BaseSystem) Active() bool {
   return s.active
}

func (s *BaseSystem) SetActive(b bool) {
   s.active = b
} 

You can embed the BaseSystem in your own system like this:

type ExampleSystem struct {
	*BaseSystem
}

func (s *ExampleSystem) Update() {
	// do stuff
}

The second type of system is a SubscriberSystem, but before we talk about that we need to talk about subscriptions...

Component Subscription & Component Filters

Before we can talk about Subscriptions or ComponentFilters, we need to know what a BitSet is.

BitSets

BitSets are just a bunch of booleans, packed up in a slice of uint64's.

type BitSet struct {
	groups []uint64
}

These are used by the EntityManager to signify which components an entity currently has . Whenever a ComponentMap adds or removes a component for an entity, the EntityManager will update the entity's bitset.

Remember how the Component types all have a unique ID? That ID corresponds to the bit index that is toggled in the BitSet when a component is being added or removed!

ComponentFilters

ComponentFilters also use BitSets, but they use them for comparisons against an entity bitset .

type ComponentFilter struct {
	Required    *BitSet
	OneRequired *BitSet
	Forbidden   *BitSet
}

When an entity bitset is evaluated by a ComponentFilter, each of the Filter's Bitsets is used to determine if the entity should be allowed to pass through the filter.

When determining if an entity's component bitset will pass through the ComponentFilter:

  • Required -- The entity bitset must contain true bits for all true bits present in the Required bitset.
  • OneRequired -- The entity bitset must have at least one true bit in common with the OneRequired BitSet
  • Forbidden -- The entity bitset must not contain any true bits for any true bits present in the Forbidden bitset
Subscriptions

Subscriptions are simply a combination of a ComponentFilter and a slice of entity ID's:

type Subscription struct {
	Filter          *ComponentFilter
	entities        []EID
}

As Components are added and removed from entities, the entity manager will pass the updated entity bitset to the subscription. If the entity bitset passes through the subscription's filter, the entity ID is added to the slice of entities for that subscription.

This leads us to the second utility system that is provided... The SubscriberSystem!

type SubscriberSystem struct {
	*BaseSystem
	Subscriptions []*Subscription
}

Examples

Creating a world
// make a world config
cfg := akara.NewWorldConfig()

// add systems to the config
cfg.With(&MovementSystem{})

// create a world instance using the world config
world := akara.NewWorld(cfg) 
Declaring a Component

Here is the bare minimum required to create a new component:

type Velocity struct {
	*Vector3
}

func (*Velocity) New() akara.Component {
	return &Velocity{
		Vector3: NewVector3(0, 0, 0),
	}
}

Initialization logic specific to this component (like creating instances of another *struct) belongs inside of the New method.

Concrete Component Factories

A concrete component factory is just a wrapper for the generic component factory, but it casts the returned values from Add and Get to the concrete component implementation. This is just to prevent you from having to cast the component interface to struct pointers.

type VelocityFactory struct {
	*akara.ComponentFactory
}

func (m *VelocityFactory) Add(id akara.EID) *Velocity {
	return m.ComponentFactory.Add(id).(*Velocity)
}

func (m *VelocityFactory) Get(id akara.EID) (*Velocity, bool) {
	component, found := m.ComponentFactory.Get(id)
	if !found {
		return nil, found
	}

	return component.(*Velocity), found
}
Creating a ComponentFilter
cfg := akara.NewFilter()

cfg.Require(
	components.Position,
	components.Velocity,
)

filter := cfg.Build()
Example System (with Subscriptions!)

For systems that use subscriptions, it is recommended that you embed an akara.BaseSubscriberSystem as it provides the generic methods for dealing with subscriptions. It also contains an akara.BaseSystem, which has other generic system methods.

It is also recommended that all component factories be placed inside of a common struct and given explicit names. This helps to keep the code clear when writing complicated systems.

As of writing, there is no general guide for how subscriptions are managed, but just embedding them in the system struct and giving them descriptive names is sufficient.

type MovementSystem struct {
	akara.SubscriberSystem
	components struct {
		positions   PositionFactory
		velocities  VelocityFactory
	}
	movingEntities []akara.EID
}

As of writing, all systems should set their World and call SetActive(false) if world is nil. After that, actual system initialization logic is added:

func (m *MovementSystem) Init(world *akara.World) {
	m.World = world

	if world == nil {
		m.SetActive(false)
		return
	}

	m.setupComponents()
	m.setupSubscriptions()
}

Here, we use BaseSystem.InjectComponent, which registers a component and assigns a component factory to the given destination.

func (m *MovementSystem) setupComponents() {
	m.InjectComponent(&Position{}, &m.components.Position.ComponentFactory)
	m.InjectComponent(&Velocity{}, &m.components.Velocity.ComponentFactory)
}

Here, we set up our only subscription. For this example, our MovementSystem is interested in Position and Velocity components.

func (m *MovementSystem) setupSubscriptions() {
	filterBuilder := m.NewComponentFilter()

	filterBuilder.Require(&Position{})
	filterBuilder.Require(&Velocity{})

	filter := filterBuilder.Build()

	m.movingEntities = m.World.AddSubscription(filter)
}

Our Update method is simple; we iterate over entities that are in our subscription. Remember, as components are added and removed from entities, all subscriptions are updated.

func (m *MovementSystem) Update() {
	for _, eid := range m.movingEntities.GetEntities() {
		m.moveEntity(eid)
	}
}

This is where our system actually does work. For the given incoming entity id, we retreive the position and velocity components and apply the velocity to the position. If either of those components does not exist for the entity, we return.

func (m *MovementSystem) moveEntity(id akara.EID) {
	position, found := m.components.positions.Get(id)
	if !found {
		return
	}

	velocity, found := m.components.velocities.Get(id)
	if !found {
		return
	}

	s := float64(m.World.TimeDelta) / float64(time.Second)
	position.Vector.Add(velocity.Vector.Clone().Scale(s))
}
Example: Static Checks for Interface Implementation

Wherever you define components and systems, it's good practice to add static checks. These prevent you from compiling if things don't implement the interfaces that they should.

// static check that MovementSystem implements the System interface
var _ akara.System = &MovementSystem{}
// static check that PositionComponent implements Component
var _ akara.Component = &PositionComponent{}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultTickRate float64 = 100

Functions

This section is empty.

Types

type BaseSystem

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

BaseSystem is the base system type

func (*BaseSystem) Activate

func (s *BaseSystem) Activate()

Activate calls Tick repeatedly at the target TickRate. This method blocks the thread.

func (*BaseSystem) Active

func (s *BaseSystem) Active() bool

Active returns true if the system is active, otherwise false

func (*BaseSystem) Deactivate

func (s *BaseSystem) Deactivate()

Deactivate marks the system inactive and stops it from ticking automatically in the background. The system can be re-activated by calling the Activate method.

func (*BaseSystem) Init

func (s *BaseSystem) Init(world *World, tickFunc func())

func (*BaseSystem) InjectComponent

func (s *BaseSystem) InjectComponent(c Component, dst **ComponentFactory)

InjectComponent is shorthand for registering a component and placing the factory in the given destination

func (*BaseSystem) IsInitialized

func (s *BaseSystem) IsInitialized() bool

func (*BaseSystem) Name

func (s *BaseSystem) Name() string

func (*BaseSystem) SetPostTickCallback

func (s *BaseSystem) SetPostTickCallback(fn func())

func (*BaseSystem) SetPreTickCallback

func (s *BaseSystem) SetPreTickCallback(fn func())

func (*BaseSystem) SetTickFrequency

func (s *BaseSystem) SetTickFrequency(rate float64)

func (*BaseSystem) Tick

func (s *BaseSystem) Tick()

Tick performs a single tick. This is called automatically when the System is Active, but can be called manually to single-step the System, regardless of the System's TickRate.

func (*BaseSystem) TickCount

func (s *BaseSystem) TickCount() uint

func (*BaseSystem) TickFrequency

func (s *BaseSystem) TickFrequency() float64

TickFrequency returns the maximum number of ticks per second this system will perform.

func (*BaseSystem) TickPeriod

func (s *BaseSystem) TickPeriod() time.Duration

TickPeriod returns the length of one tick as a time.Duration

func (*BaseSystem) Uptime

func (s *BaseSystem) Uptime() time.Duration

type C

type C = Component

for brevity

type CID

type CID = ComponentID

for brevity

type Component

type Component interface {
	New() Component
}

Component can be anything with a `New` method that creates a new component instance

type ComponentFactory

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

ComponentFactory is used to manage a one-to-one mapping between entity ID's and component instances. The factory is responsible for creating, retrieving, and deleting components using a given component ID.

Attempting to create more than one component for a given component ID will result in nothing happening (the existing component instance will still exist).

func (*ComponentFactory) Add

func (cf *ComponentFactory) Add(id EID) Component

Add a new component for the given entity ID and yield the component. If a component already exists, yield the existing component.

This operation will update world subscriptions for the given entity.

func (*ComponentFactory) Get

func (cf *ComponentFactory) Get(id EID) (Component, bool)

Get will yield the component and a bool, much like map retrieval. The bool indicates whether a component was found for the given entity ID. The component can be nil.

func (*ComponentFactory) ID

func (cf *ComponentFactory) ID() ComponentID

ID returns the registered component ID for this component type

func (*ComponentFactory) Remove

func (cf *ComponentFactory) Remove(id EID)

Remove will destroy the component instance for the given entity ID. This operation will update world subscriptions for the given entity ID.

type ComponentFilter

type ComponentFilter struct {
	Required    *bitset.BitSet
	OneRequired *bitset.BitSet
	Forbidden   *bitset.BitSet
}

ComponentFilter is a group of 3 BitSets which are used to filter any other given BitSet. A target (fourth, external) BitSet is allowed to "pass" through the filter under the following conditions:

The target bitset contains all "required" bits The target bitset contains at least one of the "one required" bits The target bitset contains none of the "forbidden" bits

If the target bitset invalidates any of these three rules, the target bitset is said to have been "rejected" by the filter.

func NewComponentFilter

func NewComponentFilter(all, oneOf, none *bitset.BitSet) *ComponentFilter

NewComponentFilter creates a component filter using the given BitSets.

The first BitSet declares those bits which are required in order to pass through the filter.

The second BitSet declares those bits of which at least one is required in order to pass through the filter.

The third BitSet declares those bits which none are allowed to pass through the filter.

func (*ComponentFilter) Allow

func (cf *ComponentFilter) Allow(other *bitset.BitSet) bool

Allow returns true if the given bitset is not rejected by the component filter

func (*ComponentFilter) Equals

func (cf *ComponentFilter) Equals(other *ComponentFilter) bool

Equals checks if this component filter is equal to the argument component filter

type ComponentFilterBuilder

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

ComponentFilterBuilder creates a component filter config

func NewComponentFilterBuilder

func NewComponentFilterBuilder(w *World) *ComponentFilterBuilder

NewComponentFilterBuilder creates a new builder for a component filter, using the given world.

func (*ComponentFilterBuilder) Build

Build iterates through all components in the filter and registers them in the world, to ensure that all components have a unique Component ID. Then, the bitsets for Required, OneRequired, and Forbidden bits are set in the corresponding bitsets of the filter.

func (*ComponentFilterBuilder) Forbid

func (cfb *ComponentFilterBuilder) Forbid(components ...Component) *ComponentFilterBuilder

Forbid makes all of the given components forbidden by the filter

func (*ComponentFilterBuilder) Require

func (cfb *ComponentFilterBuilder) Require(components ...Component) *ComponentFilterBuilder

Require makes all of the given components required by the filter

func (*ComponentFilterBuilder) RequireOne

func (cfb *ComponentFilterBuilder) RequireOne(components ...Component) *ComponentFilterBuilder

RequireOne makes at least one of the given components required

type ComponentID

type ComponentID uint

ComponentID is a unique identifier for a component type

type E

type E = EntityID

for brevity

type EID

type EID = EntityID

EID is shorthand for EntityID

type EntityID

type EntityID = uint64

EntityID is an entity ID

type Initializer

type Initializer interface {
	Init(*World)
	IsInitialized() bool
}

type S

type S = System

for brevity

type Subscription

type Subscription struct {
	Filter *ComponentFilter
	// contains filtered or unexported fields
}

Subscription is a component filter and a slice of entity ID's for which the filter applies

func NewSubscription

func NewSubscription(cf *ComponentFilter) *Subscription

NewSubscription creates a new subscription with the given component filter

func (*Subscription) AddEntity

func (s *Subscription) AddEntity(id EID)

AddEntity adds an entity to the subscription entity map

func (*Subscription) EntityIsIgnored

func (s *Subscription) EntityIsIgnored(id EID) bool

func (*Subscription) GetEntities

func (s *Subscription) GetEntities() []EID

GetEntities returns the entities for the system

func (*Subscription) IgnoreEntity

func (s *Subscription) IgnoreEntity(id EID)

func (*Subscription) RemoveEntity

func (s *Subscription) RemoveEntity(id EID)

RemoveEntity removes an entity from the subscription entity map

type System

type System interface {
	Update()
	// contains filtered or unexported methods
}

System describes the bare minimum of what is considered a system

type W

type W = World

for brevity

type World

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

World contains all of the Entities, Components, and Systems

func NewWorld

func NewWorld(optional ...*WorldConfig) *World

NewWorld creates a new world instance from the given world configs

func (*World) AddSubscription

func (w *World) AddSubscription(input interface{}) *Subscription

AddSubscription will look for an identical component filter and return an existing subscription if it can. Otherwise, it creates a new subscription and returns it.

func (*World) AddSystem

func (w *World) AddSystem(s System, activate bool) *World

AddSystem adds a system to the world. The System will become Active on the next World Update

func (*World) GetComponentFactory

func (w *World) GetComponentFactory(id ComponentID) *ComponentFactory

GetComponentFactory returns the ComponentFactory for the given ComponentID

func (*World) NewComponentFilter

func (w *World) NewComponentFilter() *ComponentFilterBuilder

NewComponentFilter creates a builder for creating

func (*World) NewEntity

func (w *World) NewEntity() EID

NewEntity creates a new entity and Component BitSet

func (*World) RegisterComponent

func (w *World) RegisterComponent(c Component) ComponentID

RegisterComponent registers a component type, assigning and returning its component ID

func (*World) RemoveEntity

func (w *World) RemoveEntity(id EID)

RemoveEntity removes an entity

func (*World) RemoveSystem

func (w *World) RemoveSystem(s System) *World

RemoveSystem queues the given system for removal

func (*World) Update

func (w *World) Update() error

Update iterates through all Systems and calls the update method if the system is active

func (*World) UpdateEntity

func (w *World) UpdateEntity(id EID)

UpdateEntity updates the entity in the world. This causes the entity manager to update all subscriptions for this entity ID.

type WorldConfig

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

WorldConfig is used to declare Systems and component mappers. This is to be passed to a World factory function.

func NewWorldConfig

func NewWorldConfig() *WorldConfig

NewWorldConfig creates a world config builder instance

func (*WorldConfig) With

func (b *WorldConfig) With(arg interface{}) *WorldConfig

With is used to add either Systems or component maps.

Examples:

builder.With(&system{})
builder.With(&component{})

builder.
	With(&movementSystem{}).
	With(&velocity{}).
	With(&position{})

Jump to

Keyboard shortcuts

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