*You can find the links to the previous parts at the bottom of this tutorial.
In the previous part, we created the first level for our game. In this part, we will be finishing up our level creation section of this tutorial by creating our second level, along with adding the final touches to our existing level.
WHAT YOU WILL LEARN IN THIS PART:
- How to work with Timer nodes.
- How to create a moving platform.
- How to work with Tilemap Layers.
LEVEL 1 FINAL TOUCHES
Right now when we run our level, we still have that grey default background at the back of our map. We will change this background to a tiled background with the help of Tilemap layers. Layers allow us to distinguish foreground tiles from background tiles for better organization.
By default, a TileMap node automatically has one premade layer. In your Main scene, select your Tilemap “Level” node and navigate to the Layers property in your Inspector panel.
You will see that there is an existing layer, but it has no name. Currently, all of the tiles that we already painted onto our Tilemap exist on this layer. We want to rename this layer to our “Foreground” layer. You will notice that your Layers property in your Tilemap panel below now reflects this change.
We want our background to show behind our Foreground layer, so let’s add a New Element to our Layers property and call it “Background”. You will now be able to switch between your layers.
Since we want this Background layer to be behind our Foreground layer, we need to rearrange our layers by dragging our Background layer above our Foreground layer property. You can do this by holding and dragging the stacked line icon next to your layer element.
Now in your Layers option in your Tilemap panel, make sure your Background layer is selected. If you select a layer, all the tiles that exist on the other layers should be darkened out.
Draw in your background. I’m using the plain dark tiles, but you can use any other tile you like. Just make sure that the tile you select does not have any collisions added to it — because this will block the player from running!
Also, ensure that you do not paint over your Ladders. We will add another Tilemap node to fill in the spaces behind the Ladders. An alternative fix to this is to drag your Ladders node above your Tilemap node in your Main scene tree — but I don’t like how the ladders stick over the map!
Let’s fill in this gap. Duplicate your Tilemap node in your Main scene, and drag it behind your Ladders node. We can rename this duplicated node to “Background”
Delete this Tilemap’s “Foreground” layer, and simply draw in the rest of your Background to get rid of those awkward grey gaps! (Or simply remove the existing background from your Level’s node, and just draw it in here).
You should end up with something like this:
Now, let’s add a few windows to our scene as decorations! In your “Level” Tilemap node, add a new Layer titled “Decorations”.
In your TileSet panel below, we need to create a new tilesheet for our Decorations. Navigate to “res://Assets/Kings and Pigs/Sprites/14-TileSets/Decorations (32x32).png” and drag the image into your Tiles property.
In your Tilemap panel, draw in your windows on your Decorations layer. You can also add other decorations — such as shelves or candles. You can find more decoration objects in the assets provided underneath the “res://Assets/Kings and Pigs/Sprites/7-Objects/” directory.
I ended up creating something like this:
LEVEL 2 + MOVING PLATFORM
Now that we have our first level set up, we can go ahead and create our second level. If you want — and you have the time — you can create levels 3 & 4 as well. For now, we’ll only create level 2. For this, we can simply duplicate our Main scene. We can name this “Main_2”.
In our next level, we already have our Player instanced. This is where our player will spawn when we change our scene to the next level, so make sure they’re placed at the start of your level.
Here is the layout plan for our second level:
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.
We will have walls that move up and down, blocking the player’s passage.
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:
In this level, we’ll add more bomb spawners at the sides to make it more difficult, we’ll also be creating a moving wall that will block our player from passing every 3 or so seconds. Now, before we go ahead to create our level, let’s create our moveable wall or platform first. For this, we’ll need to create a new scene with an Area2D node as its root. You can also use a CharacterBody2D node if you want — we just need a root node that can handle collisions and movement.
Rename this node to “Platform” and save the scene underneath your Scenes folder.
Assign this new scene with a CollisionShape2D node with a shape of type RectangleShape2D. We’ll fix this shape’s width and height later on.
Now, copy and paste your Level TileMap from your Main scene to your Platform scene. We copied this node because we want the autotiles (terrains) we created previously. Delete all of the Layers we added (Background, Foreground, and Decorations) so that we can have a clean slate to draw on.
Draw a wall that is 2 tiles wide and 5 tiles high. Also, fix the collision shape to outline it.
As a tip for the future, make sure that your Tilemap is renamed to something other than “Wall” or “Level” as this will cause the bomb to explode when interacting with this Platform later on.
Now, in your Platform Scene, connect a new script to it and save this script underneath your Scripts folder.
We also need to add one last node to our scene — a Timer node. The Timer node creates a countdown timer that counts down a specified interval and emits a signal on reaching 0. We want this timer to change the movement state of our wall after ‘x’ seconds.
We also want to connect our Timer node’s timeout() signal to our script. This signal will emit each time our timer reaches 0. So our wall will move, count down to 0, and then move again. This timer will trigger those movement states to change. In your Timer node’s Signals panel, connect the timeout() signal to your script.
This will create a func _on_timer_timeout(): function in your script. In this function, we will change our platforms moving states. Before we do that, we first need to define our movement states. Our platform will move vertically, waiting at the bottom, then moving up, waiting at the top, and moving back down, in a loop.
To change our platform’s movement states, we need to create an Enumeration which will store these states. Enums also referred to as enumerations, are a data type that contains a fixed set of constants.
### Platform.gd
extends Area2D
#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}
Next, in our _on_timer_timeout(): function, we can change our states. If the platform is in the WAIT_AT_TOP state, it should change its state to MOVING_DOWN. If the platform is in the WAIT_AT_BOTTOM state, it should change its state to MOVING_UP. We’ll need to create a variable that captures our platform’s state when the game starts as well as when this state changes. We’ll initially set it to WAIT_AT_BOTTOM.
### Platform.gd
extends Area2D
#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}
#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM
#platform direction changes on timer timeout
func _on_timer_timeout():
if current_state == State.WAIT_AT_TOP:
switch_state(State.MOVING_DOWN)
if current_state == State.WAIT_AT_BOTTOM:
switch_state(State.MOVING_UP)
We’ll also need to capture the y position of our platform when the game starts because it is the y value that we will change when the platform’s movement state changes. Let’s create a new variable that will set the platform’s y position when the game is loaded.
### Platform.gd
extends Area2D
#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}
#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM
#movement position
var initial_position
We will set the value of this variable in the built-in ready() function because this function is called when the node is added to the scene. In this function, we will set the initial_position variable to the platform’s current y position and set its state to MOVING_UP because our current_state is WAIT_AT_BOTTOM — and we want our state to change as soon as the game loads. We can find the position of a node by accessing the node’s position property.
### Platform.gd
#older code
#sets platforms y position on game start and switches the state
func _ready():
initial_position = position.y
switch_state(State.MOVING_UP)
Now, we also need to give our platform a few other variables, which will define its movement speed (how fast it moves), movement range (how far up and down it goes), progress (if it has finished moving or not), and top and bottom wait times (how long it waits before moving up and down). We will export the variables which we want to change in the Inspector panel. This will allow us to change each instanced platform’s movement values individually — i.e., some platforms will move quicker, wait longer, or go up higher than others.
### Platform.gd
extends Area2D
#platform movement states
enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}
#captures current state of platform
var current_state = State.WAIT_AT_BOTTOM
#movement position and movement progress value
var initial_position
var progress = 0.0
#platforms movement speed and range
@export var movement_speed = 50.0
@export var movement_range = 50
#wait times
@export var wait_time_at_top = 3.0 # Time in seconds to wait at top
@export var wait_time_at_bottom = 3.0 # Time in seconds to wait at top
We need to create a function that will continuously switch the state of the platform. Depending on the new state, it should reset the platform’s movement progress, set the timer and its wait_time (which is the seconds it takes to countdown), and changes the current_state variable. We will use a match statement to check and change the states of our platform. A match statement is used to branch the execution of a program. It’s the equivalent of the switch statement found in many other languages but offers some additional features.
### Platform.gd
#older code
#changes the platforms movement states
func switch_state(new_state):
current_state = new_state
match new_state:
#if state is moving up, reset progress
State.MOVING_UP:
progress = 0.0
#if state is waiting at top, start the timer to change the state
State.WAIT_AT_TOP:
$Timer.wait_time = wait_time_at_top #will wait x seconds before moving
$Timer.start()
#if state is waiting at bottom, start the timer to change the state
State.WAIT_AT_BOTTOM:
$Timer.wait_time = wait_time_at_bottom #will wait x seconds before moving
$Timer.start()
#if state is moving down, move the platform via the speed and range defined
State.MOVING_DOWN:
progress = movement_range / movement_speed
Finally, we can call our newly created switch_state() function in our physics_process() function. This function is called every physics frame. We will use it to check the current state of the platform and move it or wait based on the state. The Lerp function is used to smoothly interpolate between the bottom and top positions.
### Platform.gd
#older code
#moves our platform
func _physics_process(delta):
match current_state:
#if its moving up
State.MOVING_UP:
progress += delta
#change its position
position.y = lerp(initial_position, initial_position - movement_range, progress / (movement_range / movement_range))
if progress >= (movement_range / movement_speed):
switch_state(State.WAIT_AT_TOP)
#if its moving down
State.MOVING_DOWN:
progress -= delta
#change its position
position.y = lerp(initial_position, initial_position - movement_range, progress / (movement_range / movement_speed))
if progress <= 0:
switch_state(State.WAIT_AT_BOTTOM)
In the script, we move our platform vertically between two points, with waiting times at the top and bottom. The movement is smooth due to the use of linear interpolation (lerp), and the states ensure the different phases of the movement are handled correctly.
Your code should look like this.
If you instance your Platform in your Main_2 scene, you can see that you can change its values in the Inspector panel. Make sure that you place it down on the floor and set its movement range to stop when it reaches the floor above it — you want the edges of your platform to connect with the floor above and below. You can test these values by pressing F6 to test out the current scene.
It should be placed like this:
And its movement range should be changed so that it stops like this:
I organized my instanced platforms in a Node2D node and changed each platform’s speed, range, and wait_time to be unique.
You can now go ahead and create your map for your second level. You can do this by painting over your existing tiles in your Level node. Remember to leave enough space for your wall to move up and down since it will block two-floor levels.
This is what I ended up creating for my second level:
If you run your scene, your player should be able to make it to the top without issues, and your Platforms should move!
Troubleshooting: Fixing The Player’s Jumping Glitch
If you’ve tested your game, you would probably have noticed a slight glitch that occurs when your player jumps and changes directions. The player jumps but if you press the inputs to turn left or right it changes directions slightly before continuing to jump. We want the player to first finish the jumping animation, and then run in the new direction, instead of changing direction mid-jump. To do this, we need to fix our player_animations() function to only run if the player is not jumping. Before we can do this, let’s create a new variable in our Global script to keep track of our jumping state.
### Global.gd
extends Node
#movement states
var is_attacking = false
var is_climbing = false
var is_jumping = false
Then, in our player_animations() function we only want to fire off our run animation if our player is not jumping.
### Player.gd
#older code
#animations
func player_animations():
#on left (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_left") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = 7
#on right (add is_action_just_released so you continue running after jumping)
if Input.is_action_pressed("ui_right") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = -7
#on idle if nothing is being pressed
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
Next, we’ll need to change the state of our is_jumping variable to true if our player is jumping. We’ll also need to reset this value back to false when our gravity value resets.
### Player.gd
#older code
#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 = -160
Global.is_jumping = true
#reset gravity
else:
gravity = 200
Global.is_climbing = false
Global.is_jumping = false
If you run your scene now your player should jump and change running animations appropriately!
Congratulations on creating your second level with moving platforms! You also learned to work with Layers, and hopefully, you ended up with a cool map! You can go ahead and create a 3rd and 4th level with more platforms, and more spaces for enemies if you’d like. Speaking of enemies, we will create our Bomb and the Bomb Spawner in the next part. This bomb spawner will spawn a bomb that will travel along a certain path and explodes if it reaches the end or if it collides with the player.
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.