*You can find the links to the previous parts at the bottom of this tutorial.
In the previous part, we created our base collisions and terrains for our game’s border. I decided to break our tilemaps section into separate parts to prevent information overload. If this is your first time working with the TileMap node in Godot, don’t worry, I promise it gets easier the more you practice. That is why I encourage you to take the time at the end of each part to revise what you’ve done. In this part, we will create our ladder, as well as the rest of our platforms.
WHAT YOU WILL LEARN IN THIS PART:
- How to create Area2D scenes.
- How to connect Area2D signals.
- How to create a Global Autoload Singleton.
LADDER CREATION
Before we draw in our other platforms that our player can jump on, we first need to create our ladders. We’ll do this in a new scene. Let’s create a new scene with an Area2D node as its root node. This node creates a 2D area that detects CollisionObject2D nodes that are overlapping, entering, or exiting its shape. We will use this Area2D node to detect if our Player scene is entering its collision shape, and if it is true, we will signal the is_climbing variable to be true, which in turn allows our player to climb.
There is a warning on this node because it has no shape, so assign it a CollisionShape2D node with a RectangleShape2D as its shape.
Let’s rename our root node to “Ladder”. Save this scene underneath your Scenes folder.
Let’s visualize our Ladder by adding a Sprite2D node to our Ladder scene. Assign the image “res://Assets/wood_set/ladder/28x128/2.png” to its texture.
Now, fix the collision shape to outline the ladder.
Attach a new script to your Ladder scene and save this script in your Scripts folder.
In this script, we will get a reference to our Player’s is_climbing variable. If our Player enters our ladder’s collision, we will trigger their is_climbing variable to be true. If they exit our ladder’s collision, we will trigger their is_climbing variable to be false. We can trigger these changes using our Area2D node’s body_entered() and body_exited() signals. The body_entered() signal is emitted when the received body (our player) enters this area. The body_exited() signal is emitted when the received body exits this area. Let’s connect both of these signals to our Ladder script.
This will create two new functions, _on_body_exited() and _on_body_entered(). Now, we can go about this by getting a reference to our player node via our Loading resource, but let’s do it a better way. We will create a Global script which will be added to our AutoLoad Singleton. The Singleton pattern is a useful tool for solving the common use case where you need to store persistent information between scenes. This will allow us to access variables from our Global script from any other scene or script in our game — without loading them as a resource first!
Let’s create a new script in our Scripts folder, and call it “Global”.
Now, let’s add this to our Autoload resources so that we can access this script’s values globally. To add it to your Autoload resource, head into your Project Settings > Autoload, and add the Global.gd file to the list.
Next, we’ll need to do some refactoring in our Player script. I want us to move our is_climbing and is_attacking variables from our Player script to our Global script.
### Global.gd
extends Node
#movement states
var is_attacking = false
var is_climbing = false
Now, in our player script, let’s replace our variables with the references from our Global singleton (i.e., replace is_climbing with Global.is_climbing, and is_attacking with Global.is_attacking).
### Player.gd
extends CharacterBody2D
#player movement variables
@export var speed = 100
@export var gravity = 200
@export var jump_height = -100
#movement and physics
func _physics_process(delta):
# vertical movement velocity (down)
velocity.y += gravity * delta
# horizontal movement processing (left, right)
horizontal_movement()
#applies movement
move_and_slide()
#applies animations
if !Global.is_attacking:
player_animations()
#horizontal movement calculation
func horizontal_movement():
# if keys are pressed it will return 1 for ui_right, -1 for ui_left, and 0 for neither
var horizontal_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
# horizontal velocity which moves player left or right based on input
velocity.x = horizontal_input * speed
#animations
func player_animations():
#on left (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_left") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
#on right (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_right") || Input.is_action_just_released("ui_jump"):
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
#on idle if nothing is being pressed
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
#singular input captures
func _input(event):
#on attack
if event.is_action_pressed("ui_attack"):
Global.is_attacking = true
$AnimatedSprite2D.play("attack")
#on jump
if event.is_action_pressed("ui_jump") and is_on_floor():
velocity.y = jump_height
$AnimatedSprite2D.play("jump")
#on climbing ladders
if Global.is_climbing == true:
if Input.is_action_pressed("ui_up"):
$AnimatedSprite2D.play("climb")
gravity = 100
velocity.y = -200
#reset gravity
else:
gravity = 200
Global.is_climbing = false
#reset our animation variables
func _on_animated_sprite_2d_animation_finished():
Global.is_attacking = false
Global.is_climbing = false
With our Global variables created, we can go back to our Ladder script and set our is_climbing values to true or false if the Player scene enters/exits the Area2D body. We can do this by checking if the body entered has the name of “Player”, which is the name of our Player scene’s root node, and if true, we set the boolean values accordingly.
### Ladder.gd
extends Area2D
#sets is_climbing to true to simulate climbing
func _on_body_entered(body):
if body.name == "Player":
Global.is_climbing = true
#sets is_climbing to false to simulate climbing
func _on_body_exited(body):
if body.name == "Player":
Global.is_climbing = false
Now, we can instance our Ladder scene in our Main scene. Drag it close to your player so that we can test if our is_climbing value is changing.
Now if you run your scene, and your player runs into the ladder area, it should allow you to climb if you press W or UP on your keyboard. You can change the velocity.y value of this “climbing” input to be higher or lower if you want. You’ll notice that if we press other inputs to run whilst climbing, our player will transition into those animations instead of staying in the climbing animation. Let’s fix this by disabling those animations if our player is climbing.
### Player.gd
#older code
#movement and physics
func _physics_process(delta):
# vertical movement velocity (down)
velocity.y += gravity * delta
# horizontal movement processing (left, right)
horizontal_movement()
#applies movement
move_and_slide()
#applies animations
if !Global.is_attacking || !Global.is_climbing:
player_animations()
Your code should now look like this.
Since we’ll be adding multiple ladders to our scene, let’s organize our Ladders to be children nodes of a Node2D node. Add a new Node2D node and rename it to “Ladders”. We can drag in our existing Ladder scene to be a child node of our Ladders node.
PLATFORM CREATION
With our Ladder created, we can finally let our creativity flow because it is time to create our level. We already have our Platform_Floor terrain, which we will now use to build the rest of the floors that lead up to our exit.
Here is the layout plan for our game:
We will have our barrel spawner at the top. Instead of barrels, our spawner will spawn bombs that will roll on a certain path.
We will also have smaller obstacle spawners at the sides, who will throw boxes at our player to make the game harder. These boxes will also follow a certain path.
We will have live, attack, and score boost pickups scattered randomly on our level as well.
Finally, we will have a player spawn start point and an end point which will complete the level.
Here is an example of what this level could look like:
Keep these objects in mind when you create your level. Let’s go ahead and draw in our platform floors. Make sure you have enough space for your ladders and try to draw the floor 3 tiles above your Player’s head. There should be enough space for your player to jump, as well as cross platforms without it being too easy or too widely gapped.
To add more ladders, just instance new scenes. You can change the height of the ladders by changing its y-scale, which you can find in the node’s Inspector panel underneath Transform > Scale. Remember to unlink the chain so that it only changes the y values. There should also be adequate space for your player to climb the ladders without being blocked by your Platform’s collisions. Also, remember to move your Ladder nodes behind your TileMap node.
This is what I ended up creating for my first level:
Run your scene to test if your player can get from the start point to the endpoint smoothly, and make fixes where needed.
Troubleshooting: My Player Continues Running When Stopping In A Ladder!
We need to check if anything is pressed again in our ui_climbing input. If nothing is pressed, our animation should reset to our idle animation, but if we’re climbing, our climbing animation should play!
### Player.gd
#older code
#singular input captures
func _input(event):
#older code
#on climbing ladders
if Global.is_climbing == true:
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
if Input.is_action_pressed("ui_up"):
$AnimatedSprite2D.play("climb")
gravity = 100
velocity.y = -160
Congratulations on creating the platform for your first level! In the next part, we will be adding some decorations to our level to make it feel a bit more complete, such as a background with a few windows. We will create our second level as well. We will populate these levels later on with our bomb spawners, box spawners, and pickup items — but for now, let’s focus on creating the base for our levels.
Now would be a good time to save your project and make a backup of your project so that you can revert to this part if any game-breaking errors occur. Go back and revise what you’ve learned before you continue with the series, and once you’re ready, I’ll see you in the next part!
Next Part to the Tutorial Series
The tutorial series has 24 chapters. I’ll be posting all of the chapters in sectional daily parts over the next couple of weeks. You can find the updated list of the tutorial links for all 24 parts of this series on my GitBook. If you don’t see a link added to a part yet, then that means that it hasn’t been posted yet. Also, if there are any future updates to the series, my GitBook would be the place where you can keep up-to-date with everything!
Support the Series & Gain Early Access!
If you like this series and would like to support me, you could donate any amount to my KoFi shop or you could purchase the offline PDF that has the entire series in one on-the-go booklet!
The booklet gives you lifelong access to the full, offline version of the “Learn Godot 4 by Making a 2D Platformer” PDF booklet. This is a 451-page document that contains all the tutorials of this series in a sequenced format, plus you get dedicated help from me if you ever get stuck or need advice. This means you don’t have to wait for me to release the next part of the tutorial series on Dev.to or Medium. You can just move on and continue the tutorial at your own pace — anytime and anywhere!
This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.