An entity/component system’s attempt using Box2D

One year ago, I heard about the Entity/Component Model for the first time. It seemed to be awesome for game design / development. This is 3 links/resources for a good introduction : Gamasutra the Entity Component Model, Entity Systems are the future of MMOG development, Entity Systems. Then Richard Lord has made a very good blog post with code example using his own framework : Ash. And finally, Shaun Smith’s experience using Ash.

All the code example is available on the new Citrus Engine’s GitHub repository. Compile the src/entity/Main.as

During this year, entity/component system has always been something I wanted to try… but that was scary : this is not OOP, that’s a new way of programming. I wanted to add this system to the Citrus Engine as an option to handle complex object. For basic object OOP is great and easy to set up, but for complex game object which may have new abilities on the fly, you’ve quickly a Class with 700 lines of code and conditions. Not easy to maintain some weeks later. The Citrus Engine handles physics engine (Box2D & Nape) & a simple math-based collision-detection system. However it comes built-in with a “platformer” starter-kit based on Box2D, so I made my entity/component system’s experiments using Box2D. And that was not simple.

When I heard for the first time of entity/component system, I quickly understand that you can add behaviors very easily :

var heroEntity:Entity = new Entity();
heroEntity.add(New PhysicsComponent({x:200, y:270, width:40, height:60)).add(new InputComponent({right:Keyboard.RIGHT})).add(new ViewComponent({view:"hero.swf"}));

And add new ability in a magic way :

heroEntity.add(new LadderAbilityComponent());

But what happen when you want to add to your hero the ability to climb on a ladder? You’ve to specify the new key input, an animation, hero behaviors, collision, physics… it has an impact on all your previous component! Using a physics engine like Box2D doesn’t simplify that. The Asteroids game example offers with Ash is quite simple. I’ve understood how it works, but using physics engine makes it all much more complex.

Now it’s time for some code. I’ve worked on a Hero’s entity, it is able to walk, jump, duck, get hurt, give damage… with animations. That’s the hero on the Citrus Engine site. Play with it.
Its class code, programmed the “classic way” based on OOP (source on GitHub too).

package com.citrusengine.objects.platformer.box2d
{
 
	import Box2DAS.Common.V2;
	import Box2DAS.Dynamics.ContactEvent;
	import Box2DAS.Dynamics.b2Fixture;
 
	import com.citrusengine.math.MathVector;
	import com.citrusengine.objects.Box2DPhysicsObject;
	import com.citrusengine.physics.Box2DCollisionCategories;
	import com.citrusengine.utils.Box2DShapeMaker;
 
	import org.osflash.signals.Signal;
 
	import flash.display.MovieClip;
	import flash.ui.Keyboard;
	import flash.utils.clearTimeout;
	import flash.utils.getDefinitionByName;
	import flash.utils.setTimeout;
 
	/**
	 * This is a common, simple, yet solid implementation of a side-scrolling Hero. 
	 * The hero can run, jump, get hurt, and kill enemies. It dispatches signals
	 * when significant events happen. The game state's logic should listen for those signals
	 * to perform game state updates (such as increment coin collections).
	 * 
	 * Don't store data on the hero object that you will need between two or more levels (such
	 * as current coin count). The hero should be re-created each time a state is created or reset.
	 */	
	public class Hero extends Box2DPhysicsObject
	{
		//properties
		/**
		 * This is the rate at which the hero speeds up when you move him left and right. 
		 */
		[Inspectable(defaultValue="1")]
		public var acceleration:Number = 1;
 
		/**
		 * This is the fastest speed that the hero can move left or right. 
		 */
		[Inspectable(defaultValue="8")]
		public var maxVelocity:Number = 8;
 
		/**
		 * This is the initial velocity that the hero will move at when he jumps.
		 */
		[Inspectable(defaultValue="11")]
		public var jumpHeight:Number = 11;
 
		/**
		 * This is the amount of "float" that the hero has when the player holds the jump button while jumping. 
		 */
		[Inspectable(defaultValue="0.3")]
		public var jumpAcceleration:Number = 0.3;
 
		/**
		 * This is the y velocity that the hero must be travelling in order to kill a Baddy.
		 */
		[Inspectable(defaultValue="3")]
		public var killVelocity:Number = 3;
 
		/**
		 * The y velocity that the hero will spring when he kills an enemy. 
		 */
		[Inspectable(defaultValue="8")]
		public var enemySpringHeight:Number = 8;
 
		/**
		 * The y velocity that the hero will spring when he kills an enemy while pressing the jump button. 
		 */
		[Inspectable(defaultValue="9")]
		public var enemySpringJumpHeight:Number = 9;
 
		/**
		 * How long the hero is in hurt mode for. 
		 */
		[Inspectable(defaultValue="1000")]
		public var hurtDuration:Number = 1000;
 
		/**
		 * The amount of kick-back that the hero jumps when he gets hurt. 
		 */
		[Inspectable(defaultValue="6")]
		public var hurtVelocityX:Number = 6;
 
		/**
		 * The amount of kick-back that the hero jumps when he gets hurt. 
		 */
		[Inspectable(defaultValue="10")]
		public var hurtVelocityY:Number = 10;
 
		/**
		 * Determines whether or not the hero's ducking ability is enabled.
		 */
		[Inspectable(defaultValue="true")]
		public var canDuck:Boolean = true;
 
		//events
		/**
		 * Dispatched whenever the hero jumps. 
		 */
		public var onJump:Signal;
 
		/**
		 * Dispatched whenever the hero gives damage to an enemy. 
		 */		
		public var onGiveDamage:Signal;
 
		/**
		 * Dispatched whenever the hero takes damage from an enemy. 
		 */		
		public var onTakeDamage:Signal;
 
		/**
		 * Dispatched whenever the hero's animation changes. 
		 */		
		public var onAnimationChange:Signal;
 
		protected var _groundContacts:Array = [];//Used to determine if he's on ground or not.
		protected var _enemyClass:Class = Baddy;
		protected var _onGround:Boolean = false;
		protected var _springOffEnemy:Number = -1;
		protected var _hurtTimeoutID:Number;
		protected var _hurt:Boolean = false;
		protected var _friction:Number = 0.75;
		protected var _playerMovingHero:Boolean = false;
		protected var _controlsEnabled:Boolean = true;
		protected var _ducking:Boolean = false;
		protected var _combinedGroundAngle:Number = 0;
 
		public static function Make(name:String, x:Number, y:Number, width:Number, height:Number, view:* = null):Hero
		{
			if (view == null) view = MovieClip;
			return new Hero(name, { x: x, y: y, width: width, height: height, view: view } );
		}
 
		/**
		 * Creates a new hero object.
		 */		
		public function Hero(name:String, params:Object = null)
		{
			super(name, params);
 
			onJump = new Signal();
			onGiveDamage = new Signal();
			onTakeDamage = new Signal();
			onAnimationChange = new Signal();
		}
 
		override public function destroy():void
		{
			_fixture.removeEventListener(ContactEvent.PRE_SOLVE, handlePreSolve);
			_fixture.removeEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);
			_fixture.removeEventListener(ContactEvent.END_CONTACT, handleEndContact);
			clearTimeout(_hurtTimeoutID);
			onJump.removeAll();
			onGiveDamage.removeAll();
			onTakeDamage.removeAll();
			onAnimationChange.removeAll();
			super.destroy();
		}
 
		/**
		 * Whether or not the player can move and jump with the hero. 
		 */	
		public function get controlsEnabled():Boolean
		{
			return _controlsEnabled;
		}
 
		public function set controlsEnabled(value:Boolean):void
		{
			_controlsEnabled = value;
 
			if (!_controlsEnabled)
				_fixture.SetFriction(_friction);
		}
 
		/**
		 * Returns true if the hero is on the ground and can jump. 
		 */		
		public function get onGround():Boolean
		{
			return _onGround;
		}
 
		/**
		 * The Hero uses the enemyClass parameter to know who he can kill (and who can kill him).
		 * Use this setter to to pass in which base class the hero's enemy should be, in String form
		 * or Object notation.
		 * For example, if you want to set the "Baddy" class as your hero's enemy, pass
		 * "com.citrusengine.objects.platformer.Baddy", or Baddy (with no quotes). Only String
		 * form will work when creating objects via a level editor.
		 */
		[Inspectable(defaultValue="com.citrusengine.objects.platformer.box2d.Baddy",type="String")]
		public function set enemyClass(value:*):void
		{
			if (value is String)
				_enemyClass = getDefinitionByName(value as String) as Class;
			else if (value is Class)
				_enemyClass = value;
		}
 
		/**
		 * This is the amount of friction that the hero will have. Its value is multiplied against the
		 * friction value of other physics objects.
		 */	
		public function get friction():Number
		{
			return _friction;
		}
 
		[Inspectable(defaultValue="0.75")]
		public function set friction(value:Number):void
		{
			_friction = value;
 
			if (_fixture)
			{
				_fixture.SetFriction(_friction);
			}
		}
 
		override public function update(timeDelta:Number):void
		{
			super.update(timeDelta);
 
			var velocity:V2 = _body.GetLinearVelocity();
 
			if (controlsEnabled)
			{
				var moveKeyPressed:Boolean = false;
 
				_ducking = (_ce.input.isDown(Keyboard.DOWN) && _onGround && canDuck);
 
				if (_ce.input.isDown(Keyboard.RIGHT) && !_ducking)
				{
					velocity = V2.add(velocity, getSlopeBasedMoveAngle());
					moveKeyPressed = true;
				}
 
				if (_ce.input.isDown(Keyboard.LEFT) && !_ducking)
				{
					velocity = V2.subtract(velocity, getSlopeBasedMoveAngle());
					moveKeyPressed = true;
				}
 
				//If player just started moving the hero this tick.
				if (moveKeyPressed && !_playerMovingHero)
				{
					_playerMovingHero = true;
					_fixture.SetFriction(0); //Take away friction so he can accelerate.
				}
				//Player just stopped moving the hero this tick.
				else if (!moveKeyPressed && _playerMovingHero)
				{
					_playerMovingHero = false;
					_fixture.SetFriction(_friction); //Add friction so that he stops running
				}
 
				if (_onGround && _ce.input.justPressed(Keyboard.SPACE) && !_ducking)
				{
					velocity.y = -jumpHeight;
					onJump.dispatch();
				}
 
				if (_ce.input.isDown(Keyboard.SPACE) && !_onGround && velocity.y < 0) 				{ 					velocity.y -= jumpAcceleration; 				} 				 				if (_springOffEnemy != -1) 				{ 					if (_ce.input.isDown(Keyboard.SPACE)) 						velocity.y = -enemySpringJumpHeight; 					else 						velocity.y = -enemySpringHeight; 					_springOffEnemy = -1; 				} 				 				//Cap velocities 				if (velocity.x > (maxVelocity))
					velocity.x = maxVelocity;
				else if (velocity.x < (-maxVelocity))
					velocity.x = -maxVelocity;
 
				//update physics with new velocity
				_body.SetLinearVelocity(velocity);
			}
 
			updateAnimation();
		}
 
		/**
		 * Returns the absolute walking speed, taking moving platforms into account.
		 * Isn't super performance-light, so use sparingly.
		 */
		public function getWalkingSpeed():Number
		{
			var groundVelocityX:Number = 0;
			for each (var groundContact:b2Fixture in _groundContacts)
			{
				groundVelocityX += groundContact.GetBody().GetLinearVelocity().x;
			}
 
			return _body.GetLinearVelocity().x - groundVelocityX;
		}
 
		/**
		 * Hurts the hero, disables his controls for a little bit, and dispatches the onTakeDamage signal. 
		 */		
		public function hurt():void
		{
			_hurt = true;
			controlsEnabled = false;
			_hurtTimeoutID = setTimeout(endHurtState, hurtDuration);
			onTakeDamage.dispatch();
 
			//Makes sure that the hero is not frictionless while his control is disabled
			if (_playerMovingHero)
			{
				_playerMovingHero = false;
				_fixture.SetFriction(_friction);
			}
		}
 
		override protected function defineBody():void
		{
			super.defineBody();
			_bodyDef.fixedRotation = true;
			_bodyDef.allowSleep = false;
		}
 
		override protected function createShape():void
		{
			_shape = Box2DShapeMaker.BeveledRect(_width, _height, 0.1);
		}
 
		override protected function defineFixture():void
		{
			super.defineFixture();
			_fixtureDef.friction = _friction;
			_fixtureDef.restitution = 0;
			_fixtureDef.filter.categoryBits = Box2DCollisionCategories.Get("GoodGuys");
			_fixtureDef.filter.maskBits = Box2DCollisionCategories.GetAll();
		}
 
		override protected function createFixture():void
		{
			super.createFixture();
			_fixture.m_reportPreSolve = true;
			_fixture.m_reportBeginContact = true;
			_fixture.m_reportEndContact = true;
			_fixture.addEventListener(ContactEvent.PRE_SOLVE, handlePreSolve);
			_fixture.addEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);
			_fixture.addEventListener(ContactEvent.END_CONTACT, handleEndContact);
		}
 
		protected function handlePreSolve(e:ContactEvent):void 
		{
			if (!_ducking)
				return;
 
			var other:Box2DPhysicsObject = e.other.GetBody().GetUserData() as Box2DPhysicsObject;
 
			var heroTop:Number = y;
			var objectBottom:Number = other.y + (other.height / 2);
 
			if (objectBottom < heroTop)
				e.contact.Disable();
		}
 
		protected function handleBeginContact(e:ContactEvent):void
		{
			var collider:Box2DPhysicsObject = e.other.GetBody().GetUserData();
 
			if (_enemyClass && collider is _enemyClass)
			{
				if (_body.GetLinearVelocity().y < killVelocity && !_hurt) 				{ 					hurt(); 					 					//fling the hero 					var hurtVelocity:V2 = _body.GetLinearVelocity(); 					hurtVelocity.y = -hurtVelocityY; 					hurtVelocity.x = hurtVelocityX; 					if (collider.x > x)
						hurtVelocity.x = -hurtVelocityX;
					_body.SetLinearVelocity(hurtVelocity);
				}
				else
				{
					_springOffEnemy = collider.y - height;
					onGiveDamage.dispatch();
				}
			}
 
			//Collision angle
			if (e.normal) //The normal property doesn't come through all the time. I think doesn't come through against sensors.
			{
				var collisionAngle:Number = new MathVector(e.normal.x, e.normal.y).angle * 180 / Math.PI;
				if (collisionAngle > 45 && collisionAngle < 135)
				{
					_groundContacts.push(e.other);
					_onGround = true;
					updateCombinedGroundAngle();
				}
			}
		}
 
		protected function handleEndContact(e:ContactEvent):void
		{
			//Remove from ground contacts, if it is one.
			var index:int = _groundContacts.indexOf(e.other);
			if (index != -1)
			{
				_groundContacts.splice(index, 1);
				if (_groundContacts.length == 0)
					_onGround = false;
				updateCombinedGroundAngle();
			}
		}
 
		protected function getSlopeBasedMoveAngle():V2
		{
			return new V2(acceleration, 0).rotate(_combinedGroundAngle);
		}
 
		protected function updateCombinedGroundAngle():void
		{
			_combinedGroundAngle = 0;
 
			if (_groundContacts.length == 0)
				return;
 
			for each (var contact:b2Fixture in _groundContacts)
				var angle:Number = contact.GetBody().GetAngle();
 
			var turn:Number = 45 * Math.PI / 180;
			angle = angle % turn;
			_combinedGroundAngle += angle;
			_combinedGroundAngle /= _groundContacts.length;
		}
 
		protected function endHurtState():void
		{
			_hurt = false;
			controlsEnabled = true;
		}
 
		protected function updateAnimation():void
		{
			var prevAnimation:String = _animation;
 
			var velocity:V2 = _body.GetLinearVelocity();
			if (_hurt)
			{
				_animation = "hurt";
			}
			else if (!_onGround)
			{
				_animation = "jump";
			}
			else if (_ducking)
			{
				_animation = "duck";
			}
			else
			{
				var walkingSpeed:Number = getWalkingSpeed();
				if (walkingSpeed < -acceleration) 				{ 					_inverted = true; 					_animation = "walk"; 				} 				else if (walkingSpeed > acceleration)
				{
					_inverted = false;
					_animation = "walk";
				}
				else
				{
					_animation = "idle";
				}
			}
 
			if (prevAnimation != _animation)
			{
				onAnimationChange.dispatch();
			}
		}
	}
}

You’ve certainly noticed that the Physics code is the most important and long part. It is very difficult to separate it into different components.

Let’s show some code of my attempt to add an entity/component system to the Citrus Engine using the Hero as an example :
The GameState class :

package entity {
 
	import com.citrusengine.core.State;
	import com.citrusengine.objects.platformer.box2d.Platform;
	import com.citrusengine.physics.Box2D;
	import com.citrusengine.system.Entity;
	import com.citrusengine.system.components.InputComponent;
	import com.citrusengine.system.components.box2d.hero.HeroCollisionComponent;
	import com.citrusengine.system.components.box2d.hero.HeroMovementComponent;
	import com.citrusengine.system.components.box2d.hero.HeroPhysicsComponent;
	import com.citrusengine.system.components.box2d.hero.HeroViewComponent;
 
	/**
	 * @author Aymeric
	 */
	public class EntityGameState extends State {
 
		private var heroEntity : Entity;
		private var input : InputComponent;
		private var _view : HeroViewComponent;
		private var physics : HeroPhysicsComponent;
 
		public function EntityGameState() {
 
			super();
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var box2d:Box2D = new Box2D("box2D");
			//box2d.visible = true;
			add(box2d);
 
			heroEntity = new Entity("heroEntity");
 
			physics = new HeroPhysicsComponent("physics", {x:200, y:270, width:40, height:60, entity:heroEntity});
			input = new InputComponent("input", {entity:heroEntity});
			var collision:HeroCollisionComponent = new HeroCollisionComponent("collision", {entity:heroEntity});
			var move:HeroMovementComponent = new HeroMovementComponent("move", {entity:heroEntity});
			_view = new HeroViewComponent("view", {view:"Patch.swf", entity:heroEntity});
 
			heroEntity.add(physics).add(input).add(collision).add(move).add(_view);
			heroEntity.initialize();
 
			addEntity(heroEntity, _view);
 
			add(new Platform("platform", {x:600, y:350, width:1800, height:20}));
		}
 
	}
}

A Hero is defined by its physics, input keys, collision management, movement, and a graphic view/object. Then it is added to the state which manages update and object destruction.

The Entity class :

package com.citrusengine.system {
 
	import com.citrusengine.core.CitrusObject;
 
	import flash.utils.Dictionary;
 
	/**
	 * A game entity is compound by components. The entity serves as a link to communicate between components.
	 * It extends the CitrusObject class to enjoy its params setter.
	 */
	public class Entity extends CitrusObject {
 
		public var components:Dictionary;
 
		public function Entity(name:String, params:Object = null) {
 
			super(name, params);
 
			components = new Dictionary();
		}
 
		/**
		 * Add a component to the entity.
		 */
		public function add(component:Component):Entity {
 
			components[component.name] = component;
 
			return this;
		}
 
		/**
		 * Remove a component from the entity.
		 */
		public function remove(component:Component):void {
 
			if (components[component.name]) {
				component.destroy();
				delete components[component.name];
			}
		}
 
		/**
		 * After all the components have been added call this function to perform an init on them.
		 * Mostly used if you want to access to other components through the entity.
		 */
		public function initialize():void {
 
			for each (var component:Component in components) {
				component.initialize();
			}
		}
 
		/**
		 * Destroy the entity and its components.
		 */
		override public function destroy():void {
 
			for each (var component:Component in components) {
				component.destroy();
			}
 
			components = null;
 
			super.destroy();
		}
 
		/**
		 * Perform an update on all entity's components.
		 */
		override public function update(timeDelta:Number):void {
 
			for each (var component:Component in components) {
				component.update(timeDelta);
			}
		}
	}
}

Using dictionary and for each loop is not very optimized, but it’s ok for this entity/component system’s test.
The Component class :

package com.citrusengine.system {
 
	import com.citrusengine.core.CitrusEngine;
	import com.citrusengine.core.CitrusObject;
 
	/**
	 * A component is an object dedicate to a (single) task for an entity : physics, collision, inputs, view, movement... management.
	 * You will use an entity when your object become too much complex to manage into a single class.
	 * Preferably if you use a physics engine, create at first the entity's physics component.
	 * It extends the CitrusObject class to enjoy its params setter.
	 */
	public class Component extends CitrusObject {
 
		public var entity:Entity;
 
		protected var _ce:CitrusEngine;
 
		public function Component(name:String, params:Object = null) {
 
			super(name, params);
 
			_ce = CitrusEngine.getInstance();
		}
 
		/**
		 * Register other components in your component class in this function.
		 * It should be call after all components have been added to an entity.
		 */
		public function initialize():void {
 
		}
 
		/**
		 * Destroy the component, most of the time called by its entity.
		 */
		override public function destroy():void {
 
			super.destroy();
		}
 
		/**
		 * Perform an update on the component, called by its entity.
		 */
		override public function update(timeDelta:Number):void {
		}
 
	}
}

Wait! In Ash, a component only stores data. It doesn’t have logic, but System does. At the moment, I’ve chosen to add logic in components. I may separate them later.

The Input & View components (don’t depend directly of Starling, flash display list, Box2D, Nape…) :

package com.citrusengine.system.components {
 
	import com.citrusengine.system.Component;
 
	import flash.ui.Keyboard;
 
	/**
	 * An input component, it will inform if the key is down, just pressed or just released.
	 */
	public class InputComponent extends Component {
 
		public var rightKeyIsDown:Boolean = false;
		public var leftKeyIsDown:Boolean = false;
		public var downKeyIsDown:Boolean = false;
		public var spaceKeyIsDown:Boolean = false;
		public var spaceKeyJustPressed:Boolean = false;
 
		public function InputComponent(name:String, params:Object = null) {
			super(name, params);
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			rightKeyIsDown = _ce.input.isDown(Keyboard.RIGHT);
			leftKeyIsDown = _ce.input.isDown(Keyboard.LEFT);
			downKeyIsDown = _ce.input.isDown(Keyboard.DOWN);
			spaceKeyIsDown = _ce.input.isDown(Keyboard.SPACE);
			spaceKeyJustPressed = _ce.input.justPressed(Keyboard.SPACE);
		}
	}
}
package com.citrusengine.system.components {
 
	import com.citrusengine.system.Component;
	import com.citrusengine.view.ISpriteView;
 
	import org.osflash.signals.Signal;
 
	import flash.display.MovieClip;
 
	/**
	 * The view component, it manages everything to set the view.
	 * Extend it to handle animation.
	 */
	public class ViewComponent extends Component implements ISpriteView {
 
		public var onAnimationChange:Signal;
 
		protected var _x:Number = 0;
		protected var _y:Number = 0;
		protected var _rotation:Number = 0;
		protected var _inverted:Boolean = false;
		protected var _parallax:Number = 1;
		protected var _animation:String = "";
		protected var _visible:Boolean = true;
		protected var _view:* = MovieClip;
 
		private var _group:Number = 0;
		private var _offsetX:Number = 0;
		private var _offsetY:Number = 0;
		private var _registration:String = "center";
 
		public function ViewComponent(name:String, params:Object = null) {
			super(name, params);
 
			onAnimationChange = new Signal();
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
		}
 
		override public function destroy():void {
 
			onAnimationChange.removeAll();
 
			super.destroy();
		}
 
		public function get x():Number {
			return _x;
		}
 
		public function set x(value:Number):void {
			_x = value;
		}
 
		public function get y():Number {
			return _y;
		}
 
		public function set y(value:Number):void {
			_y = value;
		}
 
		public function get rotation():Number {
			return _rotation;
		}
 
		public function set rotation(value:Number):void {
			_rotation = value;
		}
 
		public function get parallax():Number {
			return _parallax;
		}
 
		public function set parallax(value:Number):void {
			_parallax = value;
		}
 
		public function get group():Number
		{
			return _group;
		}
 
		public function set group(value:Number):void
		{
			_group = value;
		}
 
		public function get visible():Boolean
		{
			return _visible;
		}
 
		public function set visible(value:Boolean):void
		{
			_visible = value;
		}
 
		public function get view():*
		{
			return _view;
		}
 
		public function set view(value:*):void
		{
			_view = value;
		}
 
		public function get animation():String
		{
			return _animation;
		}
 
		public function get inverted():Boolean
		{
			return _inverted;
		}
 
		public function get offsetX():Number
		{
			return _offsetX;
		}
 
		public function set offsetX(value:Number):void
		{
			_offsetX = value;
		}
 
		public function get offsetY():Number
		{
			return _offsetY;
		}
 
		public function set offsetY(value:Number):void
		{
			_offsetY = value;
		}
 
		public function get registration():String
		{
			return _registration;
		}
 
		public function set registration(value:String):void
		{
			_registration = value;
		}
	}
}

Now some Box2D components : Box2D physics, collision handler, movement :

package com.citrusengine.system.components.box2d {
 
	import Box2DAS.Collision.Shapes.b2CircleShape;
	import Box2DAS.Collision.Shapes.b2PolygonShape;
	import Box2DAS.Collision.Shapes.b2Shape;
	import Box2DAS.Common.V2;
	import Box2DAS.Dynamics.b2Body;
	import Box2DAS.Dynamics.b2BodyDef;
	import Box2DAS.Dynamics.b2Fixture;
	import Box2DAS.Dynamics.b2FixtureDef;
 
	import com.citrusengine.core.CitrusEngine;
	import com.citrusengine.physics.Box2D;
	import com.citrusengine.physics.Box2DCollisionCategories;
	import com.citrusengine.system.Component;
 
	/**
	 * The base's physics Box2D Component. Manage (just) the physics creation.
	 */
	public class Box2DComponent extends Component {
 
		protected var _box2D:Box2D;
		protected var _bodyDef:b2BodyDef;
		protected var _body:b2Body;
		protected var _shape:b2Shape;
		protected var _fixtureDef:b2FixtureDef;
		protected var _fixture:b2Fixture;
 
		protected var _x:Number = 0;
		protected var _y:Number = 0;
		protected var _rotation:Number = 0;
		protected var _width:Number = 1;
		protected var _height:Number = 1;
		protected var _radius:Number = 0;
 
		public function Box2DComponent(name:String, params:Object = null) {
 
			_box2D = CitrusEngine.getInstance().state.getFirstObjectByType(Box2D) as Box2D;
 
			super(name, params);
 
			defineBody();
			createBody();
			createShape();
			defineFixture();
			createFixture();
			defineJoint();
			createJoint();
		}
 
		override public function destroy():void {
 
			_body.destroy();
			_fixtureDef.destroy();
			_shape.destroy();
			_bodyDef.destroy();
 
			super.destroy();
		}
 
		public function get x():Number
		{
			if (_body)
				return _body.GetPosition().x * _box2D.scale;
			else
				return _x * _box2D.scale;
		}
 
		public function set x(value:Number):void
		{
			_x = value / _box2D.scale;
 
			if (_body)
			{
				var pos:V2 = _body.GetPosition();
				pos.x = _x;
				_body.SetTransform(pos, _body.GetAngle());
			}
		}
 
		public function get y():Number
		{
			if (_body)
				return _body.GetPosition().y * _box2D.scale;
			else
				return _y * _box2D.scale;
		}
 
		public function set y(value:Number):void
		{
			_y = value / _box2D.scale;
 
			if (_body)
			{
				var pos:V2 = _body.GetPosition();
				pos.y = _y;
				_body.SetTransform(pos, _body.GetAngle());
			}
		}
 
		public function get rotation():Number
		{
			if (_body)
				return _body.GetAngle() * 180 / Math.PI;
			else
				return _rotation * 180 / Math.PI;
		}
 
		public function set rotation(value:Number):void
		{
			_rotation = value * Math.PI / 180;
 
			if (_body)
				_body.SetTransform(_body.GetPosition(), _rotation); 
		}
 
		/**
		 * This can only be set in the constructor parameters. 
		 */		
		public function get width():Number
		{
			return _width * _box2D.scale;
		}
 
		public function set width(value:Number):void
		{
			_width = value / _box2D.scale;
 
			if (_initialized)
			{
				trace("Warning: You cannot set " + this + " width after it has been created. Please set it in the constructor.");
			}
		}
 
		/**
		 * This can only be set in the constructor parameters. 
		 */	
		public function get height():Number
		{
			return _height * _box2D.scale;
		}
 
		public function set height(value:Number):void
		{
			_height = value / _box2D.scale;
 
			if (_initialized)
			{
				trace("Warning: You cannot set " + this + " height after it has been created. Please set it in the constructor.");
			}
		}
 
		/**
		 * This can only be set in the constructor parameters. 
		 */	
		public function get radius():Number
		{
			return _radius * _box2D.scale;
		}
 
		/**
		 * The object has a radius or a width & height. It can't have both.
		 */
		public function set radius(value:Number):void
		{
			_radius = value / _box2D.scale;
 
			if (_initialized)
			{
				trace("Warning: You cannot set " + this + " radius after it has been created. Please set it in the constructor.");
			}
		}
 
		/**
		 * A direction reference to the Box2D body associated with this object.
		 */
		public function get body():b2Body
		{
			return _body;
		}
 
		/**
		 * This method will often need to be overriden to provide additional definition to the Box2D body object. 
		 */		
		protected function defineBody():void
		{
			_bodyDef = new b2BodyDef();
			_bodyDef.type = b2Body.b2_dynamicBody;
			_bodyDef.position.v2 = new V2(_x, _y);
			_bodyDef.angle = _rotation;
		}
 
		/**
		 * This method will often need to be overriden to customize the Box2D body object. 
		 */	
		protected function createBody():void
		{
			_body = _box2D.world.CreateBody(_bodyDef);
			_body.SetUserData(this);
		}
 
		/**
		 * This method will often need to be overriden to customize the Box2D shape object.
		 * The PhysicsObject creates a rectangle by default if the radius it not defined, but you can replace this method's
		 * definition and instead create a custom shape, such as a line or circle.
		 */	
		protected function createShape():void
		{
			if (_radius != 0) {
				_shape = new b2CircleShape();
				b2CircleShape(_shape).m_radius = _radius;
			} else {
				_shape = new b2PolygonShape();
				b2PolygonShape(_shape).SetAsBox(_width / 2, _height / 2);
			}
		}
 
		/**
		 * This method will often need to be overriden to provide additional definition to the Box2D fixture object. 
		 */	
		protected function defineFixture():void
		{
			_fixtureDef = new b2FixtureDef();
			_fixtureDef.shape = _shape;
			_fixtureDef.density = 1;
			_fixtureDef.friction = 0.6;
			_fixtureDef.restitution = 0.3;
			_fixtureDef.filter.categoryBits = Box2DCollisionCategories.Get("Level");
			_fixtureDef.filter.maskBits = Box2DCollisionCategories.GetAll();
		}
 
		/**
		 * This method will often need to be overriden to customize the Box2D fixture object. 
		 */	
		protected function createFixture():void
		{
			_fixture = _body.CreateFixture(_fixtureDef);
		}
 
		/**
		 * This method will often need to be overriden to provide additional definition to the Box2D joint object.
		 * A joint is not automatically created, because joints require two bodies. Therefore, if you need to create a joint,
		 * you will also need to create additional bodies, fixtures and shapes, and then also instantiate a new b2JointDef
		 * and b2Joint object.
		 */	
		protected function defineJoint():void
		{
 
		}
 
		/**
		 * This method will often need to be overriden to customize the Box2D joint object. 
		 * A joint is not automatically created, because joints require two bodies. Therefore, if you need to create a joint,
		 * you will also need to create additional bodies, fixtures and shapes, and then also instantiate a new b2JointDef
		 * and b2Joint object.
		 */		
		protected function createJoint():void
		{
 
		}
	}
}
package com.citrusengine.system.components.box2d {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import com.citrusengine.system.Component;
 
	/**
	 * The Box2D collision component, extends it to handle collision.
	 */
	public class CollisionComponent extends Component {
 
		public function CollisionComponent(name:String, params:Object = null) {
 
			super(name, params);
		}
 
		public function handlePreSolve(e:ContactEvent):void {
 
		}
 
		public function handleBeginContact(e:ContactEvent):void {
 
		}
 
		public function handleEndContact(e:ContactEvent):void {
 
		}
	}
}
package com.citrusengine.system.components.box2d {
 
	import Box2DAS.Common.V2;
 
	import com.citrusengine.system.Component;
 
	/**
	 * The Box2D movement component, we've to know the Box2D physics component to be able to move it.
	 */
	public class MovementComponent extends Component {
 
		protected var _physicsComponent:Box2DComponent;
 
		protected var _velocity:V2;
 
		public function MovementComponent(name:String, params:Object = null) {
			super(name, params);
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			_physicsComponent = entity.components["physics"];
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			if (_physicsComponent) {
 
				_velocity = _physicsComponent.body.GetLinearVelocity();
			}
		}
 
	}
}

You can already see that there are directly some interactions between them. If you would put those somewhere else, you will have a class managing all the logic. Not very different from our first single class Hero base.

Now the Hero specific components. Hé wait!!! OOP!? Yep. Most of the physics properties are already defined in our Box2DComponent class, but our Hero has specific physics! It must handle collision as well, animations, movement… So its physics, movement, view, collision components class :

package com.citrusengine.system.components.box2d.hero {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import com.citrusengine.physics.Box2DCollisionCategories;
	import com.citrusengine.system.components.box2d.Box2DComponent;
	import com.citrusengine.system.components.box2d.CollisionComponent;
	import com.citrusengine.utils.Box2DShapeMaker;
 
	/**
	 * The Box2D Hero physics component add the fixture listener, change its friction, restitution...
	 */
	public class HeroPhysicsComponent extends Box2DComponent {
 
		protected var _collisionComponent:CollisionComponent;
 
		protected var _friction:Number = 0.75;
 
		public function HeroPhysicsComponent(name:String, params:Object = null) {
			super(name, params);
		}
 
		override public function destroy():void {
 
			_fixture.removeEventListener(ContactEvent.PRE_SOLVE, _collisionComponent.handlePreSolve);
			_fixture.removeEventListener(ContactEvent.BEGIN_CONTACT, _collisionComponent.handleBeginContact);
			_fixture.removeEventListener(ContactEvent.END_CONTACT, _collisionComponent.handleEndContact);
 
			super.destroy();
		}
 
		override public function initialize():void {
 
			_collisionComponent = entity.components["collision"];
 
			_fixture.addEventListener(ContactEvent.PRE_SOLVE, _collisionComponent.handlePreSolve);
			_fixture.addEventListener(ContactEvent.BEGIN_CONTACT, _collisionComponent.handleBeginContact);
			_fixture.addEventListener(ContactEvent.END_CONTACT, _collisionComponent.handleEndContact);
		}
 
		override protected function defineBody():void {
 
			super.defineBody();
 
			_bodyDef.fixedRotation = true;
			_bodyDef.allowSleep = false;
		}
 
		override protected function createShape():void {
 
			_shape = Box2DShapeMaker.BeveledRect(_width, _height, 0.1);
		}
 
		override protected function defineFixture():void {
 
			super.defineFixture();
 
			_fixtureDef.friction = _friction;
			_fixtureDef.restitution = 0;
			_fixtureDef.filter.categoryBits = Box2DCollisionCategories.Get("GoodGuys");
			_fixtureDef.filter.maskBits = Box2DCollisionCategories.GetAll();
		}
 
		override protected function createFixture():void {
 
			super.createFixture();
 
			_fixture.m_reportPreSolve = true;
			_fixture.m_reportBeginContact = true;
			_fixture.m_reportEndContact = true;
		}
 
		public function changeFixtureToZero():void {
			_fixture.SetFriction(0);
		}
 
		public function changeFixtureToItsInitialValue():void {
			_fixture.SetFriction(_friction);
		}
	}
}
package com.citrusengine.system.components.box2d.hero {
 
	import Box2DAS.Common.V2;
 
	import com.citrusengine.objects.Box2DPhysicsObject;
	import com.citrusengine.system.components.InputComponent;
	import com.citrusengine.system.components.box2d.MovementComponent;
 
	import org.osflash.signals.Signal;
 
	import flash.utils.clearTimeout;
	import flash.utils.setTimeout;
 
	/**
	 * The Box2D Hero movement component. Most of its properties are here. It uses a lot of informations from the input component & 
	 * some from the Box2D Hero collision component.
	 */
	public class HeroMovementComponent extends MovementComponent {
 
		//properties
		/**
		 * This is the rate at which the hero speeds up when you move him left and right. 
		 */
		public var acceleration:Number = 1;
 
		/**
		 * This is the fastest speed that the hero can move left or right. 
		 */
		public var maxVelocity:Number = 8;
 
		/**
		 * This is the initial velocity that the hero will move at when he jumps.
		 */
		public var jumpHeight:Number = 11;
 
		/**
		 * This is the amount of "float" that the hero has when the player holds the jump button while jumping. 
		 */
		public var jumpAcceleration:Number = 0.3;
 
		/**
		 * This is the y velocity that the hero must be travelling in order to kill a Baddy.
		 */
		public var killVelocity:Number = 3;
 
		/**
		 * The y velocity that the hero will spring when he kills an enemy. 
		 */
		public var enemySpringHeight:Number = 8;
 
		/**
		 * The y velocity that the hero will spring when he kills an enemy while pressing the jump button. 
		 */
		public var enemySpringJumpHeight:Number = 9;
 
		/**
		 * How long the hero is in hurt mode for. 
		 */
		public var hurtDuration:Number = 1000;
 
		/**
		 * The amount of kick-back that the hero jumps when he gets hurt. 
		 */
		public var hurtVelocityX:Number = 6;
 
		/**
		 * The amount of kick-back that the hero jumps when he gets hurt. 
		 */
		public var hurtVelocityY:Number = 10;
 
		/**
		 * Determines whether or not the hero's ducking ability is enabled.
		 */
		public var canDuck:Boolean = true;
 
		//events
		/**
		 * Dispatched whenever the hero jumps. 
		 */
		public var onJump:Signal;
 
		/**
		 * Dispatched whenever the hero gives damage to an enemy. 
		 */		
		public var onGiveDamage:Signal;
 
		/**
		 * Dispatched whenever the hero takes damage from an enemy. 
		 */		
		public var onTakeDamage:Signal;
 
		protected var _inputComponent:InputComponent;
		protected var _collisionComponent:HeroCollisionComponent;
 
		protected var _onGround:Boolean = false;
		protected var _springOffEnemy:Number = -1;
		protected var _hurtTimeoutID:Number;
		protected var _isHurt:Boolean = false;
		protected var _controlsEnabled:Boolean = true;
		protected var _playerMovingHero:Boolean = false;
		protected var _ducking:Boolean = false;
 
		public function HeroMovementComponent(name:String, params:Object = null) {
 
			super(name, params);
 
			onJump = new Signal();
			onGiveDamage = new Signal();
			onTakeDamage = new Signal();
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			_inputComponent = entity.components["input"];
			_collisionComponent = entity.components["collision"];
		}
 
		override public function destroy():void {
 
			onJump.removeAll();
			onGiveDamage.removeAll();
			onTakeDamage.removeAll();
 
			clearTimeout(_hurtTimeoutID);
 
			super.destroy();
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			if (controlsEnabled && _physicsComponent) {
 
				var moveKeyPressed:Boolean = false;
 
				_ducking = (_inputComponent.downKeyIsDown && _onGround && canDuck);
 
				if (_inputComponent.rightKeyIsDown && !_ducking) {
					_velocity = V2.add(_velocity, getSlopeBasedMoveAngle());
					moveKeyPressed = true;
				}
 
				if (_inputComponent.leftKeyIsDown && !_ducking) {					
					_velocity = V2.subtract(_velocity, getSlopeBasedMoveAngle());
					moveKeyPressed = true;
				}
 
				//If player just started moving the hero this tick.
				if (moveKeyPressed && !_playerMovingHero) {
					_playerMovingHero = true;
					(_physicsComponent as HeroPhysicsComponent).changeFixtureToZero(); //Take away friction so he can accelerate.
				}
				//Player just stopped moving the hero this tick.
				else if (!moveKeyPressed && _playerMovingHero) {
					_playerMovingHero = false;
					(_physicsComponent as HeroPhysicsComponent).changeFixtureToItsInitialValue(); //Add friction so that he stops running
				}
 
				if (_onGround && _inputComponent.spaceKeyJustPressed && !_ducking) {
					_velocity.y = -jumpHeight;
					onJump.dispatch();
				}
 
				if (_inputComponent.spaceKeyIsDown && !_onGround && _velocity.y < 0) 					_velocity.y -= jumpAcceleration; 					 				if (_springOffEnemy != -1) { 					if (_inputComponent.spaceKeyIsDown) 						_velocity.y = -enemySpringJumpHeight; 					else 						_velocity.y = -enemySpringHeight; 					_springOffEnemy = -1; 				} 				 				//Cap velocities 				if (_velocity.x > maxVelocity)
					_velocity.x = maxVelocity;
				else if (_velocity.x < -maxVelocity) 					_velocity.x = -maxVelocity; 				 				_physicsComponent.body.SetLinearVelocity(_velocity); 			} 		} 		 		/** 		 * The hero gives damage 		 */ 		  		public function giveDamage(collider:Box2DPhysicsObject):void { 			 			_springOffEnemy = collider.y - _physicsComponent.height; 			onGiveDamage.dispatch(); 		} 		 		/** 		 * Hurts and fling the hero, disables his controls for a little bit, and dispatches the onTakeDamage signal.  		 */		 		public function hurt(collider:Box2DPhysicsObject):void { 			 			_isHurt = true; 			controlsEnabled = false; 			_hurtTimeoutID = setTimeout(endHurtState, hurtDuration); 			onTakeDamage.dispatch(); 			 			var hurtVelocity:V2 = _physicsComponent.body.GetLinearVelocity(); 			hurtVelocity.y = -hurtVelocityY; 			hurtVelocity.x = hurtVelocityX; 			if (collider.x > _physicsComponent.x)
				hurtVelocity.x = -hurtVelocityX;
			_physicsComponent.body.SetLinearVelocity(hurtVelocity);
 
			//Makes sure that the hero is not frictionless while his control is disabled
			if (_playerMovingHero) {
				_playerMovingHero = false;
				(_physicsComponent as HeroPhysicsComponent).changeFixtureToItsInitialValue();
			}
		}
 
		protected function endHurtState():void {
 
			_isHurt = false;
			controlsEnabled = true;
		}
 
		protected function getSlopeBasedMoveAngle():V2 {
			return new V2(acceleration, 0).rotate(_collisionComponent.combinedGroundAngle);
		}
 
		/**
		 * Whether or not the player can move and jump with the hero. 
		 */	
		public function get controlsEnabled():Boolean
		{
			return _controlsEnabled;
		}
 
		public function set controlsEnabled(value:Boolean):void
		{
			_controlsEnabled = value;
 
			if (!_controlsEnabled)
				(_physicsComponent as HeroPhysicsComponent).changeFixtureToItsInitialValue();
		}
 
		/**
		 * Returns true if the hero is on the ground and can jump. 
		 */		
		public function get onGround():Boolean {
			return _onGround;
		}
 
		public function set onGround(value:Boolean):void {
			_onGround = value;
		}
 
		public function get ducking():Boolean {
			return _ducking;
		}
 
		public function get isHurt():Boolean {
			return _isHurt;
		}
	}
}
package com.citrusengine.system.components.box2d.hero {
 
	import Box2DAS.Dynamics.b2Fixture;
 
	import com.citrusengine.system.components.ViewComponent;
 
	/**
	 * The Box2D Hero view component. It manages the hero's view based on its physics component and depending its movement.
	 */
	public class HeroViewComponent extends ViewComponent {
 
		public var groundContacts:Array = [];//Used to determine if he's on ground or not.
 
		protected var _physicsComponent:HeroPhysicsComponent;
		protected var _movementComponent:HeroMovementComponent;
 
		public function HeroViewComponent(name:String, params:Object = null) {
 
			super(name, params);
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			_physicsComponent = entity.components["physics"];
			_movementComponent = entity.components["move"];
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			var prevAnimation:String = _animation;
 
			if (_physicsComponent && _movementComponent) {
 
				if (_movementComponent.isHurt)
					_animation = "hurt";
				else if (!_movementComponent.onGround)
					_animation = "jump";
				else if (_movementComponent.ducking)
					_animation = "duck";
				else {
 
					var walkingSpeed:Number = getWalkingSpeed();
 
					if (walkingSpeed < -_movementComponent.acceleration) { 						_inverted = true; 						_animation = "walk"; 					} else if (walkingSpeed > _movementComponent.acceleration) {
						_inverted = false;
						_animation = "walk";
					} else {
						_animation = "idle";
					}
				}				
			}
 
			if (prevAnimation != _animation)
				onAnimationChange.dispatch();
		}
 
		/**
		 * Returns the absolute walking speed, taking moving platforms into account.
		 * Isn't super performance-light, so use sparingly.
		 */
		public function getWalkingSpeed():Number {
 
			var groundVelocityX:Number = 0;
			for each (var groundContact:b2Fixture in groundContacts) {
				groundVelocityX += groundContact.GetBody().GetLinearVelocity().x;
			}
 
			return _physicsComponent.body.GetLinearVelocity().x - groundVelocityX;
		}
	}
}
package com.citrusengine.system.components.box2d.hero {
 
	import Box2DAS.Dynamics.ContactEvent;
	import Box2DAS.Dynamics.b2Fixture;
 
	import com.citrusengine.math.MathVector;
	import com.citrusengine.objects.Box2DPhysicsObject;
	import com.citrusengine.objects.platformer.box2d.Baddy;
	import com.citrusengine.system.components.box2d.CollisionComponent;
 
	/**
	 * The Box2D Hero collision component. We need to access informations of the hero view & movement & physics component.
	 */
	public class HeroCollisionComponent extends CollisionComponent {
 
		protected var _viewComponent:HeroViewComponent;
		protected var _movementComponent:HeroMovementComponent;
		protected var _physicsComponent:HeroPhysicsComponent;
 
		protected var _enemyClass:Class = Baddy;
		protected var _combinedGroundAngle:Number = 0;
 
		public function HeroCollisionComponent(name:String, params:Object = null) {
 
			super(name, params);
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			_viewComponent = entity.components["view"];
			_movementComponent = entity.components["move"];
			_physicsComponent = entity.components["physics"];
		}
 
		override public function destroy():void {
 
			super.destroy();
		}
 
		override public function handlePreSolve(e:ContactEvent):void {
 
			super.handlePreSolve(e);
 
			if (!_movementComponent.ducking)
				return;
 
			var other:Box2DPhysicsObject = e.other.GetBody().GetUserData() as Box2DPhysicsObject;
 
			var heroTop:Number = _physicsComponent.y;
			var objectBottom:Number = other.y + (other.height / 2);
 
			if (objectBottom < heroTop)
				e.contact.Disable();
		}
 
		override public function handleBeginContact(e:ContactEvent):void {
 
			super.handleBeginContact(e);
 
			var collider:Box2DPhysicsObject = e.other.GetBody().GetUserData();
 
			if (_enemyClass && collider is _enemyClass) {
 
				if (_physicsComponent.body.GetLinearVelocity().y < _movementComponent.killVelocity && !_movementComponent.isHurt) 					_movementComponent.hurt(collider); 				else 					_movementComponent.giveDamage(collider); 			} 			 			//Collision angle 			if (e.normal) //The normal property doesn't come through all the time. I think doesn't come through against sensors. 			{ 				var collisionAngle:Number = new MathVector(e.normal.x, e.normal.y).angle * 180 / Math.PI; 				if (collisionAngle > 45 && collisionAngle < 135)
				{
					_viewComponent.groundContacts.push(e.other);
					_movementComponent.onGround = true;
					updateCombinedGroundAngle();
				}
			}
		}
 
		override public function handleEndContact(e:ContactEvent):void {
 
			super.handleEndContact(e);
 
			//Remove from ground contacts, if it is one.
			var index:int = _viewComponent.groundContacts.indexOf(e.other);
			if (index != -1)
			{
				_viewComponent.groundContacts.splice(index, 1);
				if (_viewComponent.groundContacts.length == 0)
					_movementComponent.onGround = false;
				updateCombinedGroundAngle();
			}
		}
 
		protected function updateCombinedGroundAngle():void {
 
			_combinedGroundAngle = 0;
 
			if (_viewComponent.groundContacts.length == 0)
				return;
 
			for each (var contact:b2Fixture in _viewComponent.groundContacts)
				var angle:Number = contact.GetBody().GetAngle();
 
			var turn:Number = 45 * Math.PI / 180;
			angle = angle % turn;
			_combinedGroundAngle += angle;
			_combinedGroundAngle /= _viewComponent.groundContacts.length;
		}
 
		public function get combinedGroundAngle():Number {
			return _combinedGroundAngle;
		}
	}
}

There are interactions between components, here the rubber hits the road. We don’t have separate components as it would be and data’s component may be changed from an other component, but you can easily add a new functionality (climb a ladder for example) and handle it. Easier to manage but, that’s just OOP, isn’t it?

Oh and wait! An entity/component system is designed to be modular. It means that you can add functionalities on the fly, and not programming everything into 4 – 6 components with lots of conditions if the ability/behavior is added. Yep, I know. I failed. I didn’t find an easy way to do that with all this Box2D stuff.

To conclude, this experiment was very rewarding even if I didn’t success to achieve it. Don’t hesitate to comment to put me on the right direction!!

Concerning the Citrus Engine, we are close to the 2nd beta for the V3 : lots of bug fixes, performance enhancement, Nape support, Flash Pro support as level editor is even better and this attempt of entity/component system which might be useful… or not.

11 thoughts on “An entity/component system’s attempt using Box2D

  1. Hi looks like you to are on the same path I went down. There are many game engines and Unity3d takes this approach so I wont say its wrong (I also built games like this) however it does have issues that Ember and Ash are built to resolve. These are mainly due to components being coupled together. The down side of the Ember/Ash approach is integrating existing “event driven” libraries in to a more “data driven” approach is tricky. Alec has done this with box2d in his xember example here https://github.com/alecmce/xember/blob/master/as3/demo/pong/game/sys/physics/PhysicsSystem.as. What I will say is don’t give up your 1/2 way there it just takes a while to switch your mind over to the new way of thinking.

  2. Quite an intense post. But it shows the problem of component interaction quite nicely.

    From my experience it can be easily resolved when you take a step back and ignore dynamic/data-driven runtime entity-creation for a moment completely.

    Here is some pseudo-code that illustrates my approach that I have been using for a couple of years now. I kept it basic, since I dont have much time atm:

    public class Hero extends Entity
    {
    private var input : InputComponent;
    private var _view : HeroViewComponent;
    private var _physicsComponent : HeroPhysicsComponent;
    private var _movementComponent: HeroMovementComponent;

    public function Hero(name:String, params:Object = null) {
    super(name, params);

    // creation of components
    _physicsComponent = new HeroPhysicsComponent("physics", {x:200, y:270, width:40, height:60, entity:this});
    input = new InputComponent("input", {entity:heroEntity});
    var collision:HeroCollisionComponent = new HeroCollisionComponent("collision", {entity:this});
    _movementComponent = new HeroMovementComponent("move", {entity:this});
    _view = new HeroViewComponent("view", {view:"Patch.swf", entity:this});

    heroEntity.add(_physicsComponent).add(input).add(collision).add(_movementComponent).add(_view);
    // ...
    }

    override public function initialize():void {
    super.initialize();

    // wire up interaction between components with signals & callbacks
    _view.onUpdate.add(_onViewUpdate);
    // ...
    }

    override public function destroy():void {

    // clean up
    _view.onUpdate.remove(_onViewUpdate);
    // ...

    super.destroy();
    }

    private function _onViewUpdate() {
    var prevAnimation:String = _view.getCurrentAnimation();

    if (_physicsComponent && _movementComponent) {

    if (_movementComponent.isHurt)
    _view.setAnimation("hurt");
    else if (!_movementComponent.onGround)
    _view.setAnimation("jump");
    else if (_movementComponent.ducking)
    _view.setAnimation("duck");
    else {

    var walkingSpeed:Number = _movementComponent.getWalkingSpeed();

    if (walkingSpeed _movementComponent.acceleration) {
    _inverted = false;
    _view.setAnimation("walk");
    } else {
    _view.setAnimation("idle");
    }
    }
    }
    }
    }

    As you can see, per Entity you have one class/sourcefile again. But the fun part is that you get the feat of modular code-reuse *and* component-interaction on the entity level. You end up with clean, more or less general-purpose components and the custom logic of interaction is dealt with in the entity itself with the help of signals/eventdispatchers-callbacks.

    If this raises any questions, I’ll be glad to answer them.

  3. Hey dazKind, thank you for your advices! Your approach is very interesting, however it means than all the logics are made into the entity itself, right? It doesn’t separate the logics, and finally its close to what we had with the Hero single class OOP. But maybe this is the right way!

  4. Hah, I’m afraid there is no right way.

    Yes. The entity-specific logic of inter-component-interaction moves from the components back into the entity. The reason is simple. Different entities might combine/wireup the same components in a different way. This eliminates the need for specialized types of the same component. It’s just simple composition with a common interface for the individual logic parts.

    But anyway, it gets the job done 🙂

  5. You’re right concerning “different entities might combine/wireup the same components in a different way”, this is indeed powerful! I’ve to think for this approach, thank you very much for the advices!

  6. Aymeric, your experiment failed because you put the logic in the components, as you said, logic is supposed to go in the systems.

    Systems help solve the problem of communication between different components.
    To add further flexibility to your framework you can add an entity/event functionality (this is what I did on my own Entity-Component-System framework for C#)
    With this a system is able to create an event targeting an Entity, for instance DamageEvent. Then other systems can request a list of entities that have the DamageEvent, plus a list of required components. Like this:

    //a system creates the event
    framework.FireEvent(new DamageEvent(…), anEntity);

    //another system queries for the “event” list
    var damagedFlyingEnemies = framework.GetEventList();

    Good luck!

  7. Thanks for the precision Alejandro. Indeed removing logics from components and add them to systems should do the trick! I need to take time to rethink the global behaviour. Thanks again!

  8. I forgot to specify the generics in the last code example, it should be:

    //another system queries for the “event” list
    var damagedFlyingEnemies = framework.GetEventList();

  9. It’s important to realise the the difference between an entity/component approach and an entity/component/systems approach and what the ECS approach is trying to solve by removing code from the entities and components and moving it into systems.Moving the code into systems and coupling components using nodes allows you to re-use components and use them differently in different contexts.

    I’d suggest working on some simpler examples using a traditional EC engine like pushbutton as well as a ECS engine like Ash for getting familiar with how to breakdown and wire up objects and break away from an automatic OOP thinking.

Leave a Reply

Your email address will not be published. Required fields are marked *