*You can find the links to the previous parts at the bottom of this tutorial.
Previously we set up our Health pickup with our UI for it. In this part, we’ll do the same for our attack boost pickup. Our attack pickup will activate a timer that will allow our player to destroy bombs and boxes for a short amount of time. We’ll then also allow our player to use their attack animation only if the attack timer is active.
WHAT YOU WILL LEARN IN THIS PART:
- How to connect custom signals.
- How to work with RayCast2D nodes.
STEP 1: ATTACK TIMER COUNTDOWN
In our Player scene, let’s add a new Timer node. This will serve as our countdown timer for our attack boost. If the timer is active, it will allow our player to attack. If the timer runs out we will disable our player from attacking.
Rename the timer to “AttackBoostTimer” and connect its timeout() signal to your script. We will use this timeout to reset our Global.is_attacking variable back to false. It will only do this if our timer’s wait_time reaches 0.
Our wait_time will be how long our attack boost is enabled in seconds. Change this value to something higher, such as 1000.
Now in our Player.gd script, let’s define a variable that will hold our wait_time value. When our Global.is_attacking variable is true, we will countdown this wait_time to 0.
### Player.gd
#older code
#seconds allowed to attack
var attack_time_left = 0
func _ready():
current_direction = -1
#set our attack timer to be the value of our wait_time
attack_time_left = $AttackBoostTimer.wait_time
We’ll also create a new signal that will, later on, emit our attack_time_left variables value when it changes in our UI script for our Attack element (we still need to create this).
### Player.gd
#older code
#custom signals
signal update_lives(lives, max_lives)
signal update_attack_boost(attack_time_left)
Now in our on_attack_boost_timer_timeout() function, we will set our Global.is_attacking value back to false.
### Player.gd
#older code
#attack boost timer timeout (when it reaches 0)
func _on_attack_boost_timer_timeout():
#sets attack back to false if the time on boost runs out
if attack_time_left <= 0:
Global.is_attacking = false
This will stop our player from being able to hitboxes or play their animation for the ui_attack input. Let’s update our ui_attack input to only play our animation if Global.is_attacking is true.
### Player.gd
#older code
#singular input captures
func _input(event):
#on attack
if Input.is_action_just_pressed("ui_attack"):
if Global.is_attacking == true:
$AnimatedSprite2D.play("attack")
We also need to update our _on_animated_sprite_2d_animation_finished() function to only reset our Global.is_attacking back to false if our attack_time_left value is equal to “0”. If we don’t do this, it will stop our timer before we reach 0 on our countdown since it will reset our value after our attack animation plays.
### Player.gd
#older code
#reset our animation variables
func _on_animated_sprite_2d_animation_finished():
if attack_time_left <= 0:
Global.is_attacking = false
set_physics_process(true)
We will set our Global.is_attacking value to true if our player picks up an Attack Boost pickup.
### Player.gd
#older code
#adds pickups to our player and updates our lives/attack boosts
func add_pickup(pickup):
#increases life count if we don't have 3 lives already
if pickup == Global.Pickups.HEALTH:
if lives < max_lives:
lives += 1
update_lives.emit(lives, max_lives)
#temporarily allows us to destroy boxes/bombs
if pickup == Global.Pickups.ATTACK:
Global.is_attacking = true
#increases our player's score
if pickup == Global.Pickups.SCORE:
pass
If our Global.is_attacking variable is true, it will start counting down until our timer reaches 0. If our timer reaches 0, our variable will be set back to false. We can update the value of our attack_time_left variable in our physics_process() function. This will emit the changes to our attack_time_left variable as it counts down 1 second every 1 second.
In the code below, if the player is attacking (Global.is_attacking is true), it subtracts 1 from the attack_time_left variable using the expression attack_time_left — 1. The “1” value is subtracted to ensure that the countdown progresses based on the actual elapsed time since the last frame. The result of the subtraction is clamped to a minimum value of 0 using max(0, attack_time_left — 1). This prevents the attack_time_left from becoming negative. If the subtraction result is less than 0, it sets attack_time_left to 0.
### 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_climbing == false:
player_animations()
#countdown for attack boost
if Global.is_attacking == true:
attack_time_left = max(0, attack_time_left - 1)
update_attack_boost.emit(attack_time_left)
print(attack_time_left)
If you run your scene now and you run through an Attack Pickup, the timer should activate and start counting down until it reaches 0. You should also not be able to attack unless the timer is active.
STEP 2: DESTROYING BOXES & BOMBS
Attacking does not destroy the boxes or bombs yet. Since our player does not have an attached weapon that has its collisions set up for detection, we’ll have to improvise with a RayCast2D node. A RayCast represents a line from its origin to its destination position, target_position. It is used to query the 2D space to find the closest object along the path of the ray.
We will use this raycast to check if our raycast is hitting our Bomb or Box collisions. If it is, and we are pressing our attack input, then the object will be removed. In your Player scene, add a RayCast2D node.
In the Inspector panel, change its target_positions to be (x: 50, y: 0). This is the length or the raycast in the x and y position. We will change its x value later on when we change it to be towards our current direction. Also, enable the option for it to Collide with Areas. This will allow us to trigger events if the RayCast collides with our Area2D nodes, which are the root nodes of our Box and Bomb scenes.
Let’s rename our Raycast to “AttackRayCast”.
Now, in our physics_process function let’s delete our Box or Bomb if it collides with the player’s raycast. We will get the colliders via the RayCast2D node’s get_collider() method. This returns the first object that the ray intersects, or null if no object is intersecting the ray.
### Player
#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_climbing == false:
player_animations()
#countdown for attack boost
if Global.is_attacking == true:
attack_time_left = max(0, attack_time_left - 1)
update_attack_boost.emit(attack_time_left)
#gets the colliders of our raycast
var target = $AttackRayCast.get_collider()
#is target valid
if target != null:
#remove box
if target.name == "Box" and Input.is_action_pressed("ui_attack"):
Global.disable_spawning()
target.queue_free()
#remove bomb
if target.name == "Bomb" and Input.is_action_pressed("ui_attack"):
Global.is_bomb_moving = false
If we were to run our scene from this alone, our Raycast will not face the direction that we are facing. It will always only face x: 50 (right), so if we try to hit a box from behind us, it will hit us, and we will lose a life. Let’s set our RayCast node’s target_posiition to face our current direction in our process() function.
### Player
#older code
func _process(delta):
if velocity.x > 0: # Moving right
current_direction = 1
elif velocity.x < 0: # Moving left
current_direction = -1
# If the direction has changed, play the appropriate animation
if current_direction != last_direction:
if current_direction == 1:
#limits
# Play the right animation
$AnimationPlayer.play("move_right")
# Set raycast direction to right
$AttackRayCast.target_position.x = 50
elif current_direction == -1:
#limits
# Play the left animation
$AnimationPlayer.play("move_left")
# Set raycast direction to left
$AttackRayCast.target_position.x = -50
We have an issue where now we can successfully get rid of our box and our bomb, but our bomb still does damage to us even after we’ve hit it. To fix this issue, we can create a new variable in our Global script that will set if we can get damaged or not.
### Global.gd
extends Node
#older variables
#can the player be damaged?
var can_hurt = true
In our Bomb and Box scripts, we need to only allow our body to take_damage() if our variable is set to true.
### Bomb.gd
func _on_body_entered(body):
#if the bomb collides with the player, play the explosion animation and start the timer
if body.name == "Player":
$AnimatedSprite2D.play("explode")
$Timer.start()
Global.is_bomb_moving = false
#deal damage
if Global.can_hurt == true:
body.take_damage()
### Box.gd
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()
#deal damage
#deal damage
if Global.can_hurt == true:
body.take_damage()
If we are taking damage, let’s ensure that our player can only get hurt if the variable is set to true in our Player scripts take_damage() function.
### Player.gd
#older code
# takes damage
func take_damage():
#deduct and update lives
if lives > 0 and Global.can_hurt == true:
lives = lives - 1
update_lives.emit(lives, max_lives)
#play damage animation
$AnimatedSprite2D.play("damage")
#allows animation to play
set_physics_process(false)
When our timer runs out after our attack boost is finished, we need to reset our can_hurt variable back to false, since we can get hurt if we aren’t attacking anymore.
### Player.gd
#older code
#attack boost timer timeout (when it reaches 0)
func _on_attack_boost_timer_timeout():
#sets attack back to false if the time on boost runs out
if attack_time_left <= 0:
Global.is_attacking = false
Global.can_hurt = true
Now in our physics_process() function, let’s update our code to disable our damage if we are pressing our input when focused on a Box or a Bomb, and if we aren’t pressing our input, we’ll reset our variable to damage us again. This will ensure that we can only not be damaged if our player is pressing the ui_attack input when focused on a target. If they aren’t pressing anything and they are focused on a target they can still be damaged.
### Player.gd
#older code
#movement and physics
func _physics_process(delta):
#older code
#countdown for attack boost
if Global.is_attacking == true:
attack_time_left = max(0, attack_time_left - 1)
update_attack_boost.emit(attack_time_left)
if Input.is_action_pressed("ui_attack"):
#gets the colliders of our raycast
var target = $AttackRayCast.get_collider()
#is target valid
if target != null:
#remove box
if target.name == "Box":
Global.disable_spawning()
target.queue_free()
#remove bomb
if target.name == "Bomb":
Global.is_bomb_moving = false
Global.can_hurt = false
else:
Global.can_hurt = true
Now if you run your scene and you run over your attack boost pickup, you should be able to hit the boxes and bombs without being damaged, but if you’re just standing there whilst the attack boost is active, you should still be damaged.
STEP 3: ATTACK BOOST UI
Now let’s display our timer value on a UI element. Copy your Health node under your UI tree and rename it to “Attack”.
Change the Sprite2D texture to “res://Assets/lightning bolt/lightning bolt/sprite_4.png”. Change its position property to be (x: 25, y: 19).
Then, change the Attack ColorRect’s Transform values to position (x: 140, y: 20).
Change the Label node’s size to be (x: 50, y: 23) and re-anchor it to the “center-right” position.
It should look like this:
Attach a new script to your Attack node and save it under your GUI folder.
Let’s do the same for this script as we did for our Health.gd script. Create a new function that will update the attack_time_left value.
### Attack.gd
extends ColorRect
#ref to our label node
@onready var label = $Label
# updates label text when signal is emitted
func update_attack_boost(attack_time_left):
label.text = str(attack_time_left)
In our ready() function, let’s connect our function to our signal.
### Player.gd
#older code
func _ready():
current_direction = -1
#set our attack timer to be the value of our wait_time
attack_time_left = $AttackBoostTimer.wait_time
#updates our UI labels when signals are emitted
update_lives.connect($UI/Health.update_lives)
update_attack_boost.connect($UI/Attack.update_attack_boost)
#show our correct lives value on load
$UI/Health/Label.text = str(lives)
Your code should look like this.
Now if you run your scene and you run through your boost, your timer should countdown and it should be reflected in your UI element.
Congratulations on setting up the base for your Attack Boost System. In the next part, we’ll be adding the final piece to our Pickup system — which is our score boost pickup. We’ll first have to set up our player’s score, and thereafter we’ll set up the ability for our player to increase their score throughout the game loop.
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.