Create a Flash game like iOS Pop to Save using Box2D

Read all posts about "" game

During these days I am playing a nice free physics iOS game called Pop to Save developed by Cetin Caglar.

The game combines physics with line drawing and erasing, like Way of an Idea, but enriches the gameplay with particles.

Since Way of an Idea prototype has already been discussed in this post, with some easy changes to the code we can have a working prototype of Pop to Save, try it by yourself:

You have to repeatedly hit the orange receiver with the spheres generated by the blue emitter. The receiver will absorb spheres until it explodes releasing them, and another emitter is randomly created.

MOUSE OVER the emitter to shoot spheres

CLICK AND DRAG mouse pointer to draw terrain

MOUSE OVER the terrain while keeping SPACEBAR pressed to delete terrain.

To make things easier and avoid unnecessary tasks, I am not using Box2D contact listeners to check for collisions, but only some trigonometry.

Here is the commented script, uncommented parts are taken from the original post so it won’t be a problem for you to figure out how it works.

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.KeyboardEvent;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	public class Main extends Sprite {
		private var drawing:Boolean=false;
		private var canvas:Sprite = new Sprite();
		private var pointsArray:Array;
		private var pixelDist:int=20;
		private var savedX:int;
		private var savedY:int;
		private var world:b2World=new b2World(new b2Vec2(0,10),true);
		private var worldScale:Number=30;
		private var debugSprite:Sprite=new Sprite();
		private var degToRad:Number=0.0174532925;
		// bubbles emitter
		private var emitter:Emitter=new Emitter();
		private var emitterForce:Number=50;				// force applied to generated spheres
		private var emitterAngle:Number=-45;			// emitter angle
		private var fire:Boolean=false;					// should the emitter fire?
		private var fireRatio:Number=3;					// fire ratio, 3 means "one each 3 frames"
		private var lastFire:Number=3;					// dummy variable to tell us the last time we fired
		// bubbles receiver
		private var receiver:Receiver=new Receiver();
		public function Main() {
			// adding the emitter
			addChild(emitter);
			emitter.x=60;
			emitter.y=240;
			emitter.rotation=emitterAngle;
			// placing the receiver at a random position
			addChild(receiver);
			receiver.x=Math.random()*400+200;
			receiver.y=Math.random()*250+160;
			addChild(canvas);
			canvas.graphics.lineStyle(5);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,mousePressed);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMoved);
			stage.addEventListener(MouseEvent.MOUSE_UP,mouseReleased);
			addEventListener(Event.ENTER_FRAME,update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN,keyPressed);
			// emitter fires on mouse over
			emitter.addEventListener(MouseEvent.MOUSE_OVER,emitterFire);
			addChild(debugSprite);
			debugDraw();
		}

		private function emitterFire(e:MouseEvent):void {
			// the emitter fires
			fire=true;
			emitter.removeEventListener(MouseEvent.MOUSE_OVER,emitterFire);
			emitter.addEventListener(MouseEvent.MOUSE_OUT,stopEmitterFire);
		}

		private function stopEmitterFire(e:MouseEvent):void {
			// the emitter stops firing
			fire=false;
			emitter.addEventListener(MouseEvent.MOUSE_OVER,emitterFire);
			emitter.removeEventListener(MouseEvent.MOUSE_OUT,stopEmitterFire);
		}

		private function mousePressed(e:MouseEvent):void {
			drawing=true;
			canvas.graphics.moveTo(mouseX,mouseY);
			pointsArray=new Array();
			savedX=mouseX;
			savedY=mouseY;
			pointsArray.push(savedX);
			pointsArray.push(savedY);
		}
		
		private function mouseMoved(e:MouseEvent):void {
			if (drawing) {
				var distX:int=mouseX-savedX;
				var distY:int=mouseY-savedY;
				if (distX*distX+distY*distY>pixelDist*pixelDist) {
					canvas.graphics.lineTo(mouseX,mouseY);
					savedX=mouseX;
					savedY=mouseY;
					pointsArray.push(savedX);
					pointsArray.push(savedY);
				}
			}
		}
		
		private function mouseReleased(e:MouseEvent):void {
			drawing=false;
			var sx:int;
			var ex:int;
			var sy:int;
			var ey:int;
			var distX:int;
			var distY:int;
			var dist:Number;
			var angle:Number;
			var segments:int=pointsArray.length/2-1;
			for (var i:int=0; i<segments; i++) {
				sx=pointsArray[i*2];
				sy=pointsArray[i*2+1];
				ex=pointsArray[i*2+2];
				ey=pointsArray[i*2+3];
				distX=sx-ex;
				distY=sy-ey;
				dist=Math.sqrt(distX*distX+distY*distY);
				angle=Math.atan2(distY,distX);
				addPath((sx+ex)/2,(sy+ey)/2,Math.abs(dist),4,angle);
			}
			canvas.graphics.clear();
			canvas.graphics.lineStyle(5);
		}
		
		private function keyPressed(event:KeyboardEvent):void {
			// deleting the path using the mouse while pressing SPACE key
			if (event.keyCode==32) {
				world.QueryPoint(queryCallback,new b2Vec2(mouseX/worldScale,mouseY/worldScale));
			}
		}

		private function addPath(pX:Number,pY:Number,w:Number,h:Number,angle:Number):void {
			var bodyDef:b2BodyDef= new b2BodyDef();
			var polygonShape:b2PolygonShape = new b2PolygonShape();
			polygonShape.SetAsOrientedBox(w/2/worldScale, h/2/worldScale, new b2Vec2(pX/worldScale,pY/worldScale),angle);
			var fixtureDef:b2FixtureDef = new b2FixtureDef();
			fixtureDef.density=1;
			fixtureDef.friction=0.5;
			fixtureDef.restitution=0.5;
			fixtureDef.shape=polygonShape;
			var body:b2Body=world.CreateBody(bodyDef);
			body.CreateFixture(fixtureDef);
		}

		private function queryCallback(fixture:b2Fixture):Boolean {
			var touchedBody:b2Body=fixture.GetBody();
			for (var f:b2Fixture=touchedBody.GetFixtureList(); f; f=f.GetNext()) {
				if (touchedBody.GetType()==b2Body.b2_staticBody) {
					world.DestroyBody(touchedBody);
				}
			}
			return false;
		}

		private function fireParticle(pX:Number,pY:Number,shoot:Boolean):void {
			// particle generation
			var bodyDef:b2BodyDef= new b2BodyDef();
			bodyDef.type=b2Body.b2_dynamicBody;
			bodyDef.position.Set(pX/worldScale,pY/worldScale);
			var circleShape:b2CircleShape = new b2CircleShape(5/worldScale);
			var fixtureDef:b2FixtureDef = new b2FixtureDef();
			fixtureDef.density=1;
			fixtureDef.friction=0.5;
			fixtureDef.restitution=0.5;
			fixtureDef.shape=circleShape;
			var body:b2Body=world.CreateBody(bodyDef);
			body.CreateFixture(fixtureDef);
			// if needed, apply a force to the particle
			if (shoot) {
				body.ApplyForce(new b2Vec2(emitterForce*Math.cos(emitter.rotation*degToRad),emitterForce*Math.sin(emitter.rotation*degToRad)),body.GetWorldCenter());
			}
		}

		private function update(e:Event):void {
			// updating fire counter
			if (lastFire<fireRatio) {
				lastFire++;
			}
			// should we fire?
			if (fire && lastFire==fireRatio) {
				fireParticle(emitter.x,emitter.y,true);
				lastFire=0;
			}
			world.Step(1/30,10,10);
			world.ClearForces();
			for (var b:b2Body = world.GetBodyList(); b; b = b.GetNext()) {
				if (b.GetType()==b2Body.b2_dynamicBody) {
					// removing particles falling off the screen
					if (b.GetPosition().y*worldScale>480) {
						world.DestroyBody(b);
					}
					// distance between the particle and the receiver
					var distX:Number=b.GetPosition().x*worldScale-receiver.x;
					var distY:Number=b.GetPosition().y*worldScale-receiver.y;
					var distance:Number=distX*distX+distY*distY;
					// if less than receiver radius...
					if (distance<900) {
						// draining receiver "energy"
						receiver.alpha-=0.05;
						// once receiver energy reaches zero...
						if (receiver.alpha<=0) {
							// releasing captured particles
							for (var i:Number=0; i<20; i++) {
								fireParticle(receiver.x,receiver.y,false);
							}
							// moving the receiver
							receiver.alpha=1;
							receiver.x=Math.random()*400+200;
							receiver.y=Math.random()*250+160;
						}
						// removing the particle
						world.DestroyBody(b);
					}
				}
			}
			world.DrawDebugData();
		}

		private function debugDraw():void {
			var debugDraw:b2DebugDraw=new b2DebugDraw();
			debugDraw.SetSprite(debugSprite);
			debugDraw.SetDrawScale(worldScale);
			debugDraw.SetFlags(b2DebugDraw.e_shapeBit|b2DebugDraw.e_jointBit);
			debugDraw.SetFillAlpha(0.5);
			world.SetDebugDraw(debugDraw);
		}

	}

}

If you have any question, feel free to leave a comment, meanwhile download the source code and get the original game for free.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers