Entity system for Haxe
edge works on the following principles:
- an
Entity
is a collection of components with no additional logic. - a
Component
is an instance of any type of Class. You can use anything as a component except for anonymous objects and primitive types. - a
Component
is a data object with no logic. If you want to put some logic in them, do it at your own risk. You have been warned. - a
System
manages a portion of the application logic and it is responsible for reading and writing from and to the components as needed. - A
System
is required to have at least theupdate()
method defined. Update can take zero or more arguments. Arguments types should only be components. - the
Engine
manages the entities, their pairing with the systems and the application phases. - a
Phase
is a collection of systems that need to be processed sometime in the future. - when a
Phase
is updated,Engine
invokes theupdate()
method of eachSystem
included inPhase
. Only systems that are paired with at least one entity will be triggered, and once for each entity that matchesupdate
. A System whoseupdate
method has no arguments will be invoked on each update once. World
is a general case implementation to add a scheduler based on the concept of frame with therendering
andphysics
phases. If you plan to do sophisticated things with your loops/phases, you might consider writing an alternative implementation ofWorld
.
For the official release:
haxelib install edge
For the cutting-edge/dev-version:
haxelib git edge https://github.com/fponticelli/edge.git
In the example below we create a bunch of entities some with both Position
and Velocity
and some with only Position
. The system UpdateMovement
will only affect the entities with both components, while RenderingDots
will be applied to all the entities in this context.
RenderingBackground
will clear the canvas on every frame before any other rendering operation. Note that it will only be invoked once per frame and it doesn't rely on the presence of any entity (update()
takes no argument).
See the example in action.
import edge.*;
import minicanvas.MiniCanvas;
class Game {
public static var width(default, null) = 200;
public static var height(default, null) = 200;
public static function main() {
var mini = MiniCanvas.create(width, height)
.display("basic example"),
world = new World();
for(i in 0...300)
world.engine.create([
new Position(
Math.random() * width,
Math.random() * height),
new Velocity(
Math.random() * 2 - 1,
Math.random() * 2 - 1)
]);
for(i in 0...20)
world.engine.create([
new Position(
Math.random() * width,
Math.random() * height)
]);
world.physics.add(new UpdateMovement());
world.render.add(new RenderDots(mini));
world.start();
}
}
class Position implements IComponent {
var x : Float;
var y : Float;
}
class Velocity implements IComponent {
var vx : Float;
var vy : Float;
}
class RenderDots implements ISystem {
var mini : MiniCanvas;
public function new(mini : MiniCanvas)
this.mini = mini;
function before()
mini.clear();
function update(pos : Position)
mini.dot(pos.x, pos.y, 2, 0x000000FF);
}
class UpdateMovement implements ISystem {
function update(pos : Position, vel : Velocity) {
var dx = pos.x + vel.vx,
dy = pos.y + vel.vy;
if(dx <= 0 && vel.vx < 0 || dx >= Game.width && vel.vx > 0)
vel.vx = -vel.vx;
else
pos.x = dx;
if(dy <= 0 && vel.vy < 0 || dy >= Game.height && vel.vy > 0)
vel.vy = -vel.vy;
else
pos.y = dy;
}
}
Systems must implement ISystem. System classes do not have to implement the required __process__
because this field is automatically built.
A system must at least define a method update()
. The method can take 0 or more arguments. If arguments are defined, they must be components; a component is an instance of any Class<Dynamic>
.
The update()
method will be invoked when the corresponding phase is updated. update()
will be called only once if it takes no arguments, or once for every entity that matches the function requirements. An entity matches the update
requirements if it has a matching component for each of the function arguments.
Optionally a System can expose the following members:
-
function after() : Void
Executes after each cycle of
update(/*...*/)
. It only makes sense whenupdate
takes at least one argument. -
function before() : Void
Executes before each cycle of
update(/*...*/)
. It only makes sense whenupdate
takes at least one argument. -
var engine : Engine
Gets a reference to engine. Useful to dynamically create more entities.
-
var entity : Entity
To only be declared when
update(/*...*/)
takes at least one argument.entity
will reference the container of the components that are currently processed by theupdate
method. -
var timeDelta : Float
Brings a value (in millisecond) defining the time elapsed since the latest iteration.
If the System exposes any of these members, they will be automatically populated at the right time. So no initialization is required or desired. Also they will be automatically changed to public
if they are not already.
Sometimes you want to be able to iterate over collections of entities that satisfy certain requirements. For example, it can be extremely useful for collisions. In that case you can define one (or more) fields of type edge.View(T)
. Where T
is the type of an anonymous object where each field must be have the type of a component.
var targets : View<{ position : Position, life : Life }>;
var entity : Entity;
function update(position : Position, bullet : Bullet) {
for(item in targets) {
var target = item.data; // item.data stores the components
// hit the target?
if(areNear(position, target.position)) {
// assign damage
life.hitPoints -= bullet.damage;
// remove bullet
entity.destroy();
// life is zero remove target
if(life <= 0)
item.entity.destroy(); // item.entity references the target entity
}
}
}
System can also receive notifications when an entity has been added or removed from a View
.
From the example above, if you want to perform a special operation when a new target is paired with your system you can define the following method:
function targetsAdded(entity : Entity, data { position : Position, life : Life }) {
// do something with the newly added entity/components
}
The magic here is in the name of the method that needs to follow the format:
${viewFieldName}Added
The same signature with Removed
can be used to define a method that does some cleanup when an entity is unpaired from a view.
A special View
is created for the method update
called updateItems
. For that you can define either, both or neither of updateAdded
/updateRemoved
methods.
For this update
function:
function update(position : Position, bullet : Bullet)
The added/removed methods will look like:
function updateAdded(entity : Entity, data : { position : Position, bullet : Bullet }) {}
function updateRemoved(entity : Entity, data : { position : Position, bullet : Bullet }) {}
Even if not required, your components can implement IComponent
. Doing so your components will gain the following super-powers for free:
- you don't need to setup a constructor, if it doesn't exist, one will be created for you and it will take the same arguments as the variable fields declared in the component.
- all variables are automatically made public.
- a method
toString
is also created to simplify the debugging of your code.