2D Character Controls in Godot (Sort Of)

I don't know how to set up 2D character controls in Godot. I'll just confess that right now.

I've tried, ladies and gents - I've drafted this same blog post four times, each time trying to approach the problem of 2D movement from a different angle - and every time, without fail, something has screwed up, and I've not been able to find a shred of information online on how to fix it.

October is a very busy month for me, and I'm already days behind on my monthly schedule, because I've been spending way too much time trying to figure out problems like why my character won't go up slopes or why they constantly jump in mid-air.

I've been watching several tutorials on the subject of 2D movement, but all of them approach it from a different perspective, which only ended up confusing me more. And honestly, I think if I tried to make a from-scratch movement tutorial here, it would only contradict all the other tutorials out there and confuse you even more.

But I'm a wannabe gamedev of my word, and I said that we were going to cover 2D movement in this post. So, I won't give you my tutorial on how to make a character move in 2D - I'll give you everyone else's.

Below I've copied down the methods of several tutorials I've seen on how to make 2D movement. I'm not going to pretend to understand how they all work, but it's like baking a cake - I don't know why we put flour into it or why we cook it a this precise time, all I know is I follow the instructions, and somehow, at some point down the line, I end up with a cake.

Hopefully by the end of this, I'll end up with a basic idea of what goes into 2D character movement. First of all, we'll look at one of Godot's own tutorials for how to produce 2D movement.

Method 1

This method comes from a section of the Godot Docs website called "2D movement overview", which explains 2D movement - specifically it explains 8-way movement, which is the kind of movement you'd expect from top-down or isometric games. These games take place in an overhead view, and allow your character to travel freely up, down, left and right in any direction.

The Input Map

The first step in Godot's tutorial is to decide which buttons we want to press and what we want those buttons to do. We can decide all this in the Input Map.

In the top toolbar, click Project, then click Project Settings. A new window will open up, and at the top of it is a bunch of tabs. Click over to the Input Map tab to bring up the Input Map.


Currently, the Input Map is empty. To create a new input, click on where it says "Add New Action" and type in the name of the action you want. Let's start with an action for going right. In Add New Action, I'll type in "right" and click the "Add" button next to it.

he new action will appear in the list, and right on the opposite side of it will be a plus sign (+) button. Click on it to decide which button we want mapped to our "right" action.


A new window will open up, and at the top it will say "listening for input". Before you click anything else, type in whichever key you want your action to be mapped to - in my case it will be "D".


Godot will automatically select the key you just pressed as the one for the action to be mapped to. Now you can press OK.


Keys assigned to the action will appear right underneath that action. You can also apply multiple buttons to a single action at once. Click the little plus sign again to bring up the key select screen, and this time I'll hit the right-arrow button.


Now Godot understands that when we press either D or the right arrow key, we're activating the "right" command. We'll decide what it is that command actually does in the code itself.

Now I'm going to create another action in the Add New Action area called "left". Then I'll click the plus button that appears besides where it says "left" in our list and assign the A key to it. Then I'll click the plus again and assign the left arrow key to it.


Now Godot understands that when we press either A or the left arrow key, we want to activate the "left" command.

Now we can add the "up" and "down" actions. Our "up" action will be mapped to W and the up arrow key, while our "down" action will be mapped to S and the down arrow key.


Since these are all the directions we want to go in, we can now close the Input Map.

Creating a New Script

To create a new script that's not attached to any pre-existing object, we can right-click in the bottom-right FileSystem menu and select "New Script"


A new window will open up. In "inherits", I want to write the name of the node that this script is going to be attached to - in my case the CharacterBody2D node.


If you click on the drop-down for "template", you can pick from three code templates. One of them, "CharacterBody2D: Basic Movement", is a pre-built script that handles left-to-right movement and jumping, which is great but not what I want right now. The second template, "Node: Default", contains two empty functions, "ready" and "process", that will be familiar to you if you're coming over from Unity. The third, "Object: Empty", is just an empty block of code you can fill in from scratch.

The Godot tutorial uses Object: Empty, but I'm going to pick Node: Default, just so I can explain a little bit about the script layout, then click Create.


A new window will open up in which to write your code. Our Node: Default template is already split into four areas: the "extends" title, the gap underneath it, the "ready" function and the "process" function.



Every Godot code begins with an "extends" part. This just tells us what is is the code will be applied to; in this case our CharacterBody2D.

Between the "extends" line and the first function is a gap where we can put our variables. Variables are properties of our object that we want to change. You know how in an RPG you can play as characters who have certain stats, like a strength value or a magic value? Those are variables, because those are properties of the character that can change (like when you level up, for instance). In movement code, variables might include a speed value or a variable for jump height, for example.

A "function" (also known as a method) is a chunk of code - it helps to group lines of code together into chunks so you can keep track of what everything does. Some of these functions Godot understands to have very specific purposes, like the two functions in the code here:
  • The "ready" function (known in Unity as the "start method") is for code that happens at the very start of the game. Any code written in this section will happen once as soon as the object it's attached to first appears in the game.
  • The "process" function (called the "Update method" in Unity) is for code that happens constantly throughout the game. Any code written here will be carried out once every frame. Since most games go sixty frames per second, this means the code here will be carried out sixty times every second - and it will do this over and over again for as long as the object it's attached to stays in the game.

Adding a Variable

The Godot tutorial only needs us to add one variable, and that's a variable for speed. It writes it down like this:

@export var speed = 400

"Var" means "this is a variable, "speed" is the title we've given our variable, and 400 is the value we've given it. So now, whenever we type "speed" in the code, Godot will know that we mean 400 pixels per frame.


But what does "@export" mean? Putting this at the front of the code means we can access and change this variable's value at any time from the inspector. Without it, if we felt our speed was too much or too little, we'd have open up the code again and manually type in a new value every time we wanted to change it. By "exporting" it out to the inspector, we can just edit the value whenever we want in the CharacterBody2D's inspector menu on the right-hand side, where it will appear right at the top.

Adding New Functions

Right now, we have two functions we can type code in - "ready", which happens at the start of the game, and "process" which happens constantly throughout the game. But our Godot tutorial doesn't actually use either of these functions. Instead, it uses the "physics process" function, and it's created a custom function of its own called "get_input".

Why has it changed "process" to "physics process"? Well, process is a function that runs its code once every frame - however, for whatever reason, it struggles with doing any kind of code that relates to physics. So, when we want to have physics-based things such as movement happen constantly throughout the game, we use its older brother: "_physics_process", which does the same thing as process except being better able to calculate physics.

As for "get_input", well, that function doesn't do anything on its own - but it makes for a nice and organized place to put in the code we'll be using to access the buttons we've assigned in the Input Map.

To add these two functions, you write them out like this:

func get_input():

func _physics_process(delta):

"Func" means "this is a function", followed by the title we want to give our function. 

Delta

 But what is "delta"? Why is that at the end of our process and physics process functions? Well, "delta" is something that we put at the end of a lot of our code if it's for frame-based stuff. Unity calls this time.deltaTime, and it means the same thing here.

You see, some computers out there are faster than others, meaning they go through a lot more frames per second than other computers. The problem with this is, since our process and physics process code happens every frame, this code will be carried out faster on quicker computers than on slower ones.

For instance, if something in-game is going at a speed of one pixel per frame, a computer going at 60 frames per second will mean the object will be moving a 60 pixels a second. However, if that same object is put on another computer that only goes 30 frames per second, the object will only move 30 pixels a second.

To stop this from happening, we multiply our movement commands by delta - how long it's been since the last frame. I don't know precisely how it works, but adding delta to it somehow makes sure that the movement is the same speed on every computer.


Vectors

In the get_input function we just made, we're going to create another variable. When you put a variable at the top of the code, it's a variable that you expect to be using across several functions. If you've got a variable that you think you'll only need to use for one function, you can just put it at the top of that function instead, like we're doing here.

The new variable that we're adding in the get_input function looks like this:

var input_direction = Input.get_vector("left", "right", "up", "down")

"Var", we know, means "this is a variable", and we've called this variable "input_direction". Presumably, this means it wants to know the direction of our player's input - in other words, it wants to know whether we're pressing left, right, up or down (which we assigned buttons to using the Input manager).

But instead of assigning it a value, this variable has an in-built Godot command at the end of it: Input.get_vector. What does that do?

You know when you chart coordinates on a map and the result you get is two numbers in parentheses, like "(X, Y)"? This is called a vector - specifically, it's a Vector2, because there's two coordinates.

In a 2D game, all of our movement would be in Vector2s, since there's only two dimensions you can move in - left-and-right (X) or up-and-down (Y).

Say the red dot is where we are in a 2D game.  If we want to go across, we can see that we have to keep adding positive numbers to our X value, and if we want to go down, we keep adding positive numbers to our Y. So, in order to do the reverse - to go left instead of right, or up instead of down - we have to subtract instead of add. This is how Godot understands axis-based movement: right is plus on the X, left is minus on the X, down is plus on the Y and up is minus on the Y.

Input.get_vector is a command that automatically gets our vectors for going left (-1 on the X), right (1 on the X), up (-1 on the Y) and down (1 on the Y). If we multiply this by our speed variable, we'll have our velocity - our speed in whichever of these directions we're going. We can type this into the code like so:

velocity = input_direction * speed

Velocity is an in-built quality of  Godot object, so we don't have to make it into a variable.

Calling Functions in Other Functions

That's the get_input function finished - now we can move on to the physics process function. If you remember, this function is for physics-based code, and it runs this code continuously throughout the game. The first thing that the tutorial puts into the physics process function is:

get_input()

 ...which if you remember, is just the name of our other function, the get input function. This is called "calling" a function inside of another function. Since we've called our get input function inside of our physics process function, the get input function will run whenever the physics process function runs - and since it runs constantly all the time, the get input function will run all the time too. This is just a way of tidying up the code - otherwise we'd be typing our whole "get vector" thing in the physics process function, and all that code could bloat it up and make it hard to read.

Underneath this, the tutorial puts something else, too:

move_and_slide()

Now, at the very bottom, the Godot tutorial has added one more line of code: "move_and_slide()". What is move-and-slide? ...I don't know. Genuinely; to me this is some kind of magic spell. It seems to be some kind of command that you attach to your CharacterBody2D that automatically makes it so you can move it around - and not only that, but also add collision detection to it, so it can land on platforms and other colliders without going through them.

I don't know what's going on with it, but I know that nothing will happen in play mode if I don't add move_and_slide at the end of my physics process code. And since I've been tried writing functioning movement code four times already to no avail, I'm not ready to start questioning it now. 


Now our script is ready to be applied to our CharacterBody2D, if it isn't already. You'll find your scripts down in the FileSystem menu at the bottom-left (they'll be the ones with the gear icons next to them). Click-and-drag the name of the code you want to apply up to the CharacterBody2D's name in the scene menu above it to apply the code to the object.


Now, if you go into play mode by hitting the play button at the top-right of the screen, you can move your character up, down, left and right. Plus, thanks to the move_and_slide command, your character will bump into any collideable objects you might have in your scene.


Adding a Camera

I've also added a quick camera to follow the player around by adding in a new Camera2D node as a child of the CharacterBody2D node. In the viewport, this camera will appear as a red reticule. If you move it on top of the player character, you can focus it on them.



Here's what the entire code looks like, if you just want to copy-paste it into your own project:

extends CharacterBody2D

@export var speed = 400

func get_input():
var input_direction = Input.get_vector("left", "right", "up", "down")
velocity = input_direction * speed

func _physics_process(delta):
get_input()
move_and_slide()

Method 2

The next tutorial that I've tried is Mina Pêcheux's "Implementing a 2D character controller". This is a more traditional side-scrolling character controller that allows the character to move left, move right, and jump. I've also combined it with her "Using input actions" tutorial since we've already covered adding input actions.

In fact, let's start this one off by adding the input actions. I'll go back into my project settings to get to my Input Map, then I'll use the trash can buttons to delete the up and down commands. Then, I'll replace them with a single "jump" action, and assign the spacebar to be its corresponding button. We can leave the left and right inputs as they are.


Now in the script we can move on to deciding the variables. In the gap at the top, we've added three variables:
  • @export var moveSpeed = 150
    • Our movement speed, which has been set at 150 pixels per second, and exported so we can edit it in the inspector.
  • @export var jumpVelocity = 400
    • How high we jump, in this case 400 pixels high (though we can edit this in the inspector as well since we've exported it).
  • var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
    • Our gravity value, which is a little different from the rest. If you recall, our CharacterBody2D is immune to the effects of in-scene gravity that rigidbodies are susceptible to, which means if we want them to come back down after they jump up, we need to set our own gravity variable for them. But here, instead of assigning a random value, we've gone into Project Settings and taken the default gravity setting using the code above, and have made that into our character's gravity value, so everything's consistent. We can't export this one out to the inspector - if we want to change the default gravity of our scene, we have to go into Project Settings to do so.
 

If Statements

Now, the only function that we have in this code is the physics process function. At the start of it, we're going to make sure that our gravity value only works on the character if they're standing on the ground - otherwise it will be hard to move them when they're crushed under all that gravity.

Surely we don't need our crushing downwards force to be working all the time - only when the character is in the air. Or, to phrase it another way, if our character is on the floor, we don't want this gravity y-axis value to apply.

This brings us neatly to the "if" statement. This is a type of code that looks out for if something happens. We type down "if", and then next to it we type out what we want to check happens. Then, underneath the statement, we write down what we want to have happen when this first thing happens. All in all, it's laid out like this:

if (something happens):

(something else happens)

So, we want an if statement that checks if the player character is on the floor. Luckily, Godot has an in-built command that checks this called is_on_floor(). So we just type in "if is_on_floor()"...

...actually, no. We don't want to check if the player is on the floor here - we want to check if they're not on the floor. Funnily enough, writing that out is as simple as putting "not" after "if":

if not is_on_floor():

 And underneath this we can put our downwards velocity code, so the whole thing looks like:

if not is_on_floor():

velocity.y += gravity * delta

So this code says "if we're not on the floor, our y-axis velocity goes up by our gravity value (because for Godot, down is plus) multiplied by delta (to keep it framerate-kosher)".

When we put a plus sign in front of a = sign ("+="), this is just Godot's way of saying "goes up by". If we were to put a minus sign in front of it (-=), it would mean "goes down by".


A Warning

At this point, I had a huge scare, and it almost made me give up on the tutorial. If you're sharp-eyed, you'll have looked at the above image and noticed I made a huge mistake.

When I added move_and_slide and pressed the play button to test this, not only did nothing happen, but the game window wouldn't open up outright. So I skipped ahead and wrote down all the other pieces of code, then tried it out, and... still nothing. In the debug log, this message appeared in red:


And I couldn't at all figure out what this meant. I tried looking it up to no avail. I was freaking out as I tried looking over my code. It didn't make sense! Godot already has an in-built velocity for its CharacterBody2D node - it starts off as 0 in the X and 0 on the Y (for standing still). And movement is about taking these values and adding them to either velocity.x to go sideways, or velocity.y to go up and -

And then I looked at my code again and realized the stupid mistake I'd made - I had forgotten to put ".y" at the end of my "velocity". No wonder Godot didn't know what to do with it - it couldn't tell whether I wanted to add this gravity value to the X axis or the Y axis!

Of course, I put ".y" at the end of my velocity to fix this and it worked just fine from then on. I bring all this up as a cautionary tale: usually Godot will highlight a line of code in red if it's not happy with it, but if something's going wrong and you have no idea why, try going back over your code and double-checking to see if everything's written correctly. It could just be the case that you've misplaced a comma somewhere.


Get Axis

The next step is to get our character moving left and right. Now, we can just do another couple of if statements - if we're pressing the right button, add to the X velocity and go right, while if we're pressing the left button, take away from the X velocity and go left.

But there's a way of shortening this process - the Input.get_axis command. This is a command that takes two inputs in its brackets (the inputs we've decided in the input manager), turns one of them into -1, and the other one into 1. How does it do this? I have absolutely no idea. But just like the mysterious move-and-slide function, somehow it just magically works.

The way of writing out this command in our case looks like this:

velocity.x += Input.get_axis("left", "right") * moveSpeed

This code is basically saying "our velocity on the X axis goes up by left (-1) or right (+1) multiplied by our moveSpeed value (150)."


If Statements with Multiple "Ifs"

Now we're coming to making our character jump. The way of doing this is another simple if statement, which says if we press the "jump" button, our velocity on the Y axis goes down by our jump velocity (remember, for Godot, down is plus and up is minus, so we want to subtract from our jump velocity here).

We can do this with the "is_action_pressed" command, which very simply checks to see if one of our input actions is being pressed. In this case we want it to be the "jump" action, which we assigned to the spacebar in the Input Map. Underneath this if statement, we can take our velocity on the Y axis and make it go down by our jumpVelocity. All in all, that statement looks like this:

if Input.is_action_pressed("jump"):

velocity.y -= jumpVelocity

(Remember that "-=" means "goes down by".) 

 Now, if we try out our game...


We go up... and we keep going up... and we just keep going up.

The problem here is that "is_action_pressed" is a command that looks out for if you press and hold the button in question - so our code is just saying "press and hold the jump button to go up".

Instead of "is_action_pressed", we want to use its sister command: "is_action_just_pressed". Instead of looking to see if you're pressing and holding a button, this command will just check for pressing the button, then it will carry out the "go up" command once, and that's it.

So I've changed "is_action_pressed" to "is_action_just_pressed", and if we try it out in play mode...


Well, now we only jump once, but if I press the jump button again while in mid-air, the character will go even further upwards. It's like they're not waiting it hit the ground before they can jump again. We can fix this by adding another command to our if statement, the "is_on_floor" command, which we talked about earlier.

But... how do we add multiple commands to one if statement? Well, between the commands, we type in two ampersands ("&&") - this is what Godot understands as "and". The whole code in question looks like this:

if Input.is_action_just_pressed("jump") && is_on_floor: 
velocity.y -= jumpVelocity

The if statement now reads "if we press the jump button, and we're on the floor, our velocity on the Y axis goes down by our jumpVelocity".

So now if we try it out in play mode, our game runs like this:


It, uh... well, it works, but it's very floaty. The character doesn't fall down, they just leisurely float to the ground - maybe my default gravity setting is a little too low. Also, for some reason it hasn't actually fixed the whole "jumping while in mid-air" problem. It should, I don't think there's anything wrong with the code, but it's just as if Godot just doesn't recognize the tiles I've laid down as a "floor".

Unfortunately, this is where our tutorial here comes to an end (well, it goes on a little about adding sprite animations, but I won't cover that right now), so I'm just going to have to find some other way of addressing it - if there's a stupidly obvious solution or mistake I've made, let me know in the comments.

All the same, this is what the entire code for the character looks like:

extends CharacterBody2D

@export var moveSpeed = 150
@export var jumpVelocity = 400
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta):
if not is_on_floor():
velocity.y = gravity * delta
if Input.is_action_just_pressed("jump") && is_on_floor:
velocity.y -= jumpVelocity
velocity.x += Input.get_axis("left", "right") * moveSpeed
move_and_slide()

Method 3

This next tutorial comes from DevWorm's "How to Create SMOOTH Platformer Player Movement in Godot 4". This one begins with a code template that's completely empty save for the "extends CharacterBody2D" title at the top.

This code is for a little more advanced and precise 2D movement, which makes use of things like acceleration and friction. That's why, as well as our speed, jumpPower and gravity variables, we've also included variables for acceleration and friction.


But what does "const" mean, and why are we writing that instead of "var" like we usually do? Well, "const" is short for "constant", which is a type of variable that never changes. These are for variables that we know is going to be the same no matter what happens in the game - you could just leave them as "var"s if you want to, but making some of the variables constants lets us know which ones we do and don't want to change if we have a lot of variables in our script.

Next, we create a "_physics_process(delta)" function once again, since we're going to be dealing with movement physics. But now, the tutorial has created another function underneath it, that looks like this:

func input() -> Vector2:

So we've created a new function, called it "input", and added a weird arrow to it ("->"). This arrow, apparently, makes the function return a specific value type - in this case it will return everything it works out as a Vector2. What does all this mean? No idea.

But next we add in a new variable inside of our input function that looks like this:

var inputDirection = Vector2.ZERO

So we've made a new variable for the function, called it "inputDirection", and assigned its value to be Vector2.ZERO (as in 0 on the X axis and 0 on the Y axis).

Now, since we've made our "inputDirection" variable into a Vector2, that means we've given it an X and a Y, just like with velocity. We can use this along with the "get axis" command we talked about earlier to turn this X value into 1 or -1 using our input commands:

 inputDirection.x = Input.get_axis("left", "right")

All this means the whole "input" function, which we said will return as a Vector2, will return with either -1 if we're pressing left, or 1 if we're pressing right.

Normalization

Now, underneath this line, we've written this:

 inputDirection = inputDirection.normalized()

What is "normalized"? It's a command that makes sure whatever Vector2 we get is always going to be either 1 or -1. This has implications for diagonal movement - if we were moving diagonally, then we'd be moving both 1 on the X axis and 1 on the Y. Problem is, 1 and 1 make 2, and Godot will actually add these two together in its vector calculations, meaning we'd be going faster diagonally than we would moving normally. "Normalized" just makes sure that this doesn't happen by keeping the overall result of the Vector2 at 1, no matter what's going into it.

Finally, we've added a line that says "return inputDirection" right at the end of our input function - meaning all the sums in the function will come together to add up to the Vector2 result we're looking for in the title of the function - either -1 on the X axis or +1.

Now we've created a new function, this time called "func accelerate(direction):". This function will be fiddling around with our acceleration variable. I'll try and explain why we put "(direction)" at the end of its name in a minute.

Move Toward

Inside this funciton, we've written this command:

velocity = velocity.move_toward(speed * direction, acceleration)

"Move_toward". That's a new thing. What does that do?

"Move toward" - as it's being used here - is a command that takes one side of a Vector2 (and remember that velocity is itself by default a Vector2 in Godot) and moves it towards the other side of the vector by whatever sum we put in the brackets. So, for example, say we had a Vector2 where one of those vectors was 4 and the other was 8. "Move toward" would take the 4 and make into 8 by increments of whatever we put in the brackets - so if I put 2 in the brackets, Godot would keep adding 2 to the 4 until it turned it into 8.

In this case, the sum in our brackets is our speed variable (which is 550) multiplied by our direction (which will be either 1 or -1), then adding the acceleration variable to it (50).

I think what will happen is the velocity value we get from our Input Map (which thanks to "get_axis" is either 1 or -1) will be ramped up by our move_toward command adding the speed and acceleration values to it so the resulting movement is much faster.

Now we'll declare a new function called "add_friction". This function will, of course, deal with our friction variable. This one will also have a velocity.move_toward command, but it will be laid out like this:

 velocity = velocity.move_toward(Vector2.ZERO, friction)

"Vector2.ZERO" here will just keep us moving in the direction we're already moving in (don't ask me how), and we add our friction value (which we set as 70) to it.

Now we'll make one more function that will bring all the above together to work out the overall player movement. And we only put one thing in here: "move_and_slide". 

Now we go back to our physics process function. First thing we want to do is is get our input, so we'll create a new variable for this function, call it "inputDirection" (and since it's a variable inside of one function and not split across multiple functions, it's alright if we use the same name twice), and type it out like so:

 var inputDirection: Vector2 = input()

If you remember, "input()" is the name of one of our functions. So, we've created a new variable, called it "inputDirection", made sure it's a Vector2, and made it so it equals our input function - or rather, the returned result of our input function, which will either be 1 or -1.

Does Not Equal (!=)

Underneath this, we'll write a new if statement, which will say if our inputDirection's Vector2 is not 0 (since that will mean we're pressing a button), we activate our acceleration function. How do we check to see if something doesn't equal something else? Instead of writing "=", we put an exclamation point at the front ("!") to turn the equals sign into what Godot recognizes as a "does not equal" sign ("!="). The whole if statement looks like this:

if inputDirection != Vector2.ZERO:

accelerate(inputDirection)

This translates into "if our inputDirection is not 0, activate the accelerate function, but turn that 'direction' in its brackets into 'inputDirection'". I think.

When that happens, we activate the accelerate function, meaning we take our velocity and ramp it up to what works out as either 550 (right) or -550 (left). I think.

Else Statements

Now, we could add another if statement with which to apply our friction, but since we "if" we're looking for is simply if the previous "if" is not happening, we can write "else" instead of "if" to build off from this previous statement. It looks like this:

else:
add_friction()

...which means "otherwise, activate the 'add_friction' function", when we put it underneath an if statement. When the add friction function is activated, the velocity of 1 or -1 is taken and moved towards 0 by our 70 friction variable. It's all a bit elaborate and Rube-Goldberg-ish, but now it's starting to make sense.

And finally, in our physics process function, we call the player_movement function, just to activate the move_and_slide so we can actually move.


And now, if we press play to try it...


...we can see that not only do we move left and right, but our character actually speeds up from their starting position to go left and right, and slows down to a stop when we let go of a button.

Double Jumps

Oh, right, we haven't done jumping or gravity yet. To begin with, we'll go back at the top and add two more variables:

const maxJumps = 2
var currentJumps = 1


This time, we're actually going to try implementing a double-jump, so we'll make one variable that determines how many jumps we can do at once (in this case 2) and another to keep track of how many jumps we've done so far.

Now, we'll create a new function and call it "func jump():". Now we'll make an "if" statement to check if we're pressing the jump button... in fact, we'll add another if statement inside of this if statement that asks whether our currentJumps value isn't over our maxJumps value.

if Input.is_action_just_pressed("jump"):
if currentJumps < maxJumps:

So now, when the first if statement is satisfied that we've pressed the jump button, it will then ask the second if statement - is our current jumps value smaller than our max jumps value (remember "<" means "less than").

Underneath this second if statement, we add two lines:

velocity.y = jumpPower
currentJumps += 1

The first line, "velocity.Y = jumpPower", you'll recognize by now as our going-up-on-the-y-axis code. Since we made our jumpPower a negative number in the variables list ("-2000"), it will automatically know to go up instead of down.

Underneath it, we've added another line that says "currentJumps goes up by 1" (you can also write this as "currentJumps = currentJumps + 1").

Now, in an else statement underneath both if statements, we'll add our gravity value:

else:
velocity.y += gravity

So if we're not pressing the jump button, our gravity value will pull us down.

Now, if we leave the code as it is, we'll only be able to jump twice in the whole game. When we hit the ground, we want to re-fill our currentJumps meter back to one.

So, we'll add a new if statement:

if is_on_floor():
currentJumps = 1

This will reset our currentJumps variable to 1 when we hit the ground.


Now all that we need to do is call our jump function inside of our physics process function, and if we press play...


...it works very well. The character can jump twice in the air, but that's all they can manage before they come back down. Sure, they jump a little high, but that's something we can edit to our preference by changing the gravity variable.

It works a lot more smoothly for me than the method 2 code... which is starting to make me worry that I wrote it down wrong.

Here's the code in its entirety:

extends CharacterBody2D

const speed = 500
const jumpPower = -2000

const acceleration = 50
const friction = 70

const gravity = 120

const maxJumps = 2
var currentJumps = 1

func _physics_process(delta):
var inputDirection: Vector2 = input()
if inputDirection != Vector2.ZERO:
accelerate(inputDirection)
else:
add_friction()
player_movement()
jump()

func input() -> Vector2:
var inputDirection = Vector2.ZERO
inputDirection.x = Input.get_axis("left", "right")
inputDirection = inputDirection.normalized()
return inputDirection

func accelerate(direction):
velocity = velocity.move_toward(speed * direction, acceleration)

func add_friction():
velocity = velocity.move_toward(Vector2.ZERO, friction)

func player_movement():
move_and_slide()
func jump():
if Input.is_action_just_pressed("jump"):
if currentJumps < maxJumps:
velocity.y = jumpPower
currentJumps += 1
else:
velocity.y += gravity
if is_on_floor():
currentJumps = 1

Method 4

The fourth and final method that I'll look at today is the pre-built movement script template that Godot's already made for us. I stayed away from this earlier, because I wouldn't be able to understand a word, but now that I've been through the above three methods, I think I'll have a better idea of what's going on.


So, if we look at it through fresh eyes, we can see that the first thing the code gives us, other than the "extends CharacterBody2D" title, are three variables - two constants, one for speed and one for jump velocity, and one gravity variable.

We can see that our speed value here is 300 pixels a second, while our jump value is -400. Remember why it's a negative? Because Godot thinks plus is down on the Y axis and minus is up - so when we say our velocity.y = jump_velocity, it will equal -400, which means it will go up.

The third variable is gravity, which has been set to the default gravity of the scene through the "get_setting" command, which we encountered in method 2.

Now, the only function here is the physics process function, and it begins if an if statement that says "if the character is not on the floor, velocity on the Y axis goes up by our gravity variable, multiplied by delta to keep the framerate from interfering.

Below that is another if statement that deals with jumping. It says "if we're pressing (and just pressing, not pressing and holding) the jump button, and we're on the floor, our velocity on the Y axis goes up by our jump velocity (which if you remember is -400, meaning the Y velocity actually goes down by 400, and our character goes up as a result).

Next, another variable has been created, one exclusive to this function. It's a variable for direction, and its value is the get_axis -1 or 1 that is determined by us pressing the left or right buttons.

What follows is an if/else statement: if our direction variable... is active at all, our velocity on the X axis becomes that direction multiplied by our speed value. Otherwise, our velocity on the X axis moves from what it is currently down to 0, by way of our speed value (so -300 pixels each frame). This is another way of using the move_toward command that doesn't deal with multiplication - the first value in the brackets is the number we're moving from, the second is the number we're moving to, and the third is by what increments it will move towards this. I think.

And finally, at the bottom is the move_and_slide command, which lets us move around.

(By the way, if you're wondering about all the greyed-out writing, those are comments that you can add to your code by prefacing them with a "#". These comments won't affect the code itself, but they can help you keep track of where different parts of the code are and what they're doing.)

There's only a couple of things I'd like to change about this code: take a look at all those "ui_whatevers" that are showing up in the input-based commands. As it turns out, Godot already has some inputs that are pre-assigned to certain buttons. "Ui_left" is assigned to the left arrow button, for example, and "ui_right" is assigned to the right arrow button.

However, neither of these have A or D as part of their inputs - so if we run the game now, we'd only move left and right if we were pressing the left and right arrow keys. I'd rather be able to press A and D as well, so I'll change these "ui" inputs in the code to the "left", "right" and "jump" inputs that we created at the very start in the Input Map.


And, if we try it out in play mode...


...it works just fine. Or at least as fine as I can expect.

I won't lie to you - I think there might be something wrong with my tiles. That big slope right in the middle of the map? My character can go down it just fine, but they can't go up it so easily - they always get stuck at the very top. I don't know what the problem or the solution is for this - my tile creation run-through is right here, so if there's anything glaringly wrong with it, be sure to let me know so I can correct it.

That's all I'll go through for now. The videos I've linked to also have advice on how to animate your character sprite, so they're worth watching if you haven't already. Next time we'll look at coding movement in a 3D game, which is apparently more difficult, but it's also the one I'm more familiar with.

Comments

Popular posts from this blog

Waiting for Godot (to Work)

Help with Writing - Story and Scene Structure

Making a TileSet for Godot