Skip to content

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.

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.

  1. 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
  2. Size them to be slightly smaller than the player, so we can fit more in and somewhat visually distinct them from the player.

  3. Set the CharacterBody2D’s Motion Mode property to Floating like you did with the player.

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

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

  1. Get rid of everything below the line func _physics_process(delta): and above move_and_slide(), but not those lines, and delete gravity and JUMP_VELOCITY.

  2. Give this script the class name Enemy with class_name Enemy before extends CharacterBody2D.

  3. Reduce the speed from 300.0 to 100.0 or 150.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.

  1. 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 Node
    var player_health: int = 100
    @onready var player_node: Player
  2. 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!

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

  2. 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_collision and get_slide_collision_count methods. Look at the description of get_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.

  3. 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 -= DAMAGE
    queue_free()
  4. 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.

Contribute Donate