03.making-the-turret-aim-and-rotate

Making the turret aim and rotate

We will now write the turret’s code.

We’ll do it in four parts:

  1. First, we’ll write the basic outline of the code we need.
  2. Then, we will make the turret shoot.
  3. We’ll have it find a target.
  4. Finally, we’ll make it turn.

To get started, reopen the Turret scene by clicking the icon next to the turret node in the Scene dock.

Attach a new script to the Turret node and delete everything except for the top line: extends Area2D.

Shooting rockets every second

We need the turret to:

  1. Shoot a rocket every second from the position and angle of the Position2D node.
  2. If a mob enters the CollisionShape2D, rotate the Sprite towards that mob.
  3. If a mob exits the detection field, rotate the Sprite back to a neutral position.

Let’s start by shooting rockets. As we saw in the previous lesson, the rocket will automatically move in the direction it’s facing.

So in the turret’s code, we only need to:

  1. Load and instantiate the rocket.
  2. Add it as a child.
  3. Orient it and place it on the cannon’s tip.
onready var timer := $Timer
onready var cannon := $Sprite/Position2D

func _ready() -> void:
    # We use the timer to control the rate of fire. By default, timers run
    # continuously and emit their timeout signal periodically.
    timer.connect("timeout", self, "_on_Timer_timeout")

func _on_Timer_timeout() -> void:
    # We load the rockt scene and instantiate it.
    var rocket: Area2D = preload("common/Rocket.tscn").instance()
    add_child(rocket)
    # This line copies the `cannon`'s position, rotation, and scale at once,
    # placing and orienting the rocket properly.
    rocket.global_transform = cannon.global_transform

Assigning the cannon’s global_transform to the rocket is equivalent to writing this:

rocket.rotation = cannon.rotation
rocket.global_position = cannon.global_position
rocket.scale = cannon.scale

But using the transform is shorter and clearer once you know what it does.

You can already run the scene with the turret and the mobs to see the turret shoot every second.

Selecting a Target and the “null” value

Let’s declare a target member variable at the top of the script. This tracks the turret’s current target.

When a mob enters the turret’s CollisionShape2D, we will store that mob in the target variable. When it leaves the area, we will empty this variable.

We start by defining a new member variable to store the turret’s target.

var target: PhysicsBody2D = null

We use the value null to mean that there is no target at first. That’s how many programming languages represent the concept of empty. In programming, saying that a variable has the value null means that it is not set.

You can’t use null to clear every type of variable. For example, a number can be 0, but it cannot be null. However, all variables that hold nodes can store a value of null.

Note: Writing var target: PhysicsBody2D has the same effect as writing var target: PhysicsBody2D = null. If you do not set a variable, it will be null by default.

We specify null to be explicit. We did not forget to specify the value. Instead, we want it to start as null.

When a PhysicsBody2D walks into the CollisionShape2D, we assign it to the target variable to keep track of it. When it leaves, we unset the target value.

func _ready() -> void:
    # ...
    connect("body_entered", self, "_on_body_entered")
    connect("body_exited", self, "_on_body_exited")

func _on_body_entered(body: PhysicsBody2D) -> void:
    target = body


func _on_body_exited(_body: PhysicsBody2D) -> void:
    target = null

Notice how we use _ in front of body in the second function. Like before, we use this convention to indicate that we intentionally leave the function unused.

The turret can now acquire targets. All that’s left is to aim at them.

Turning towards the target

We want the turret to animate, turning towards the target. We need to:

  1. Calculate the angle to the target.
  2. Rotate the turret a little bit each frame.

We’ll use the powerful Vector2.angle_to_point() and lerp_angle() functions to achieve that.

Finding the angle to the target

With two Vector2 coordinates, A and B, you can calculate the angle from point A to point B by calling A.angle_to_point(B).

So, to calculate the angle from our turret to our target, we could write:

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

Note: We use the nodes’ global_position variable instead of position so that the calculations work regardless of where the nodes are in the scene tree. The global_position is relative to the game world’s origin, while the position property is relative to the parent node’s position.

We could then assign the target_angle to the sprite’s rotation directly, like so:

sprite.rotation = target_angle

However, this makes our turret instantly face the target in one frame. This looks unnatural, and it would hurt the overall quality of the game.

Animating the turret turning

In the Learn GDScript From Zero app, you got a glimpse of the lerp() function, short for linear interpolate. It’s a function people often use for animation.

We will use a similar function here, but for angles, called lerp_angle().

Let’s define a rotation factor first.

We could write it anywhere, but to keep in line with conventions, write it around the top of the script, after the line starting with extends.

export (float, 0.01, 1.0) var rotation_factor := 0.1

The part that says export (float, 0.01, 1.0) is not required, but it will allow us to manipulate the value in the Inspector with a slider. This is nice, as it makes the value more easily accessible.

The two numbers in parentheses mean that the interface will restrict the value between 0.01 and 1.0.

A value of 1.0 will instantly snap the turret to the target. A value of 0.01 will make the turret rotate extremely slowly.

We calculate the target angle and rotate the sprite in _physics_process().

onready var sprite := $Sprite

func _physics_process(_delta: float) -> void:
    # We want a default target angle in case there's no target
    var target_angle := PI / 2.0
    if target:
        # If there's a target, we replace the default target angle by finding
        # the angle to the PhysicsBody2D.
        target_angle = target.global_position.angle_to_point(global_position)
    # Every frame, we rotate the sprite towards the target angle by the rotation
    # factor. If the rotation factor is 1.0, the sprite will instantly face the
    # target.
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)

With that, you can run the scene, drag and drop mobs in the circular area, and the turret will shoot at one of them.

If you move the mobs around, you may notice a bug.

Try to do the following:

  1. Move one mob in range. The turret turns and shoots at it.
  2. Move another mob in range. The turret turns to shoot at the new mob.
  3. Take either mob out. The turret returns to a neutral position, even if there’s still a target in range.

That’s because when any PhysicsBody2D goes out of range, we set the target to null!

How would we handle switching to a different target instead? We’ll learn how in the next lesson.

Practice: The slowing turret

Open the practice The slowing turret.

In this project, mob cars are racing along a track. We want them to slow down when in the tower’s radius.

When you complete the practice, the cars should move like in the animation below.

Practice: Planet defender

Open the practice Planet defender.

In this practice, you’ll make a gun automatically aim and fire at space mice.

The code

Here’s the complete code for Turret.gd.

extends Area2D

export (float, 0.01, 1.0) var rotation_factor := 0.1

var target: PhysicsBody2D = null

onready var sprite := $Sprite
onready var timer := $Timer
onready var cannon := $Sprite/Position2D


func _ready() -> void:
    # We use the timer to control the rate of fire. By default, timers run
    # continuously and emit their timeout signal periodically.
    timer.connect("timeout", self, "_on_Timer_timeout")
    connect("body_entered", self, "_on_body_entered")
    connect("body_exited", self, "_on_body_exited")


func _physics_process(_delta: float) -> void:
    # We want a default target angle in case there's no target
    var target_angle := PI / 2.0
    if target:
        # If there's a target, we replace the default target angle by finding
        # the angle to the PhysicsBody2D.
        target_angle = target.global_position.angle_to_point(global_position)
    # Every frame, we rotate the sprite towards the target angle by the rotation
    # factor. If the rotation factor is 1.0, the sprite will instantly face the
    # target.
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)


func _on_body_entered(body: PhysicsBody2D) -> void:
    target = body


func _on_body_exited(_body: PhysicsBody2D) -> void:
    target = null


func _on_Timer_timeout() -> void:
    # We load the rockt scene and instantiate it.
    var rocket: Area2D = preload("common/Rocket.tscn").instance()
    add_child(rocket)
    # This line copies the `cannon`'s position, rotation, and scale at once,
    # placing and orienting the rocket properly.
    rocket.global_transform = cannon.global_transform