Create a breakout game with the Citrus Engine

Today this is a new tutorial on the great Citrus Engine framework. Before starting my school project, I wanted to try using box2D inside the Citrus Engine, so I will show you how create a breakout game !

Click here to play the game, don’t forget to click in the swf to enable keyboard.

But first of all, I would like to come back on my previous tutorial on the Citrus Engine, and add some comments:
– instead of using sensor to change the way that baddy moves (if they will fall), you can now specify a limit with leftBound and rightBound parameters. A nice feature added by Eric !
– it is possible to create cloud plateform by setting the proprety oneWay to true on a Plateform. It will enable you to jump on even if your are below.
– with the new update we can create a MovingPlatform as a new class of the core engine.
– the pause is now enable : playing = false 😉
– a console have been added to the Citrus Engine, you can access it by pressing the TAB key. You can change properties on the fly (for example : set Box2D visible false) and add your own command :

this.console.addCommand("fullscreen", _fullscreen);
this.console.addCommand("play", _playGame);
 
private function _fullscreen():void {
	stage.displayState = "fullScreen";
}
 
private function _playGame():void {
	this.playing = !this.playing;
}

Ok, now let start the tutorial. With the Citrus Engine, we can use Flash IDE as a Level Editor. It is really easy to use it and so powerful ! You can save lot of time.

Create a fla and add MovieClip on the stage for the Plateforms, Ball, Bricks… Inside each MovieClip specify the class name :

var className = "Ball"
var params = {
	radius:15
}

You can add easily some parms. This is my stage
Our level is now created.

Now let’s start coding :
The Main class :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package {
 
	import com.citrusengine.core.CitrusEngine;
 
	import flash.display.Loader;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.net.URLRequest;
 
	/**
	 * @author Aymeric
	 */
	public class Main extends CitrusEngine {
 
		private var _level:MovieClip;
		private var _hud:Hud;
		private var _countBricks:uint;
 
		public function Main() {
 
			super();
 
			_hud = new Hud();
			addChild(_hud);
 
			this.console.addCommand("fullscreen", _fullscreen);
			this.console.addCommand("play", _playGame);
 
			var loader:Loader = new Loader();
			loader.load(new URLRequest("LevelA.swf"));
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, _levelLoaded);
		}
 
		private function _levelLoaded(evt:Event):void {
 
			_level = evt.target.loader.content;
			_restartGame();
 
			evt.target.loader.unloadAndStop();
		}
 
		private function _fullscreen():void {
			stage.displayState = "fullScreen";
		}
 
		private function _playGame():void {
			this.playing = !this.playing;
		}
 
		private function _brickTaken(gEvt:GameEvent):void {
 
			++_countBricks;
			_hud.scoreCoin = _countBricks;
 
			if (_countBricks == GameConst.nbrCoins) {
				_restartGame();
			}
		}
 
		private function _restartGame(gEvt:GameEvent = null):void {
 
			state = new GameState(_level);
 
			_hud.scoreCoin = _countBricks = 0;
 
			state.addEventListener(GameEvent.RESTART_GAME, _restartGame);
			state.addEventListener(GameEvent.TAKE_BRICK, _brickTaken);
		}
	}
}

Compared to the last tutorial the main change are console command and the loader for our level.

Now the GameState which show how access to our MovieClip’s level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import com.citrusengine.core.CitrusObject;
	import com.citrusengine.core.State;
	import com.citrusengine.objects.platformer.Sensor;
	import com.citrusengine.physics.Box2D;
	import com.citrusengine.utils.ObjectMaker;
 
	import flash.display.MovieClip;
 
	/**
	 * @author Aymeric
	 */
	public class GameState extends State {
 
		private var _level:MovieClip;
 
		public function GameState(level:MovieClip) {
 
			super();
			_level = level;
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var box2D:Box2D = new Box2D("Box2D");
			add(box2D);
			box2D.visible = true;
 
			// Create objects from our level
			ObjectMaker.FromMovieClip(_level);
 
			var ball:Ball = Ball(getFirstObjectByType(Ball));
			ball.gravity = 0;
			ball.onEndContact.add(_takeBrick);
 
			var sensor:Sensor = Sensor(getFirstObjectByType(Sensor));
			sensor.onBeginContact.add(_resetLevel);
 
			var bricks:Vector.<CitrusObject> = getObjectsByType(Brick);
			for each (var brick:Brick in bricks) {
				brick.onEndContact.add(_takeBrick);
			}
		}
 
		private function _takeBrick(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Brick) {
				remove(cEvt.other.GetBody().GetUserData());
				this.dispatchEvent(new GameEvent(GameEvent.TAKE_BRICK));
			}
		}
 
		private function _resetLevel(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Ball) {
				this.dispatchEvent(new GameEvent(GameEvent.RESTART_GAME));
			}
		}
	}
}

It is really easy to access to our objects from the swf with these functions : getObjectsByType, getObjectByName, getFirstObjectByType. I put ball’s gravity to 0 to handle physics movement by myself. Nothing too complex here, let’s start to create our first Citrus Engine object : a brick !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import com.citrusengine.objects.platformer.Platform;
 
	import org.osflash.signals.Signal;
 
	/**
	 * @author Aymeric
	 */
	public class Brick extends Platform {
 
		public var onEndContact:Signal;
 
		public function Brick(name:String, params:Object = null) {
 
			super(name, params);
			onEndContact = new Signal(ContactEvent);
		}
 
		override public function destroy():void {
 
			onEndContact.removeAll();
			_fixture.removeEventListener(ContactEvent.END_CONTACT, handleEndContact);
			super.destroy();
		}
 
		override protected function createFixture():void {
 
			super.createFixture();
			_fixture.m_reportEndContact = true;
			_fixture.addEventListener(ContactEvent.END_CONTACT, handleEndContact);
		}
 
		protected function handleEndContact(e:ContactEvent):void {
			onEndContact.dispatch(e);
		}
	}
}

A brick is a simple Plateform which reports if something collides with it.

Now the paddle :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package {
 
	import Box2DAS.Common.V2;
	import Box2DAS.Dynamics.b2Body;
	import Box2DAS.Dynamics.b2BodyDef;
 
	import com.citrusengine.objects.PhysicsObject;
 
	import flash.ui.Keyboard;
 
	/**
	 * @author Aymeric
	 */
	public class Paddle extends PhysicsObject {
 
		public var acceleration:Number = 1;
		public var maxVelocity:Number = 7;
 
		private var _friction:Number = 0;
		private var _playerMovingHero:Boolean = false;
 
		public function Paddle(name:String, params:Object = null) {
			super(name, params);
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			var velocity:V2 = _body.GetLinearVelocity();
 
			var moveKeyPressed:Boolean = false;
 
			if (_ce.input.isDown(Keyboard.RIGHT)) {
				velocity.x += (acceleration);
				moveKeyPressed = true;
			}
 
			if (_ce.input.isDown(Keyboard.LEFT)) {
				velocity.x -= (acceleration);
				moveKeyPressed = true;
			}
 
			if (moveKeyPressed && !_playerMovingHero) {
				_playerMovingHero = true;
				_fixture.SetFriction(0);
				// Take away friction so he can accelerate.
			} else if (!moveKeyPressed && _playerMovingHero) {
				_playerMovingHero = false;
				_fixture.SetFriction(_friction);
				// Add friction so that he stops running
			}
 
			// 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);
		}
 
		override protected function defineBody():void {
 
			_bodyDef = new b2BodyDef();
			_bodyDef.type = b2Body.b2_kinematicBody;
			_bodyDef.position.v2 = new V2(_x, _y);
			_bodyDef.angle = _rotation;
			_bodyDef.fixedRotation = true;
			_bodyDef.allowSleep = false;
		}
 
		override protected function defineFixture():void {
 
			super.defineFixture();
			_fixtureDef.friction = _friction;
			_fixtureDef.restitution = 1;
		}
 
		override protected function createFixture():void {
 
			super.createFixture();
			_fixture.m_reportBeginContact = true;
			_fixture.m_reportEndContact = true;
		}
	}
}

The Paddle may extends Citrus’ Hero class, but I prefer to extends PhysicsObject and copy/paste some properties and function from Hero class. It is lighter. I use a kinematic body instead of a dynamic body because of collision forces : with a dynamic body, my paddle moves to the bottom after each collision with the ball, whereas with a kinematic body it doesn’t.

And finally the ball :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package {
 
	import Box2DAS.Collision.Shapes.b2CircleShape;
	import Box2DAS.Common.V2;
	import Box2DAS.Dynamics.ContactEvent;
	import Box2DAS.Dynamics.b2FixtureDef;
 
	import com.citrusengine.objects.PhysicsObject;
 
	import org.osflash.signals.Signal;
 
	/**
	 * @author Aymeric
	 */
	public class Ball extends PhysicsObject {
 
		public var onBeginContact:Signal;
		public var onEndContact:Signal;
 
		private var _radius:Number;
		private var _linearVelocity:V2;
 
		private var _afterFirstBounce:Boolean = false;
 
		public function Ball(name:String, params:Object = null) {
 
			super(name, params);
 
			onBeginContact = new Signal(ContactEvent);
			onEndContact = new Signal(ContactEvent);
 
			_linearVelocity = _body.GetLinearVelocity();
			_linearVelocity.y = 5;
		}
 
		override public function destroy():void {
 
			onBeginContact.removeAll();
			onEndContact.removeAll();
			super.destroy();
		}
 
		override protected function createShape():void {
 
			_shape = new b2CircleShape();
			b2CircleShape(_shape).m_radius = _radius;
		}
 
		override public function update(timeDelta:Number):void {
 
			if (_afterFirstBounce == true) {
				_linearVelocity = _body.GetLinearVelocity();
			}
			_body.SetLinearVelocity(_linearVelocity);
		}
 
		override protected function createFixture():void {
 
			super.createFixture();
			_fixture.m_reportBeginContact = true;
			_fixture.m_reportEndContact = true;
			_fixture.addEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);
			_fixture.addEventListener(ContactEvent.END_CONTACT, handleEndContact);
		}
 
		override protected function defineFixture():void {
 
			_fixtureDef = new b2FixtureDef();
			_fixtureDef.shape = _shape;
			_fixtureDef.density = 1;
			_fixtureDef.friction = 0;
			_fixtureDef.restitution = 1;
		}
 
		private function handleBeginContact(cEvt:ContactEvent):void {
 
			onBeginContact.dispatch(cEvt);
 
			var diff:Number;
			var ratio:Number;
			var speed:Number;
 
			if (cEvt.other.GetBody().GetUserData() is Paddle) {
				_afterFirstBounce = true;
				// get a reference to the paddle
				var paddle:Paddle = cEvt.other.GetBody().GetUserData() as Paddle;
 
				// only change collisions that are against the top surface of the paddle (let it shank off if it hits an edge).
				var collisionNormal:V2 = cEvt.getWorldManifold().normal;
				if (collisionNormal.x == 0 && collisionNormal.y == 1) {
					diff = x - paddle.x;
					// x distance between ball and paddle.
					ratio = diff / (paddle.width / 2);
 
					// distance as a ratio (between -1 and 1)
					if (ratio < -1 || ratio > 1) // it will shank.
						return;
 
					// Create a new velocity vector and set it to the ball's current speed.
					// var velocity:V2 = _body.GetLinearVelocity();
					_linearVelocity = _body.GetLinearVelocity();
					speed = _linearVelocity.length();
 
					// set the ball to be going straight up at the same speed as it came.
					_linearVelocity = new V2(0, -1);
					_linearVelocity.normalize(speed);
 
					// rotate the velocity angle between a specific range (I chose 1 radian) as a proportion ball's distance from center of paddle.
					_linearVelocity.rotate(1 * ratio);
					_body.SetLinearVelocity(_linearVelocity);
				}
			}
		}
 
		private function handleEndContact(cEvt:ContactEvent):void {
			onEndContact.dispatch(cEvt);
		}
 
		public function get radius():Number {
			return _radius * _box2D.scale;
		}
 
		public function set radius(value:Number):void {
			_radius = value / _box2D.scale;
		}
	}
}

A big thanks to Eric for helping me with the ball movement !
With a restitution set to 1, the ball bounces on each Platform.

Finally it was pretty easy to create my own object inside the Citrus Engine ! For a first try with box2D it was successful too !

By default, we specify a width and a height when we create an Object inside the Citrus Engine, but maybe the radius property should be added into the core to create circle easily.

1
2
3
4
5
6
7
8
9
protected function createShape():void {
	if (_radius == null) {
		_shape = new b2PolygonShape();
		b2PolygonShape(_shape).SetAsBox(_width / 2, _height / 2);
	} else {
		_shape = new b2CircleShape();
		b2CircleShape(_shape).m_radius = _radius;
	}
}

To conclude to this little project, I recommend to everyone interested in AS3 game development to give a try to the Citrus Engine. Moreover Eric is a likeable person who takes time to reply on the forum and help you.

My zip.
The box2D manual.

5 thoughts on “Create a breakout game with the Citrus Engine

  1. Hi Aymeric,
    Having difficulty opening the BreakOut and LevelA fla files in your zip for this game. I am using flash cs4 for this and wondered if these were created in a later or different version? Also have the same problem with simple Mario game fla

    Other files open fine but I get a “frame label not found in source error” when running from Flash develop, so wanted to check your flash source for a possible reason. Thanks Paul

  2. Hi Paul,

    I’ve updated the zip file, it was saved for Flash CS5.

    The error comes from older CE version there was an incompatibility with FP 10.2 compiler, compiling in 10.1 is ok. Now, if you update CE’s library it should works fine!

  3. The Kinematic body type for the paddle will not detect collisions with the Platform objects. How can the boundaries of the game screen be checked against the paddle without checking the position manually?

    Great tutorial, by the way! It is very helpful.

  4. I think that we have no other choice here. Maybe using a distance joint but that sound over complicated compared to just check its position with the stage size.

Leave a Reply

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