cog

A Macro Powered B.Y.O.E. (Bring Your Own Entity!) ECS Framework


Keywords
ecs, framework, gamedev, haxe, haxelib
License
MIT
Install
haxelib install cog 1.0.0

Documentation

cog

A Macro powered B.Y.O.E. (Bring Your Own Entity!) ECS Framework written in Haxe.

Build Status

ECS concepts & implementation inspired by exp-ecs.

Why?

As a big fan of the Macro approach and concepts in exp-ecs, I originally wrote cog as a piece of the ghost framework to provide that same kind of workflow. But as that project evolved and changed it's focus to 2D only, I found I wanted to be able to plug it's dead simple ECS implementation into any kind of project, with no extra dependencies required. So I ripped the ECS out of the ghost framework, and cog was born!

Getting Started

Cog requires Haxe 4 to run.

Install the library from haxelib:

haxelib install cog

Alternatively the dev version of the library can be installed from github:

haxelib git cog https://github.com/AustinEast/cog.git

Then include the library in your project's .hxml:

-lib cog

For OpenFL users, add this into your Project.xml:

<haxelib name="cog" />

For Kha users (who don't use haxelib), clone cog to the Libraries folder in your project root, and then add the following to your khafile.js:

project.addLibrary('cog');

Usage

Concepts

Component

A Component is an object that holds data that define an entity. Generally a component only holds variables, with little-to-no logic. Cog Components are created by implementing the IComponent into any Class, like so:

class Position implements IComponent {
  public var x:Float = 0;
  public var y:Float = 0;
}

Components

A Components object is a container that holds Component instances. This class is meant to be integrated into your own project's base object class (ie Entity, GameObject, Sprite, etc).

System

A System tracks collections of Components (as Nodes) for the purpose of performing logic on them.

Engine

The Engine is the entry point for Cog - it's main purpose is keeping track of Components objects and updating each System.

Node

A Node object keeps reference to a Components object and its relevant Component instances.

Nodes

A Nodes object tracks all the Components objects in the Engine, creating a Node for every Components object that contains all of it's required Component instances. Nodes objects are used by System objects to perform logic on Components.

Integration

A build macro is available to add custom fields to the Components class, such as an Entity class:

in build.hxml:

--macro cog.Macros.add_data("entity", "some.package.Entity")

in Main.hx:

var components = new cog.Components();
components.entity = new some.package.Entity();

This will also add a reference to the custom field into every Node instance:

class TestSystem extends System {
  @:nodes var nodes:Node<Position>;

  override function step(dt:Float) {
    super.step(dt);

    for (node in nodes) {
      // The `Entity` custom field can be accessed through the `Components` object
      trace('${node.components.entity.name}');

      // OR it can be accessed directly from the Node
      trace('${node.entity.name}');
    }
  }
}

Example

import cog.IComponent;
import cog.Components;
import cog.System;
import cog.Engine;
import cog.Node;

// Creating Component classes is as simple as implementing the `IComponent` interface.
// When the interface is implemented, the required Component fields are all added automatically.

@:structInit
class Position implements IComponent {
  public var x:Float = 0;
  public var y:Float = 0;
}

@:structInit
class Velocity implements IComponent {
  public var x:Float = 0;
  public var y:Float = 0;
}

// Plug the `Components` class into your own `Entity` class
class Entity {
  public var components:Components;
  public var name:String = '';

  public function new() {
    components = new Components();

    // Assign the Entity field on the Components instance
    // This is only available by using the integration build macro, detailed here: https://github.com/AustinEast/cog#integration
    components.entity = this;

    // Create the Position & Velocity Components, then add them to the Entity's Components instance
    var position:Position = {};
    var velocity:Velocity = {};
    components.add(position);
    components.add(velocity);
  }
}

// Create a System to randomly move Entities
class MovementSystem extends System {
  // Using the `@:nodes` metadata, create a collection of Nodes.
  // The Nodes class automatically tracks any `Components` object that has the Position and Velocity components,
  // and will create a `Node` object for each one
  @:nodes var nodes:Node<Position, Velocity>;

  // This method is called when a System is added to the Cog Engine
  // Override this to apply any needed initialization logic to your Nodes
  override function added(engine:Engine) {
    super.added(engine);

    // Set a random velocity on each Node that already exists in the System
    for (node in nodes) {
      node.velocity.x = Math.random() * 200;
      node.velocity.y = Math.random() * 200;
    }

    // Subscribe to the Node list's `added` event to set a random velocity to each Node as it gets added to the System
    nodes.added.add(node -> {
      node.velocity.x = Math.random() * 200;
      node.velocity.y = Math.random() * 200;
    });
  }

  // This method is called when a System is removed from the Cog Engine
  // Override this to apply any needed disposal logic to your Nodes
  override function removed() {
    super.removed();
  }

  // This method is called every time the Cog Engine is stepped forward by the Game Loop
  override public function step(dt:Float) {
    super.step(dt);
    for (node in nodes) {
      // Increment each Node's Position by it's Velocity
      // Each Node holds reference to the `Components` object, along with a reference to each Component defined by the Nodes list
      node.position.x += node.velocity.x * dt;
      node.position.y += node.velocity.y * dt;
    }
  }
}

// Create a System to "Render" the entities
class RenderSystem extends System {
  @:nodes var nodes:Node<Position>;

  override function step(dt:Float) {
    super.step(dt);

    // Log the Entities' Positions
    for (node in nodes) {
      trace('${node.entity.name} is at position (${node.position.x}, ${node.position.y})');
    }
    trace('---------- End Frame ------------');
  }
}

class Main {
  static function main() {
    // Create an Array to hold the Game's Entities
    var entities = [];

    // Create the Cog Engine
    var engine = new Engine();

    // Define a method to remove Entities from the Game
    inline function remove_entity(entity:Entity) {
      // Remove the Entity from the Game's Entity List
      entities.remove(entity);
      // Remove the Entity's `Components` from the Cog Engine
      engine.remove_components(entity.components);
    }

    // Define a method to add Entities to the Game
    inline function add_entity(entity:Entity) {
      // Remove the Entity from the Game first, to make sure we arent adding it twice
      remove_entity(entity);
      // Add the Entity to the Game's Entity List
      entities.push(entity);
      // Add the Entity's `Components` to the Cog Engine
      engine.add_components(entity.components);
    }

    // Add some Entities in random spots
    for (i in 0...10) {
      var entity = new Entity();
      entity.name = 'Entity ${i + 1}';
      var position = entity.components.get(Position);
      if (position != null) {
        position.x = Math.random() * 1000;
        position.y = Math.random() * 1000;
      }
      add_entity(entity);
    }

    // Add the Movement and Render Systems to the Cog Engine
    engine.add_system(new MovementSystem());
    engine.add_system(new RenderSystem());

    // Simulate a Game Loop
    new haxe.Timer(16).run = () -> {
      // Update the Cog Engine.
      engine.step(16 / 1000);
    }
  }
}

Roadmap

  • Source Documentation
  • Improve Disposal