Behind The Code


Thanks so much for checking out Platformer Toolkit. I hope the game gave you an insight into how small changes, adjustments, and design decisions can radically change how a game feels to play.

If you’re interested in taking the next step and actually making a platformer for yourself, you might be interested in how some of the code works. So I've attached the scripts that control Kit's movements - you can find them attached below.

In this blog I’m going to walk you through the code blocks that drive some of Kit’s core features, like acceleration, the jump arc, coyote time, and variable jump height.

This is an intermediate level blog, so if you wish to apply these yourself you’ll need some familiarity with a game engine, and some programming skills to make the code all snap together properly. If you get really stuck, drop a comment below and I or someone else will help out.

Running with acceleration, deceleration, turn speed

On the running page of the toolkit we not only get to change how fast Kit moves, but also how fast she accelerates, decelerates, and turns. Here’s how that is achieved.

directionX = context.ReadValue<float>();

So...we find out which direction the player is inputting, and store it in directionX. It’s a float which is either -1 (left), 0 (no input), or 1 (right).

desiredVelocity = new Vector2(directionX, 0f) * maxSpeed;

We can then get the desired velocity, which is the player’s direction multiplied by Kit’s max speed.

onGround = ground.GetOnGround();
acceleration = onGround ? maxAcceleration : maxAirAcceleration;

In FixedUpdate, we first check if the player is on the ground or in mid-air, and put the correct stats into the acceleration, deceleration, and turnSpeed variables. 

 if (directionX != 0)
        {
            if (Mathf.Sign(directionX) != Mathf.Sign(velocity.x))
            {

We then check if the player is currently inputting a direction. If they are, we check if the sign (i.e. positive or negative) of the input direction matches the sign of Kit’s current direction - if they don’t, it means Kit is in the process of turning around, so we should use the turn speed. If they’re aligned, we should use acceleration instead. And if no button is being pressed, use deceleration.

velocity.x = Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);
body.velocity = velocity;

Then, we move from Kit’s current velocity, to her desired velocity, at the rate of whatever we just picked, and apply it to the Rigidbody. 

Creating a jump from three stats

The jumping panel allows us to define Kit’s jump by two simple numbers: how high should she jump, and how long should it take to reach that apex before coming back down? And when coming down, we can set a downward gravity multiplier for a snappier landing.

To calculate all this, I use some complex maths that I definitely don’t understand. Thanks to the GMTK Discord for helping with this one!

We start by reading the input from the jump button, and passing it to FixedUpdate. 

Vector2 newGravity = new Vector2(0, (-2 * jumpHeight) / (timeToJumpApex * timeToJumpApex));
body.gravityScale = (newGravity.y / Physics2D.gravity.y) * gravMultiplier;

In Update, we change the character’s gravity scale, using the jump height and duration (this is the maths bit I don't understand). It’s divided by the physics engine’s gravity, and multiplied by a, erm, multiplier.

if (body.velocity.y == 0) { gravMultiplier = 1; }
if (body.velocity.y < -0.01f) { gravMultiplier = downwardMovementMultiplier; }

That multiplier is determined in FixedUpdate: if Kit’s velocity is negative (i.e, she’s falling), we kick on the downward gravity multiplier. Otherwise, it’s a flat one.

jumpSpeed = Mathf.Sqrt(-2f * Physics2D.gravity.y * body.gravityScale * jumpHeight);
           if (velocity.y > 0f)
           {
               jumpSpeed = Mathf.Max(jumpSpeed - velocity.y, 0f);
           }
           else if (velocity.y < 0f)
           {
               jumpSpeed += Mathf.Abs(body.velocity.y);
           }

Then, when the jump actually happens, we create a jumpSpeed variable using some more complicated maths (help me...) and apply it to the character. By determining our character’s velocity and changing the jumpSpeed to match, we make sure we get the same jump even if we’re currently rising or falling (handy for double jumps and springy pads).

velocity.y += jumpSpeed;

We then apply to the Rigidbody!

Adding variable jump height

Variable jump height means the height of your jump is determined by how long you hold down the button. We've just set the maximum height, so now let’s add a few extra tweaks to make it so the character drops when you let go of the jump button.

First...

if (context.started)
{
    desiredJump = true;
    pressingJump = true;
}
if (context.canceled)
{
    pressingJump = false;
}

we can use Unity’s input system to determined when we start and cancel the input, which we save in the pressingJump bool. (We also need a currentlyJumping bool, which is made true when you jump, and false when you hit the ground - it's not shown in the code block above, but it's in the full script).

if (body. velocity.y > 0.01f)
{
    if (pressingJump && currentlyJumping)
    {
        gravMultiplier = 1f;
    } else
    {
    gravMultiplier = jumpCutoff;
    }

Then, if Kit’s velocity is less than 0 (i.e. she’s going up), and pressingJump and currentlyJumping are not both true, it means we’ve let go of the jump button. So apply a gravity multiplier, just like the previous step. This one’s called jumpCutOff.

Adding Coyote Time

The assist panel has a few features that allow us to bias the game's controls in the player's favour.

Coyote time is one of those handy player grace features. In this one, you can still jump even if you’ve just run off the edge of a platform. Here’s how it works.

So first up...

if (!currentlyJumping && !onGround)
{
    coyoteTimeCounter += Time.deltaTime;
}
else
{
    coyoteTimeCounter = 0;
}

In update, we check if the player is not on the ground, and also not jumping - this means they’ve walked off the edge of a platform. At this point, we start counting up the Coyote Time counter.

if (onGround | | (coyoteTimeCounter > 0.03f && coyoteTimeCounter < coyoteTime))

Then, we add another check before letting the player do a jump: is the Coyote Time counter below Coyote Time (a number set by the developer - usually something like 0.2). If it is, allow the jump to happen and reset the counter.

Adding a Jump Buffer

Another grace mechanic is jump buffer. This means we can press jump a few frames before hitting the ground, and it will still trigger the jump when we land.

So, when we hit the jump button we tell the game we desire a jump, and it checks if we’re on the ground (or in Coyote Time). If we’re not, it immediately turns off the “desiredJump” bool. 

jumpBufferCounter += Time.deltaTime;
if (jumpBufferCounter > jumpBuffer)
    {
    desiredJump = false;

But now, we instead start a jump buffer counter - and only turn off desiredJump if the counter hits its max. This way, the game will repeatedly try to jump for a few frames - and will successfully trigger a jump when Kit hits the ground. 

Those are the main ones! I couldn't have done this without some help, so here's where I stole borrowed code from, or received help:

Cheers!

Mark

Files

Character Controller Unity Scripts 10 kB
Jun 22, 2022

Get Platformer Toolkit

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

(1 edit) (+2)

Small note for those that opt to rewrite this code manually instead of downloading the files:

The movement script needs to copy the rigidbody velocity or it will set the Y velocity to 0. Just add:
velocity = body.velocity;

Place it under the onGround check in FixedUpdate. As far as I know this makes it all work as intended. Lastly:

The variables you can input in the game itself aren't accurate reflections of their Unity Editor values. Check the downloaded scripts to see what the in game options are for these values.


Update: I believe I have found and solved the problem giving the player a super jump. If you're rewriting the code yourself here's what's missing:

When you (are on) land, your gravity multiplier is supposed to be 1. There's an if statement in FixedUpdate asking if we're going up (body.velocity.Y > 0.01f). This if statement is incomplete. In fact, it's part of a larger check. Long story short, your gravity multiplier is being set to your jump cutoff variable which causes the following jump to overcompensate. I suggest looking in the scripts added to this page. Inside the jump script you'll find a calculateGravity function. All the checks you need are in there.

(+2)

Can you update this for Godot?

can someone explain this line of code involving the raycasts in the ground detection system? That entire section is barely making any sense to me and there's barely any explanation in the guide. 

onGround = Physics2D.Raycast(transform.position + colliderOffset, Vector2.down, groundLength, groundLayer) || Physics2D.Raycast(transform.position - colliderOffset, Vector2.down, groundLength, groundLayer);

Thanks :D

(+1)

This code is looking below the character to see if there is anything identified as "ground" close enough to the character.

The Raycast function looks like this:

RaycastHit2D Raycast(Vector2 origin, Vector2 direction, float distance, int layerMask)

The function will shoot a ray in the specified direction and return information about any objects that the ray hits.

In this case, the ray begins at transform.position + colliderOffset and is directed downward (Vector2.down). 

The ray extends a distance of groundLength (which is probably based on the size of the character). If this is too large, then onGround would return true before you land. If it is too small, then the rays may not reach the edge of the collider and won't detect the ground even if the character is standing on it.

The LayerMask groundLayer identifies which physics layers should be considered as "ground". The ray will ignore other layers.

If the ray doesn't hit anything, then Raycast will return null which is interpreted as "false" when used as a bool. If the ray hits something, it returns a valid RayCastHit2D object with additional information about the collision. But in this case we only care whether or not there was a hit, so a valid object is interpreted as "true" for the onGround bool.

There are two raycasts, one with origin transform.position - colliderOffset and one with origin transform.position + colliderOffset. I expect that this is to cast rays starting from the left edge and right edge of the object so that you detect ground even if you are partially hanging off of a ledge.

(+2)

Great tutorial! SUPER informative and fun at the same time. Thank you for making and sharing this with us <3


I wanted to understand the math responsible for jumping, so just in case if anyone else is interested, here's what I got so far.

I'm excluding gravity scaling for the sake of simplicity and clarity, but it can be easily plugged into the formulas.


New Gravity Calculation:

Vector2 newGravity = new Vector2(0, (-2 * jumpHeight) / (timeToJumpApex * timeToJumpApex)); 
body.gravityScale = (newGravity.y / Physics2D.gravity.y) * gravMultiplier;

using the formula:      

distanceTravelled = 0.5 * gravity * time^2



Jump Speed calculation, based on current gravity:

jumpSpeed = Mathf.Sqrt(-2f * Physics2D.gravity.y * body.gravityScale * jumpHeight);

using formulas:

currentVelocity = gravity * time

distanceTravelled = 0.5 * gravity * time^2


In the code, the multiplier inside sqrt is -2,  because the in unity gravity has a direction (down, duh...), thus it's negative. Whereas this formula considers g as magnitude, without direction, making it  positive. To account for this, we multiply g by -1, and get  -2 * g * d. (I hope this gibberish makes sense)

So I am having this problem after putting the code in my project.

When I press and hold the "Jump" button again while in mid-air, I somehow get a sudden downward momentum and bring my player much faster than the up movement.

It also happens when I time the jump to the frame when the player becomes grounded in which case the player jumps at a higher speed than normal.

At first, I thought it was because I was using the old Unity input system. So I changed it to the new input system with the "context.called" thingies just like in the code but I am still having problems. It also happens after limiting max air jumps to zero, setting to no variable jump height and disabling both jump buffers and coyote time.


So yeah, I am at a complete loss on how to fix this for the past few days. Any clue on how to fix this would be greatly appreciated. (Think I need to change something in the new input manager?)

I also hit the issue with the "super" jump, when jumping just before landing.


I think it's happening at the moment when the player is just above the ground -> ground check with raycasts says that the player is grounded, but the player's gravity scale is still upscaled for falling.


At that moment, the jump velocity is calculated based on "falling" gravity scale, but at the next frame the gravity scale is downscaled, because player is moving up now. This means that the players velocity is too high, for the new gravity scale, and the player will jump too high.


The fast and easy fix was to update gravityMultiplier and gravityScale at the top of the FixedUpdate, before DoJump() function is called, so it can operate on correct/newest values. Maybe there's a better solution, but this will work for now.


Idk if this is still relevant to you, just posting in case if anyone else stumbles across this issue. 

Oh my thank you for your explanation and fix!

I tried this and it didn't solve it. It only reduced the likelihood. I now understand why  the superjump happens though:

This dev log doesn't contain the full gravity multiplier check needed to handle jumping properly. Copying just what you see here will make your gravityMultiplier variable set itself to your jump cutoff. This is because it's missing a ground check. There's a lot more stuff missing for this gravityMultiplier. Go check out the calculateGravity function in the scripts you can download here.

It's really neat to read through the code and try to use it for a game! But I'm still having trouble understanding it.

Specifically, I really want to know about where "duration" of the jump and the "Jump Height" is in the code.

I don't understand how the "jump height" doesn't seem to affect the "duration" of it.

From what I understand, the jump height and duration are set as constants, and the speed at which player is jumping and falling is being calculated based on those constants.

I manage to implement the code to a Unity project, but when I wanted to put the values I found cool for a platformer in Unity, i dunno how to fill the variables with those values, especially for the jump… Can anyone help me ?

I tried putting the characterMovement and ground script into a new unity project but can't move when I press keys. How do you get these scripts to work?

(1 edit)

Personally, i do some change in the code but it’s working :
Original

    public void OnMovement(InputAction.CallbackContext context)
    {
        //This is called when you input a direction on a valid input type, such as arrow keys or analogue stick
        //The value will read -1 when pressing left, 0 when idle, and 1 when pressing right.

        if (moveLimit.characterCanMove)
        {
            directionX = context.ReadValue<float>();
        }
    }

My modified version

    public void OnMovement()
    {
        //This is called when you input a direction on a valid input type, such as arrow keys or analogue stick
        //The value will read -1 when pressing left, 0 when idle, and 1 when pressing right.
        directionX = UnityEngine.Input.GetAxis("Horizontal");
        directionX = directionX != 0 ? directionX / Mathf.Abs(directionX) : 0; //Set the var at 1 if > 0, -1 if < 0;
    }

private void Update()
{
    OnMovement();
    ...

Maybe there’s a better/simpler way, but this work so you can try it

Thanks so much!

Cool project, I really enjoyed it. Just one thing, in the movement part you probably want to use Time.fixedDeltaTime instead of Time.deltaTime (deltaTime being the time between two Update(), and fixedDeltaTime being the time between twn FixedUpdate()).

(+1)
Hey, quick correction. You don't have to use Time.fixedDeltaTime. According to Time.deltaTime Unity Docs. (https://docs.unity3d.com/ScriptReference/Time-deltaTime.html)
'When this is called from inside MonoBehaviour.FixedUpdate, it returns Time.fixedDeltaTime.'

Ah yes my bad, I totally forgot about this !

(1 edit) (+5)

A video explaining how to implement this code would be great. Since the scripts are complete it would be great to see the process of writing each script with a game project in the editor instead of just copy/pasting your code.

(+1)

movementLimiter  jumpTester  optionsManagement  labOpener

These four classes seem never defined in the scripts? They are causing errors

(+1)

Hey! Yes, the scripts are part of the wider Platformer Toolkit so will need some adjustment to work standalone. All of those things can be removed

THX! I still find a problem, the character falls much slower and seems weird.  

this code is one of the best things that happened to me this week... it doesn't compete against much either, but it's really spectacular
 I download the code and i'm triyng to add the characterMovement to my game, but I need help with this line in the code in particular:

 [SerializeField] movementLimiter moveLimit; (13 line characterMovement)

Unity says that the movementLimiter namespace doesn't exist. I need download some package, or i need to add other script, or how can i fix the error? 
Sorry, I'm new to programming

Hello, I'm having the same problem, did you find a solution? thanks

not yet :(

Hey! You can just remove that bit - it's related to another class that I use to stop the player from moving. 

You can remove all `SerializeField`. 

What they do is add an option to tweak the value in Unity's interface

Uh huhm, interesting...

👍

sweet

Great work!