We will now write the turret’s code.
We’ll do it in four parts:
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
.
We need the turret to:
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:
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.
connect("timeout", self, "_on_Timer_timeout")
timer.
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.
= cannon.global_transform rocket.global_transform
Assigning the cannon
’s global_transform
to
the rocket
is equivalent to writing this:
= cannon.rotation
rocket.rotation = cannon.global_position
rocket.global_position = cannon.scale rocket.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.
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 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 , 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:
= body
target
func _on_body_exited(_body: PhysicsBody2D) -> void:
= null target
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.
We want the turret to animate, turning towards the target. We need to:
We’ll use the powerful Vector2.angle_to_point()
and
lerp_angle()
functions to achieve that.
With two 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:
= target_angle sprite.rotation
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.
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.global_position.angle_to_point(global_position)
target_angle # 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.
= lerp_angle(sprite.rotation, target_angle, rotation_factor) sprite.rotation
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:
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.
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.
Open the practice Planet defender.
In this practice, you’ll make a gun automatically aim and fire at space mice.
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.
connect("timeout", self, "_on_Timer_timeout")
timer.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.global_position.angle_to_point(global_position)
target_angle # 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.
= lerp_angle(sprite.rotation, target_angle, rotation_factor)
sprite.rotation
func _on_body_entered(body: PhysicsBody2D) -> void:
= body
target
func _on_body_exited(_body: PhysicsBody2D) -> void:
= null
target
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.
= cannon.global_transform rocket.global_transform