When discussing games, people often refer to controls that feel responsive as “tight." But, what makes controls feel good? The obvious answer is that controls should always do what the player is thinking of doing. They need to be predictable to the user depending on their inputs. The issue is that players are human, and as such, we can make slight timing mistakes that produce an unexpected result. From a programming point of view the action displayed on screen made sense (since code is consistent), but in the player’s mind, the game “cheated” them. This is a terrible feeling to have as a player and as designers, we must find ways to mitigate the instances where controls don’t react the way they were intended to.
This is where adding some leeway to your controls in the form of timers (or extra frames to perform actions) helps your controls feel tighter. These concepts can be used in many situations and in any game genre to perform actions such as jumping, attacking, interacting with menus, or even queuing actions. Thanks to my experience creating platforming games, I will talk about three examples in this article related to that genre, namely: Coyote time, jump buffering, and sticky walls. Let’s get started.
Players like to maximize their jumps in platforming games, leading them to wait until the very last moment (usually at the edge of the ground) to jump. When done right, the player will perform a jump that will cover a nice distance. However, when done wrong, their character will simply fall without jumping, usually killing them with strategically placed spikes below (because every platformer NEEDS spikes). To fix this issue, we can implement something called coyote time, also referred to as “ledge tolerance." This feature adds the ability for the player to jump even after their character is no longer touching the ground, for a few extra frames.
Here’s an example of how the demo looks like when coyote time is zero (meaning it’s off).
The yellow dot represent the moment when I press the jump button. As you can see, unless the player is touching the platform, the jump won’t happen, but in a couple of those cases it was so close that it looks wrong, and the player would be frustrated by that. Let’s implement a timer to fix it.
Disclaimer: All the code shown in this article will be as engine/language agnostic as possible. As such, it may look a bit like pseudo-code, but the implementation on your engine/language of choice should be pretty trivial.
// Character’s Create Event
var coyoteTimer = 0;
...
// Character’s Update Event
if (cBelow)
coyoteTimer = 0;
var numCoyoteFrames = 4;
if (kJump && (coyoteTimer < numCoyoteFrames))
{
Jump();
coyoteTimer = numCoyoteFrames;
}
++coyoteTimer;
First, we create a variable in our character’s create event called coyoteTimer and we initialize it to zero. Then in our update event, we check if the character is on the floor (represented by the cBelow variable, but should be replaced by however you are checking for ground collisions), and if it is, then we reset our timer back to zero. In the next section, we create a variable with the number of extra frames that your character can be off the ground and still be able to jump. We then check if the jump key was pressed (kJump) and if the timer is less than the number of extra frames. If both of these conditions apply, we execute our jump logic and then we set the timer to be the number of extra frames so that we can’t jump twice. Last, we increase our timer by one every frame. That is all!
When we test the game now, this is what it looks like:
if (cBelow)
coyoteTimer = current_time;
var maxCoyoteTime = 200; // In milliseconds
if (kJump && ((current_time - coyoteTimer) < maxCoyoteTime))
{
Jump();
coyoteTimer = 0;
}
The code is similar to the previous one, but this time, we are resetting our timer using the current game time, and then we are checking for the jump key as well as the difference between the current time and the time at which we left the ground. If that difference is lower than our maximum coyote air time, then we execute our jump code and set the timer back to zero so as not to jump twice. In this example, we don’t need to increase the timer since it’s being handled automatically by the game time.
The next feature we’ll cover is called jump buffering (also referred to as input buffering). The idea is that if the player performs an action a few frames before it’s allowed, by some other condition (such as being on the ground to be able to jump), the action will still happen. To achieve that effect, we start a timer when the button is pressed and if the number of frames between the moment in which the input was sent and the moment the condition is reached (touching the ground) is less that the number of frames defined by our tolerance, the action still happens (we jump). The code looks somewhat similar to the previous section:
// Character’s Create Event
var jumpBufferTimer = max_int; // Or any number higher than your tolerance variable
...
// Character’s Update Event
var numJumpBufferFrames = 4;
if (cBelow && (jumpBufferTimer < numJumpBufferFrames))
{
Jump();
jumpBufferTimer = numJumpBufferFrames;
}
++jumpBufferTimer;
if (kJump)
jumpBufferTimer = 0;
We begin by creating our timer variable jumpBufferTimer in our character’s create event (or you set it as a member variable in your object) and we initialize it to a high number (max_int in my case). It needs to be higher than the tolerance frames variable that we will create later to prevent the character from jumping once when a room loads. Next, we go to our update section and create our tolerance variable called numJumpBufferFrames and I’m setting it to four initially as a test. The first conditional checks if the character is colliding with the ground using the cBelow variable and it checks if the timer is less than our tolerance. If these conditions are met, we will perform our jump code and then we will set our timer to be numJumpBufferFrames to prevent jumping twice. Last, we increase our timer by one each frame and we check if the jump button kJump is pressed, resetting the timer to zero if it is. With these simple lines, the jump buffer is already working, and should look like this.
If you look carefully, you’ll notice that even when I press the jump button slightly above the ground (indicated by the yellow dots) the character still jumps as soon as it lands on the ground. An example with a larger tolerance (say, eight frames) can make this more obvious to spot:
Just like with coyote time, finding the perfect tolerance for this feature will require you to implement it in your game and test many values out until one feels right to you. Also, just like before, we can modify the code a bit to make it work with time instead of frames, using the following:
// Character’s Create Event
var jumpBufferTimer = -max_int; // Or any really large negative number
...
// Character’s Update Event
var maxJumpBufferTime = 200; // In milliseconds
if (cBelow && ((current_time - jumpBufferTimer) < maxJumpBufferTime))
{
Jump();
jumpBufferTimer = 0;
}
if (kJump)
jumpBufferTimer = current_time;
We first need to set the initial timer value to a negative number higher than your tolerance time (I’m using negative max int to be safe). In the update, we change the number of frames to maxJumpBufferTime and we set it to 200 milliseconds. Later, we check whether the character is on the ground and if the difference in time between the current time and the timer is less than the tolerance. If true, we make the character jump and we set the timer to zero to prevent jumping multiple times. We skip increasing the timer by one, since that’s done automatically as time goes on, and we end it by checking the jump key and setting the timer to the current time if it was pressed this frame.
To combine both of these features, you can place them one under another, but a cleaner way of doing it to avoid duplicate checks is:
var numJumpBufferFrames = 4;
var numCoyoteFrames = 4;
if (cBelow))
{
coyoteTimer = 0;
if (!hasJumped && jumpBufferTimer < numJumpBufferFrames)
{
Jump();
jumpBufferTimer = numJumpBufferFrames;
hasJumped = true;
}
}
++jumpBufferTimer;
if (kJump)
{
jumpBufferTimer = 0;
if (!hasJumped && coyoteTimer < numCoyoteFrames)
{
Jump();
coyoteTimer = numCoyoteFrames;
hasJumped = true;
}
}
++coyoteTimer;
if (kJumpReleased)
{
VariableJumpLogic();
hasJumped = false;
}
Notice that I also added a hasJumped flag. This is to prevent a bug where the player would double jump when hitting the corner of a block going upwards. It should be easy to extrapolate how to join both features using the millisecond timers by looking at the code above and their separate implementations. This example shows both features implemented at the same time (with slightly bloated frame tolerances to show it better):
The last feature I want to talk about I refer to as Sticky Walls. The concept is to have the character stick to the wall for a few frames when the player points in the opposite direction of the wall. We do this to prevent the character from falling down if the player gives a directional input before pressing the jump button off a wall. I will try to explain the code below as best as I can, but this section will look very differently in every game, so adjustments are necessary to make it work.
// Character’s Create Event
stickyWallTimer = 0;
...
// Character’s Update Event
var stickyWallFrames = 4;
--stickyWallTimer;
if (!onWall || cBelow)
stickyWallTimer = 0;
else if ((((kRightPressed && cLeft) || (kLeftPressed && cRight)))
stickyWallTimer = stickyWallFrames;
// Some gravity code
ApplyGravity();
if (stickyWallTimer <= 0)
CharacterMovement();
if (onWall)
Walljump();
Just like in the previous cases, we will create a variable in the create event of the character: called stickyWallTimer, initializing it to zero. Next, in the update event we can define our tolerance value stickyWallFrames which I’ve set to four. We will be decreasing this value every frame by one to act as our timer. Here comes the fun part. We reset our timer if we are not on a wall or if we are on the ground. However, if one of those conditions is false, we check if we are giving a directional input that is opposite to the side of the wall we are touching (kRightPressed/kLeftPressed being the directional inputs and cLeft/cRight being the side we are colliding with). For these inputs, make sure to be checking for the first frame it was triggered, not if the player is holding it. This is where it can differ from game to game, but I apply my gravity code to the character next. Then, I wrap my movement code in a conditional checking if the timer is less than or equal to zero. This is what will make the character stay stuck to the wall for a few frames until the timer runs out, at which point movement will resume as normal. Finally, I write my wall jumping code if the player is colliding with a wall.
The outcome should look like this (notice the player turns green when the player is sticking to the wall).
And again, an example with an exaggerated tolerance, such as 14 frames:
If you want to implement the version using the time, this is what you need to change:
// Character’s Update Event
var maxStickyWallTime = 200; // In milliseconds
if (!onWall || cBelow)
stickyWallTimer = 0;
else if ((((kRightPressed && cLeft) || (kLeftPressed && cRight)))
stickyWallTimer = current_time;
// Some gravity code
ApplyGravity();
if ((current_time - stickyWallTimer) > maxStickyWallTime)
CharacterMovement();
if (onWall)
Walljump();
You can download the demo I used to create the GIFs in this article. The demo allows you to customize the tolerances so you can play around with it and test how it affects the feeling of the character.
Controls
I hope you take these ideas and implement them in your project to make your players feel more in control of their characters and not cheated by the game. Remember that the application of these ideas is endless and can include other stuff, such as menu navigation. If you have any suggestions or questions, you can contact me on Twitter (@AleHitti). Thank you for reading. Cheers!