Skip to content

Quickstart

genar edited this page Nov 6, 2023 · 32 revisions

I bet you don't wanna read tons of documentation, theory and other boring stuff right?
Let's just ignore all that deep knowledge and jump in directly.

ECS

Entity Component System (ECS) is a software architectural pattern mostly used for the representation of game world objects or data-oriented design in general. An ECS comprises entities composed of components of data, with systems or queries which operate on entities' components.

ECS follows the principle of composition over inheritance, meaning that every entity is defined not by a type hierarchy, but by the components that are associated with it.

World

The world acts as a management class for all its entities, it contains methods to create, destroy and query them and handles all the internal mechanics.
Therefore it is the most important class, you will use the world heavily.
Multiple worlds can be used in parallel, each instance and its entities are completely encapsulated from other worlds. Currently, worlds and their content can not interact with each other, however, this feature is already planned.

Worlds are created and destroyed like this...

var world = World.Create();  // Create world
world.TrimExcess();          // Frees unused memory, should NOT be called every frame
World.Destroy(world);        // Destroy world

or

using var world = World.Create();  // World is disposable
...

There can be up to 2,147,483,647 with up to 2,147,483,647 entities each.

Entity

An entity represents your game entity.
It is a simple struct with some metadata acting as a key to access and manage its components.

public readonly struct Entity : IEquatable<Entity> {
        
    public readonly int Id;          // Its id/key in the world
    public readonly int WorldId;    // The world the entity lives in ( can be removed with #define PURE_ECS ) 
    ....
}

Entities are being created by a world and will "live" in the world in which they were created.
When an entity is created, you need to specify the components it will have. Components are the additional data or structure the entity will have. This is called "Archetype".

var entity = world.Create(new Position(), new Velocity(),...);

or

var archetype = new ComponentType[]{ typeof(Position), typeof(Velocity), ... };
var entity = world.Create(archetype);

world.Destroy(in entity);

Component

Components are data assigned to your entity. With them you define how an entity looks and behaves, they define the game logic with pure data.
It's recommended to use struct components since they offer better speed.

To ease writing code, you can access the entity directly to modify its components or to check its metadata. You can also access the world which mirrors the same API. Let's take a look at the most important methods.

entity.IsAlive();                     // True if the entity is still existing in its world
entity.Has<Position>();               // True if the entity has a position component
entity.Set(new Position( X = 10 ));   // Replaces the position component and updates it data
entity.Get<Position>();               // Returns a reference to the entity position, can directly access and update position attributes

entity.Add<Velocity>(new Velocity()); // Adds a velocity component to it and moves it to a different archetype.
entity.Remove<Velocity>();            // Removes the velocity component and moves it to a different archetype.

entity.GetComponentTypes();           // Returns an array of its component types. Should be treated as readonly 
entity.GetComponents();               // Returns an array of all its components and allocates memory. 

With those utility methods, you can implement your game logic.
A small example could look like this...

var entity = world.Create(new Position(), new Velocity());

ref var position = ref entity.Get<Position>();    // Get reference to the position
position.X++;                                     // Update x
position.Y++;                                     // Update y

if(entity.Has<Position>())                        // Make sure that entity has a position ( Optional )
    entity.Set(new Position{ X = 10, Y = 10 };    // Replaces the old position 

entity.Remove<Velocity>();                         // Removes a velocity component and moves it to a new archetype.
entity.Add<Velocity>(new Velocity{ X = 1, Y = 1);  // Adds a velocity component and moves the entity back to the previous archetype. 

Warning

Ensuring the presence or absence of a component is often important. Not doing so can lead to undefined behavior.

It's important to mention that their generic overloads for each of the calls above are way more performant since they batch the operations.

entity.Add<T0...T9>();
entity.Remove<T0...T9>();
entity.Get<T0...T9>();
entity.Set<T0...T9>();
entity.Has<T0...T9>();

System aka. Query

Queries aka. Systems are used to iterate over a set of entities to apply logic and behavior based on their components.

This is performed by using the world ( remember, it manages your created entities ) and by defining a description of which entities we want to iterate over.

// Define a description of which entities you want to query
var query = new QueryDescription().    
            WithAll<Position,Velocity>().      // Should have all specified components
            WithAny<Player,Projectile>().      // Should have any of those
            WithNone<AI>();                    // Should have none of those

// Execute the query
world.Query(in query, (Entity entity) => { /* Do something */ });

// Execute the query and modify components in the same step, up to 10 generic components at the same time. 
world.Query(in query, (ref Position pos, ref Velocity vel) => {
    pos.X += vel.Dx;
    pos.Y += vel.Dy;
});

Warning

Entity creation, destruction, and structural changes are potentially possible in a query but depending on how, this can lead to problems.

In the example above we want to move our entities based on their Position and Velocity components. To perform this operation we need to iterate over all entities having both a Position and Velocity component (All). We also want that our entity either is a Player or a Projectile (Any). However, we do not want to iterate and perform that calculation on entities that are controlled by an AI (None).

The world.Query method then smartly searches for entities having both a Position and Velocity, either a Player or Projectile, and no AI component and executes the defined logic for all of those fitting entities.

Besides All, Any, and None, QueryDescription can also target an exclusive set of components via Exclusive. If that's set, it will ignore All, Any, and None and only target entities with an exactly defined set of components. It's also important to know that there are multiple different overloads to perform such a query.

world.Query(in query);                                                   // Returns a query which can be iterated over manually
world.Query(in query, (Entity entity) => {});                            // Passes the fitting entity
world.Query(in query, (ref T1 t1, T2 t2, ...) => {})                     // Passes the defined components of the fitting entity, up to 10 components
world.Query(in query, (Entity en, ref T1 t1, ref T2 t2, ...) => {})      // Passed the fitting entity and its defined components, up to 10 components 

var filteredEntities = new List<Entity>();
var filteredArchetypes = new List<Archetype>();
var filteredChunks = new List<Chunk>();

world.CountEntities(query);                                             // Counts amount of entities fitting the query and returns the amount
world.GetEntities(query, filteredEntities);                             // Fills all fitting entities into the passed list
world.GetArchetypes(query, filteredArchetypes);                         // Fills all fitting archetypes into the list
world.GetChunks(query, filteredChunks);                                 // Fills all fitting chunks into the list 

Note

The less you query in terms of components and the size of components... the faster the query is!

Archetypes and Chunks are internal structures of the world and store entities with the same component types. You will mostly never use them directly, therefore more on them later.

Enumerators

There are several high-performance enumerators which can be used to enumerate various classes.
They might save you time and code.

foreach(ref var archetype in world){ /** LOGIC **/ }
foreach(ref var chunk in archetype){ /** LOGIC **/ }

foreach(ref var chunk in myQuery){ /** LOGIC **/ }                             // Automatically loops over all chunks fitting the query ( same as myQuery.GetChunkIterator())
foreach(ref var archetype in myQuery.GetArchetypeIterator()){ /** LOGIC **/ }  // Automatically loops over all archetypes fitting the query. 
foreach(ref var chunk in myQuery.GetChunkIterator()){ /** LOGIC **/ }          // Automatically loops over all chunks fitting the query. 

foreach(var index in someChunk){}  // Automatically loops over all valid entities inside the chunk

If you do not want to use the high-level queries you can make use of those enumerators. They are great to implement more low-level like iteration techniques over your entities and components.

var myQuery = world.Query(in desc);
foreach(ref var chunk in myQuery.GetChunkIterator()){
   
   var chunkSize = chunk.Size; // How many entities are in the chunk, used for iteration
   var transforms = chunk.GetArray<Transform>();
   var velocity = chunk.GetArray<Velocity>();
   ...
}

Outlook

This is all you need to know, with this little knowledge you are already able to bring your worlds to life.
However, if you want to take a closer look at Arch and performance techniques :

Clone this wiki locally