Talking about The Moops - Combos of Joy game, Actionscript 3, Box2D, Flash and Game development.
Did you play The Moops – Combos of Joy?
It’s a cute physics game we can build a prototype in a few minutes (don’t worry, Plants Vs Zombies fans, as next step is about to come).
Let’s see game’s features:
* It’s a physics game so we are using Box2D to create it.
* The player fires a ball with the mouse. The direction can be decided according to mouse position, but power can’t. Player fires balls with the same, predefined speed.
* A series of shapes “fall” down from the top of the stage. Actually they don’t fall because there is no gravity in the game. Let’s say they move from top to bottom at a constant speed.
* When the ball hits a square, the square reacts according to physics, changing its direction and speed, but the ball does not exactly follow physics rules as its speed never changes.
Obviously there are more features in the original game but at the moment we’ll see these ones. Since it’s a long time we don’t deal with Box2D, I am using a line by line approach to explain the code.
This is the script:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.Timer;
import flash.events.TimerEvent;
import Box2D.Dynamics.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
public class moops extends Sprite {
private var world:b2World=new b2World(new b2Vec2(0,0),true);
private var worldScale:int=30;
private var timeCount:Timer=new Timer(500);
public function moops():void {
timeCount.start();
debugDraw();
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.CLICK,onClick);
timeCount.addEventListener(TimerEvent.TIMER, onTime);
}
private function onClick(e:MouseEvent):void {
var angle:Number=Math.atan2(mouseX,mouseY-480)-Math.PI/2;
addball(20*Math.cos(angle),-20*Math.sin(angle));
}
private function onTime(event:TimerEvent):void {
addBox(300+Math.random()*200,-100,40);
}
private function addball(xVel:Number,yVel:Number):void {
var ball:b2BodyDef= new b2BodyDef();
ball.userData="ball";
ball.type=b2Body.b2_dynamicBody;
ball.position.Set(20/worldScale, 460/worldScale);
var circle:b2CircleShape=new b2CircleShape(15/worldScale);
var ballFixture:b2FixtureDef = new b2FixtureDef();
ballFixture.shape=circle;
ballFixture.friction=0;
ballFixture.density=1;
ballFixture.restitution=1;
var ballBody:b2Body=world.CreateBody(ball);
ballBody.CreateFixture(ballFixture);
ballBody.SetLinearVelocity(new b2Vec2(xVel,yVel));
}
private function debugDraw():void {
var debugDraw:b2DebugDraw = new b2DebugDraw();
var debugSprite:Sprite = new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
private function addBox(xOrigin:Number,yOrigin:Number,size:Number):void {
var box:b2BodyDef= new b2BodyDef();
box.position.Set(xOrigin/worldScale,yOrigin/worldScale);
box.type=b2Body.b2_dynamicBody;
var square:b2PolygonShape = new b2PolygonShape();
square.SetAsBox(size/2/worldScale, size/2/worldScale);
var boxFixture:b2FixtureDef = new b2FixtureDef();
boxFixture.shape=square;
boxFixture.friction=0;
boxFixture.density=4;
boxFixture.restitution=1;
var boxBody:b2Body=world.CreateBody(box);
boxBody.CreateFixture(boxFixture);
boxBody.SetLinearVelocity(new b2Vec2(0,5));
}
private function update(e:Event):void {
world.Step(1/30,10,10);
world.ClearForces();
world.DrawDebugData();
for (var currentBody:b2Body = world.GetBodyList(); currentBody; currentBody=currentBody.GetNext()) {
if (currentBody.GetPosition().y*worldScale>600 || currentBody.GetPosition().y*worldScale<(100*-1)) {
world.DestroyBody(currentBody);
}
if (currentBody.GetUserData()=="ball") {
var velocity:b2Vec2=currentBody.GetLinearVelocity();
if (velocity.Length()!=20) {
var speedOffset:Number=20/velocity.Length();
currentBody.SetLinearVelocity(new b2Vec2(velocity.x*speedOffset,velocity.y*speedOffset));
}
}
}
}
}
}
Let's see how does it work:
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.utils.Timer;
import flash.events.TimerEvent;
Imports the required Flash libraries. We are using the mouse to fire the ball and a timer to add new shapes.
import Box2D.Dynamics.*;
import Box2D.Collision.Shapes.*;
import Box2D.Common.Math.*;
Imports the required Box2D libraries.
private var world:b2World=new b2World(new b2Vec2(0,0),true);
private var worldScale:int=30;
word
will contain the physics environment itself. The first argument of the b2World
constructor represents the gravity, which in this game is disabled, as you can see from the (0,0)
vector.
worldScale
is the amount of pixels used to represent a meter. Refer to Understanding pixels and meters with Box2D for more information.
public function moops():void {
timeCount.start();
debugDraw();
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.CLICK,onClick);
timeCount.addEventListener(TimerEvent.TIMER, onTime);
}
This is the main function. We start the timer (refer to Understanding AS3 timer class for more information), we setup all listeners and we call debugDraw
function.
This is the function:
private function debugDraw():void {
var debugDraw:b2DebugDraw = new b2DebugDraw();
var debugSprite:Sprite = new Sprite();
addChild(debugSprite);
debugDraw.SetSprite(debugSprite);
debugDraw.SetDrawScale(worldScale);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
debugDraw.SetFillAlpha(0.5);
world.SetDebugDraw(debugDraw);
}
Basically we are using the Box2D built in API to represent the world on the stage. Refer to Understanding Box2D debug draw for more information.
private function onTime(event:TimerEvent):void {
addBox(300+Math.random()*200,-100,40);
}
This is the function triggered by the timer event. It simply calls addBox
function. Its arguments represent the x
and y
origin of the box, and the size, in pixels.
private function addBox(xOrigin:Number,yOrigin:Number,size:Number):void {
var box:b2BodyDef= new b2BodyDef();
box.position.Set(xOrigin/worldScale,yOrigin/worldScale);
box.type=b2Body.b2_dynamicBody;
var square:b2PolygonShape = new b2PolygonShape();
square.SetAsBox(size/2/worldScale, size/2/worldScale);
var boxFixture:b2FixtureDef = new b2FixtureDef();
boxFixture.shape=square;
boxFixture.friction=0;
boxFixture.density=4;
boxFixture.restitution=1;
var boxBody:b2Body=world.CreateBody(box);
boxBody.CreateFixture(boxFixture);
boxBody.SetLinearVelocity(new b2Vec2(0,5));
}
addBox function adds a box to the world. Its basics are explained at Box2D tutorial for the absolute beginners, but I want you to focus on the last line.
SetLinearVelocity
method gives the box a linear velocity according to its vector argument.
private function onClick(e:MouseEvent):void {
var angle:Number=Math.atan2(mouseX,mouseY-480)-Math.PI/2;
addball(20*Math.cos(angle),-20*Math.sin(angle));
}
When the player clicks, we have to determine the angle of the mouse pointer relative to the bottom-left corner of the stage, to fire the ball.
From the angle, we can calculate the horizontal and vertical velocity of the ball.
Then we call addball
function, which works in a similar way as addBox.
private function addball(xVel:Number,yVel:Number):void {
var ball:b2BodyDef= new b2BodyDef();
ball.userData="ball";
ball.type=b2Body.b2_dynamicBody;
ball.position.Set(20/worldScale, 460/worldScale);
var circle:b2CircleShape=new b2CircleShape(15/worldScale);
var ballFixture:b2FixtureDef = new b2FixtureDef();
ballFixture.shape=circle;
ballFixture.friction=0;
ballFixture.density=1;
ballFixture.restitution=1;
var ballBody:b2Body=world.CreateBody(ball);
ballBody.CreateFixture(ballFixture);
ballBody.SetLinearVelocity(new b2Vec2(xVel,yVel));
}
Again, look how I set the linear velocity of the ball.
private function update(e:Event):void {
world.Step(1/30,10,10);
world.ClearForces();
world.DrawDebugData();
for (var currentBody:b2Body = world.GetBodyList(); currentBody; currentBody=currentBody.GetNext()) {
if (currentBody.GetPosition().y*worldScale>600 || currentBody.GetPosition().y*worldScale<(100*-1)) {
world.DestroyBody(currentBody);
}
if (currentBody.GetUserData()=="ball") {
var velocity:b2Vec2=currentBody.GetLinearVelocity();
if (velocity.Length()!=20) {
var speedOffset:Number=20/velocity.Length();
currentBody.SetLinearVelocity(new b2Vec2(velocity.x*speedOffset,velocity.y*speedOffset));
}
}
}
}
update
function is the core of the script as it handles the physics simulation at every frame.
Nothing new here, just refer to the Box2D tutorial for the absolute beginners for general concepts and to the basic Filler engine to keep the ball moving at a constant speed.
The rule is: at every frame, check ball's velocity with GetLinearVelocity
method and if the length of the vector representing the velocity is different than 20, then adjust it.
And this is the result:
Click with the mouse on the stage to fire the ball.
Next time, the same collision management you see in the original game.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.