[Flash 11]

Octobre 2012

Data Driven Programming ou programmation basé sur les entités

On en entend pas mal parlé, et il y a beaucoup de moteurs de jeux basés dessus comme la citrus engine, ash ou smashIO, très bon article de Iann Lobb

[swfobj src=”http://www.yopsolo.fr/ressources/Entity.swf” alt=”Animation Flash” width=”640″ height=”480″ class=”flashObject” allowfullscreen=”false” bgcolor=”#000000″ required_player_version=”11″ wmode=”direct”]


J’ai construis un moteur très simple capable de gérer des entités possédant jusqu’à 5 composants

  1. positionnement
  2. physique
  3. armes
  4. santé
  5. visualisation

Ci dessous la classe qui s’occupe des vaisseaux (joueur et EvilTwin )

package asteroids
{
	import asteroids.stateShip.*;
	import engine.Body;
	import engine.E;
	import engine.Entity;
	import engine.events.*;
	import engine.fsm.StateMachine;
	import engine.Health;
	import engine.Keypad;
	import engine.Physics;
	import engine.View;
	import flash.display.GraphicsPathWinding;
	import flash.display.Sprite;
	import flash.events.KeyboardEvent;
	import flash.ui.Keyboard;
	import engine.EventBroker;
	
	public class Ship extends Entity
	{
		// FSM
		public static const NORMAL:String = "normal_state";
		public static const SHIELD:String = "shield_state";
		public static const POWER_UP:String = "power_up_state";
		public static const BLINKING:String = "blinking_state";
		private var _fsm:StateMachine
		
		// UserInput
		protected var keypad:Keypad;
		
		public function Ship()
		{
			//
			body = new Body(this);
			body.x = E.WIDTH >> 1;
			body.y = E.HEIGHT >> 1;
			body.radius = 20;
			//body.angle = E.degreesToRadians(270);
			
			//
			physics = new Physics(this);
			physics.vX = 0;
			physics.vY = 0;
			physics.drag = 0.95;
			
			//
			health = new AHealth(this);
			health.hp = 5;
			
			//
			weapon = new PlasmaGun(this);
			weapon.ammo = 50;
			
			//
			view = new View(this);
			view.sprite.x = E.WIDTH >> 1;
			view.sprite.y = E.HEIGHT >> 1;
			view.sprite.graphics.lineStyle(1.5, 0xCCCCCC);
			view.sprite.graphics.drawPath(	Vector.<int>([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), 
											Vector.<Number>(
											[ -7.3, 10.3, -5.5, 10.3, -7, 0.6, -0.5, -2.8, 6.2, 0.3, 4.5, 10.3, 
											6.3, 10.3, 11.1, -1.4, -0.2, -9.6, -11.9, -1.3, -7.3, 10.3]), 
											GraphicsPathWinding.NON_ZERO);			

			
			_fsm = new StateMachine;			
			_fsm.addState( NORMAL, new StateNormal(this), [SHIELD, POWER_UP, BLINKING] );
			_fsm.addState( POWER_UP, new StatePowerUp(this), [NORMAL/*, SHIELD*/] );
			_fsm.addState( SHIELD, new StateShield(this), [NORMAL/*, POWER_UP*/] );
			_fsm.addState( BLINKING, new StateBlink(this), [NORMAL, SHIELD, POWER_UP] );
			
			_fsm.state = NORMAL;
			
			keypad = new Keypad(E.stage);
		}		
		
		public function get state():String
		{
			return _fsm.state;
		}
		
		public function set state( state:String ):void
		{
			_fsm.state = state;
		}			
		
		override public function onHit():void 
		{
			this.state = BLINKING;
		}
		
		override public function onDie():void 
		{
			destroy();			
		}				
		
		override public function destroy():void 
		{
			// destruction totale
			keypad.destroy();
			keypad = null;
			
			super.destroy();
			
			group.length = 0;
			targets.length = 0;
			this.body = null;
			this.physics = null;
			this.weapon = null;
			this.health = null;
			this.view = null;
		}
		
		override public function update():void
		{
			super.update();
			
			// --
			if (keypad.isDown(Keyboard.UP))
			{
				physics.impulse( .6 );
			}			
			
			if (keypad.isDown(Keyboard.LEFT))
			{
				body.angle -= .1;
			}
			
			if (keypad.isDown(Keyboard.RIGHT))
			{
				body.angle += .1;
			}			
		}
		
	}

}

Cette classe est la plus complète car elle est composée des 5 composants tandis que d’autres n’ont ni arme, ni physique (les bonus par exemple).

Le bon côté de cette approche je trouve, c’est que l’on code de manière ‘horizontale’ par opposition à une approche basé uniquement sur de l’héritage où l’on qui fonctionne plus de manière ‘verticale’, ce qui amène souvent à créer beaucoup de classes avec des comportements spécifiques.
( pour ce jeu la profiondeur d’héritage ne dépasse jamais 2 niveaux : Entity -> Ship -> EnemmyShip )

Autre exemple avec la classe/composant ‘Physic’

package engine
{
public class Physics
{
public var entity:Entity;
public var drag:Number = 1.; // frottement null
public var vX:Number = .0;
public var vY:Number = .0;

public function Physics(entity:Entity)
{
this.entity = entity;
}

public function update():void
{
entity.body.x += vX;
entity.body.y += vY;

vX *= drag;
vY *= drag;

// monde torique, toutes les entités y sont soumises
if (entity.body.x > 850) entity.body.x -= 900;
if (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y -= 700;
if (entity.body.y < -50) entity.body.y += 700; } public function impulse( pow:Number ):void { vX += Math.sin(-entity.body.angle) * pow; vY += Math.cos(-entity.body.angle) * pow; } } }

Ainsi toutes les entités qui seront décorées avec le composant 'Physic' seront soumissent aux contraintes d'un monde torique ^^

Le piège principal de cette approche c'est la conso de mémoire.
Pour chaque entité on crée des objets pour les comportements, par exemple 25 objets Ship donneront au total 125 objets ( 25 * 5 ) d'où la nécessité d'utiliser du Pooling d'objets de recycler un maximum.
Dans ce jeu par exemple j'ai fait 2 pool, une pour les tirs et une pour les astéroïdes.

Les musiques viennent de Thunder force 3, les bruitages ont été fait avec bfxr