TGB/Tutorials/Platformer/Player

From TDN


Platformer Mechanics Tutorial

Written for TGB Version: 1.7.2


Creating a Player

This section covers setting up a player character through the use of Torquescript.


In the Editor


To start off, we need to have a player object in the scene we created in the previous section. From the "Animated Sprites" rollout in the "Create" section of right sidebar, drag the thumbnail that represents the "PlayerStandAnimation" out onto the level. The exact position of the animated sprite is not too important in Y Direction (Figure 2.1), once gravity is turned on the player will drop to the floor when the scene is loaded.

Figure 2.1
Figure 2.1


Next hit "Edit" on the right sidebar to change some of the animation's properties. We are going to be looking at the "Scripting" rollout, we want to make this "playerStand" animation the main object that represents our player in script. Set the Class to "playerClass" and the Name to "player" as show in (Figure 2.2).

Figure 2.2
Figure 2.2


Now we need to turn on some collision detection for our player object. To do this edit the settings of the "Collision" rollout so that the properties look like those in (Figure 2.3).

Figure 2.3
Figure 2.3


Now it is time to edit the collision bounds. To do this, hover the mouse over the player sprite and a bunch of tooltips will appear. Click on the first one on the left side (Figure 2.4).

Figure 2.4
Figure 2.4


Change the collision bounds to look like the figure below (Figure 2.5).

Figure 2.5


Save the changes to the collision bounds and open up the Physics rollout for the player object. Check the gravitic box and change the Force Scale to 4 (Figure 2.6).

Figure 2.6
Figure 2.6


With that done we are now ready to begin scripting the movement for our player object.

Keyboard Input


It's time to write a script that will control the actions of our player. In your ./games/MiniPlatformer/gameScripts folder create a file named playerClass.cs. In order for TGB to recognize this new file, we need to add an exec statement to the existing script file game.cs. Change the startGame function to look like the following:

function startGame(%level)
{
   exec("./playerClass.cs");
   
   Canvas.setContent(mainScreenGui);
   Canvas.setCursor(DefaultCursor);
   
   new ActionMap(moveMap);   
   moveMap.push();
   
   $enableDirectInput = true;
   activateDirectInput();
   enableJoystick();
   
   sceneWindow2D.loadLevel(%level);
}


Alternately, you could also create the playerClass.cs file in your project's behaviors folder. Although the file will not be a behavior, TGB automatically compiles all scripts in this folder every time the play button is clicked in the editor and there is no need to add an exec statement like above.

Open the playerClass.cs file with a text editor and add the following function at the beginning of the file.

function playerClass::onLevelLoaded(%this, %scenegraph)
{
     $player = %this;
      
      moveMap.bindCmd(keyboard, "left", "playerLeft();", "playerLeftStop();");
      moveMap.bindCmd(keyboard, "right", "playerRight();", "playerRightStop();");
      moveMap.bindCmd(keyboard, "space", "playerJump();", "");
      
      %this.enableUpdateCallback();
}


The playerClass::onLevelLoaded() function is called whenever an object with a class type of "playerClass" is loaded into the scene. If you recall when we were editing the Scripting properties of our player we set the Class field to "playerClass". That means when TGB loads our level with our player animation it will call this function.

The first line of the function stores a reference to the object being loaded to a global variable named $player that we can use to reference the player in other functions.

Next you can see that we are binding the left and right arrow keys as well as the spacebar, when these keys are pressed or released they call their corresponding functions. For example, when the the left arrow key is pressed, the "playerLeft()" function will be called, when it is released the "playerLeftStop()" function will be called. We can use these functions to track what the player should be doing.

Let's add those key handler functions now.

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

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

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

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

function playerJump()
{
     if(!$player.airborne)
     {
          $player.setLinearVelocityY(-150);
          $player.airborne = true;
     }
}


Here we are using that $player variable we initialized in playerClass::onLevelLoaded() to store the movement state of the player. Just by using the .moveRight and .moveLeft members on the $player object, we are adding them as playerClass dynamic variables, so that we can use them later.

To handle jumping, any time you press the jump button a new boolean variable, "airborne" is checked. If "airborne" is false (you are on the ground), the player will be given a considerable Y velocity straight up. (Notice that negative Y values in Torque move upwards on the screen.) The "airborne" variable is then set to true as we have just jumped.

Note: We are using a global variable, $player, to communicate to our playerClass object from the functions bound to the keyboard (playerLeft(), playerLeftStop(), etc.). This means that there can be only one playerClass object in existence. If we wanted to have a game that could include more than one such object, we would have to do this part differently.

Player Movement


Great! Now we are tracking the keystates. But that doesn't really get us anywhere in terms of a game. We need our player to respond to those keystates and actually MOVE!

Let's add a movement handler function to take care of those keystates.

function playerClass::updateHorizontal(%this)
{
   if (%this.moveLeft == %this.moveRight)
   {
      %this.setLinearVelocityX(0);
      return;
   }
   
   if (%this.moveLeft)
   {
      if (!%this.againstLeftWall)
      {
         %this.againstRightWall = false;
         %this.setLinearVelocityX(-30);
      }
   }

   if (%this.moveRight)
   {
      if (!%this.againstRightWall)
      {
         %this.againstLeftWall = false;
         %this.setLinearVelocityX(30);
      }
   }
}


You'll notice our function is named with the "playerClass::" namespace identifier. This means that this function will work with any object who's class variable is set to "playerClass". What this function does is check the state of the moveLeft and moveRight class members and assigns a linear velocity to the object if it finds either to be true. If the left and right arrow keys are both pressed, or both not pressed — that is, if they have the same state — we want to be sure to stop the player from moving. To do this we set the x velocity to 0.

OK, horizontal movement is covered - what about vertical? Let's create the next function. Add the following:

function playerClass::updateVertical(%this)
{
   %yVelocity = %this.getLinearVelocityY();
   
   %this.setLinearVelocityY(5);
   %collision = %this.castCollision(0.005);
   
   %normalX = getWord(%collision, 4);
   %normalY = getWord(%collision, 5);
   
   // no collision
   if (%collision $= "")
   {
      %this.airborne = true;
      %this.setConstantForceY(100);
      %this.setLinearVelocityY(%yVelocity);
      return;
   }
   
   // collides with wall to the left
   if (%normalX == 1 && %normalY == 0)
   {
      %this.againstLeftWall = true;
      %this.setLinearVelocityX(0);
      %this.setLinearVelocityY(%yVelocity);
      return;
   }
   
   // collides with wall to the right
   if (%normalX == -1 && %normalY == 0)
   {
      %this.againstRightWall = true;
      %this.setLinearVelocityX(0);
      %this.setLinearVelocityY(%yVelocity);
      return;
   }
   
   // on ground with no wall collisions
   if (%normalX == 0 && %normalY == -1)
   {
      %this.airborne = false;
      %this.againstLeftWall = false;
      %this.againstRightWall = false;
      %this.setConstantForceY(0);
      %this.setLinearVelocityY(%yVelocity);
      return;
   }
   
   // in air and hits platform with head
   if (%normalY == 1)
   {
      %this.airborne = true;
      %this.setLinearVelocityX(0);
      %this.setConstantForceY(100);
      %this.setLinearVelocityY(%yVelocity);
      return;
   }
   
   // in case another type of collision normal was detected
   error("another collison type" SPC %normalX SPC %normalY);
   %this.airborne = false;
   %this.againstLeftWall = false;
   %this.againstRightWall = false;
   %this.setLinearVelocityY(%yVelocity);
}


A lot of things are happening here. First, we get the current velocity in the Y direction. Next, we override that velocity and use the castCollision function. What does castCollision do? It will check for collisions in the future based on an amount of time we specify. Here we are telling TGB to find any collisions that would occur 0.005 seconds into the future with our object travelling in the Y direction at 10 units per second and our X direction velocity determined by the current player movement.

After this, we grab the normals from that future collision and compare them to a bunch of predefined collision types. If you remember how we set up the player collision bounds, the normals would then correspond like this (Figure 2.7):

Figure 2.7
Figure 2.7


This way, when the player hits the floor - we can do things like turn off gravity and set airborne to false so we can jump again.

For the final step, we want to continuously adjust the direction in which our player is moving based on what state the keys are in. To do this we need to call the object's onUpdate() function. The onUpdate function is referred to as a "Callback" and called every tick (32 ms). Using many objects that have onUpdate callbacks can reduce performance on lower end computers, so this callback has to be enabled on a per object basis. If you look at the playerClass::onLevelLoaded function, you will see we have already included this function. Now add the following to your playerClass.cs file.

function playerClass::onUpdate(%this)
{
   %this.updateHorizontal();
   %this.updateVertical();
   %this.setCurrentAnimation();
}


Every 32 ms we are calling the updateHorizontal and updateVertical functions. We are also updating the animation through setCurrentAnimation(). Where is that function you ask? Let's add it in the next section.

Animating the Player


It would look rather unbelieveable if the player moves and jumps looking like it does now with just the "stand" animation. We need a way to detect what type of movement (horizontal/vertical) is happening every moment and update the player animation state accordingly. We have already added a call to a setCurrentAnimation in our onUpdate callback, so let's add this now:

function playerClass::setCurrentAnimation(%this)
{
   %xVelocity = %this.getLinearVelocityX();
   %yVelocity = %this.getLinearVelocityY();

   if (%xVelocity > 0)
   {
      %this.setFlip(false, false);
   }
   else if (%xVelocity < 0)
   {
      %this.setFlip(true, false);
   }
   
   if (%this.airborne)
   {
      if(%yVelocity < 0)
      {
         %this.playAnimation(PlayerJumpUpAnimation);
         return;
      }else
      {
         %this.playAnimation(PlayerJumpDownAnimation);
         return;
      }
   }

   if ($player.moveLeft == true || $player.moveRight == true)
   {
      if (%this.getAnimationName() $= "playerRunAnimation")
      {
         if(%this.getIsAnimationFinished())
         {
            %this.playAnimation(PlayerRunAnimation);
            return;
         }
      }else
      {
         %this.playAnimation(PlayerRunAnimation);
      }
   }else
   {
      %this.playAnimation(PlayerStandAnimation);
   }
}


First, we find out our X and Y velocities. Then, setFlip flips the orientation of our player object in the X and Y directions. When we are moving to the left we want to flip the art since all our art is facing right. When we move to the right we want to undo that flip so we call setFlip again with false to undo it.

Next, we check to see if we are jumping. If we are, we set the appropriate animation if we are jumping up or falling down. The last section of the function checks if we are moving left or right. If yes, then we set the run animation. If we are already running, we wait for that animation to finish before playing it again. Obviously, if we are not moving left or right then we must be standing still.

That covers the basics for setting up the player. Make sure you have saved playerClass.cs (and game.cs if you used the exec function). Open up the TGB Editor and try your level out. If everything has been correctly done, you can now move around and jump.

Player Control via a Behavior


Work in progress behavior to replace the movement and animation code found in this section:

//-----------------------------------------------------------------------------
// Torque Game Builder
// Copyright (C) GarageGames.com, Inc.
// WIP Behavior by Mike Lilligreen
//-----------------------------------------------------------------------------

if (!isObject(PlatformerControlsBehavior))
{
   %template = new BehaviorTemplate(PlatformerControlsBehavior);
   
   %template.friendlyName = "Platformer Controls";
   %template.behaviorType = "Input";
   %template.description  = "Platformer style movement control";
   
   %template.addBehaviorField(leftKey, "Key to bind to left movement", keybind, "keyboard left");
   %template.addBehaviorField(rightKey, "Key to bind to right movement", keybind, "keyboard right");
   %template.addBehaviorField(jumpKey, "Key to bind to jump movement", keybind, "keyboard space");
   
   %template.addBehaviorField(horizontalSpeed, "Speed when moving horizontally", float, 20.0);
   %template.addBehaviorField(jumpSpeed, "Speed when jumping", float, -150.0);
   %template.addBehaviorField(gravity, "Force of gravity", float, 100.0);
}

function PlatformerControlsBehavior::onBehaviorAdd(%this)
{
   if (!isObject(moveMap))
      return;
   
   moveMap.bindObj(getWord(%this.leftKey, 0), getWord(%this.leftKey, 1), "moveLeft", %this);
   moveMap.bindObj(getWord(%this.rightKey, 0), getWord(%this.rightKey, 1), "moveRight", %this);
   moveMap.bindObj(getWord(%this.jumpKey, 0), getWord(%this.jumpKey, 1), "jumpAction", %this);
   
   %this.left = 0;
   %this.right = 0;
   %this.jump = 0;
}

function PlatformerControlsBehavior::onBehaviorRemove(%this)
{
   if (!isObject(moveMap))
      return;
   
   moveMap.unbindObj(getWord(%this.leftKey, 0), getWord(%this.leftKey, 1), %this);
   moveMap.unbindObj(getWord(%this.rightKey, 0), getWord(%this.rightKey, 1), %this);
   moveMap.unbindObj(getWord(%this.jumpKey, 0), getWord(%this.jumpKey, 1), %this);
   
   %this.left = 0;
   %this.right = 0;
   %this.jump = 0;
}

function PlatformerControlsBehavior::moveLeft(%this, %val)
{
   %this.left = %val;
   %this.updateMovementX();
   %this.updateAnimation();
}

function PlatformerControlsBehavior::moveRight(%this, %val)
{
   %this.right = %val;
   %this.updateMovementX();
   %this.updateAnimation();
}

function PlatformerControlsBehavior::jumpAction(%this, %val)
{
   %this.jump = %val;
   %gravity = %this.owner.getConstantForceY();
   
   if (!%gravity)
      %this.updateMovementY();
   
   %this.updateAnimation();
}

function PlatformerControlsBehavior::updateMovementX(%this)
{
   %this.owner.setLinearVelocityX((%this.right - %this.left) * %this.horizontalSpeed);
   
   // Here we get a point just below the player object
   %positionX = %this.owner.getPositionX();
   %positionY = %this.owner.getPositionY();
   %sizeY = %this.owner.getSizeY() / 2;
   %pointY = %positionY + %sizeY + 1;
   %point = %positionX SPC %pointY;
   
   // Get the scene graph and then use pick point to find out if a platform is
   // below the player. If there is no platform below the player, gravity is
   // turned on. Note that platforms are all in group 1, objects in any other
   // group are ignored.
   %sceneGraph = sceneWindow2D.getSceneGraph();
   %object = %sceneGraph.pickPoint(%point, bit(1));
   
   if (!isObject(%object))
      %this.owner.setConstantForceY(%this.gravity);

   if (%this.right || %this.left)
      %this.schedule(100, "updateMovementX");
}

function PlatformerControlsBehavior::updateMovementY(%this)
{
   %this.owner.setLinearVelocityY(%this.jump * %this.jumpSpeed);
   
   if (%this.jump)
   {
      %this.owner.setConstantForceY(%this.gravity);
   }
}

function PlatformerControlsBehavior::updateAnimation(%this)
{
   %xVelocity = %this.owner.getLinearVelocityX();
   %yVelocity = %this.owner.getLinearVelocityY();
   
   if (%xVelocity > 0)
   {
      %this.owner.setFlip(false, false);
   }
   else if (%xVelocity < 0)
   {
      %this.owner.setFlip(true, false);
   }
   
   if(!%yVelocity == 0)
   {
      //%this.owner.playAnimation(PlayerJumpUpAnimation);
      //return;
   }
   
   if (!%xVelocity == 0)
   {
      if (%this.owner.getAnimationName() $= "playerRunAnimation")
      {
         if(%this.owner.getIsAnimationFinished())
         {
            %this.owner.playAnimation(PlayerRunAnimation);
            return;
         }
      }else
      {
         %this.owner.playAnimation(PlayerRunAnimation);
      }
   }else
   {
      %this.owner.playAnimation(PlayerStandAnimation);
   }
}

function PlatformerControlsBehavior::onCollision(%srcObject, %dstObject, %srcRef, %dstRef, %time, %normal, %contacts, %points)
{
   // Only turn off gravity if the player's "feet" collide with the ground
   %normalY = getWord(%normal, 1);
   
   if (%normalY == -1)
   {
      // turn gravity off
      %srcObject.owner.setConstantForceY(0);
      
      // get the location of the top surface of the platform
      %platformPosY = %dstObject.getPositionY();
      %platformSizeY = %dstObject.getSizeY() / 2;
      %topSurface = %platformPosY - %platformSizeY;
      
      // get the size of the player to place the feet on the top surface of the platform properly
      %sizeY = %srcObject.owner.getSizeY() / 2;
      %srcObject.owner.setPositionY(%topSurface - %sizeY);
      
      // have the player inherit the velocity of the platform
      %platformVelocity = %dstObject.getLinearVelocityY();
      %srcObject.owner.setLinearVelocityY(%platformVelocity);
   }
      
   %srcObject.updateAnimation();
}


The next section covers camera control for platformers: Click here to continue.

Return to Platformer Mechanics Tutorial Hub