Create a terrain like the one in Tiny Wings with Flash and Box2D – adding textures

Read all posts about "" game

If you are following the series about the creation of a Tiny Wings-like terrain, you should know one of the most popular requests once I placed a car running on the hills was how to get rid of the debug draw graphics and use your own textures.

First, you need a seamless texture, or a texture without seam at least horizontally. I suggest you to pick a texture of the same length of the hills (640 pixels in this case) or a texture which width divides the lenght of the hills.

I used a rock texture taken from 40 watt.

Then, as soon as you place the polygons representing the hill slices, you also have to draw on a sprite the same shape you are giving your hill.

This will act as a mask for your seamless texture. And obviously remember to remove both the mask and the texture when they leave the screen to the left side.

This is what you will get:

Use UP and DOWN arrow keys to control the cart, and LEFT/RIGHT to balance it while in the air.

This is the fully commented source code:

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import Box2D.Dynamics.*;
	import Box2D.Collision.*;
	import Box2D.Collision.Shapes.*;
	import Box2D.Common.Math.*;
	import Box2D.Dynamics.Joints.*;
	public class Main extends Sprite {
		private var world:b2World=new b2World(new b2Vec2(0,10),true);
		private var worldScale:int=30;
		// variables which will be used to determine key pressed
		private var left:Boolean=false;
		private var right:Boolean=false;
		private var up:Boolean=false;
		private var down:Boolean=false;
		// the body of the cart
		private var cart:b2Body;
		// wheels motor speed
		private var motorSpeed:Number=0;
		// front and rear wheels revolute joints
		private var rearWheelRevoluteJoint:b2RevoluteJoint;
		private var frontWheelRevoluteJoint:b2RevoluteJoint;
		// random hill's height
		private var nextHill:Number=140+Math.random()*200;
		// build another hill when stage's x position is lower than this value
		private var buildNextHillAt:Number=0;
		// this is the seamless texture
		private var seamlessTexture:seamlessTextureMc;
		// this is the sprite which will be use to mask the samless texture
		private var theMask:Sprite;
		// this is just a container
		private var hillSprite:Sprite=new Sprite();
		// background sky
		private var sky:skyMc=new skyMc();
		public function Main():void {
			// first I add the sky
			addChild(sky);
			// then the debug draw sprite
			debugDraw();
			// an dfinally the cointainer of the hills
			addChild(hillSprite);
			// we start with two hills
			nextHill=drawHill(10,0,nextHill);
			nextHill=drawHill(10,640,nextHill);
			addEventListener(Event.ENTER_FRAME,updateWorld);
			stage.addEventListener(KeyboardEvent.KEY_DOWN,keyPressed);
			stage.addEventListener(KeyboardEvent.KEY_UP,keyReleased);
			// add the cart
			var carBodyDef:b2BodyDef = new b2BodyDef();
			carBodyDef.type=b2Body.b2_dynamicBody;
			carBodyDef.position.Set(320/worldScale,50/worldScale);
			carBodyDef.userData=new Object();
			var box:b2PolygonShape = new b2PolygonShape();
			box.SetAsBox(30/worldScale,10/worldScale);
			var boxDef:b2FixtureDef = new b2FixtureDef();
			boxDef.density=0.5;
			boxDef.friction=3;
			boxDef.restitution=0.3;
			boxDef.filter.groupIndex=-1;
			boxDef.shape=box;
			cart=world.CreateBody(carBodyDef);
			cart.CreateFixture(boxDef);
			// wheel shape
			var wheelShape:b2CircleShape=new b2CircleShape(12/worldScale);
			// wheel fixture
			var wheelFixture:b2FixtureDef = new b2FixtureDef();
			wheelFixture.density=1;
			wheelFixture.friction=3;
			wheelFixture.restitution=0.1;
			wheelFixture.filter.groupIndex=-1;
			wheelFixture.shape=wheelShape;
			// wheel body definition
			var wheelBodyDef:b2BodyDef = new b2BodyDef();
			wheelBodyDef.type=b2Body.b2_dynamicBody;
			// real wheel
			wheelBodyDef.position.Set(cart.GetWorldCenter().x-(16/worldScale),cart.GetWorldCenter().y+(15/worldScale));
			var rearWheel:b2Body=world.CreateBody(wheelBodyDef);
			rearWheel.CreateFixture(wheelFixture);
			// front wheel
			wheelBodyDef.position.Set(cart.GetWorldCenter().x+(16/worldScale),cart.GetWorldCenter().y+(15/worldScale));
			var frontWheel:b2Body=world.CreateBody(wheelBodyDef);
			frontWheel.CreateFixture(wheelFixture);
			// rear joint
			var rearWheelRevoluteJointDef:b2RevoluteJointDef=new b2RevoluteJointDef();
			rearWheelRevoluteJointDef.Initialize(rearWheel,cart,rearWheel.GetWorldCenter());
			rearWheelRevoluteJointDef.enableMotor=true;
			rearWheelRevoluteJointDef.maxMotorTorque=10000;
			rearWheelRevoluteJoint=world.CreateJoint(rearWheelRevoluteJointDef) as b2RevoluteJoint;
			// front joint
			var frontWheelRevoluteJointDef:b2RevoluteJointDef=new b2RevoluteJointDef();
			frontWheelRevoluteJointDef.Initialize(frontWheel,cart,frontWheel.GetWorldCenter());
			frontWheelRevoluteJointDef.enableMotor=true;
			frontWheelRevoluteJointDef.maxMotorTorque=10000;
			frontWheelRevoluteJoint=world.CreateJoint(frontWheelRevoluteJointDef) as b2RevoluteJoint;
		}
		private function drawHill(pixelStep:int,xOffset:Number,yOffset:Number):Number {
			var hillStartY:Number=yOffset;
			var hillWidth:Number=640;
			var hillSliceWidth=hillWidth/pixelStep;
			var hillVector:Vector.;
			var randomHeight:Number=Math.random()*100;
			// creates a new masking sprite
			theMask = new Sprite();
			// adjusts x property according to hill's offset
			theMask.x=xOffset;
			// a simple line style
			theMask.graphics.lineStyle(1,0xffffff);
			// creates a new texture sprite
			seamlessTexture = new seamlessTextureMc();
			// adds the sprite to the container
			hillSprite.addChild(seamlessTexture);
			// adjust x property with the same concept seen before
			seamlessTexture.x=xOffset;
			// sets the mask
			seamlessTexture.mask=theMask;
			if (xOffset!=0) {
				hillStartY-=randomHeight;
			}
			for (var j:int=0; j();
				// once I create the Box2D slices, I also paint the mask sprite
				theMask.graphics.beginFill(0xffffff,1);
				theMask.graphics.moveTo(j*pixelStep,480);
				hillVector.push(new b2Vec2((j*pixelStep+xOffset)/worldScale,480/worldScale));
				theMask.graphics.lineTo(j*pixelStep,hillStartY+randomHeight*Math.cos(2*Math.PI/hillSliceWidth*j));
				hillVector.push(new b2Vec2((j*pixelStep+xOffset)/worldScale,(hillStartY+randomHeight*Math.cos(2*Math.PI/hillSliceWidth*j))/worldScale));
				theMask.graphics.lineTo((j+1)*pixelStep,hillStartY+randomHeight*Math.cos(2*Math.PI/hillSliceWidth*(j+1)));
				hillVector.push(new b2Vec2(((j+1)*pixelStep+xOffset)/worldScale,(hillStartY+randomHeight*Math.cos(2*Math.PI/hillSliceWidth*(j+1)))/worldScale));
				theMask.graphics.lineTo((j+1)*pixelStep,480);
				hillVector.push(new b2Vec2(((j+1)*pixelStep+xOffset)/worldScale,480/worldScale));
				theMask.graphics.endFill();
				var sliceBody:b2BodyDef=new b2BodyDef ;
				var centre:b2Vec2=findCentroid(hillVector,hillVector.length);
				sliceBody.position.Set(centre.x,centre.y);
				for (var z:int=0; z, count:uint):b2Vec2 {
			var c:b2Vec2 = new b2Vec2();
			var area:Number=0.0;
			var p1X:Number=0.0;
			var p1Y:Number=0.0;
			var inv3:Number=1.0/3.0;
			for (var i:int = 0; i < count; ++i) {
				var p2:b2Vec2=vs[i];
				var p3:b2Vec2=i+11000) {
				motorSpeed=1000;
			}
			// setting wheels motor speed
			rearWheelRevoluteJoint.SetMotorSpeed(motorSpeed);
			frontWheelRevoluteJoint.SetMotorSpeed(motorSpeed);
			world.Step(1/30,10,10);
			world.ClearForces();
			for (var currentBody:b2Body=world.GetBodyList(); currentBody; currentBody=currentBody.GetNext()) {
				// getting cart x position
				if (currentBody.GetUserData()!=null) {
					// adjusting stage position to keep cart in the vertical middle of the stage
					x=320-currentBody.GetPosition().x*worldScale;
					// checking if it's time to add a new hill
					if (x<=buildNextHillAt) {
						buildNextHillAt-=640;
						nextHill=drawHill(10,- buildNextHillAt+640,nextHill);
					}
				}
				if (currentBody.GetPosition().x*worldScale<(x*-1)-640) {
					world.DestroyBody(currentBody);
				}
			}
			// have to remove mask and texture once the leave the screen...
			for (var i:int = 0; i=hillSprite.getChildAt(i).x+640) {
					hillSprite.removeChildAt(i);
				}
			}
			// ... and update backgound position
			sky.x=-x;
			world.DrawDebugData();
		}
	}
}

Download the full source code and give me feedback.

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