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