Skip to content

yarbelk/ecs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ecs

Build Status

This is our implementation of the "Entity Component System" model in Go. It was designed to be used in engo, however it is not dependent on any other packages so is able to be used wherever!

Basics

In the Entity Component System paradigm, you have three elements;

  • Entities
  • Components
  • Systems.

In our implementation, we use the type World to work with those Systems. Each System can have references to any number (including 0) of entities. And each Entity can have as many Components as desired.

An example of creating a World, adding a System to it, and update all systems

// Declare the world - you can also use "var world ecs.World" 
world := ecs.World{}

// You can add as many Systems here as you like. The RenderSystem provided by `engo` is just an example. 
world.AddSystem(&engo.RenderSystem{})

// This will usually be called within the game-loop, in order to update all Systems on every frame.  
world.Update(0.125) // 0.125 would be the time in seconds since the last update

System

We've been talking about Systems, but what are they? Anything that implements the interface, can be used as a System:

type System interface {
	// Update is ran every frame, with `dt` being the time in seconds since the last frame
	Update(dt float32)

	// Delete should remove the entity from the system completely
	Remove(e BasicEntity)
}

What does this say? It needs to have an Update method (which is called from world.Update), and it needs to have a Remove(ecs.BasicEntity) method. Why require a Remove method, but not an Add method? Because there's no 'generic' Add method (the parameters may change), while in order to remove something, all you need it the unique identifier (as provided by the BasicEntity).

Initialization

Optionally, your System may implement the Initializer interface, which allows you to do initialization for the given World. Basically, it allows you to initialize values, without having to call the function manually before adding it to the World. Whenever you add a System (one that implements the Initializer interface) to the world, the New method will be called.

type Initializer interface {
	// New is the initialisation of the System, and may be used to initialize some values beforehand, like storing
	// a reference to the World
	New(*World)
}

Priority

Optionally, your System may implement the Prioritizer interface, which allows the World to sort the Systems based on that priority. If omitted, a value of 0 is assumed.

type Prioritizer interface {
	// Priority indicates the order in which Systems should be executed per iteration, higher meaning sooner. Default is 0
	Priority() int
}

Entities and Components

Where do the entities come in? All game-logic has to be done within Systems (the Update method, to be precise)). Components store data (which is used by those Systems). An Entity is no more than a wrapper which combines multiple Components and adds a unique identifier to the whole. This unique identifier is nothing magic: simply an incrementing integer value - nothing to worry about.

Because the precise definition of those Components can vary, this ecs package provides no Components -- we only provide examples here. The engo.io/engo/common package offers lots of Components and Systems to work with, out of the box.

Let's view an example:

type SpaceComponent struct {
    Width  float32
    Height float32
}

type HealthComponent struct {
    HealthPercentage float32
    ManaPercentage   float32
}

type Player struct {
    ecs.BasicEntity
    SpaceComponent
    HealthComponent
}

Here, the type Player is made out of three elements: the unique identifier (ecs.BasicEntity) and two Components. A System may make use of one or more of those Components. Which are required, is defined by the Add method on that System.

Let's view a few examples:

func (MySystem1) Add(basic *ecs.BasicEntity, space *SpaceComponent) { /* ... */ }

func (MySystem2) Add(basic *ecs.BasicEntity, health *HealthComponent) { /* ... */ }

func (MySystem3) Add(basic *ecs.BasicEntity, space *SpaceComponent, health *HealthComponent) { /* ... */ }

These three different Add methods are all valid, and use different Components. But how can I add my Entity to the System, if I didn't save a reference to that System?

// Initialize our custom Entity
// NOTE: we have to call `ecs.NewBasic` here, to give our Entity a new unique identifier
player := Player{BasicEntity: ecs.NewBasic()}

// Loop over all Systems
for _, system := range world.Systems() {

    // Use a type-switch to figure out which System is which
    switch sys := system.(type) {
    
        // Create a case for each System you want to use
        case *MySystem1:
            sys.Add(&player.BasicEntity, &player.SpaceComponent)
        case *MySystem3:
            sys.Add(&player.BasicEntity, &player.SpaceComponent, &player.Healthcomponent)
    }
}

That is all there is to it.

Custom Systems - How to save Entities?

You more than likely will want to create Systems yourself. We will now go in depth on what you should do when defining your own Add method for your System. As seen above, you can create any number (and type of) parameters you want.

We do ask you to let the first argument be of type *ecs.BasicEntity - as a general rule.

Your System should include an array, slice or map in which to store those entities. Now it is important to note that you're not receiving entities per se -- you are receiving references to the Components you need. The actual Entity (type Player in our example) may contain way more Components. You will most-likely want to create a struct for you to store those pointers in. An example:

type myAWesomeEntity struct {
    *ecs.BasicEntity
    *SpaceComponent
}

type MyAwesomeSystem struct {
    entities []myAwesomeEntity
}

func (m *MyAesomeSystem) Add(basic *ecs.BasicEntity, space *SpaceComponent) {
    m.entities = append(m.entities, myAwesomeEntity{basic, space})
}

NOTE

As a convention, please include "System" in the name of your System -- at the end. When you define a struct (which contains pointers, as opposed to the Player struct we created earlier), please replace that System part with Entity. You should only use this newly-created struct in your similarly-named System. You will usually never want to export that Entity definition, as it is only being used in that System. If your system would be called BallMovementSystem, then your struct would be called ballMovementEntity.

Removing Entities from your System

Your System must implement the Remove method as specified by the System interface. Whenever you start storing entities, you should define this method in such a way, that it removes the custom-created non-exported Entity-struct from the array, slice or map. An ecs.BasicEntity is given for you to figure out which element in the array, slice or map it is.

// Remove removes the Entity from the System. This is what most Remove methods will look like
func (m *MyAwesomeSystem) Remove(basic ecs.BasicEntity) {
  	var delete int = -1
  	for index, entity := range m.entities {
    		if entity.ID() == basic.ID() {
    			delete = index
    			break
  		  } 
  	}
  	if delete >= 0 {
    		m.entities = append(m.entities[:delete], m.entities[delete+1:]...)
  	}
}

// OR, if you were using a `map` instead of a `slice`:

// Remove removes the Entity from the System. As you see, removing becomes easier when using a `map`. 
func (m *MyAwesomeSystem) Remove(basic ecs.BasicEntity) {
  	delete(m.entities, basic.ID())
}
//

NOTE

Even though that a map looks easier, if you want to loop over that map each frame, writing those additional lines to use a slice instead, is definitely worth it in terms of runtime performance. Iterating over a map is a lot slower.

Custom Systems - The Update method

Whatever your System does on the Update method, is up to you. Each System is unique in that sense. If you're storing entities, then you might want to loop over them each frame. Again, this depends on your use-case.

func (m *MyAwesomeSystem) Update(dt float32) {
    for _, entity := range m.entities {
        fmt.Println("I would like to tell you", entity.ID(), "that it has been", dt, "seconds since the last time we spoke. ")
    }
}

About

A Go-implementation of the Entity-Component-System paradigm

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%