DEV Community

Cover image for Let’s Learn Godot 4 by Making an RPG — Part 14: Enemy Shooting & Dealing Damage🤠
christine
christine

Posted on • Updated on

Let’s Learn Godot 4 by Making an RPG — Part 14: Enemy Shooting & Dealing Damage🤠

It won’t be fair if the player is the only entity in our game that can deal damage. That would make them an overpowered bully with no threat. That’s why in this part we’re going to give our enemies the ability to fight back, and they’re going to be able to do some real damage to our player! This process will be similar to what we did when giving our player the ability to shoot and deal damage. This time it would just be the other way around.

This part might take a while, so get comfortable, and let’s make our enemy worthy of being our enemy!


WHAT YOU WILL LEARN IN THIS PART:
· How to use the AnimationPlayer node.
· How to use the RayCast2D node.
· How to work with modulate values.
· How to copy/paste nodes, and duplicate objects.


Enemy Shooting

Previously, in our enemy script, we added some bullet and attack variables that we have not used yet. We aren’t controlling our enemies, so we need some way to determine whether or not they are facing us. We can make use of a RayCast2D node which will create a ray or line which will hit the nodes with collisions around the enemy. This ray cast will then be used to see if they are hitting the Player’s collision which is called “Player”. If they are, we will trigger the enemy to shoot at us since this means that they are facing us. This will spawn a bullet which, if it hits our player, will damage us.

Let’s add this node to our Enemy scene tree.

Godot RPG

You will see that a ray or arrow is now coming from your enemy. You can change the length of this ray in the Inspector panel. I’ll leave mine at 50 for now.

Godot RPG

We want to move this ray in the direction that our Enemy is facing. Since we do all of our movement code in our *_physics_process() *function, we can just do this there. We will turn the ray cast in the direction of our enemy, times the value of their ray cast arrow length (which for me is 50). This is the extent that they’ll be able to hit other collisions.

    ### Enemy.gd

    extends CharacterBody2D

    # Node refs
    @onready var player = get_tree().root.get_node("Main/Player")
    @onready var animation_sprite = $AnimatedSprite2D
    @onready var animation_player = $AnimationPlayer
    @onready var timer_node = $Timer
    @onready var ray_cast = $RayCast2D

    # older code

    # ------------------------- Movement & Direction ---------------------
    # Apply movement to the enemy
    func _physics_process(delta):
        var movement = speed * direction * delta
        var collision = move_and_collide(movement)

        #if the enemy collides with other objects, turn them around and re-randomize the timer countdown
        if collision != null and collision.get_collider().name != "Player":
            #direction rotation
            direction = direction.rotated(rng.randf_range(PI/4, PI/2))
            #timer countdown random range
            timer = rng.randf_range(2, 5)
        #if they collide with the player 
        #trigger the timer's timeout() so that they can chase/move towards our player
        else:
            timer = 0
        #plays animations only if the enemy is not attacking
        if !is_attacking:
            enemy_animations(direction)
        # Turn RayCast2D toward movement direction  
        if direction != Vector2.ZERO:
           ray_cast.target_position = direction.normalized() * 50
Enter fullscreen mode Exit fullscreen mode

If you enable your collision visibility in your Debug menu, and you run your game, you will see your enemies run around with a ray cast that hits any collision that is in the direction that they’re facing.

Godot RPG

Godot RPG

Let’s organize our Player underneath a new group called “player”.

Godot RPG

Now we can change our process() function to spawn bullets and play our enemy’s shooting animation if they are colliding with nodes in the “player” group. This whole process is similar to our Player code under ui_attack — without the time calculation.

    ### Enemy.gd

    # older code

    #------------------------------------ Damage & Health ---------------------------
    func _process(delta):
        #regenerates our enemy's health
        health = min(health + health_regen * delta, max_health)
        #get the collider of the raycast ray
        var target = ray_cast.get_collider()
        if target != null:
            #if we are colliding with the player and the player isn't dead
            if target.is_in_group("player"):
                #shooting anim
                is_attacking = true
                var animation  = "attack_" + returned_direction(new_direction)
                animation_sprite.play(animation)
Enter fullscreen mode Exit fullscreen mode

SPAWNING BULLETS

We will also spawn our bullet in our func _on_animated_sprite _finished(): function, because only after the shooting animation has played do we want our bullet to be added to our Main scene. Before we can go about instantiating our scene, we need to create a Bullet scene for our enemy. This is because our existing Bullet scene is being told to ignore collisions with the player, so it would just be easier to duplicate our existing scene and swap out the code to ignore the enemy.

Go ahead and duplicate both the Bullet scene and script and rename these to EnemyBullet.tscn and EnemyBullet.gd.

Godot RPG

Rename your new duplicated scene root to EnemyBullet and attach the EnemyBullet script to it. Also reconnect the Timer and AnimationPlayer’s signals to the EnemyBullet script instead of the Bullet script.

Godot RPG

Godot RPG

In the EnemyBullet script, swap around the “Player” and “Enemy” strings in your on_body_entered() function.

    ### EnemyBullet.gd

    # older code

    # ---------------- Bullet -------------------------
    # Position
    func _process(delta):
        position = position + speed * delta * direction

    # Collision
    func _on_body_entered(body):
        # Ignore collision with Enemy
        if body.is_in_group("enemy"):
            return
        # Ignore collision with Water
        if body.name == "Map":
            #water == Layer 0
            if tilemap.get_layer_name(Global.WATER_LAYER):
                return
        # If the bullets hit player, damage them
        if body.is_in_group("player"):
            body.hit(damage)
        # Stop the movement and explode
        direction = Vector2.ZERO
        animated_sprite.play("impact")
Enter fullscreen mode Exit fullscreen mode

Now in your Global script, preload the EnemyBullet scene.

    ### Global.gd

    extends Node

    # Scene resources
    @onready var pickups_scene = preload("res://Scenes/Pickup.tscn")
    @onready var enemy_scene = preload("res://Scenes/Enemy.tscn")
    @onready var bullet_scene = preload("res://Scenes/Bullet.tscn")
    @onready var enemy_bullet_scene = preload("res://Scenes/EnemyBullet.tscn")
Enter fullscreen mode Exit fullscreen mode

We can now go back to our Enemy scene and spawn our bullet in our func _on_animated_sprite_2d_animation_finished() function. We’ll have to create another instance of our EnemyBullet scene, where we will update its damage, direction, and position as the direction the enemy was facing when they fired off the round, and the position 8 pixels in front of the enemy — since we want them to have a further shooting range than our player.

    ### Enemy.gd

    # older code

    # Bullet & removal
    func _on_animated_sprite_2d_animation_finished():
        if animation_sprite.animation == "death":
            get_tree().queue_delete(self)    
        is_attacking = false
        # Instantiate Bullet
        if animation_sprite.animation.begins_with("attack_"):
            var bullet = Global.enemy_bullet_scene.instantiate()
            bullet.damage = bullet_damage
            bullet.direction = new_direction.normalized()
            # Place it 8 pixels away in front of the enemy to simulate it coming from the guns barrel
            bullet.position = player.position + new_direction.normalized() * 8
            get_tree().root.get_node("Main").add_child(bullet)
Enter fullscreen mode Exit fullscreen mode

DAMAGING PLAYER

Now we need to go ahead and add a damage function in our Player script so that our body.hit(damage) code in our EnemyBullet script can work. Before we do this, we’ll also go ahead and add that damage animation that we added to our Enemy to our Player. You can recreate it if you want to practice working with the AnimationPlayer, but I’m just going to copy the node from my Enemy scene and paste it into my Player scene.

Godot RPG

Godot RPG

Because we already have an AnimatedSprite2D node in our Player scene, it would automatically connect the animation to our modulate value.

Godot RPG

In our code, we can go ahead and create our damage function. This function is similar to the one we created in our enemy scene, where we get damaged after being hit by a bullet. The red “damage” indicator animation also plays upon the damage, and our health value gets updated. We are not going to add our death functionality in this part, because we want to implement that in the next part along with our game-over screen.


    ### Player.gd

    extends CharacterBody2D

    # Node references
    @onready var animation_sprite = $AnimatedSprite2D
    @onready var health_bar = $UI/HealthBar
    @onready var stamina_bar = $UI/StaminaBar
    @onready var ammo_amount = $UI/AmmoAmount
    @onready var stamina_amount = $UI/StaminaAmount
    @onready var health_amount = $UI/HealthAmount
    @onready var animation_player = $AnimationPlayer

    # older code

    # ------------------- Damage & Death ------------------------------
    #does damage to our player
    func hit(damage):
        health -= damage    
        health_updated.emit(health, max_health)
        if health > 0:
            #damage
            animation_player.play("damage")
            health_updated.emit(health)
        else:
            #death
            set_process(false)
            #todo: game overYour final code should look like this.
Enter fullscreen mode Exit fullscreen mode

Now if you run your scene, your enemy should chase you and shoot at you, and when the bullet impacts your player node, it should decrease the player’s health value.

Godot RPG

We also need to connect our Animation Player’s animation_finished() signal to our main script and reset our value there so that it will reset the modulate even when the animation gets stuck hallway through completion.

Godot RPG


    ### Player.gd

    func _ready():
        # Connect the signals to the UI components' functions
        health_updated.connect(health_bar.update_health_ui)
        stamina_updated.connect(stamina_bar.update_stamina_ui)
        ammo_pickups_updated.connect(ammo_amount.update_ammo_pickup_ui)
        health_pickups_updated.connect(health_amount.update_health_pickup_ui)
        stamina_pickups_updated.connect(stamina_amount.update_stamina_pickup_ui)

        # Reset color
        animation_sprite.modulate = Color(1,1,1,1)

    func _on_animation_player_animation_finished(anim_name):
        # Reset color
        animation_sprite.modulate = Color(1,1,1,1)
Enter fullscreen mode Exit fullscreen mode

Congratulations, you now have an enemy that can shoot your player! Next up, we’re going to be giving our player the ability to die, and we’ll be implementing our game over system. Remember to save your project, and I’ll see you in the next part.

Your final code for this part should look like this.

Buy Me a Coffee at ko-fi.com


FULL TUTORIAL

Godot RPG

The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.

If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊

You can find the updated list of the tutorial links for all 23 parts in this series here.

Top comments (0)