06.coding-a-turret-that-shoots-multiple-enemies

Coding a turret that shoots multiple enemies

In this lesson, we will create our first variation of the existing turret: one that shoots at all mobs in range simultaneously.

Create a new script by right-clicking on the TowerDefense/ directory and selecting New Script.

Name it TurretMultiShot.gd, then click Create.

Double-click the new file in the FileSystem dock to open the script and erase everything in it. Once it’s completely empty, write this single line at the top:

extends "Turret.gd"

This script now extends the code in the Turret.gd file and can do everything our Turret can do.

Be careful; the quotes "" and the .gd extension are both mandatory.

When you extend, for example, an Area2D, you write extends Area2D, without quotes or file extension.

That’s because Area2D is a type built into the Godot engine. When you extend your own scripts, you have to specify a valid file path.

You may use a relative path like extends "../../SomeScript.gd" or an absolute path like extends "res://SomeDirectory/SomeScript.gd".

Because the Turret script is inside the same directory as this script, we can write extend "Turret.gd".

Shooting at multiple targets

Since our Turret already stores multiple targets, all we have to do is shoot at all of them at once.

We need to change the _on_Timer_timeout() function, as this is when the turret shoots.

Overriding the timeout callback function

When we extend a script, if we write a new function that has the same name as an existing function, we overwrite its functionality.

We say that we override the function. Let’s override _on_Timer_timeout().

In TurretMultiShot.gd, write:

func _on_Timer_timeout() -> void:
    pass

Now, the new turret will not shoot every second. This is because our new function, which does nothing, replaces the parent Turret.gd script’s _on_Timer_timeout() function.

Using a loop to shoot every target

We want the new turret’s attack to work differently from before:

  1. We want to shoot a rocket for each mob in range.
  2. We want each rocket to face its target. We cannot use the cannon’s transform property. Instead, we have to calculate the angle to each mob.

To shoot at every target, we need a loop. In each loop iteration, we create a rocket and add it to the stage:

func _on_Timer_timeout() -> void:
    for target in target_list:
        var rocket := preload("common/Rocket.tscn").instance()
        add_child(rocket)

Then, we need to find the angle from the mouth of the cannon to the mob itself.

We already know how to get the angle to a mob. We’ve done it before for the Turret:

var target_angle: float = target.global_position.angle_to_point(global_position)

We need to change the calculation slightly to get the angle to the cannon because some rockets will not align with the turret. Switch global_position to cannon.global_position.

func _on_Timer_timeout() -> void:
    for target in target_list:
        # ...
        rocket.rotation = target.global_position.angle_to_point(cannon.global_position)

Finally, we place the rocket at the cannon’s tip.

func _on_Timer_timeout() -> void:
    for target in target_list:
        # ...
        rocket.global_position = cannon.global_position

Drag the script to the second turret to test it.

Run the scene and drag mobs in range of the turret. You’ll see that it shoots at multiple targets simultaneously!

Changing how the turret turns

We could stop here, but we can make it a little better.

At the moment, the turret only turns towards the first target. It would look nicer if it aimed at the midpoint between all the targets.

To calculate the average between multiple points, we add all the points, then divide by the number of points.

Let’s override the _rotate_to_target() function.

Like before, we define a target_angle with a default value, but if we have targets, we calculate the targets’ average position and the angle to that point.

func _rotate_to_target() -> void:
    var target_angle := PI / 2
    if target_list:
        # We define a variable to calculate the targets' average position.
        var average_position := Vector2.ZERO
        # We loop over all targets and sum their positions.
        for target in target_list:
            average_position += target.global_position
        # We divide by the number of positions. That's our average position.
        average_position /= target_list.size()
        # We then calculate the angle to that point like before.
        target_angle = average_position.angle_to_point(global_position)

We can now rotate the turret’s sprite accordingly.

func _rotate_to_target() -> void:
    # ...
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)

If you run the scene now you should see the turret aim at the average of the targets in range.

As you can see, a few code changes can produce drastically different results.

In the next lesson, we’ll code a turret that aims at the target with the lowest health.

The code

Here’s the complete code for TurretMultiShot.gd.

extends "Turret.gd"

func _rotate_to_target() -> void:
    var target_angle := PI / 2
    if target_list:
        # We define a variable to calculate the targets' average position.
        var average_position := Vector2.ZERO
        # We loop over all targets and sum their positions.
        for target in target_list:
            average_position += target.global_position
        # We divide by the number of positions. That's our average position.
        average_position /= target_list.size()
        # We then calculate the angle to that point like before.
        target_angle = average_position.angle_to_point(global_position)
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)


func _on_Timer_timeout() -> void:
    for target in target_list:
        var rocket := preload("common/Rocket.tscn").instance()
        add_child(rocket)
        rocket.rotation = target.global_position.angle_to_point(cannon.global_position)
        rocket.global_position = cannon.global_position