Learn Godot 4 by Making a 2D Platformer — Part 14: Lives, Score, & Attack Boosts #2
Tags: gamedev, beginners, programming, tutorial
Published: 2023-08-01 03:01:00

**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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*SGRX7btbXcyZXPRBv_5Fng.png)

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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*9pic25l52w3f5RsBnPe_0g.png)

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2208/1*5LDO7Stsw15z72aV5SoCdA.png)

Our wait_time will be how long our attack boost is enabled in seconds. Change this value to something higher, such as 1000.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*yDRDmCTJ5zYV1jlYkQ5MOA.png)

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.
```python
    ### 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).
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*k4whbJ8aKJjH76fqNrEDww.png)

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/3840/1*hTX706D9YmQOOe8XjKr10g.png)

## **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 ](https://docs.godotengine.org/en/stable/classes/class_raycast2d.html#description)node. A RayCast represents a line from its origin to its destination position, [target_position](https://docs.godotengine.org/en/stable/classes/class_raycast2d.html#class-raycast2d-property-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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2830/1*TKnp3IghWXIa74QD7VPAfg.png)

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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*LCWxi8sjp0vmKwRbFAN-LQ.png)

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*l9ZOKRzXIufs8dAt2QA9TQ.png)

Let’s rename our Raycast to “AttackRayCast”.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*Hw85JmaswohtklGUUSELaQ.png)

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()](https://docs.godotengine.org/en/stable/classes/class_raycast2d.html#class-raycast2d-method-get-collider) method. This returns the first object that the ray intersects, or null if no object is intersecting the ray.
```python
    ### 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.
```python
    ### 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
```
![Godot 2D Platformer](https://cdn-images-1.medium.com/max/3840/1*XgNYwBgQ6hJUwLI3V0MQMQ.png)

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/3840/1*xgs0uULOLQoFLwIySDSKNQ.png)

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.
```python
    ### 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.
```python
    ### 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()
```
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.
```python
    ### 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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/3840/1*rvlKnEh9hakbH4ojn7RenA.png)

## **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”.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*iJOecUrFubHF0NTPXaMz_Q.png)

Change the Sprite2D texture to “res://Assets/lightning bolt/lightning bolt/sprite_4.png”. Change its position property to be (x: 25, y: 19).

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2818/1*tZT-peHEHtdvcvThdDAgaQ.png)

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*73StSQGp6sIelIEWBNb1dQ.png)

Then, change the Attack ColorRect’s Transform values to position (x: 140, y: 20).

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*YIVWJE-IdHI5U_ULIi9iBw.png)

Change the Label node’s size to be (x: 50, y: 23) and re-anchor it to the “center-right” position.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*QWMAeFKv2RQv_HaFBn_HFg.png)

**It should look like this:**

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2010/1*HDFkdda2gD1hg4A26G9g_A.png)

Attach a new script to your Attack node and save it under your GUI folder.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/1*9pZAtRujg4JyJa9tsCD-yQ.png)

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.
```python
    ### 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.
```python
    ### 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](https://github.com/christinec-dev/CastleClimbersParts/tree/main/Part14).

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.

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/3840/1*MvL45SbEzlnHmV7Dz5XfdA.png)

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](https://oops-i-devd.gitbook.io/christinedevs/godot-tutorials/castle-climbers/tutorial). 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 ](https://oops-i-devd.gitbook.io/christinedevs/communication/updates-and-notifications)to the series, my GitBook would be the place where you can keep up-to-date with everything!

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/0*TLFhoNqnjMYLWAGA.png)

---

## Support the Series & Gain Early Access!

If you like this series and would like to support me, you could [donate](https://ko-fi.com/christinedevs) any amount to my KoFi shop or you could [purchase the offline PDF](https://ko-fi.com/s/75433f1d4b) 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!

![Godot 2D Platformer](https://cdn-images-1.medium.com/max/2000/0*S6n1i41QDrVlLJUy.png)

*This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.*