Using Box2d to Create a Side Scrolling Game – Part 2: Adding Keyboard Input and An Enemy

This article will show you how to add user input to control the “hero” object created in the previous article.

Additionally, we will add an enemy that will wander back and forth, stop and reverse direction at the ends of the screen, and will bounce up the hero when jumped onto.

Tutorial Demo


Use ‘Left’ and ‘Right’ arrow keys to move, ‘Space’ to jump (after clicking on it)

Planning

The first thing we need to add is keyboard input to control out hero box. Additionally, we have to make sure it doesn’t go off the screen, so we’ll stop it when it reaches either the right or left side of the screen.

We also want to add a simple enemy to make the world seem more alive. We’ll add another block that is the same size of the hero, have it move back and forth along the floor of the screen, and reverse direction whenever it hits either edge of the screen, or hits touches an object on either of its sides.

New Code

The new version of the ‘Game.as’ is shown below:

Game.as

package Game 
{
	import Box2D.Collision.b2WorldManifold;
	import Box2D.Collision.Shapes.b2PolygonShape;
	import Box2D.Common.Math.b2Vec2;
	import Box2D.Dynamics.b2Body;
	import Box2D.Dynamics.b2BodyDef;
	import Box2D.Dynamics.b2DebugDraw;
	import Box2D.Dynamics.b2Fixture;
	import Box2D.Dynamics.b2FixtureDef;
	import Box2D.Dynamics.b2World;
	import Box2D.Dynamics.Contacts.b2ContactEdge;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Point;
	
	//NEW for easily getting names for keycodes
	import flash.ui.Keyboard;
	/**
	 * ...
	 * @author Chris Moeller
	 */
	public class Game 
	{
		public var stage:Stage;
		
		public const gravity:Number = 9.8; //gravity direction, positive is down in screen coordinates
		public const scale:Number = 30; //the scale the Box2d world will be compared to screen size
		
		public var world:b2World; //create the "physics world"
		
		public var hero_box:b2Body; //the object we'll use to control our "hero"/ main character
		
		//NEW for keyboard input
		public var keys_down:Array; //array to hold which keys are currently down
		public var hero_speed:Number; //the amount we'll add to the impulse of the hero when moving left or right
		public var hero_max_speed:Number; //the maximum amount we will let the hero's linear velocity move sideways (x direction)
		public var jump_speed:Number; //the amount of impulse to make the hero "jump"
		public var hero_size:int; //The hero total width/ height in pixels
		public var hero_normal:b2Vec2; //the normal vector of the hero - which direction and the amount it is colliding with other objects
		
		public var enemy_box:b2Body; //the new enemy object
		public var enemy_dir:int; //which direction the enemy is moving (either +1 for right, or -1 for left
		public var enemy_max_speed:Number;// max speed of the enemy
		
		
		public function Game(stage:Stage) 
		{
			this.stage = stage; //assign the main "stage" to a variable in "Game.as" so it has access to it, and can add any objects to the screen it needs to
			world = new b2World(new b2Vec2(0, gravity), true); //initialize the world; first parameter a 2d vector to represent the direction+magnitude of gravity (straight down and at 9.8m/s^2
			
			//add the "debug drawings" to the screen:
			var debug_sprite:Sprite = new Sprite(); //our debug sprite, where all of the physical objects can be drawn upon before we add graphics
			stage.addChild(debug_sprite);
			
			var debug_draw:b2DebugDraw = new b2DebugDraw();
			debug_draw.SetSprite(debug_sprite);
			debug_draw.SetDrawScale(scale);
			debug_draw.SetFlags(b2DebugDraw.e_shapeBit);
			world.SetDebugDraw(debug_draw);
			
			//create a static/unmoving box along the bottom to use as the ground
			CreateBox(0, stage.stageHeight - 50, stage.stageWidth, 50, false);
			
			//NEW
			//create your "hero" box to be used as your character later on
			hero_size = 50;
			hero_box = CreateBox(200, 100, hero_size, hero_size, true, 1,1, true, "hero");
			
			//create a couple "blocks" on the screen
			CreateBox(200, stage.stageHeight - 50 * 4, 50, 50, false);
			CreateBox(350, stage.stageHeight - 50 * 4, 50, 50, false);
			CreateBox(400, stage.stageHeight - 50 * 4, 50, 50, false);
			
			enemy_box = CreateBox(550, 300, hero_size, hero_size, true, 1, 1, true, "enemy");
			enemy_dir = 1; //setting the enemy to go right initially
			
			stage.addEventListener(Event.ENTER_FRAME, RunGame); //main game loop which will perform calculation simulations each frame, and game processes
			
			//NEW for keyboard input
			hero_speed = 2; //just test and find values that work well
			hero_max_speed = 4;
			hero_normal = new b2Vec2(0, 0);
			jump_speed = 16;
			enemy_max_speed = 2;
			keys_down = new Array();
			//adding the listeners to call the functions when a key has been pressed or released
			stage.addEventListener(KeyboardEvent.KEY_DOWN, KeyDown);
			stage.addEventListener(KeyboardEvent.KEY_UP, KeyUp);
			
		}
		/*
		function used to create boxes quickly, without having to do all the box2d initializations each time
		NEW: added 'friction', 'fixed_rotation' and 'name' that can be passed in when creating a box
		*/
		public function CreateBox(x:Number, y:Number, width:Number, height:Number, is_dynamic:Boolean, density:Number = 1, friction:Number=.5, fixed_rotation:Boolean = false, name:String="" ):b2Body
		{
			//first tranfer all the pixel units into box2d units
			x = Con2B2D(x);
			y = Con2B2D(y);
			width = Con2B2D(width);
			height = Con2B2D(height);
			
			var box_body:b2BodyDef = new b2BodyDef();
			//NEW
			box_body.fixedRotation = fixed_rotation;
			
			box_body.position.Set(x+width/2, y+height/2); //NOTE: box2d sets the position of the center of an object, not the top left like normal. So we have to add the middle (width/2, height/2) to the position to position where we actually want to have it
			
			if (is_dynamic)
				box_body.type = b2Body.b2_dynamicBody;
			
			var box_poly_shape:b2PolygonShape = new b2PolygonShape();
			box_poly_shape.SetAsBox(width / 2, height / 2);
			var box_fixture:b2FixtureDef = new b2FixtureDef();
			box_fixture.shape = box_poly_shape;
			box_fixture.density = density;
			box_fixture.friction = friction;
			
			//to store a custom value to a box2d object, you set it's fixture's "userData"- in this case so we can tell an object by it's name string
			box_fixture.userData = {name:name}
			var world_box_body:b2Body = world.CreateBody(box_body);
			world_box_body.CreateFixture(box_fixture);
			
			return world_box_body;
		}
		/*
			Main Loop: Used to run the physics simulation each frame, as well as any game processes
		*/
		public function RunGame(e:Event):void
		{
			world.Step(1/30,10,10); //performs a time step in the box2d simulation
			world.ClearForces(); //used to clear the forces after performing the time step
			world.DrawDebugData(); //draw the updated debug draw/graphics before adding out graphics
			
			//lets set the hero box to moving forward, as running along the ground
			
			//NEW for keyboard input

			if (hero_normal.x >= 0) //if the hero is pressing against the side of a block, applying impulse in the x-dir makes him "stick" to it
				if (KeyIsDown(Keyboard.LEFT))
					if (hero_box.GetLinearVelocity().x > - hero_max_speed) //if we haven't reached the max speed in this direction
						hero_box.ApplyImpulse(new b2Vec2( -hero_speed, 0), hero_box.GetWorldCenter());
						
			if (hero_normal.x <= 0)			
				if (KeyIsDown(Keyboard.RIGHT))
					if (hero_box.GetLinearVelocity().x <  hero_max_speed)
						hero_box.ApplyImpulse(new b2Vec2( hero_speed, 0), hero_box.GetWorldCenter());
			
					
			//Now to check if the hero can jump (if it is touching the ground)
			//reset the hero normals to nothing, otherwise if it isn't touching something it will maintain the normals from the last frame
			hero_normal.x = 0;
			hero_normal.y = 0;
			var can_jump:Boolean = false;
			var edge:b2ContactEdge = hero_box.GetContactList(); //get all the objects the hero is contacting with
			while (edge)
				{
					var a:b2WorldManifold = new b2WorldManifold();
					edge.contact.GetWorldManifold(a);
					var normal1:b2Vec2 = a.m_normal;	
					if (edge.contact.IsTouching())
					{	
						//trace(normal1.y);
						if (normal1.y >0) //if the normal is positive/ the hero is being pushed up/ sitting on top of another object
						{
							//allow the hero to jump
							if ( edge.contact.GetFixtureA().GetUserData().name == "hero")
							can_jump = true;
							//if the hero jumped on the enemy, make it "bounce"
							if ( edge.contact.GetFixtureB().GetUserData().name == "enemy")
							{
								hero_box.SetLinearVelocity(new b2Vec2(hero_box.GetLinearVelocity().x, -jump_speed / 2));
								//and "re-spawn" the enemy
								enemy_box.SetPosition(new b2Vec2(Con2B2D(550), Con2B2D(100)));
								
							}
						}
						//if the hero is touching an enemy on the side, re-spawn him
						if (normal1.x !=0)
							if (edge.contact.GetFixtureB().GetUserData().name == "enemy"||edge.contact.GetFixtureA().GetUserData().name == "enemy")
								hero_box.SetPosition(new b2Vec2(Con2B2D(100), Con2B2D(100)));
								
						hero_normal = normal1;						
					}	
					edge = edge.next;
				}
			//if the hero is able to jump and space was pressed, apply the impulse up in the y direction to make the hero jump
			if(can_jump)
				if (KeyIsDown(Keyboard.SPACE))
						hero_box.ApplyImpulse(new b2Vec2(0,  -jump_speed), hero_box.GetWorldCenter());	
				
			//if the hero is trying to go off the left or the right side, stop him
			if (ConFromB2D(hero_box.GetPosition().x) + hero_size/2 > stage.stageWidth)//the right side/ screenwidth
				hero_box.SetPosition(new b2Vec2(Con2B2D(stage.stageWidth-hero_size/2), hero_box.GetPosition().y));
			else if(ConFromB2D(hero_box.GetPosition().x)- hero_size/2<0)
				hero_box.SetPosition(new b2Vec2(0 + Con2B2D(hero_size / 2), hero_box.GetPosition().y));
				
				
			//NEW for enemy box
			if (enemy_dir > 0)//if enemy if going right
			{
				//if the velocity is less than the max velocity, add more speed by applying impulse
				if (enemy_box.GetLinearVelocity().x < enemy_max_speed)
					enemy_box.ApplyImpulse(new b2Vec2( hero_speed, 0), enemy_box.GetWorldCenter());
			}
			else
			{
				if (enemy_box.GetLinearVelocity().x > -enemy_max_speed)
						enemy_box.ApplyImpulse(new b2Vec2( -hero_speed, 0), enemy_box.GetWorldCenter());
			}
			
			//need to check if the enemy touches the hero, and to change direction if that happens
			var enemy_touching:Boolean = false;
			var edge2:b2ContactEdge = enemy_box.GetContactList();
			while (edge2)
				{
					var a:b2WorldManifold = new b2WorldManifold();
					edge2.contact.GetWorldManifold(a);
					var normal1:b2Vec2 = a.m_normal;	
					if (edge2.contact.IsTouching())
					{	
						if (normal1.x != 0)
							enemy_touching = true;
					}	
					edge2 = edge2.next;
				}
			
			//if the enemy is trying to go off the left or right side, or touching an object(the hero) reverse his direction
			if (ConFromB2D(enemy_box.GetPosition().x) + hero_size / 2 > stage.stageWidth || ConFromB2D(enemy_box.GetPosition().x) - hero_size / 2 < 0||enemy_touching)//the right side/ screenwidth
			{
					enemy_dir *= -1;
					enemy_box.SetLinearVelocity(new b2Vec2( -enemy_box.GetLinearVelocity().x, enemy_box.GetLinearVelocity().y));
					enemy_box.SetPosition(new b2Vec2(enemy_box.GetPosition().x+enemy_dir/10, enemy_box.GetPosition().y));
			}
		}
		/*
		Helper function - used to convert normal pixel coordinates into Box2d coodinates (which are set at 1/scale or 1/30 right now)
		*/
		public function Con2B2D(num:Number):Number
		{
			return num / scale;
		}
		/*
		Helper function - used to convert box2d coordinates into pixels
		*/
		public function ConFromB2D(num:Number):Number
		{
			return num * scale;
		}
//NEW functions for keyinput
		/*
		 Used to store in the array "keys_down" when a key is pressed down
		 */
		 public function KeyDown(e:KeyboardEvent):void
		{
			//check if the keycode is already in our array, and return out of function if it is already there
			for (var i in keys_down)
				if (keys_down[i] == e.keyCode)
					return; //leave the function in the middle of the loop if the keycode was found
			
			//if the key wasn't in the array already, add it
			keys_down.push(e.keyCode);	
		}
		/*
		 Used to remove keycode from keys_down array
		 */
		 public function KeyUp(e:KeyboardEvent):void
		{
			//going to find where the keycode is in the array
			var pos:int = -1;
			for (var i in keys_down)
				if (keys_down[i] == e.keyCode)
				{
					pos = i;
					break;
				}
			
			//now that we have the position of the keycode, remove it
			keys_down.splice(pos,1);	
		}
		/*
		 Helper function to look through the keys_down array, and return true if the keycode is down
		 */
		 public function KeyIsDown(keycode:int):Boolean
		{
			for (var i in keys_down)
				if (keys_down[i] == keycode)
					return true;
			//if it didn't find the keycode/ already return true, return false
			return false;
		}
	}
}

Making The Box Move With The Keyboard

The first thing we want to do is add key control to move the 'hero' box.

Line 20: We need to import 'flash.ui.Keyboard' to be able to refer to keyCodes easily such as: "Keyboard.SPACE".

Line 36-46: Our new class level variables to control the hero, and to move the enemy.

Line 76-90: Setting up the initial value of the new variables

Line 96: The updated version of our "CreateBox" function with additional parameters

Line 96: The updated version of our "RunGame" function that will allow the hero and enemy to move, as well as handle collision detection

Line 251: The three helper key functions, which allow us to store when a key is up or has been released, as well as a function to check the array easily for a specific key code.

Conclusion

When you compile your program, you can move around your 'hero' character, and the enemy will "patrol" around, get killed when bounced on, and "kill" the hero and have him re-spawn when touched on the side.

It's not a perfect solution, since when checking for contact points, we are only storing one normal value for the hero, while he can actually be touching multiple objects, which will cause bugs- so it would be better to create helper functions to easily tell which objects he is colliding with, and the direction of each.

In the next section we'll make the world larger, and have the box2d world scroll with the hero, so it will look like the hero is "walking through the world", while keeping him in the same spot on the screen.

Download the source Code and please leave me any comments or feedback you have.

Other Articles in this Series

Bookmark the permalink.

2 Comments

  1. Hi Chris

    Thanks again for a really helpful post – I’d tried to implement this functionality after the first tutorial but hadn’t figured out how to make the player only jump while on the ground.

    I’d be interested in hearing how to make non-square platforms, and how to have levels wider than 1 screen, if you’re taking requests for part 3! 🙂

    • I happened to come across this post and read your comment. A common way to make a player only jump when they’re on the ground is to check that their vertical velocity is exactly 0. If it’s not then they’re in the air. If it is then there’s all but an astronomically small chance that they’re on a surface.

Leave a Reply

Your email address will not be published. Required fields are marked *