*You can find the links to the previous parts at the bottom of this tutorial.
If we only had our bomb spawner, our game would be pretty easy to beat. That’s why we need some sort of secondary enemy that would make our game more difficult. In this part, we’ll be adding our secondary enemy, which is a box spawner who will throw boxes at us from the side. This spawner is just like our bomb spawner, but it will serve as a threat to us on multiple platforms.
WHAT YOU WILL LEARN IN THIS PART:
- How to change set properties via code.
- How to create independent spawners from a singular scene.
STEP 1: BOX SCENE SETUP
Our Box Scene will be the same as our Bomb Scene, so let’s duplicate our Bomb Scene and rename it to “Box”.
In our newly created Box scene, let’s detach all the existing scripts and signals from our nodes.
Now, let’s attach a new Script to our Box scene and save it under our Scripts folder.
Connect the Area2D node’s (the root node) body_entered() signal to your script.
We’ll also go ahead and connect our AnimatedSprite2D node’s animation_finished() signal to our script. We’ll get more into how we’ll be using this newly created _on_animated_sprite_2d_animation_finished() function later on.
The Box Scene should look like this:
In the AnimatedSprite2D node’s Animations panel, let’s delete the existing animation frames. Don’t delete the animations, just the existing sprites.
Change both your moving and explode animations’ FPS value to 1. For your moving animation, navigate to “res://Assets/Kings and Pigs/Sprites/08-Box/Idle.png” and crop out the singular frame for your animation.
For your explode animation, navigate to “res://Assets/Kings and Pigs/Sprites/08-Box/Hit.png” and crop out the singular frame for your animation. Disable its looping value.
STEP 2: BOXSPAWNER SCENE SETUP
Our BoxSpawner will be similar to our BombSpawner scene. Duplicate the BombSpawner scene and rename the duplicated scene to “BoxSpawner”.
In our newly created BoxSpawner scene, let’s detach all the existing scripts and signals from our nodes. We also need to remove the CannonHandler from our scene.
It should look like this:
Now, let’s attach a new Script to our BoxSpawner scene and save it under our Scripts folder.
We also need to connect our Timer node’s timeout() signal to our BoxSpawner script.
Instead of spawning our Box onto our Path added to our Main scene, we will spawn it on a Path added directly in our BoxSpawner scene. Add a new Node2D node and rename it to “BoxPath”.
Then add the following nodes — just like we did in our Main scenes — as children to your BoxPath node: Path2D > PathFollow2D & AnimationPlayer.
The BoxSpawner Scene should look like this:
In the AnimatedSprite2D nodes Animations panel, let’s delete all of the existing animations.
Create two new animations: pig_idle and pig_throw.
For your pig_idle animation, navigate to “res://Assets/Kings and Pigs/Sprites/04-Pig Throwing a Box/Idle (26x30).png” and crop out all 9 frames for your animation. Change its FPS to 9, and leave the looping value enabled.
For your pig_throw animation, navigate to “res://Assets/Kings and Pigs/Sprites/04-Pig Throwing a Box/Throwing Box (26x30).png” and crop out all 5 frames for your animation. Make sure the first frame is the same as the last frame by copying and pasting it. Change its FPS to 6, and disable its looping value
We also need to add our movement animation in our AnimationPlayer node. We did this before in our Main scene when we created our BombSpawner. Select your AnimationPlayer node in your BoxSpawner scene and create a new animation called “box_movement”.
Add a new Property Track animation and connect it to your PathFollow2D node. We want to animate the progress_ratio property of our PathFollow2D node.
Add two keyframes at the start and end of your animation. Change the Value property of your last keyframe (at time 1) to be equal to 1.
Finally, let’s change the animation time of our timeline. I changed mine to 10 since this is the speed that I want my box to move from point A to point B.
Now let’s create our path. Draw two Points on your Path2D node (select the Curve2D property in your Inspector panel to add new Points) from the start of your pig to the end of the blue border on your scene.
STEP 3: BOXSPAWNER & BOMBSPAWNER COLLISIONS
Currently, if our player runs over our BombSpawner at the top of our scene, they’ll just run through our cannon. We don’t want that to be possible. Instead, we want our BoxSpawner and our BombSpawner to be able to block our player so that they can’t just run through them.
In your BombSpawner, add a StaticBody2D node and rename it to “Collisions”. We are using this node since it can hold a CollisionShape2D node without us having to programmatically tell it what to block, and it won’t process any movement.
Add a CollisionShape2D node and outline your CannonHandler plus your cannon with it.
Do the same for your BoxSpawner scene and outline your pig with your CollisionShape.
STEP 4: BOX SCENE SCRIPTING
We need our box to only spawn at the start of the box spawner path (progress_ratio == 0). If our box hits our player, the box should be destroyed and removed from our scene. The box should then only be allowed to respawn when the path restarts (progress_ratio == 0). In our Global script, let’s create two new functions that will enable or disable our box’s spawning. In these functions, we will simply switch a boolean variable to indicate if our spawning ability is true or false.
### Global.gd
extends Node
#movement states
var is_attacking = false
var is_climbing = false
var is_jumping = false
# Indicates if box can be spawned
var can_spawn = true
#current scene
var current_scene_name
#bomb movement state
var is_bomb_moving = false
func _ready():
# Sets the current scene's name
current_scene_name = get_tree().get_current_scene().name
# Function to disable box spawning
func disable_spawning():
can_spawn = false
# Function to enable box spawning
func enable_spawning():
can_spawn = true
Now in our Box.gd script, let’s disable spawning if our box hits our player. Disabling our spawner here will prevent our boxes from respawning if it hasn’t finished following the spawner path (progress_ratio == 1). So if a box hits us in the middle of the platform it won’t respawn behind us to continue the path. It will wait until the progress_ratio == 0 again to respawn. We’ll also set our default animation on Box spawning to be our moving animation.
### Box.gd
extends Area2D
# Default animation on spawn
func _ready():
$AnimatedSprite2D.play("moving")
func _on_body_entered(body):
# If the bomb collides with the player, play the explosion animation and disable spawning
if body.name == "Player":
$AnimatedSprite2D.play("explode")
# Disable spawning in BoxSpawner
Global.disable_spawning()
If our box hits our wall, we want to remove it immediately so that our BoxSpawner can respawn a new box.
### Box.gd
extends Area2D
# Default animation on spawn
func _ready():
$AnimatedSprite2D.play("moving")
func _on_body_entered(body):
# If the bomb collides with the player, play the explosion animation and disable spawning
if body.name == "Player":
$AnimatedSprite2D.play("explode")
# Disable spawning in BoxSpawner
Global.disable_spawning()
# If the bomb collides with our Wall scene, remove so that it can be respawned
if body.name.begins_with("Wall"):
queue_free()
Then in our _on_animated_sprite_2d_animation_finished() function, we will check if our explode animation is playing, and if it’s true we will remove the box from the scene. The _on_animated_sprite_2d_animation_finished() function is called when the animation on the AnimatedSprite2D node finishes playing.
We’re removing our box immediately in our Wall conditional since we want to respawn our box immediately. We’re removing our box in our Player conditional via our _on_animated_sprite_2d_animation_finished() function because we want the explode animation to play and then only after it’s finished playing should the box be removed from our scene.
### Box.gd
#older code
#if the box's explode animation is playing, remove it from the scene
func _on_animated_sprite_2d_animation_finished():
if $AnimatedSprite2D.animation == "explode":
queue_free()
We are done with our Box script for now. In this script, we played the appropriate animations, removed the box from the scene, and disabled our spawning for when the box collides with objects.
STEP 5: BOXSPAWNER SCENE SCRIPTING
Our BoxSpawner will work similarly to our BombSpawner. Let’s define the variables (just like in our BombSpawner script) that will hold the BoxPath nodes and reference our Box scene. We’ll also initiate their variables in our ready() function.
### BoxSpawner.gd
extends Node2D
# Box scene reference
var box = preload("res://Scenes/Box.tscn")
# References to our scene, PathFollow2D path, and AnimationPlayer path
var box_path
var box_animation
# When it's loaded into the scene
func _ready():
# Initiates paths
box_path = $BoxPath/Path2D/PathFollow2D
box_animation = $BoxPath/Path2D/AnimationPlayer
#enables box movement on path on load
box_animation.play("box_movement")
#default animation on load
$AnimatedSprite2D.play("pig_throw")
Then, in our _on_timer_timeout() function, we will spawn a box instance on our box_path if our box is enabled to spawn and if no boxes have been spawned onto the path already. We did something similar to this in our BombSpawner script.
### BoxSpawner.gd
#older code
# Shoot and spawn box onto path
func _on_timer_timeout():
# Reset animation back to idle if not throwing
$AnimatedSprite2D.play("pig_idle")
# Spawns a box onto our path if there are no boxes available and can_spawn is True
if box_path.get_child_count() <= 0 and Global.can_spawn == true:
var spawned_box = box.instantiate()
box_path.add_child(spawned_box)
Now, since we’ll be putting our pigs on each side of our map to throw boxes at us from the left and right sides, we’ll need to find a way to flip our AnimatedSprite2D node around so that our pig can face the other side. We can do this by enabling/disabling the Flip_H and Flip_V values of our scene.
bool flip_h — If true texture is flipped horizontally.
bool flip_v — If true texture is flipped vertically.
### BoxSpawner.gd
#older code
# Allows us to flip our pigs around in the editor
@export var flip_h = false
@export var flip_v = false
Now if you click on the root node of your BoxSpawner, you can enable/disable their Flip_H and Flip_V values, but this won’t do anything yet.
We’ll change the flip_h and flip_v values of our AnimatedSprite2D node in our process() function. We’ll also check if our path has reached the end of the progress_ratio value (>=1) and if true, we will enable the Box’s spawning value and respawn a new box.
### BoxSpawner.gd
#older code
func _process(delta):
#allow us to flip the pigs around in editor
$AnimatedSprite2D.flip_h = flip_h
$AnimatedSprite2D.flip_v = flip_v
# Check if the boxes have reached the end of the path
if box_path.progress_ratio >= 1:
#respawn box
box_path.progress_ratio = 0
Global.enable_spawning()
box_animation.play("box_movement")
#play throwing animation if path resets
if box_path.progress_ratio == 0:
$AnimatedSprite2D.play("pig_throw")
Your code should look like this.
STEP 6: ADDING BOXSPAWNERS IN OUR LEVEL
In your Main scene, you can now instance your BoxSpawner. You can instance as many BoxSpawners as you’d like — just make sure to organize them underneath a Node2D node called “BoxSpawners” for better organization.
You can flip the sprite of your selected BoxSpawner around by enabling/disabling its flip_h and flip_v values in the Inspector panel. For the spawners on the left side of the map, I enabled my Flip H value. For the spawners on the right side of the map, I enabled my Flip H and Flip V values.
We’re enabling the spawners on the left side of the map’s Flip_H values because we need to rotate them around to -180 degrees. This is because we need their Path2D node to spawn boxes towards the right, and not the left. You can rotate the spawners that you place on the left side of your map via the rotate tool, or you can change their rotation value to -180 in the Inspector panel underneath Transform.
This is what I ended up with in my Main scene:
This is what I ended up with in my Main_2 scene:
Now if you run your scene, your boxes should spawn and your BoxSpawners should be spawning and facing in the right direction. If your player runs into the box, the box should be removed and only respawned when the path restarts (after all the other boxes reach the end of their paths).
Congratulations on creating your secondary enemy! In the next part, we’ll be setting up the functionalities for our player to be damaged by the boxes and the bombs. 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.