Torque 2D/GenreTutorials/PlatformerMovement

From TDN

This page is a Work In Progress.

Note: Portions of this tutorial are out of date. For a more current platformer tutorial, check out the MiniPlatformerTutorial


Contents

Introduction


In this tutorial, we will be expanding our platformer demo to include a player character with user controlled movement. Specifically, I will cover moving left and right across a surface, jumping, and controlling the player in the air. It is very simple to get basic user control with TGB, but there are several quirks and special cases that we have to take care of to get everything working smoothly. You can exclude some of these things, like ramps, from your level design, but I would rather just tackle the problems and allow levels of any style. So we will.


The Player Sprite


For this tutorial we are going to keep things simple and use a static sprite. However, a further tutorial will cover animation. To this end, we will use the image which contains all our animation frames. If you are not artistically inclined, Philip Mansfield has created some nice character art. All of the animations used in these tutorials are a part of his free mannequin man animation pack which you can get here. Here is the image that I will be using for the rest of the tutorials. If You want to use animations of your own, the concepts will still apply, but you will have to modify things slightly to fit your image.

Image:platformerPlayerAnimation.png


Right click on the image and select "Save Image As..." or something similar. Save it as player.png in the Platformer/data/images directory of this project.

In the Level Builder, click on the "Create a new ImageMap" button and select the player.png file. In the Image Builder, set the Image Mode to Cell and the cell width/height to 128. If you are having trouble seeing the individual cells, you can click on the background button and change either the background or border color.

Image:TGB_Platformer2_1.jpg
Figure 2.1

From the Static Sprites rollout, drag the player sprite into the scene view. Exactly where is up to you, try to place it as close to the ground as possible (depending on how fine your snap to grid is). As you can probably see though, our sprite is a bit too big for our level:

Image:TGB_Platformer2_2.jpg
Figure 2.1

Either by dragging the handles on the sprite or in the Edit Tab, reduce the size of the sprite to your liking. Again, this is a design decision: the spacing of tiles/platforms and the relative size of the player sprite and it's eventual jumping ability all play a factor. In this example, the size has been reduced slightly from 16x16 to 12x12.

Image:TGB_Platformer2_3.jpg
Figure 2.3

We're not quite done yet. There's a few more properties to edit on our player sprite. In the scripting rollout, enter player as the class. In the Collision rollout, check the send/receive boxes and the collision callback. And in the physics rollout, set the constant force in Y at 100.

Image:TGB_Platformer2_4.jpg Image:TGB_Platformer2_5.jpg
Figure 2.4Figure 2.5


Also, select the tile layer again. We need to turn collisions on for this as well, so go to the collision rollout, check receive collision and the callback, and turn off the send/receive physics as well.

Image:TGB_Platformer2_6.jpg
Figure 2.6

With that done, save the level and exit the Level Builder. It's time to script.


The ActionMap


To capture user input, Torque uses the concept of an action map. Basically, you, as the programmer, tell the engine which keys you want to respond to and the engine tells you when the state of those keys is toggled. The default action map called moveMap is located in the main.cs file in your project folder. It does not have any bound keys though. We will define our movement keys in our player's onLevelLoaded function. To do this, first create a new script file called player.cs in your Platformer/gameScripts directory and exec it by adding this "exec("./player.cs");" to the startGame() function in game.cs:

function startGame(%level)
{
   exec("./player.cs");
   
   // Set The GUI.
   Canvas.setContent(mainScreenGui);
   Canvas.setCursor(DefaultCursor);
   
   moveMap.push();
   
   if( isFile( %level ) || isFile( %level @ ".dso"))
      sceneWindow2D.loadLevel(%level);
}

Now that the engine knows about our new file, we can write some code. In the player.cs file, create the player::onLevelLoaded function. Here are the binding functions:

   // Player movement functions
   moveMap.bindCmd(keyboard, "left", "playerLeft();", "playerLeftStop();");
   moveMap.bindCmd(keyboard, "right", "playerRight();", "playerRightStop();");
   moveMap.bindCmd(keyboard, "space", "playerJump();", "");

There are a couple of other things we will want to have in the onLevelLoaded function, namely a global variable reference for our player and a way to recgonize the value of gravity in the Level Builder. This way whenever we want to test different gravity values we do not have to change anything in script. Here is the complete onLevelLoaded function:

function player::onLevelLoaded(%this, %scenegraph)
{
   // Define the player global variable
   $player = %this;
   
   // Get the value of gravity from the Level Builder
   %force = $player.getConstantForce();
   $gravity = getWord(%force, 1);
   
   // Player movement functions
   moveMap.bindCmd(keyboard, "left", "playerLeft();", "playerLeftStop();");
   moveMap.bindCmd(keyboard, "right", "playerRight();", "playerRightStop();");
   moveMap.bindCmd(keyboard, "space", "playerJump();", "");
}



Using the first line as an example, the bind method is telling the engine to call the function "playerLeft" whenever the left arrow is pressed and "playerLeftStop" when it is released. As you can see, we have also defined key bindings for moving right and jumping.

Physics and Collision in the Context of a Platformer


The physics requirements of a 2D platformer are not very complex and often times not specifically realistic. In many cases, if we tried to apply real world physics, gameplay would suffer. For instance, you will most likely want your character to be able to jump well over its own height. And, applying a strict 9.81 meter/second^2 falling acceleration may not ‘feel’ as good as a speed twice that or half that. Many liberties can be taken, and should be taken, if it improves the gameplay. Usually, experimentation is necessary to get the exact feel you are looking for.

What we will require is a main character that is bound by gravity, able to walk along a surface, jump, and collide with the ground, walls, and obstacles. The ground and walls just need to sit and look pretty.

Thanks to TGB and the Level Builder, it is pretty much just as simple to implement these few rules as it was to lay them out. All of the complicated stuff is handled internally by the engine. We just tell it what to do.


Basic Movement (WIP)



Note: This section and all of the following sections and tutorials are from an older version of TGB/T2D. Names of functions and script files may be different than what was used at the beginning of the tutorial. You can try to figure things out yourself, however be aware for new users, many have struggled with getting things to work. If you get things to work and wish to update this tutorial, please do!

Just getting the player to move left and right when we press the left and right arrows is extremely easy. We would just need to call setLinearVelocityX on the player with a negative number for left, a positive number for right, and 0 when we let go of the key.

But then what would happen if the user let go of right while still pressing left? The player would stop. What we would want it to do is start moving left. This is just one of many scenarios that need to be solved. Luckily, it is very simple. Instead of responding based on what key was just pressed, we need to respond based on what keys are currently pressed.

So lets create the functions that will map keys pressed to player movements. We'll do that using the global variable "$player". We will create atributes like "moveLeft" and "moveRight", and later, will read this to discover what keys where pressed.

OBS: global variables on Torque Script are donoted with a "$" in front of them.

In player.cs, add these functions:

function playerLeft(){
   $player.moveLeft = 1;//true
}

function playerLeftStop()
{
    $player.moveLeft = 0;//false
}

function playerRight()
{
     $player.moveRight = 1;//true
}

function playerRightStop()
{
     $player.moveRight = 0;//false
}

function playerJump()
{
     //echo("nothing here yet!");
}

With this code, every time the left or right arrow keys are pressed or released, the corresponding variable will be updated. Note that we are using numbers instead of "trues" and "falses". This have its reasons, but for now just accept it ;).


But before we can use these variables to move the player, we need to set up a couple of other things.

First, we are going to give the player some attributes. Add this to the end of the createPlayer function in player.cs:

   $player.runSpeed = 40;     //speed of the player
   $player.airSpeed = 16;     //speed it can move when in the air
   $player.jumpHeight = 10;   //height it can jump
   $player.maxRunSurfaceAngle = 35; //maximum steepness of a ramp that that the player can run on

OBS: aside from maxRunSurfaceAngle, which is an angle in degrees, these don’t have units. You can just mess with the numbers until they feel right.

Now, your code does move the player. But as we will be using a y force on our player, you need to set up a "Collision Group" for your player and for your "platform", so that when there is a collision, we can discover who is coliding with who.
Click on your player and go to "Edit" tab. In "Scene Object" you will find a "Group" field.
Select number "1" for your player, and "2" for the platforms (click on your Tile Map, and modify it's group field).
Update your "onLevelLoaded" function with this two lines:

   $platformGroup = 2; //I'm doing this because i don't know how to get a Tile Map GraphGroup :-.
   $playerGroup = $player.getGraphGroup();


Then, open up player.cs and add this function:

function t2dSceneObject::onCollision(%srcObj, %dstObj, %srcRef,
   %dstRef, %time, %normal, %contactCount, %contacts)
{

   switch (%srcObj.getGraphGroup())
   {
      case $playerGroup:
         //the source of the collision is the player
         switch (%dstObj.getGraphGroup())
         {
            //the destiny is the plataform
            case $platformGroup:
               collidePlayerPlatform(%normal);
         }
   }
}

This is the collision callback function called by the engine every time a collision occurs in the scene. For a description of all the parameters, check the T2D Reference PDF that comes with the engine. The ones we are using are %srcObj, %dstObj, and %normal. %srcObj is the object doing the colliding and %dstObj is the object being collided with. %normal is the vector perpendicular to the surface of collision. Basically, it tells us the angle that the surface is facing.

The first thing we do in this function is figure out which objects are involved in the collision. For this, we use the collision group that we put the object in when we created it. What we are saying here is, if %srcObj is in the group $playerGroup and %dstObj is in the group $platformGroup, call the function collidePlayerPlatform. Or, more simply, if the player is colliding with a surface, call the function collidePlayerPlatform. But we have no function called collidePlayerPlatform. Thus, we must define. In player.cs, add the function:

function collidePlayerPlatform(%normal)
{
   %move = $player.moveRight - $player.moveLeft;
   $player.setLinearVelocityX(%move * $player.runSpeed);
}

Finally, we moved the player. Run the game now and you should be able to move the GarageGames logo along the surfaces of your tile map. The only thing we have left to do is defining which surfaces the player can walk up, and which surfaces it will slide down. Remember the maxRunSurfaceAngle variable we set up earlier. It’s his time to shine. Add this function to game.cs:

function isRunSurface(%normal, %maxAngle)
{
   if (getWord(%normal, 1) > 0)
      return 0;
   
   %angle = mRadToDeg(mAsin(getWord(%normal, 0)));
   
   if (mAbs(%angle) < %maxAngle)
      return 1;
      
   return 0;
}

The purpose of this function is to determine whether or not a surface, defined by %normal, is not so steep that the player can’t run on it. First we check if the y component of the normal is positive. If it is, the surface is somewhere between a ceiling and a wall, so it is definitely not walkable. The next line finds the steepness angle of the surface using a bit of trig. If that angle is less than the maximum angle, we can walk on the surface, otherwise we can’t.

And the final step. Change the collidePlayerPlatform function in player.cs to look like this:

 
function collidePlayerPlatform(%normal)
{
   %move = $player.moveRight - $player.moveLeft;
   $runSurface = isRunSurface(%normal, $player.maxRunSurfaceAngle);
   if ($runSurface > 0)
   {
      $player.setLinearVelocityX(%move * $player.runSpeed);
   }
}

Now, we are only updating the player’s velocity if the surface we are on is not too steep. Try it out. If you have both a 30 degree ramp and a 45 degree ramp visible in your tile map, you should find that the player can run up the 30 degree one, but not the 45 degree one. If you can’t see that part of your tile map, you can either change the position of it by changing the line "%layer.setPosition("0 0");" in the createLevel function in game.cs, or you can wait until we hook up the camera in the next tutorial.




Jumping (WIP)


Adding jumping is very simple when we don’t have to deal with animations. Those are later, so for now this is going to be easy. The only thing we have to check for is that the player is on the ground so you can’t jump while in mid air. First, we need to store when the jump button (in our case, the spacebar) is pressed. In actionMap.cs, add this to the jump function:

$jump = %val;

Now, in player.cs, add this function:

function playerJump()
{
   $player.setLinearVelocityY(-10 * $player.jumpHeight);
}

Before we can call this jump function, we need a place to call it from. For this, we need to use the onUpdateScene callback provided by the engine. Add this to game.cs:

function t2dSceneGraph::onUpdateScene(%this)
{
   if (%this != t2dscene.getID())
      return;

   updatePlayer();
   $runSurface = -1;
}

First we make sure that the scenegraph that this function was called on (%this) is the scenegraph of our game. We have to do this because the editors all have scenegraphs that call this function, and we don’t care about them. Next we call updatePlayer, which we will define in a second. And finally, we set $runSurface to -1. $runSurface stores whether or not the player is currently on a run surface. Meaning, it is not in mid air. We set $runSurface to 1 or 0 when the player collides with a surface, but there is no callback that tells us when the player stops colliding with a surface. So, we set it to false here, which is called every frame, and rely on the fact that if the player is actually on a run surface, the onCollision callback will update the variable before we need it again.

updatePlayer. Add this to player.cs:

 
function updatePlayer()
{
   if ($runSurface > 0)
   {
      if ($jump)
         playerJump();
   }
}

This should all make pretty good sense. If the player is on a surface, and the jump button was pressed, then jump.

If you run the game, you should be able to press the space bar to make your character jump. Which brings us to the last topic of movement: controlling the player while it’s in the air.

It's worthy to note that when you execute your game at this point and enter the level editor you'll get a warning in the console that says something similar to:

T2D/gameScripts/game.cs (55): Unable to find object: 't2dScene' attempting to call function 'getId'

This is normal. When you launch the game the warning goes away because you moved from the level builder scene to the game scene. If you plan on spending some time in the level builder you may want to comment out those lines in game.cs so the warnings do not bog down your console - I imagine they can slow down your machine after a while! (remember to uncomment those lines if you run the game again or else you'll find yourself having animation problems later on).





Air Control (WIP)


Adding air control is just like adding ground control, except we apply the velocity change only when the player is not on the ground instead of only when it is.

Add this to the beginning of the updatePlayer function in player.cs:

 
   %move = $moveRight - $moveLeft;

And this to the end of it:

   else if ($runSurface < 0)
   {
      %speed = $player.getLinearVelocityX();
      
      if (%move)
      {
         if (((%speed * %move) <= 0) || (mAbs(%speed) < $player.airSpeed))
            $player.setLinearVelocityX(%move * $player.airSpeed);
      }
   }

We are first checking to see if we are in the air, then checking if the player is trying to move, and finally checking if the desired direction of movement is different from the player’s current direction. If all of these things are true, then we update the player’s speed. The reason for all of this is we want to maintain the momentum of the player when it first left the ground. Test it, it works.

When you execute T2D and look at the console you will receive a warning that says something similar to:

T2D/gameScripts/player.cs (63): Unable to find object: attempting to call function 'getLinearVelocityX'

Again, this is normal. When you launch the game (and get the player into the scene) the warning will go away. It is also worthy of note that this code will allow you to move onto and land on a ledge connected to a wall you are touching. Without this code you will not be able to jump over a wall if you are touching it - you will have to back up and time your jump accordingly.

Your entire updatePlayer function should now look like this:

function updatePlayer()
{
   %move = $moveRight - $moveLeft;
   if ($runSurface > 0)
   {
      if ($jump)
         playerJump();
   }
   else if ($runSurface < 0)
   {
      %speed = $player.getLinearVelocityX();
      
      if (%move)
      {
         if (((%speed * %move) <= 0) || (mAbs(%speed) < $player.airSpeed))
            $player.setLinearVelocityX(%move * $player.airSpeed);
      }
   }	
}

You should be able to experiment more with the player's movement once we get a camera system in place.