edge
edge copied to clipboard
Entity system for Haxe
edge
Entity system for Haxe
introduction
edge works on the following principles:
- an
Entityis a collection of components with no additional logic. - a
Componentis an instance of any type of Class. You can use anything as a component except for anonymous objects and primitive types. - a
Componentis 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
Systemmanages a portion of the application logic and it is responsible for reading and writing from and to the components as needed. - A
Systemis required to have at least theupdate()method defined. Update can take zero or more arguments. Arguments types should only be components. - the
Enginemanages the entities, their pairing with the systems and the application phases. - a
Phaseis a collection of systems that need to be processed sometime in the future. - when a
Phaseis updated,Engineinvokes theupdate()method of eachSystemincluded inPhase. Only systems that are paired with at least one entity will be triggered, and once for each entity that matchesupdate. A System whoseupdatemethod has no arguments will be invoked on each update once. Worldis a general case implementation to add a scheduler based on the concept of frame with therenderingandphysicsphases. If you plan to do sophisticated things with your loops/phases, you might consider writing an alternative implementation ofWorld.
install
For the official release:
haxelib install edge
For the cutting-edge/dev-version:
haxelib git edge https://github.com/fponticelli/edge.git
example
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;
}
}
ISystem
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() : VoidExecutes after each cycle of
update(/*...*/). It only makes sense whenupdatetakes at least one argument. -
function before() : VoidExecutes before each cycle of
update(/*...*/). It only makes sense whenupdatetakes at least one argument. -
var engine : EngineGets a reference to engine. Useful to dynamically create more entities.
-
var entity : EntityTo only be declared when
update(/*...*/)takes at least one argument.entitywill reference the container of the components that are currently processed by theupdatemethod. -
var timeDelta : FloatBrings 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 }) {}
IComponent
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
toStringis also created to simplify the debugging of your code.