TorqueX/PlatformerFrameworkTimeBasedJump

From TDN

Contents

Introduction

The Platformer Starter Kit (PSK) for Torque X is a great way to get things up and running in a hurry, but it lacks a few features common to most platformers. One of these features is a time-based jump — your character jumps higher the longer you hold the jump button. After a bit of tinkering I’ve managed to implement this. Now you can too!

Materials Needed

You will need the following to complete this tutorial:

  • Visual C# Express
  • XNA
  • Torque X (TX)
  • Platformer Starter Kit (PSK)
  • A new project using the Platformer Demo

Optionally, you will want:

  • Torque X Builder (TXB)

Adding the Time-Based Jumping Logic

Your freshly created Platformer Demo project will contain many files, but for now we're interested in ActorComponent.cs. ActorComponent.cs is a large file (nearly 3000 lines) the fine folks at Garage Games have done an excellent job of dividing into regions. Further, the component is basically two sections, the ActorComponent stuff and the Actor Physics States.

To start we'll add some code to the ActorComponent section. In the private, protected and internal fields region add three variables:

// Times in seconds
protected float _jumpDuration = 0.0f;
protected float _jumpTime = 0.25f;
protected bool _isJumping;

These variables will allow us to keep track of how long the player has held the jump button (_jumpDuration), how long a jump can last (_jumpTime) and set a flag when the actor is jumping (_isJumping). To make things more flexible we’ll expose _jumpTime via a getter and setter so it can be set from Torque X Builder (TXB). Add the following code in the Public properties, operators, constants, and enums region of ActorComponent.

[TorqueXmlSchemaType(DefaultValue = "0.25")]
 public float JumpTime
 {
       get { return _jumpTime; }
       set { _jumpTime = value; }
 }

Now, rebuild your solution and reload the project in TXB. Any scene object with an ActorComponent (or any component derived from ActorComponent) will now have a field for setting the jump time.

Since we’ve added a public property we’ll need to add it to the ActorComponent’s CopyTo method. Scroll to the Public methods region and add the following to CopyTo:

obj2.JumpTime = JumpTime;

While we’re on the Public methods region we need to add one method, IsMaxJumpTime. This method will check to see if the player’s jump duration has exceeded the allowed jump time. In other words, this method tells us whether we can keep jumping or not. IsMaxJumpTime will return true of the player’s jump duration has exceeded the allowed jump time. If the player has yet to hit the jump time it returns false.

public bool IsMaxJumpTime()
{
      if (_jumpDuration < _jumpTime)
      {
            return false;
      }
      return true;
}

So that’s it for the ActorComponent. Now it’s time to move onto the Actor Physical States region (around line 2500). Here we’re interested in two physics states: OnGroundState and InAirState. Prior to all our meddling OnGroundState is the place where jumping in handled for actors derived from ActorComponent. To keep making updates to our jump over time we’ll also need to add some logic to InAirState.

To start, in OnGroundState look for the following line:

else if (actor._Jumping)

This else block handles jumping for the actor. First we’ll need to alter the condition slightly. Change the previous else if statement to the following:

else if (actor._Jumping && !actor.IsMaxJumpTime())

So now our actor will only jump if it’s in a jumping state and the jump has not timed out. Now the first thing we need to do is update the jump duration. Inside the else if block add this as the first line:

actor._jumpDuration += (elapsed / 100);

Elapsed is a variable passed into the UpdatePhysics method to tracks time for us, we need to divide it by 100 to get it on our time scale. The next line in the else if block should be:

actor._actor.Physics.VelocityY = actor._groundVelocity.Y - actor._jumpForce;

This is the line that does the actual jumping. Here you need to take a step back and really think about how you want your jump to work. I want my default jump (quick tap of the jump button) to be of moderate height, which for a platformer seems to be between 1 and 2 times the height of the on-screen player. I then want the player to be able to jump higher than that if the button is held down longer. The trick is that I don’t want jumping to be touchy and thus require extreme precision. Rephrasing this, we could say, “I want jumps to start slow and build up steam.”

Fortunately there is a simple mathematical operation for this: exponentiation. Even more fortunately, C# (or is it .net?) has built-in method for this in the Math library. So we’re going to change our jumping code to the following:

actor._actor.Physics.VelocityY = actor._groundVelocity.Y - (actor._jumpForce * ((float)Math.Exp(actor._jumpDuration) / 2));

What Math.Exp() does is raise e (Euler's number) to the specified number, in this case our jump duration. So we’re raising e to a number that increases over time and multiplying the result by our jump force (effectively the height of the jump). Now, you’ll notice that before we multiply the result of Math.Exp() by the jump force we halve it. This is completely arbitrary and was done because it felt right for my game. You could change this, or pull it out into a variable that could be set in TXB, thus allowing for different jump characteristics on different actors. So that’s our jump code. Now we need to add one more line of code at the bottom of the else if block:

actor._isJumping = true; 

This will ensure that our jump is processed in the InAirState. We’re now going to add an else to the end of our if…else block:

else
 {
   actor._jump = false; // this prevents the little bounce at the end of the jump
   actor._isJumping = false;
   actor._jumpDuration = 0.0f;
 }

This will reset our jump variables when we’re not jumping.

As stated earlier we’ll need to continue processing our jump in the InAirState of our actor. Find the UpdatePhysics method of the InAirState and add the following block of code to the end of the UpdatePhysics method:

// do pressure sensitive jumping
if (actor._Jumping && actor._isJumping)
{
   actor._jumpDuration += (elapsed / 100);
   // Separated from the main if clause for clarity
   if (!actor.IsMaxJumpTime())
   {
      // jump up
      actor._actor.Physics.VelocityY = actor._groundVelocity.Y - (actor._jumpForce * ((float)Math.Exp(actor._jumpDuration) / 2));
      // set the appropriate animation state for jumping
      if (Math.Abs(actor._moveSpeed.X) < 0.01f)
         FSM.Instance.SetState(actor._animationManager, "jump");
      else
         FSM.Instance.SetState(actor._animationManager, "runJump");

      actor._jump = false;
      actor._jumpDown = false;
      actor._onGround = false;
   }
}
else
{
   actor._jump = false; // this prevents the little bounce at the end of the jump
   actor._isJumping = false;
   actor._jumpDuration = 0.0f;
}

The above is essentially the same jump logic from UpdatePhysics in OnGroundState. With that we've added all the necessary logic for processing a time-based jump using the PSK the only thing left to do is hook up the control.

Adding Some Control

To edit the control open PlayerController.cs and scroll to the ProcessTick method in the Public methods region. Here you'll see an if...else block:

// set jump only on initial button down and button release
if (move.Buttons[0].Pushed)
{
   if (!_jumpButton)
   {
      _jump();

      if (movingDown)
         _jumpDown();

      _jumpButton = true;
   }
}
else if (_jumpButton)
{
   _jumpButton = false;
}

This block of code handles jumping. Specifically it only allows one jump per button press, so if the button is held down the player will not continue to jump. We have a problem here because we want the player to continue to jump so long as we have the jump button pressed, with a few caveats. First, we only want the actor to continue to jump so long as it's jump duration is less than it's total allotted jump time. This is handled for us in ActorComponent.cs by the IsMaxJumpTime() method. The second thing we want to avoid is double-jumping or air jumping (i.e., jumping again while still in the air). This is handled for us by the _isJumping flag also set in ActorComponent.cs.

So now all we need to do is replace the existing if...else block with:

if (move.Buttons[0].Pushed)
{
   _jump();
   if (movingDown)
   {
      _jumpDown();
   }
}

Taking it For a Test Drive

Click "Start Debugging" and you'll now have time-based jumping on you actors. There is, however, one problem still remaining. We may have eliminated double-jumping but ActorComponent is still receiving jump commands from PlayerController when the actor is in the air. The end result of this is that if the player tries a double- or triple-jump the actor does little mini jumps after the initial jump. The effect is somewhat like the actor is bouncing.

I don't have a really graceful method for handling this, so I'll present you with the hack-tastic version. In the very beginning of the UpdatePhysics method of the OnGroundState add the following if statement:

// This is equivalent to:
// if(actor.PreviousState.ToString() == "InAir")
// but should by much faster.
if (actor.PreviousState.Equals("InAir"))
{
   actor._jump = false;
}

This little snippet checks to see if the previous ActorState was "InAir", the state used when jumping, if so it sets the actor's _jump flag to false. In effect this cancels any jump commands that were issued by PlayerController while the actor was jumping. With that, you should have a fully functional time-based jumping component.

NOTE: If your player starts the level in the "inAir" state, then this will cancel all jumps. See the fix I added to the sections above that fix the "bounce" issue this was meant to address.

Notes

In this tutorial I put everything directly into ActorComponent.cs which is the base component for all actors. You may or may not want all actors to have this functionality. Enemy actors, for example, may not need time-based jumping. It may be better to add the code discussed here to a component derived from ActorComponent (PlayerActorComponent perhaps).

Credits

Original article and code by Sean Monahan

Added corrections to fix the "bounce" issue: Jason Fox