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
Since 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
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_healthvariable and set it to 100.
Script
Get rid of everything below the line
func _physics_process(delta):and abovemove_and_slide(), but not those lines, and deletegravityandJUMP_VELOCITY.Give this script the class name
Enemywithclass_name Enemybeforeextends CharacterBody2D.Reduce the speed from
300.0to100.0or150.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
To move towards the player, we're going to need to find the player.
In your singleton, add an
@onreadyvariable 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 Node var player_health: int = 100 @onready var player_node: PlayerThe player script's ready function should look similar to:
func _enter_tree(): Singleton.player_node = self
Statically typing means that you are making sure that a variable stays as the type you want it to be. For example, if you don't want an integer (a whole number, without decimal places) to ever have decimal places, statically type the variable with : int after the name and before setting the value.
This is very, very useful for debugging later on, and making sure you don't make a mistake.
The reason we can statically type with : Player on the player_node variable is because we set the class name of the player script to be Player. In this case, it'll help ensure we don't accidentally set any other nodes to be the singleton's player node. It also helps Godot's code editor autocomplete any variables, functions, or signals we might have in the player script.
Other common use-cases are the primitive types like bool (boolean, true or false), float (floating-point number, number with possible decimal places), String (text, defined with "quotes around it, like this"), or to set variables, like the player_node variable, with complex types like CharacterBody2D to have access to code-completion in the Godot editor.
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.
Vector maths is a large part of making any video game, but it's not very hard after you get the basic idea that a vector can represent a lot of things and begin to understand that. Godot's docs have a genuinely wonderful page about vector maths.
Check it out here! Please do read it, vectors are essential to any game developer in any engine. It's not very long, but it is quite a bit to wrap your mind around.
Anyway, the enemy should now move towards the player. Hooray!
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
DAMAGEin 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) ) const Vector2 get_floor_normal ( ) const Vector2 get_last_motion ( ) const KinematicCollision2D get_last_slide_collision ( ) Vector2 get_platform_velocity ( ) const Vector2 get_position_delta ( ) const Vector2 get_real_velocity ( ) const KinematicCollision2D get_slide_collision ( int slide_idx ) int get_slide_collision_count ( ) const Vector2 get_wall_normal ( ) const bool is_on_ceiling ( ) const bool is_on_ceiling_only ( ) const bool is_on_floor ( ) const bool is_on_floor_only ( ) const bool is_on_wall ( ) const bool is_on_wall_only ( ) const bool move_and_slide ( )Here we can see there's the
get_slide_collisionandget_slide_collision_countmethods. Look at the description ofget_slide_collisionthere'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_collisionmethod. 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_collisiondescription 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 -= DAMAGE queue_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.
- I've added enemies
- They move toward they player
- And they deal damage