GameDev – Entity Component System for Phaser.io, part 2

This is Part 2. Here’s Part 1 of the series.

The Magical World of Entities and Components

Hello and welcome back for the next iteration in this ECS adventure. You’ll learn about how I chose to make components, tied to the same entity, talk together. I believe this is the coolest part.

You’ll need:

  • Phaser.Signal
  • Your game.

Hah. That’s it.

And now, let’s dive into a…

Concrete Example

Let’s imagine we have a game where there are WOODEN CRATES, and WOODEN DOORS. That means we have two entities, the Crate entity, and the Door entity. Also, wood can break, and wood can burn. These are two components.

  • Wooden Crate
    • breakable
    • flammable
  • Wooden Door
    • breakable
    • flammable

It lends itself pretty well to our system. Let’s see what are our components’ outstanding traits:

Breakable

Properties
- isBroken - bool
- healthPoints - number

Events
- onBroken

Methods
- hurt(damage)
- break()

Flammable

Properties
- isBurnt - bool
- isBurning - bool
- burnPoints - number (like health points, removed by burning)

Events
- onBurnt

Methods
- burn()
- burnt()

This is totally BAREBONES, we could add a lot more properties and methods for the sake of making this even more interesting, but this is a bit out of the scope of this tutorial, and in time you’ll be able to do it yourself.

ALRIGHT! Remember our part 1 on Entity Component System? We’ll continue with what we learned on that tutorial. Let’s take a look at what our components look like in a file.

Breakable
var Components = Components || {};

Components.Breakable = Components.Breakable || function() {

	// Reduce conflicts
	var self = this;

	// Name
	self.name = "Breakable";

	// Target
	self.target = null;


	// -------------------------
	// Properties
	// -------------------------
	self.isBroken = false;
	self.healthPoints = 100;


	// -------------------------
	// Events
	// -------------------------
	self.onBroken = new Phaser.Signal();


	// -------------------------
	// Methods
	// -------------------------
	self.setTarget = function(t) {
		self.target = t;
		console.log("[Breakable] :: added target " + t);
	}

	self.hurt = function(damage) {

		if (self.isBroken) {
			return;
		}

		self.healthPoints -= damage;

		console.log("[Breakable] :: hit for " + damage + " points. " + self.healthPoints + " points remaining.");

		if (self.healthPoints <= 0) {

			self.isBroken = true;
			self.break();
		}
	}

	self.break = function() {
		console.log("[Breakable] :: break");	
		self.onBroken.dispatch();
	}

	self.update = function() {
		// console.log("[Breakable] :: update");
	}

}
Flammable
var Components = Components || {};

Components.Flammable = Components.Flammable || function() {

	// Reduce conflicts
	var self = this;

	// Name
	self.name = "Flammable";

	// Target
	self.target = null;

	// -------------------------
	// Properties
	// -------------------------
	self.isBurning = false;
	self.isBurnt = false;
	self.burnPoints = 1000;


	// -------------------------
	// Events
	// -------------------------
	self.onBurnt = new Phaser.Signal();


	// -------------------------
	// Methods
	// -------------------------
	self.setTarget = function(t) {
		self.target = t;
		console.log("[Flammable] :: added target " + t);
	}

	self.burn = function() {
	    if (self.isBurnt) return;
	    
	    self.isBurning = true;
		self.burnPoints--;
		console.log("[Flammable] :: burn (" + self.burnPoints + ")");

		if (self.burnPoints <= 0) {
			self.burnt();
		}
	}

	self.burnt = function() {
		self.isBurning = false;
		self.isBurnt = true;
		console.log("[Flammable] :: burnt");
		self.onBurnt.dispatch();
	}

	self.update = function() {
		// console.log("[Flammable] :: update");
		if (self.isBurning) {
			self.burn();
		}
	}
}

So do you see how the only thing in these components is just handling something VERY specific? They are ZERO aware of the door, and of any other components. They are blind.


But you ask…

“But wait!”

– you say, clearly flustered, your fists ready to fight –

“If the components are not aware of each other, how can the flames break the entity?”

Excellent question! The glue resides on the entity. BEHOLD!

// How do you make entities communicate?

// First you add them
var comp1 = this.addComponent(new Component1());
var comp2 = this.addComponent(new Component2());
// ... how many as you need
var compN = this.addComponent(new ComponentN());


// Then you LISTEN to them
comp1.onEvent.add(compN.method, this);
comp2.onEvent.add(compN.method, this);
compN.onEvent.add(comp1.method, this);
compN.onEvent.add(comp2.method, this);


...

// Practical example
var Flammable = this.addComponent(new Components.Flammable());
var Breakable = this.addComponent(new Components.Breakable());

// This is the GLUE!
Flammable.onBurnt.add(Breakable.break, this);

So as you can see, communication between components is as easy as
masterComponent.signal.add ( slaveComponent.method, this );

Now that we have our glue, our components and our entities, it’s easy to tie them all together. Here’s the complete file for a simple Door.

// Create the Door entity
Door = function(game, x, y, state) {
	Phaser.Sprite.call(this, game, x, y, 'door');
	this.game = game;
	this.state = state;

	this.components = {};
	game.add.existing(this);
}

Door.prototype = Object.create(Phaser.Sprite.prototype);
Door.prototype.constructor = Door;


// Method to add components
Door.prototype.addComponent = function(comp) {
	this.components[comp.name] = comp;
	this.components[comp.name].setTarget(this);

    console.log("[Door] :: added component " + comp.name);
	return comp;
}

Door.prototype.preload = function() {
	console.log("[Door] :: preload");
};

Door.prototype.create = function() {
	console.log("[Door] :: create");

	// Add Components
	var Flammable = this.addComponent(new Components.Flammable());
	var Breakable = this.addComponent(new Components.Breakable());

    // This is the GLUE!
	Flammable.onBurnt.add(Breakable.break, this);
};

Door.prototype.getComponent = function(componentName) {
	return this.components[componentName];
}

Door.prototype.update = function() {

	Object.keys(this.components).forEach(function(a, b, c) {
		this.components[a].update();
	}, this);
}

Now, your Door only needs to catch fire to start its terrible non-zen journey into destruction.

// In the Door class...
// ...

this.getComponent("Flammable").burn();

// ...

And voilà! FIRE! DESTRUCTION! PAIN! TERRIBLE THINGS!
All of this with components 😀


Well?

Thank you for reading, and don’t hesitate to comment and share!

Comments

  1. Excellent write up! Stuff like this really lights up the imagination on ways to use phaser to its full potential. I’ve often found myself reinventing the wheel, forgetting that so many of that code is tucked away in phaser somewhere. No more 🙂