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.
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.
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.
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
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.
Let’s organize our Player underneath a new group called “player”.
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)
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.
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.
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")
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")
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)
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.
Because we already have an AnimatedSprite2D node in our Player scene, it would automatically connect the animation to our modulate value.
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.
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.
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.
### 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)
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.
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.