Shrink it Box2D prototype – Step 2

Read all posts about "" game

In the previous step I showed you how to shrink/expand any kind of polygon.

Anyway in the original game, you can’t expand objects as much as you want, because you need mass to do it, and this adds strategy to the gameplay, because you have to shrink objects to gather the necessary mass to expand other ones.

I suppose the required amount of mass is the difference between the final and the initial mass.

At the same time, when we shrink an object, we’ll gather mass… probably determined by the difference between the initial and the final mass.

When you want to play with masses, the first thing you must setup carefully is the expand/shrink ratio. In the previous step I used a 10% for both shrinking and expanding.

This leads to a glitchy gameplay because if I have a sphere with a radius = 100 and I shrink it by 10% I get a sphere with a radius = 90. But if I expand 90 by 10% I get a sphere with a radius = 99. I don’t get the original sphere. So I cannot use these values, because I need to get the initial object if I expand it and then shrink it or if I shrink it and then expand it.

So in this example I am using 20% shrinking and 25% expanding. This way, our object with radius = 100 shrinked by 20% will have a radius of 80… and a radius = 80 expanded by 25% returns 100 again.

It’s up to you to find a couple of compatible numbers.

About masses, this is the concept: when the player shrinks an object, there isn’t any problem, I just have to compare the old mass with the new one and add the difference to the available mass. To get a body’s mass, use GetMass().

When the player tries to expand an objects, things become a little harder because we can’t know the future mass of the body, unless we want to heavily play with geometry.

So we have to make a little trick: first we remove the original shape, then we create and attach the new, expanded shape to the body, so we can get the mass of the final body. If we have enough mass, then we render the body, otherwise we remove the new shape and restore the old one. Since we do everything before rendering the frame, it will work nicely.

This is the script:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.KeyboardEvent;
	import flash.text.TextField;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	public class shrink extends Sprite {
		var body:b2Body;
		public var m_world:b2World;
		public var m_iterations:int=10;
		public var m_timeStep:Number=1.0/30.0;
		public var mousePVec:b2Vec2 = new b2Vec2();
		// variable to store if the player is pressing SPACE
		public var space_pressed:Boolean=false;
		// initial available mass
		public var avail_mass:Number=20;
		// text field to display available mass
		var text_field:TextField = new TextField();
		public function shrink() {
			var worldAABB:b2AABB = new b2AABB();
			var bodyDef:b2BodyDef = new b2BodyDef();
			var polygon:b2PolygonDef = new b2PolygonDef();
			var circleDef:b2CircleDef= new b2CircleDef();
			worldAABB.lowerBound.Set(-100.0, -100.0);
			worldAABB.upperBound.Set(100.0, 100.0);
			m_world=new b2World(worldAABB,new b2Vec2(0,10),true);
			// debug draw start
			var m_sprite:Sprite;
			m_sprite = new Sprite();
			addChild(m_sprite);
			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);
			// debug draw end
			// ground
			bodyDef.position.Set(10, 12);
			polygon.SetAsBox(30, 3);
			polygon.density=0;
			polygon.friction=0.3;
			polygon.restitution=0.2;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(polygon);
			body.SetMassFromShapes();
			// circle
			bodyDef.position.Set(3,5);
			circleDef.radius=2;
			circleDef.density=1;
			circleDef.friction=0.5;
			circleDef.restitution=0.2;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(circleDef);
			body.SetMassFromShapes();
			// box
			bodyDef.position.Set(13, 5);
			polygon.SetAsBox(2, 2);
			polygon.density=1;
			polygon.friction=0.5;
			polygon.restitution=0.2;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(polygon);
			body.SetMassFromShapes();
			// triangle
			bodyDef.position.Set(13,3);
			polygon.vertexCount=3;
			polygon.vertices[0].Set(0,-2);
			polygon.vertices[1].Set(2,2);
			polygon.vertices[2].Set(-2,2);
			polygon.density=1;
			polygon.friction=0.5;
			polygon.restitution=0.2;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(polygon);
			body.SetMassFromShapes();
			// custom shape
			bodyDef.position.Set(8,4);
			polygon.vertexCount=5;
			polygon.vertices[0].Set(0,-2);
			polygon.vertices[1].Set(2,0);
			polygon.vertices[2].Set(1,2);
			polygon.vertices[3].Set(-1,2);
			polygon.vertices[4].Set(-2,0);
			polygon.density=1;
			polygon.friction=0.5;
			polygon.restitution=0.2;
			body=m_world.CreateBody(bodyDef);
			body.CreateShape(polygon);
			body.SetMassFromShapes();
			//
			addChild(text_field);
			text_field.text="Available mass: "+avail_mass.toString();
			text_field.y=330;
			text_field.x=20;
			text_field.width=300;
			//
			addEventListener(Event.ENTER_FRAME, Update, false, 0, true);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, key_down);
			stage.addEventListener( KeyboardEvent.KEY_UP, key_up);
			stage.addEventListener(MouseEvent.MOUSE_DOWN, GetBodyAtMouse);
		}
		// detecting if the player pressed SPACE
		public function key_down(event:KeyboardEvent):void {
			if (event.keyCode==32) {
				space_pressed=true;
			}
		}
		// detecting if the player released SPACE
		public function key_up(event:KeyboardEvent):void {
			if (event.keyCode==32) {
				space_pressed=false;
			}
		}
		//
		public function GetBodyAtMouse(e:MouseEvent):b2Body {
			// scale multiplyers. Remember to choose compatible multipliers
			// in this case, 0.8*1.25=1 => compatible
			// 0.9*1.1=0.99 => not compabile. Must be 1
			var mult:Number=0.8;
			if (space_pressed) {
				mult=1.25;
			}
			var mouseXWorldPhys = (mouseX)/30;
			var mouseYWorldPhys = (mouseY)/30;
			mousePVec.Set(mouseXWorldPhys, mouseYWorldPhys);
			var aabb:b2AABB = new b2AABB();
			aabb.lowerBound.Set(mouseXWorldPhys - 0.001, mouseYWorldPhys - 0.001);
			aabb.upperBound.Set(mouseXWorldPhys + 0.001, mouseYWorldPhys + 0.001);
			var k_maxCount:int=10;
			var shapes:Array = new Array();
			var count:int=m_world.Query(aabb,shapes,k_maxCount);
			var body:b2Body=null;
			for (var i:int = 0; i < count; ++i) {
				var tShape:b2Shape=shapes[i] as b2Shape;
				var inside:Boolean=tShape.TestPoint(tShape.GetBody().GetXForm(),mousePVec);
				if (inside) {
					body=tShape.GetBody();
					break;
				}
			}
			// if I selected a STATIC body...
			if (body&&! body.IsStatic()) {
				// gettinc current mass
				var cur_mass:Number=body.GetMass();
				// variable to store new shape's mass
				var new_mass:Number;
				var s:b2Shape=body.GetShapeList();
				var type:int=s.GetType();
				switch (type) {
					case 0 :
						// I know it's a circle, so I am creating a b2CircleShape variable
						var circle:b2CircleShape=body.GetShapeList() as b2CircleShape;
						// getting the radius..
						var r=circle.GetRadius();
						// removing the circle shape from the body
						body.DestroyShape(circle);
						// creating a new circle shape
						var circleDef:b2CircleDef;
						circleDef = new b2CircleDef();
						// calculating new radius
						circleDef.radius=r*mult;
						circleDef.density=1.0;
						circleDef.friction=0.5;
						circleDef.restitution=0.2;
						// attach the shape to the body
						body.CreateShape(circleDef);
						// determine new body mass
						body.SetMassFromShapes();
						// determining new mass
						new_mass=body.GetMass();
						// calculating available mass after scaling
						avail_mass += (cur_mass-new_mass);
						// if there isn't enough mass...
						if (avail_mass<0) {
							// remove new circle shape and restore last used circle shape
							avail_mass+=new_mass-cur_mass;
							circle=body.GetShapeList() as b2CircleShape;
							body.DestroyShape(circle);
							circleDef = new b2CircleDef();
							circleDef.radius=r;
							circleDef.density=1.0;
							circleDef.friction=0.5;
							circleDef.restitution=0.2;
							body.CreateShape(circleDef);
							body.SetMassFromShapes();
						}
						// displaying mass
						text_field.text="Available mass: "+avail_mass.toString();
						break;
					case 1 :
						// now I know it's a polygon
						var poly:b2PolygonShape=body.GetShapeList() as b2PolygonShape;
						// UNIVERSAL POLYGON SCALING ROUTINE THANX TO ILYA
						var vertex_num:int=poly.GetVertexCount();
						var vertex_array:Array=poly.GetVertices();
						for each (var vert:b2Vec2 in vertex_array) {
							vert.Multiply(mult);
						}
						body.DestroyShape(poly);
						var new_shape:b2PolygonDef = new b2PolygonDef();
						new_shape.vertexCount=vertex_num;
						new_shape.vertices=vertex_array;
						new_shape.friction=0.5;
						new_shape.density=1;
						new_shape.restitution=0.2;
						body.CreateShape(new_shape);
						body.SetMassFromShapes();
						// determining new mass
						new_mass=body.GetMass();
						// calculating available mass after scaling
						avail_mass+=cur_mass-new_mass;
						// if there isn't enough mass...
						if (avail_mass<0) {
							// remove new polygon shape and restore last used polygon shape
							avail_mass+=new_mass-cur_mass;
							poly=body.GetShapeList() as b2PolygonShape;
							body.DestroyShape(poly);
							for each (vert in vertex_array) {
								vert.Multiply(1/mult);
							}
							new_shape = new b2PolygonDef();
							new_shape.vertexCount=vertex_num;
							new_shape.vertices=vertex_array;
							new_shape.friction=0.5;
							new_shape.density=1;
							new_shape.restitution=0.2;
							body.CreateShape(new_shape);
							body.SetMassFromShapes();
						}
						// displaying mass
						text_field.text="Available mass: "+avail_mass.toString();
						break;
				}
			}
			return body;
		}
		public function Update(e:Event):void {
			m_world.Step(m_timeStep, m_iterations);
		}
	}
}

And this is the result:

Click on a body to shrink it, click + SPACE to enlarge it, and look at your mass meter in the lower left corner.

No need to download, simply copy/paste this code in the file you can find in the previous step.

Next time, we'll add the final gameplay.

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

215 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
// Stairs
// 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