Creation of a Flash Stabilize! clone using Box2D – part 5

Talking about Stabilize! game, Actionscript 3, Flash and Game development.

Welcome to the 5th part.

Today we’ll add some gameplay… in part 4 we managed collisions, now it’s time to add a one-minute timer (yes, this is a concept for the 60 seconds to fame contest).

I think, having colored stuff on the stage, making a “match 3 to remove” game would be too obvious… so the concept is this one: make a crate collide (collide! not stay in touch!) with three other crates of the same color to make it disappear.

The one I am showing you right now is just a prototype, but I am about to finish the complete game and it can lead to interesting gameplay variations, where bouncing is preferred to stacking.

Then, a simple timer made using the concepts seen at Understanding AS3 Timer Class and Drawing arcs with AS3 is placed in the center of the stage.

This is the main file:

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.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	//
	public class stabilize extends Sprite {
		public var m_sprite: Sprite = new Sprite();
		public var m_world:b2World;
		// the_crate is the crate sprite
		public var the_crate:crate=new crate();
		// next_crate
		public var next_crate:crate=new crate();
		// droppable_area is the sprite representing the area your mouse
		// must overlap to drop a crate
		public var droppable_area:drop_area = new drop_area();
		// this is the weight of the current crate I am dropping
		public var cur_drop:int=Math.ceil(Math.random()*9);
		// this is the color of the current crate I am dropping
		public var cur_color:int=Math.ceil(Math.random()*5);
		// weight and color of "next" crate
		public var next_drop:int=Math.ceil(Math.random()*9);
		public var next_color:int=Math.ceil(Math.random()*5);
		// this is my custom contact listener class
		public var contact_listener=new custom_contact_listener();
		// the progressive number just keep track of dropped crates and
		// assign an unique number to each of them
		public var progressive:int=1;
		// timer with a tick at every second
		var time_count:Timer=new Timer(1000);
		// I am going to represent the time on my_cancas sprite
		var my_canvas:Sprite = new Sprite();
		// degrees to radians conversion
		var deg_to_rad=0.0174532925;
		//
		public function stabilize() {
			var gravity:b2Vec2=new b2Vec2(0,9.8);
			var worldAABB:b2AABB = new b2AABB();
			worldAABB.lowerBound.Set(-1000,-1000);
			worldAABB.upperBound.Set(1000,1000);
			m_world=new b2World(worldAABB,gravity,true);
			// adding the contact listener
			m_world.SetContactListener(contact_listener);
			m_sprite = new Sprite();
			addChild(my_canvas);
			addChild(m_sprite);
			addChild(droppable_area);
			addChild(the_crate);
			// assigning the crate current weight and color
			the_crate.weight.text=cur_drop.toString();
			the_crate.gotoAndStop(cur_color);
			addChild(next_crate);
			// assigning "next" crate weight and color
			next_crate.weight.text=next_drop.toString();
			next_crate.gotoAndStop(next_color);
			next_crate.x=250;
			next_crate.y=385;
			debug_draw();
			AddBox(250/30,350/30,10/30,10/30,0,false);
			AddBox(250/30,350/30,200/30,10/30,20,false);
			// starting the timer
			time_count.start();
			// adding timer listener
			time_count.addEventListener(TimerEvent.TIMER, show_time);
			addEventListener(Event.ENTER_FRAME,Update);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
		}
		// showing the time pie
		function show_time(event:TimerEvent) {
			my_canvas.graphics.clear();
			my_canvas.graphics.lineStyle(5,0xffffff,1);
			draw_arc(my_canvas,250,200,50,0,360,1);
			my_canvas.graphics.lineStyle(5,0x000000,1);
			draw_arc(my_canvas,250,200,50,270,270+event.target.currentCount*6,1);
			trace(event.target.currentCount);
		}
		public function mousePressed(e:MouseEvent) {
			// look at hitTestPoint... the final true value says it's going to to check against the actual pixels
			// instead of the bounding box
			if (droppable_area.hitTestPoint(mouseX,mouseY,true)&&! the_crate.hitTestObject(m_sprite)) {
				// notice the cur_drop parameter, I am passing the weight of the current crate to the AddBox function
				AddBox(mouseX/30,mouseY/30,0.5,0.5,cur_drop,true);
			}
		}
		public function mouseMoved(e:MouseEvent) {
			the_crate.x=mouseX;
			the_crate.y=mouseY;
			if (droppable_area.hitTestPoint(mouseX,mouseY,true)) {
				the_crate.alpha=1;
			} else {
				the_crate.alpha=0.5;
			}
		}
		// function AddBox
		// px: x position
		// py: y position
		// _halfwidth: half of the box width
		// _halfheight: half of the box height
		// density: density of the box (0: static)
		// is_crate: if true, it's a crate (and you should render the proper movieclip
		public function AddBox(px:Number,py:Number,_halfwidth:Number,_halfheight:Number,density:Number,is_crate:Boolean) {
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.position.Set(px,py);
			var boxDef:b2PolygonDef = new b2PolygonDef();
			boxDef.SetAsBox(_halfwidth,_halfheight);
			boxDef.density=density;
			boxDef.friction=0.3;
			boxDef.restitution=0.2;
			if (is_crate) {
				bodyDef.userData = new crate();
			}
			var body:b2Body=m_world.CreateBody(bodyDef);
			body.CreateShape(boxDef);
			body.SetMassFromShapes();
			if (is_crate) {
				addChild(bodyDef.userData);
				// assigning current weight and color to the Box2D sprite bound to the box I created
				bodyDef.userData.weight.text=cur_drop.toString();
				// collz is the text that will count the number of collisions
				bodyDef.userData.collz.text=0;
				// assigning the progressive number to the crate...
				bodyDef.userData.prog=progressive;
				// ... and incrementing it for next one
				progressive++;
				bodyDef.userData.gotoAndStop(cur_color);
				// updating current weight and color with the next ones
				cur_drop=next_drop;
				cur_color=next_color;
				// changing current crate movieclip according to its weight and color
				the_crate.weight.text=cur_drop.toString();
				the_crate.gotoAndStop(cur_color);
				// calculating next crate weight and color
				next_drop=Math.ceil(Math.random()*9);
				next_color=Math.ceil(Math.random()*5);
				// updating next crate movieclip according to its weight and color
				next_crate.weight.text=next_drop.toString();
				next_crate.gotoAndStop(next_color);
			}
		}
		public function Update(e:Event) {
			var crate_num:int;
			m_world.Step(1/30,10);
			for (var bb:b2Body = m_world.m_bodyList; bb; bb = bb.m_next) {
				if (bb.m_userData is Sprite) {
					bb.m_userData.x=bb.GetPosition().x*30;
					bb.m_userData.y=bb.GetPosition().y*30;
					bb.m_userData.rotation = bb.GetAngle() * (180/Math.PI);
					// checking for the number of collisions. 
					// if it's bigger than 2...
					if (bb.m_userData.collz.text>2) {
						// removing the crae movieclip
						removeChild(bb.m_userData);
						// removing the Box2D object from the world
						bb.m_userData=null;
						m_world.DestroyBody(bb);
					}
				}
			}
		}
		public function debug_draw() {
			var dbgDraw:b2DebugDraw = new b2DebugDraw();
			var dbgSprite:Sprite = new Sprite();
			m_sprite.addChild(dbgSprite);
			dbgDraw.m_sprite=m_sprite;
			dbgDraw.m_drawScale=30;
			dbgDraw.m_alpha=1;
			dbgDraw.m_fillAlpha=0.5;
			dbgDraw.m_lineThickness=1;
			dbgDraw.m_drawFlags=b2DebugDraw.e_shapeBit;
			m_world.SetDebugDraw(dbgDraw);

		}
		public function draw_arc(movieclip,center_x,center_y,radius,angle_from,angle_to,precision) {
			var angle_diff=angle_to-angle_from;
			var steps=Math.round(angle_diff*precision);
			var angle=angle_from;
			var px=center_x+radius*Math.cos(angle*deg_to_rad);
			var py=center_y+radius*Math.sin(angle*deg_to_rad);
			movieclip.graphics.moveTo(px,py);
			for (var i:int=1; i<=steps; i++) {
				angle=angle_from+angle_diff/steps*i;
				movieclip.graphics.lineTo(center_x+radius*Math.cos(angle*deg_to_rad),center_y+radius*Math.sin(angle*deg_to_rad));
			}
		}
	}
}

And this is the collision manager. Refer to part 4 to see how can you use it.

package {
	//
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Dynamics.Contacts.*;
	import Box2D.Dynamics.*;
	import Box2D.Common.Math.*;
	import Box2D.Common.*;
	//
	public class custom_contact_listener extends b2ContactListener {
		// this array will hold all collisions among crates
		var collz:Array=new Array  ;
		//
		public override function Add(point:b2ContactPoint):void {
			// if the bodies that collided have an userData different from null,
			// then we can say they are crates.
			// So, if collided bodies are crates...
			if (point.shape1.GetBody().GetUserData()!=null&&point.shape2.GetBody().GetUserData()!=null) {
				// if both crates have the same color...
				if (point.shape1.GetBody().GetUserData().currentFrame==point.shape2.GetBody().GetUserData().currentFrame) {
					// getting progressive number of both crates
					var num1:int=point.shape1.GetBody().GetUserData().prog;
					var num2:int=point.shape2.GetBody().GetUserData().prog;
					// if the collision array of the num1-th crate does not exists...
					if (collz[num1]==undefined) {
						// create it
						collz[num1]=new Array  ;
						// populate it with the num2-th crate
						collz[num1].push(num2);
					} else {
						// if already exists, check if the num2-th crate already exists...
						if (collz[num1].indexOf(num2)==-1) {
							// if not, add it just like before
							collz[num1].push(num2);
						}
					}
					// same thing for the collision array of the num2-th crate
					if (collz[num2]==undefined) {
						collz[num2]=new Array  ;
						collz[num2].push(num1);
					} else {
						if (collz[num2].indexOf(num1)==-1) {
							collz[num2].push(num1);
						}
					}
					// updating the number of collision shown in both crates
					point.shape1.GetBody().GetUserData().collz.text=collz[num1].length;
					point.shape2.GetBody().GetUserData().collz.text=collz[num2].length;
				}
			}
		}
		//
		public override function Persist(point:b2ContactPoint):void {

		}
		//
		public override function Remove(point:b2ContactPoint):void {
		}
		//
		public override function Result(point:b2ContactResult):void {
		}
	}
}

And this is the result:

Make crates collide until the collision counter reaches 3 to make them disappear. No need to download, just cut/paste these scripts on the ones you can find at part 4.

If you make something interesting out of it, let me know and I'll publish it.