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. 
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.
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