Torque 2D/GenreTutorials/PlatformerWinning

From TDN

Contents

Introduction


Winning in platformers can take many forms. Some games have you beating bosses, some have you just getting to certain places, and some don’t really have defined levels but are instead a series of mazelike areas. Losing conditions are generally either getting hit by enemies or falling in a pit of some sort. In this tutorial, we will be creating a trigger system that will allow you to end a level when the player reaches a certain location. We will also implement collision between the enemies and player so the player can kill and be killed.





Being Defeated by Enemies


Allowing the player to be hit by enemies is a pretty straight forward task. There are a couple of design decisions to be made, such as what happens to the player when it gets hit, or how many hits it can take before dying, but these are fairly minor. We will be giving the player an amount of life so it takes more than one hit to kill it. You can specify the exact number yourself.

The first thing to do is enable collisions. In player.cs, delete the collision masks in createPlayer(); and add the following collision masks to resetPlayer();

   $player.setCollisionMasks(BIT($platformGroup) | BIT($enemyGroup),
                             BIT($platformLayer) | BIT($enemyLayer));

Now the engine will consider the player and enemies in its collision detection. We also need to route the callback to a function we can define later. Add this after the “$platformGroup” case in the “$playerGroup” case of the onCollision callback in game.cs:

            case $enemyGroup:
               collidePlayerEnemy(%dstObj, %normal);

There are three more attributes we need to add to the player. Put this in the createPlayer function:

   $player.lives = 5;

And these in the resetPlayer function:

   $player.life = 100;
   $player.active = true;

Lives is, obviously, the number of lives the player has. The life variable stores the player’s life. You can change these values to whatever you want. The active variable will be used to keep the user from being able to control the player when it is dead or during a short time after it is hit. Also add this to the resetPlayer function:

   $player.setLinearVelocity("0 0");

This is just here to make sure velocity from a previous life won’t be carried over to this one. Now we need to code what happens when a collision occurs. Add this function to player.cs:

function collidePlayerEnemy(%enemy, %normal)
{
   if (!$player.active)
      return;
   
   %impulse = t2dVectorAdd(%normal, "0 -1");
   $player.schedule(0, "setImpulseForce", t2dVectorScale(%impulse, 100));
   $player.active = false;
   
   playerHit(10);
   
   if ($player.life <= 0)
      playerDie();
   else
      setPlayerState($playerHitState);
}

When the player gets hit, we are going to send it away from the enemy using an impulse. The first line is setting the impulse vector we want to use. We are using the normal, so the player is guaranteed to be moving away from the enemy, and skewing it upwards a little bit so the player will be lifted off the ground. Just an aesthetic touch. The next line applies the impulse and increases the magnitude slightly. This is being scheduled to happen at the beginning of the next frame because the engine doesn’t like applying impulse forces during collision callbacks. You can change the 100 to increase or decrease the amount the player gets knocked away. The next line sets the player inactive. This doesn’t do anything yet, but it will in a second. Then, we subtract some life from the player with this playerHit function:

function playerHit(%damage)
{
   $player.life -= %damage;
   
   if ($player.life < 0)
      $player.life = 0;
}

Next we check if the player has any life left. If it does, we just change its state to the hit state. Otherwise, it dies with this function:

function playerDie()
{
   if ($player.state == $playerDeadState)
      return;
      
   $player.Lives--; 
   $player.setCollisionMasks(BIT($platformGroup), BIT($platformLayer));
   setPlayerState($playerDeadState);
   
   if ($player.Lives < 0)
      echo("Game Over");
   else
      schedule(1000, 0, "resetPlayer", $spawnPoint);
}

First we make sure the player isn’t already dead. Then we subtract a life, change the collision masks so enemies no longer collide with it, and change its state to the dead state. If the player has no lives left, nothing happens yet, but it will when we add guis in the next tutorial. If it does have lives left, we schedule a respawn to happen in 1 second.

We just introduced two new states to the player which means we need to add to our state machine and create two more animations. The image you downloaded as a part of the animation tutorial already has the two animations in it, but we still need to create the datablocks. Here they are, to be added to datablocks.cs:

new t2dAnimationDatablock(playerHitAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "42 43 44 43 42";
   animationTime = 0.3;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerDeadAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "42 43 44 45 46 47 48 49 50 51";
   animationTime = 0.6;
   animationCycle = false;
   randomStart = false;
};

Adding new states merely involves two more state definitions in player.cs:

$playerHitState = new ScriptObject() { animation = playerHitAnimation; };
$playerDeadState = new ScriptObject() { animation = playerDeadAnimation; };

And a small addition to the switch statement in the updatePlayerAnimation function:

      case $playerHitState:
         if ($player.getIsAnimationFinished())
         {
            $player.active = true;
            setPlayerState($playerFallState);
         }

When the player gets hit, it waits until the “getting hit” animation is done, then gets reactivated and enters the fall state. We don’t have an entry for the playerDeadState here because the state is automatically reset during the resetPlayer function.

Lastly, we need to make use of the $player.active variable to keep the player from moving when it’s dead. Right after the call to updatePlayerAnimation in the updatePlayer function, add this:

   if (!$player.active)
      return;

And change the collidePlayerPlatform function to this:

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

Now the player will only move if it’s active.

Now to tie up the loose ends we created with that last code block. We need a playerDieAnimation datablock, a resetPlayer function, a setPlayerActive function, and to use the active flag to deactivate player movement. Here is the die animation datablock which should be put in datablocks.cs:

new t2dAnimationDatablock(playerDieAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "24 25 26 27 28 29 30 31 32 33";
   animationTime = 0.75;
   animationCycle = false;
   randomStart = false;
};

Here are our two new functions. Put them in player.cs:






Defeating Enemies


To defeat enemies, we are going to have the player jump on their heads like in the Mario Brothers games. There are many other options you have, such as giving the player a weapon, but that is a more common feature of shooters instead of platformers. Should you choose to give your player a weapon, you can learn how from the shooter tutorial.

Since we set up most of the collision previously, there is really not much to this. All we have to do is check if the player is on top of the enemy when they collide. If so, the enemy gets hurt. Otherwise the player gets hurt. Change the collidePlayerEnemy function in player.cs to this:

function collidePlayerEnemy(%enemy, %normal)
{
   if (!$player.active)
      return;
      
   if (getWord(%normal, 1) < -0.5)
      killEnemy(%enemy);
   else
   {
      %impulse = t2dVectorAdd(%normal, "0 -1.0");
      $player.schedule(0, "setImpulseForce", t2dVectorScale(%impulse, 100)); 
      $player.active = false;
      
      playerHit(10);
      
      if ($player.life <= 0)
         playerDie();
      else
         setPlayerState($playerHitState);
   }
}

All we did here was add a check to see if the player is above the enemy. We grabbed the y component of the normal, and if it is less than -0.5, then the enemy should get hurt. Otherwise we do exactly what we did before for hurting the player. Add this killEnemy function to enemy.cs:

function killEnemy(%enemy)
{
   %enemy.setCollisionSuppress(true);
   %enemy.setConstantForce("0 0");
   scaleEnemy(%enemy);
}

First we disable collision, then we stop the enemy’s movement, then we call scaleEnemy. scaleEnemy is just an aesthetic that will shrink the height of the enemy until it’s 0, at which point the enemy will be removed from the scene. Here is the scaleEnemy function to be added to enemy.cs:

function scaleEnemy(%enemy)
{
   if (%enemy.getHeight() > 1)
   {
      %enemy.setHeight(%enemy.getHeight() - 2);
      %enemy.setPositionY(%enemy.getPositionY() + 1);
      schedule(20, %enemy, "scaleEnemy", %enemy);
   }
   else
   {
      %enemy.safeDelete();
   }
}

As long as the height of the enemy is greater than 1, we shrink it more. When it falls below 1, we delete it from the scene. If you wanted to check if all of the enemies in the level were killed, you could do it in the else section of this function with something like this:

      if (EnemyGroup.getCount() < 1)
         // No enemies left.

Using that, you could end a level when all the enemies are defeated (with the help of the level progression tutorial), spawn more enemies with the createEnemy function, or anything else your game design requires.





Triggers


Triggers are a genre agnostic construct that are useful in pretty much any game. They take up an area in the level, and perform an action when the player enters or leaves them. The system I am implementing here can be used in any other game and has no platformer specific dependencies. The reason we need them is so we can have a destination for the player to reach that triggers the end of a level. Most of the code will be in a new file called trigger.cs. Create that now and exec it from initializeT2D.cs:

   exec("./trigger.cs");

Now add this to the new file:

new SimGroup(TriggerCheckGroup);

function createTrigger(%pos, %size, %enter, %stay, %leave)
{
   %trigger = new t2dSceneObject()
   {
      scenegraph = t2dscene;
   };
   %trigger.setPosition(%pos);
   %trigger.setSize(%size);
   %trigger.setGraphGroup($triggerGroup);
   %trigger.setCollisionMasks(BIT($playerGroup), BIT($playerLayer));
   %trigger.setLayer($platformLayer);
   %trigger.setCollisionActive(true, false);
   %trigger.enterAction = %enter;
   %trigger.stayAction = %stay;
   %trigger.leaveAction = %leave;
}

function collidePlayerTrigger(%trigger)
{
   TriggerCheckGroup.add(%trigger); 
   %trigger.inNow = true;
}

function checkTrigger(%trigger)
{
   if (!%trigger.inPrev && %trigger.inNow)
      schedule(0, 0, "eval", %trigger.enterAction);
      
   else if (%trigger.inPrev && %trigger.inNow)
      schedule(0, 0, "eval", %trigger.stayAction);
      
   else if (%trigger.inPrev && !%trigger.inNow)
   {
      schedule(0, 0, "eval", %trigger.leaveAction);
      TriggerCheckGroup.remove(%trigger);
   }
   
   %trigger.inPrev = %trigger.inNow;
   %trigger.inNow = false;
}

First we create a new SimGroup to hold all of the triggers. We will use this in much the same way as EnemyGroup. The createTrigger function sets up the trigger with its necessary collision and rendering properties. The parameters it takes are the position in the world to place the trigger, its size, and the actions to take when the trigger is first entered, when it is lingered in, and when it is left. The collidePlayerTrigger function is called when the player collides with a trigger. In it we add the collided with trigger to TriggerCheckGroup and set the status to show that the player is currently colliding with the trigger. TriggerCheckGroup works like this: Whenever a player enters a trigger, it is added to the group so we can process its status in onUpdateScene. We remove it when the player stops colliding. checkTrigger is called on every trigger in TriggerCheckGroup every frame. We have two state variables for each trigger. One holds whether or not the trigger was collided with the previous frame, and one holds whether or not the trigger was collided with this frame. Using these two variables, we can determine the frame that the player enters the trigger and the frame that it leaves. At these times, we call the code that was passed as %enter, %stay, or %leave in the createTrigger function.

So, to make it all work we just need to call the collision and update functions. Add this to the switch statement in the $playerGroup case in onCollision in game.cs:


            case $triggerGroup:
               collidePlayerTrigger(%dstObj);

We also need to define $triggerGroup. At the beginning of the gameData.cs file:

$triggerGroup = 3;

And add this at the end of onUpdateScene:

      for (%i = 0; %i < TriggerCheckGroup.getCount(); %i++)
         checkTrigger(TriggerCheckGroup.getObject(%i));

So, to create a trigger we would do something like this in the createLevel function of game.cs:

   createTrigger("0 0", "10 10", "echo(\"Enter\");", "", "echo(\"Leave\");");

I will provide a good use for triggers later in this tutorial, but there real purpose is for level progression, which is covered in the next tutorial.






Falling to Your Death


This would have fit better with the enemy collision section, but it requires the use of triggers, so I delayed it until here. All we are going to do is place a trigger at the bottom of each pit that calls a function designating our player as falling to its death. Add this trigger to the createLevel function of gameData.cs:

   createTrigger("20 100", "20 5", "playerFall();", "", "");

You will have to mess around with the position and size to get it placed just right in your tile map, but it shouldn’t be too difficult. To make the process easier, add this line to the createTrigger function:

   %trigger.setDebugOn(BIT(0));

This will show you the outline of the trigger’s area, allowing you to see where it is. Once you get it placed right, remove that line.

Now all we need is the playerFall function. Add it to player.cs:

function playerFall()
{
   $player.setWorldLimit(OFF);
   playerDie();
}

First we set the world limit off so the player can fall all the way through the pit of death, then we kill it. Pretty simple.

The next tutorial is going to include a lot of changes to the way objects are created and should hopefully make things more organized and easier to implement by sectioning objects into separate levels. It is going to involve a lot of moving of code around and could get confusing so you should make sure you understand all of the concepts presented so far before you move on.