Entity Systems Project

Reading

Existing libraries:
* A-Frame (Javascript): Website "A web framework for building 3D/AR/VR experiences. Make 3D worlds with HTML and Entity-Component. For Quest, Rift, WMR, SteamVR, mobile, desktop." Docs
* Artemis (C#)/Artemis (Java)
* Ape-ECS (Javascript): "Entity-Component-System library for JavaScript."
* ECSY (Javascript)
* ensy (Javascript): "A very simple Entity System for JavaScript." Docs
* javascript-entity-component-system (Javascript): Lightweight ECS
* Legion (Rust)
* SPECS (Rust)

From "How to Build an Entity Component System Game in JavaScript":

With ECS, entities are just collections of components; just a collection of data.

With this approach, you avoid having to create gnarly inheritance chains. With this approach, for example, a half-orc isn't some amalgamation of a human class and an orc class (which might inherit from a Monster class). With this approach, a half-orc is just a grouping of data.

An entity is just a like a record in a database. The components are the actual data. Here's a high level example of what the data might look like for entities, shown by ID and components. The beauty of this system is that you can dynamically build entities - an entity can have whatever components (data) you want.

Everything is tagged as an entity. A bullet, for instance, might just have a "physics" and "appearance" component. Entity Component System is data driven. This approach allows greater flexibility and more expression. One benefit is the ability to dynamically add and remove components, even at run time. You could dynamically remove the appearance component to make invisible bullets, or add a "playerControllable" component to allow the bullet to be controlled by the player. No new classes required.

This can potentially be a problem as systems have to iterate through all entities. Of course, it's not terribly difficult to optimize and structure code so not all entities are hit each iteration if you have too many, but it's helpful to keep this constraint in mind, especially for browser based games.

[Thus] we just need to do a few things:

  1. Set up the initial entities
  2. Set up the order of the systems which we want to use
  3. Set up a game loop, which calls each system and passed in all the entities
  4. Set up a lose condition
  5. Let's take a look at the code for setting up the initial entities and the player entity:

var self = this;
var entities = {}; // object containing { id: entity  }
var entity;

// Create a bunch of random entities
for(var i=0; i < 20; i++){
    entity = new ECS.Entity();
    entity.addComponent( new ECS.Components.Appearance());
    entity.addComponent( new ECS.Components.Position());

    // % chance for decaying rects
    if(Math.random() < 0.8){
        entity.addComponent( new ECS.Components.Health() );
    }

    // NOTE: If we wanted some rects to not have collision, we could set it
    // here. Could provide other gameplay mechanics perhaps?
    entity.addComponent( new ECS.Components.Collision());

    entities[entity.id] = entity;
}

// PLAYER entity
// Make the last entity the "PC" entity - it must be player controlled,
// have health and collision components
entity = new ECS.Entity();
entity.addComponent( new ECS.Components.Appearance());
entity.addComponent( new ECS.Components.Position());
entity.addComponent( new ECS.Components.Collision() );
entity.addComponent( new ECS.Components.PlayerControlled() );
entity.addComponent( new ECS.Components.Health() );

// we can also edit any component, as it's just data
entity.components.appearance.colors.g = 255;
entities[entity.id] = entity;

// store reference to entities
ECS.entities = entities;

Note how we can modify any of the component data directly. It's all data that can be manipulated however and whenever you want! The player entity step could be even further simplified by using assemblages, which are basically entity templates. For instance (using our assemblages):

entity = new ECS.Assemblages.CollisionRect();
entity.addComponent( new ECS.Components.Health());
entity.addComponent( new ECS.Components.PlayerControlled() );
  1. Next, we setup the order of the systems:
// Setup systems
// Setup the array of systems. The order of the systems is likely critical, 
// so ensure the systems are iterated in the right order
var systems = [
    ECS.systems.userInput,
    ECS.systems.collision,
    ECS.systems.decay, 
    ECS.systems.render
];
  1. Then, a simple game loop
// Game loop
function gameLoop (){
    // Simple game loop
    for(var i=0,len=systems.length; i < len; i++){
        // Call the system and pass in entities
        // NOTE: One optimal solution would be to only pass in entities
        // that have the relevant components for the system, instead of 
        // forcing the system to iterate over all entities
        systems[i](ECS.entities);
    }

    // Run through the systems. 
    // continue the loop
    if(self._running !== false){
        requestAnimationFrame(gameLoop);
    }
}
// Kick off the game loop
requestAnimationFrame(gameLoop);
  1. Finally, a lose condition
// Lose condition
this._running = true; // is the game going?
this.endGame = function endGame(){ 
    self._running = false;
    document.getElementById('final-score').innerHTML = +(ECS.$score.innerHTML);
    document.getElementById('game-over').className = '';

    // set a small timeout to make sure we set the background
    setTimeout(function(){
        document.getElementById('game-canvas').className = 'game-over';
    }, 100);
};


Tags: gamedev   patterns   reading   architecture  

Last modified 14 January 2022