In this tutorial, you will learn to make a physics-based platform game in the quickest way possible with the World Construction Kit.
Final Result Preview
Let's take a look at the final result we will be working towards:
It's a little wonky, but that can be fixed -- and wait until you see how quick and easy it was to make!
Step 1: Download
Download the Box2D Alchemy Port and WCK libraries. Get the source from github and for more information, see www.sideroller.com.
Step 2: New FlashDevelop Project
Click on 'Project' and select 'New Project' from the list. Select AS3 Project as your project template, name your project, point it to an empty directory and click OK.
Locate the Box2D/WCK libraries that you downloaded in Step 1 and place the following folders in your new project's 'lib' folder: Box2D, Box2DAS, extras, gravity, misc, shapes, and wck.
Click on 'Project' again and select Properties. Click on the 'Classpaths' tab and add your lib folder.
Open Main.as in the source file and add the highlighted code. FlashDevelop should have auto-generated the rest.
public class Main extends WCK { public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point } }
Step 3: Set Up the Flash IDE
Open Flash Professional. Press Ctrl+Shift+F12 to open Publish Settings. Click the Flash tab. Select the option 'Export SWC'
....and then click the Settings button next to the ActionScript 3.0 combobox.
In the Source Path tab, click on the 'browse to path' icon and select your lib folder. Then click on the Library Path tab and select the 'browse to SWC' icon. Select the file lib/Box2DAS/Box2D.swc.
Click OK in Advanced Actionscript 3 Settings and again on the Publish Settings window. Save your FLA in the \src\ folder of your FlashDevelop project (the same folder with Main.as).
Finally, press Ctrl+F3 to open the document properties and set the Document Class to Main.
Step 4: Your First World Object
Start by using the rectangle tool to draw a rectangle on the stage.
Convert the rectangle to a symbol by selecting it and pressing F8.
Set the registration point to the center. *Note: It is very important that you register all of the game symbols this way. Failure to do so will affect how your object responds to gravity and collisions.
Click 'export for Actionscript' and set the Base Class to shapes.Box
Step 5: Create the World
This may seem counter-intuitive, that you made a world object before you made your world. You could do it either way, but you'll see that it's quicker to do it this way.
Select your Static Box object on the stage and press F8.
Just like you did with the Static Box, set the World's registration point to the center and check Export for ActionScript.
Set the base class to wck.World
Step 6: Define World Component
Right click on your newly created World symbol in the library.
Select "Component Definition..."
In the Class field, type wck.World
This is a major selling-point for the World Construction Kit. If you now click on the World object on the stage and open the properties panel by pressing Ctrl + F3, you can edit a bunch of the World component's inspectable properties under the heading 'Component Parameters'.
Step 7: Define Static Object Component
Ok, now we're going to do the same thing with our static object.
Right click on your Static Box symbol in the library.
Select "Component Definition..."
In the Class field, type wck.BodyShape
Open the properties panel by selecting the Static Box object on the stage and pressing Ctrl + F3.
Scroll the Component Parameters window to the bottom and change the 'type' from dynamic to static. If you forget to do this, your static components (walls, floors, platforms) will become subject to gravity and fall off the screen at runtime.
Step 8: Build the Floor and Walls
Select your Static Object inside of the World. Copy and paste it a couple times.
Select each instance of Static Object and, using 'Free Transform', stretch, skew, and move your static objects around to form walls and a floor. You do not need to keep the boundaries on the stage.
Here is an example of one of my attempts:
Clearly, 'being an artist' is not a prerequisite for this tutorial..
Step 9: Build Your Hero!
What's a good platformer without a compelling protagonist?
While inside of the World object, draw a rectangle. Feel free to get creative here. This is the best I was able to do:
Convert your character to a symbol, but don't declare a Base Class just yet.
Right click your new Hero symbol in the library.
Select "Component Definition..."
In the Class field, type wck.BodyShape
Step 10: Create the Player Class
Open FlashDevelop.
Make sure your project is open. In the \src\ folder, make a new folder called 'View.' In 'View' create a new folder called 'Characters.'
Right click 'View' and Add New Class.
Name your class something like HeroCharacter.as and set the base class to shapes.Box
.
Your folder structure should now look like this:
Step 11: Override the Create Method
This is the entry point for adding functionality to your characters.
Add the following code to our new class:
public class HeroCharacter extends Box { private var contacts:ContactList; public override function create():void { reportBeginContact = true; reportEndContact = true; contacts = new ContactList(); contacts.listenTo(this); fixedRotation = true; listenWhileVisible(world, StepEvent.STEP, world_stepEventHandler, false, 0, true); listenWhileVisible(this, ContactEvent.BEGIN_CONTACT, this_beginContactHandler, false, 0, true); super.create(); } }
By setting reportBeginContact
and reportEndContact
to true
, we are setting properties on the BodyShape
class. We are indicating that we would like the BodyShape
to dispatch ContactEvents
when collisions begin and when collisions end. We then instantiate a ContactList
and ask it to "listenTo
this
". ContactList.listenTo(this)
creates listeners for ContactEvent.BEGIN_CONTACT
and ContactEvent.END_CONTACT
. It then creates handlers for each that store the collision information. You can see all of this by putting your cursor on ContactList
and pressing Ctrl+F4 in FlashDevelop.
By setting fixedRotation
to true
, we ensure that our hero will not rotate forwards or backwards when colliding with objects.
listenWhileVisible
is another way of adding event listeners. We could have used addEventListener(StepEvent.STEP, parseInput, false, 0, true);
but the added functionality here is that listenWhileVisible
will remove the event listeners and designate them for garbage collection when the Entity
has been removed from the game. For our purposes, listenWhileVisible
is a more optimized version of addEventListener
. *Note: As with addEventListener
, always use weak references so that unused objects are eligible for garbage collection.
By using super.create()
we call the create()
method on BodyShape
. This lets us extend the functionality of the create()
method instead of replacing it.
Step 12: Handle Player Input
Let's start by creating our StepEvent
handler for player input.
private function world_stepEventHandler(e:StepEvent):void { }
At every time interval, a StepEvent
will be dispatched from the b2World
class in Box2D. The default time step is .05 seconds. You can change the timeStep
parameter easily by going back to Flash Professional and opening the World component parameters.
Next, we will use the Input utility to determine what keys are currently being pressed by the user.
private function world_stepEventHandler(e:StepEvent):void { var left:Boolean = Input.kd('LEFT'); var right:Boolean = Input.kd('RIGHT'); var jump:Boolean = Input.kp('UP'); }
The Input.kd method can accept multiple arguments. So, if we wanted to let the user to be able to control the HeroCharacter with WASD and the spacebar, we could amend the code as follows:
private function world_stepEventHandler(e:StepEvent):void { var left:Boolean = Input.kd('LEFT', 'A'); var right:Boolean = Input.kd('RIGHT', 'D'); var jump:Boolean = Input.kp('UP', ' ', 'W'); }
Input.kd()
listens for when a key is down, while Input.kp()
listens for the instant a key is pressed.
Step 13: Apply Impulse to Move the Player
When impulse is applied to a rigid body, the momentum of the body is changed. Momentum is the product of mass and velocity. So when we want to change the velocity (speed and direction) of our player, we will use a method on b2body
called ApplyImpulse()
.
private function world_stepEventHandler(e:StepEvent):void { var left:Boolean = Input.kd('LEFT', 'A'); var right:Boolean = Input.kd('RIGHT', 'D'); var jump:Boolean = Input.kp('UP', ' ', 'W'); if (jump) { b2body.ApplyImpulse(new V2(0, -2), b2body.GetWorldCenter()); } else if(left) { b2body.ApplyImpulse(new V2(-2, 0), b2body.GetWorldCenter()); } else if(right) { b2body.ApplyImpulse(new V2(2, 0), b2body.GetWorldCenter()); } }
ApplyImpulse()
accepts two parameters: the world impulse vector and the point of application of the impulse. For now, we'll pass a new 2D vector as the first parameter for jumping, moving left and right (we'll have to make an adjustment to how we handle jumping a little later). The second parameter for each ApplyImpulse
method is b2body.GetWorldCenter()
. This method returns the world position of the center mass of our hero. This is important because ApplyImpulse
will change our hero's angular velocity if it doesn't act upon his center mass (this is also why we used center registration on the hero in Flash).
Step 14: Deal with Normal Force
Go back into Flash Professional and set the Hero symbol's Class to "view.characters.HeroCharacter" and leave the Base Class blank. Next, set the instance name of your Hero instance to 'hero.'
In the component parameters of the World component, deselect 'allowDragging' and select 'scrolling.' This way, the user won't be able to drag your character around with the mouse and the camera will follow your player when he moves. Finally, in the 'focusOn' field, type in 'hero,' your Hero's instance name.
Press Ctrl+Enter to test the movie. You'll notice that you can move your character around by pressing left and right and can jump with space. But if you keep pressing space, you will keep jumping up indefinitely.
The reason we can't keep jumping up indefinitely is that once we're airborne, there is nothing for our feet to push on to thrust us up. There is no equal force at our feet pushing back. When we are planted firmly on the ground, the force that aids us in jumping upward and keeps us from falling through the floor is called normal force. What we need to do is determine what the normal force is on our players feet. If there is no normal force, then he cannot take a jump. We'll do that by making use of our ContactList.
Go back into FlashDevelop. Let's amend our step event handler once more:
private function world_stepEventHandler(e:StepEvent ):void { var manifold:b2WorldManifold = null; if(!contacts.isEmpty()) { manifold = getNormalForce(); } var left:Boolean = Input.kd('LEFT', 'A'); var right:Boolean = Input.kd('RIGHT', 'D'); var jump:Boolean = Input.kp('UP', ' ', 'W'); if (jump && manifold) { var v:V2 = manifold.normal.multiplyN( -3); b2body.ApplyImpulse(v, b2body.GetWorldCenter()); } else if(left) { b2body.ApplyImpulse(new V2(-.5, 0), b2body.GetWorldCenter()); } else if(right) { b2body.ApplyImpulse(new V2(.5, 0), b2body.GetWorldCenter()); } }
We'll write the code for the getNormalForce()
method in just a second. What we want to do here is look for contacts (is our player touching anything?) get a manifold describing where our player is touching a contact (on the side or bottom) and accelerate the player upward if he is making contact with the ground. If there are no contacts, our hero must be in mid-air. In that case, the manifold would be null and the player would be unable to jump.
Now let's write the getNormalForce()
method.
private function getNormalForce():b2WorldManifold { var manifold:b2WorldManifold = null; contacts.forEach(function(keys:Array, contactEvent:ContactEvent) { var tempManifold:b2WorldManifold = contactEvent.getWorldManifold(); if (tempManifold.normal.y > 0) { tempManifold.normal = new V2(0, tempManifold.normal.y); manifold = tempManifold; } }); contacts.clean(); return manifold; }
Before calling getNormalForce()
, we check to see if our player is in contact with anything. If he isn't, then we know he is airborne. The whole reason this function exists is to prevent the player from taking a second jump off of a wall or the side of a platform.
First we declare a local variable called manifold
and set it to null
. This is the parameter we will be returning. If the hero character is in contact with something on his right left or top (but not the ground) this function will return a null manifold.
Using the method contacts.forEach()
, we can check each ContactEvent in our ContactList. All ContactEvents have a worldManifold property. So we create another local variable called tempManifold and set it to the value returned by each contactEvent.GetWorldManifold. Next, we check to see if temp.normal.y is greater than zero. Here we are asking, is there y-axis normal force?
If the hero is on the ground or a platform, we zero out any x-axis normal force. Failure to do this results in buggy jumping when the player is pushed up against a wall. Feel free to experiment with this. If you don't zero the x, the player gets a cool (yet unreliable) kind of Metroid wall-jump ability.
Finally, clean the ContactList. We don't want to handle the same contacts more than once.
Step 15: Add Coins
Now that we have a protagonist that can run around and jump, let's add some items that he can pick up. Go back into Flash Professional, draw a circle or ellipse for a coin and convert it to a symbol. Set the Class and Base class as shown:
Put as many instances of the Coin Class as you want on the Stage. In Component Parameters, I set each Coin's type to static
so that they are unaffected by gravity and can float in place like in Mario, but it's up to you.
Step 16: Handle Collisions With Coins
Right now, the coins are immovable, static objects. We'd like to change that. Go back to FlashDevelop and open the HeroCharacter
class. Add an event handler for collisions like this:
private function this_beginContactHandler(e:ContactEvent):void { }
This is the handler for the listener that we created in Step 11. Add the following code:
private function this_beginContactHandler(e:ContactEvent):void { var coin:Coin = e.other.m_userData as Coin; if(coin) { coin.remove(); } }
First we create a local var called coin
that is the same type as the Coin Class you created in Flash. ContactEvent keeps track of the other Box2D fixture involved in the collision. If it is Coin, we remove it from the Stage, giving the illusion that it has been collected.
Step 17: Keep Score
Create a folder inside the \src\ directory called 'model'. Inside 'model' make a folder called 'scoreboard' and make a new class called ScoreBoard
that extends EventDispatcher
. Since we only want to ever have one instance of the scoreboard around at one time, we're going to follow the Singleton design pattern. There was a Quick Tip about the Singleton pattern on Activetuts+ earlier this year if you want a reference.
Write the following code in the ScoreBoard Class:
package model.scoreboard { import flash.errors.IllegalOperationError; import flash.events.Event; import flash.events.EventDispatcher; public class ScoreKeeper extends EventDispatcher { private static var _instance:ScoreKeeper; public function ScoreKeeper() { if (_instance != null) { throw new IllegalOperationError("Use ScoreBoard.getInstance() to get a reference to the Singleton ScoreKeeper."); } else { initialize(); } } private function initialize():void { } public static function getInstance():ScoreKeeper { if (_instance == null) _instance = new ScoreKeeper(); return _instance; } } }
This is the Singleton pattern. We expect any Class that wants to access the ScoreKeeper to use the static function getInstance()
. If an instance already exists and someone (another developer on your team, for example) tries to instantiate the ScoreKeeper through its constructor, they will receive our error message telling them that the ScoreKeeper should only be accessed through getInstance()
.
The ScoreKeeper extends EventDispatcher so that it can dispatch Events when the score changes. We will build a score board as a view component that will subscribe to the ScoreKeeper events.
Now we need the ScoreKeeper to actually begin keeping score. We need a variable to hold the score, a method that increments the score, a getter for the score so that other classes can access it and a public static const
to store our Event type.
package model.scoreboard { import flash.errors.IllegalOperationError; import flash.events.Event; import flash.events.EventDispatcher; public class ScoreKeeper extends EventDispatcher { public static const SCORE_CHANGED:String = "SCORE_CHANGED"; private var _score:uint; private static var _instance:ScoreKeeper; public function ScoreKeeper() { if (_instance != null) { throw new IllegalOperationError("Use ScoreBoard.getInstance() to get a reference to the Singleton ScoreKeeper."); } else { initialize(); } } private function initialize():void { _score = 0; } public function incrementScore():void { _score++; dispatchEvent(new Event("SCORE_CHANGED")); } public static function getInstance():ScoreKeeper { if (_instance == null) _instance = new ScoreKeeper(); return _instance; } public function get score():uint { return _score; } } }
And that's all we need for our ScoreKeeper. Now let's make a view component to display the score number. Go into Flash and on the stage (not inside of the World symbol) draw out a scoreboard. The only important thing here is that you use the Text Tool to draw a TextField with the instance name 'score
'. Convert the TextField to a movie clip symbol called ScoreBoard
.
Back in FlashDevelop, in the world folder, create a Class called 'ScoreDisplay' that extends MovieClip. All we need to do here is get an instance of ScoreKeeper and subscribe to its events. It should look like this:
package view.world { import flash.display.MovieClip; import flash.events.Event; import flash.text.TextField; import model.scoreboard.ScoreKeeper; public class ScoreDisplay extends MovieClip { private var _scoreKeeper:ScoreKeeper = ScoreKeeper.getInstance(); public function ScoreDisplay() { this.score.text = "0"; _scoreKeeper.addEventListener(ScoreKeeper.SCORE_CHANGED, scoreBoard_ScoreChangedHandler, false, 0, true); } private function scoreBoard_ScoreChangedHandler(e:Event):void { this.score.text = _scoreKeeper.score.toString(); } } }
Go back to Flash and open the properties of the ScoreBoard symbol in the library. Change the Class to view.world.ScoreDisplay
.
You have one last step. Go back to the HeroCharacter class and add two lines of code:
private function this_beginContactHandler(e:ContactEvent):void { var coin:Coin = e.other.m_userData as Coin; if(coin) { coin.remove(); scoreBoard.incrementScore(); } }
public class HeroCharacter extends Box { private var contacts:ContactList; private var scoreKeeper:ScoreKeeper = ScoreKeeper.getInstance();
Step 18: Add Static Platforms
Go into Flash Professsional and place an instance of StaticBox
(the same one we used to make walls and the floor) inside the World instance. Make sure that you set its type to static
in the Component Parameters and that the platform is low enough that your player can jump to it.
Step 19: Add Suspended Platforms With Box2D Joints
WCK makes creating swinging platforms very easy. We can do the whole thing in the Flash IDE without writing any code.
Start by drawing a circle. Convert the circle to a symbol called Joint
and set the Base Class to wck.Joint
. Next, right-click the Joint
symbol in the library and go to Component Definition. Set the Class as wck.Joint
. In the Properties panel, set the instance name as anchor
and in Component Parameters, change the type
to Revolute
. This is the joint that will give our platform a pendulum action.
Draw a platform with the Rectangle tool. Select it and convert it to a symbol. Set the Base Class to extras.Platform
. Right click on the symbol in the library and in Component Definition, set the Class to extras.Platform
.
Drag out two more instances of the Joint Class into World and place each one at either end of the Platform. The layout should look like this:
For each new Joint instance, go into Component Parameters and change type
to 'Distance
' and in the target2Name
field write 'anchor
'. Test your movie and you should have a swinging platform.
Step 20: Add Enemies
In FlashDevelop, add a new class to the \characters\ folder called EnemyCharacter
. Here's the code we're going to write (this will look very familiar):
package view.characters { import Box2DAS.Common.V2; import Box2DAS.Dynamics.ContactEvent; import Box2DAS.Dynamics.StepEvent; import shapes.Box; import wck.ContactList; public class EnemyCharacter extends Box { private var contacts:ContactList; private var left:Boolean = true; private var right:Boolean; public override function create():void { fixedRotation = true; reportBeginContact = true; super.create(); contacts = new ContactList(); contacts.listenTo(this); listenWhileVisible(world, StepEvent.STEP, world_stepEventHandler, false, 0, true); listenWhileVisible(this, ContactEvent.BEGIN_CONTACT, this_beginContactHandler, false, 0, true); } private function world_stepEventHandler(e:StepEvent ):void { if(left) { b2body.ApplyImpulse(new V2(-.1, 0), b2body.GetWorldCenter()); } else if(right) { b2body.ApplyImpulse(new V2(.1, 0), b2body.GetWorldCenter()); } } private function this_beginContactHandler(e:ContactEvent):void { var wall:StaticBox = e.other.m_userData as StaticBox; if(wall) { left = !left; right = !right; } } } }
The only new thing here is that every time the object collides with a wall, it changes direction. And every step event, the enemy character is going to have an impulse applied in the direction he is facing.
Go back into Flash and draw an enemy character, and convert it to a symbol with the Base Class set to view.characters.EnemyCharacter
and Class set to Enemy
.
The last thing we need to do is handle contact between the player character and the enemy character. In the HeroCharacter
class, add the following code:
private function this_beginContactHandler(e:ContactEvent):void { var coin:Coin = e.other.m_userData as Coin; trace(coin); if(coin) { coin.remove(); scoreKeeper.incrementScore(); } else { var enemy:EnemyCharacter = e.other.m_userData as EnemyCharacter; if (enemy) { var tempManifold:b2WorldManifold = e.getWorldManifold(); if (tempManifold.normal.y > 0) { Util.addChildAtPosOf(world, new BadGuyFX(), enemy); enemy.remove(); } } } }
If our hero makes contact with something and it isn't a coin, we will check to see if it is the EnemyCharacter
. If it is, we'll check the manifold of the ContactEvent
to determine if we hit the bad guy on top or on the side. If we jumped on top of him, he will be removed from the stage.
I wanted to add an animation of the EnemyCharacter getting squashed so in Flash I made a movie clip with a timeline animation of the enemy getting crushed. I set the Base Class of that BadGuyFX
object to misc.FX
, a Class in the WCK library that plays through its own timeline animation once and then sets itself to null
. Then I added it to the Stage with the Util
method addChildAtPosOf()
. The animation makes the enemy removal not seem so sudden.
Conclusion
Now that you have a working prototype of a platformer, I encourage you to keep exploring what WCK has to offer. I especially recommend playing around in the Component Parameters of your game objects. This is a really fun and quick way to alter the physics of your game world without writing any code. I hope you enjoyed this tutorial! Thanks for reading!
Comments