Adding enemies
Enemies in survivors-like games are usually very simple. The difficulty comes from just how many of them there are and having to duck and dodge through the hoards while your weapons go to work.
Typically, there are a few types. But since most of the variation comes from just switching up how much health they have, how big their hitbox is, and other minor things, you can make new inherited scenes later after we build our basic enemy.
Quick Main Scene
Section titled Quick Main SceneSince we need to test out the enemy scene in conjunction with the player scene, make a main.tscn
scene with a Node2D root node.
If you pressed F5/Run Project instead of F6/Run Current Scene for your player and set your main scene to be the player scene, go to Project > Project Settings… > Run and change the main scene to your new scene.
Use the chain-looking button next to the Add Child Node button in the Scene dock to instantiate your player scene as a child of your Node2D root node.
Setting Up
Section titled Setting Up-
Make a new scene with the root node CharacterBody2D and call it
enemy.tscn
.- CharacterBody2D
- CollisionShape2D - Set shape to New RectangleShape2D
- Sprite2D - Set texture to New PlaceholderTexture2D
- CharacterBody2D
-
Size them to be slightly smaller than the player, so we can fit more in and somewhat visually distinct them from the player.
-
Set the CharacterBody2D’s Motion Mode property to Floating like you did with the player.
-
Instantiate your enemy scene in your main scene and move the instantiated enemy away from the player. Run your game with F5/Run Project and set the main scene as the main scene of the project if you haven’t already.
-
Finally, this enemy is going to need a way to reduce the player’s health. However, the player has no health variable yet. In your singleton, which you can find how to make one on the Universal Features page, add a new
player_health
variable and set it to 100.
Script
Section titled Script-
Get rid of everything below the line
func _physics_process(delta):
and abovemove_and_slide()
, but not those lines, and deletegravity
andJUMP_VELOCITY
. -
Give this script the class name
Enemy
withclass_name Enemy
beforeextends CharacterBody2D
. -
Reduce the speed from
300.0
to100.0
or150.0
. Enemies need to be slower than the player in survivors-like games because the challenge is from avoiding hoards of them while your weapons work. Not from conserving health for when an enemy unfairly tackles you out of nowhere and you can’t do anything to get away from it until your weapons finally take it down.
Moving Towards the Player
Section titled Moving Towards the Player-
To move towards the player, we’re going to need to find the player.
In your singleton, add an
@onready
variable that holds the player.In your player’s script, add the built-in
_enter_tree()
function and set the Singleton’s player_node variable to the player itself.The singleton script should look like this:
extends Nodevar player_health: int = 100@onready var player_node: Player -
The player script’s ready function should look similar to:
func _enter_tree():Singleton.player_node = self
Now, to finally edit the enemy script itself.
Before move_and_slide()
add the totally simple line:
velocity = global_position.direction_to(Singleton.player_node.global_position) * SPEED
Breakdown of this line:
First, with Singleton.player_node.global_position
we get the player’s global position, which is a Vector2 since it’s the x and y position.
Next, we use global_position.direction_to()
to get the direction to the player from the global_position
of the enemy. It gets this direction in a Vector2, in which both x and y add up to 1. We call this a normalized vector, since it’s used for simple directions instead of anything like magnitude, position, or anything else. Normalized vectors always sum up to 1.
Then, we multiply that normalized vector with SPEED
so now the vector that adds up to 1 in the direction of the player, will multiply up to 1 times the value of speed to also indicate how fast the player should move.
Since we now have a variable that says the speed in which the player should move and the direction it should be moving in, in the form of a Vector2, we can make that the velocity of the enemy.
Anyway, the enemy should now move towards the player. Hooray!
Dealing Damage
Section titled Dealing Damage-
Start by making a damage constant value at the top of your script next to the speed constant. Make it 5 and name it
DAMAGE
in all caps since it’s a constant.Every time you call
move_and_slide()
it’s taking the velocity variable of the CharacterBody2D, then moves the player that much, then checks for collisions and slides the player along any collisions it makes, then it stores what collisions have been made. -
If we look at the CharacterBody2D in Search Help, we can see a list of properties and methods that might be helpful:
void apply_floor_snap ( )float get_floor_angle ( Vector2 up_direction=Vector2(0, -1) ) constVector2 get_floor_normal ( ) constVector2 get_last_motion ( ) constKinematicCollision2D get_last_slide_collision ( )Vector2 get_platform_velocity ( ) constVector2 get_position_delta ( ) constVector2 get_real_velocity ( ) constKinematicCollision2D get_slide_collision ( int slide_idx )int get_slide_collision_count ( ) constVector2 get_wall_normal ( ) constbool is_on_ceiling ( ) constbool is_on_ceiling_only ( ) constbool is_on_floor ( ) constbool is_on_floor_only ( ) constbool is_on_wall ( ) constbool is_on_wall_only ( ) constbool move_and_slide ( )Here we can see there’s the
get_slide_collision
andget_slide_collision_count
methods. Look at the description ofget_slide_collision
there’s an explanation and a block of code showing you how to use both methods in conjunction to go through all the collided objects.If you’re eagle-eyed, you might have also spotted the
get_last_slide_collision
method. We don’t want to use this because it only returns the last collision made, not all the collisions made. With how many enemies the game has it’s not going to be unlikely that the enemy will hit the player and then slide into another enemy in the same frame. Just checking the last slide collision won’t work, so we’ll use the other two-slide collision methods to check every collision made. -
Use the provided code-block in the
get_slide_collision
description and modify it so that instead of the print statement in the for-loop, add this:if collision.get_collider() is Player:Singleton.player_health -= DAMAGEqueue_free() -
For this simple game, we’re just going to run
queue_free()
so the player does damage and then disappears, so the next enemy can deal damage. If we didn’t do this, it would deal damage every single frame the player is touching the enemy and it would deal insane amounts of damage very quickly.
If you want, you can use a Timer node, set it to one shot, replace queue_free()
with starting the timer, and then check if the timer is stopped as part of the if collision.get_collider() is Player:
if statement. This would stop the enemy from doing insane damage and limit it to once whenever the timer’s value was set at.
In theory, what we have now though should work. Of course, we don’t have anything to tell us how much health the player has, so let’s quickly make death and a health bar.