In this lesson, we will create a turret that shoots homing missiles.
If you have been following this series closely, you already have all the knowledge necessary to do a homing rocket turret.
Think back on the steering ship you built in the first project.
While it was going in one direction, you gave it a new destination through keyboard input.
The ship would then steer towards that desired velocity, not reaching it right away.
Our rocket will work similarly. We only have to change one thing: instead of using the keyboard, the rocket will decide its direction on its own.
Let’s create the homing rocket first. We’ll extend the Rocket scene that we included in the project.
Locate the file TowerDefense/common/Rocket.tscn
,
right-click on it, and select New Inherited Scene.
Rename the top node to HomingRocket and save the file as
TowerDefense/HomingRocket.tscn
Create a new script called HomingRocket.gd
and assign it
to the HomingRocket node.
Before trying to create our script, we should reopen the steering ship and reread its code. Do you remember how it worked?
Here’s the code we used back then.
func _process(delta: float) -> void:
var direction := Vector2.ZERO
= Input.get_axis("move_left", "move_right")
direction.x = Input.get_axis("move_up", "move_down")
direction.y
if direction.length() > 1.0:
= direction.normalized()
direction
if Input.is_action_just_pressed("boost"):
= boost_speed
max_speed get_node("Timer").start()
var desired_velocity := max_speed * direction
var steering_vector := desired_velocity - velocity
+= steering_vector * drag_factor
velocity += velocity * delta
position = velocity.angle() rotation
We can reuse some of the code, but we have to change how to calculate
the direction
.
Instead of using keyboard keys, we need to know the direction of the
rocket to its target. We can do this with the
Vector2.direction_to()
function, as you’ll see in a
moment.
Let’s finally start writing the script. Open
HomingRocket.gd
, and erase everything in it.
Write the first line:
extends "common/Rocket.gd"
Like in SteeringShip.gd
, we define two variables,
velocity
and drag_factor
.
var velocity := Vector2(0, 0)
var drag_factor := 0.1
We also need to track the target to steer towards.
# The turret will provide the rocket with this target, one of the mobs.
var target: PhysicsBody2D = null
We can now write our steering behavior in
_physics_process()
. We want to:
We first ensure that the rocket’s target is set and is valid. Mobs can get killed by another rocket before this one hits them, in which case they’ll get erased from the computer’s memory.
This is a widespread issue in computer programming. If we try to access some memory that got erased, we will get bugs. Sometimes, they can be major ones.
Godot protects you against those issues. If you try to access a node that was freed from memory, it will show an error.
Still, you don’t want to do that because that is still unexpected behavior.
To ensure our target is still valid, we can call the
is_instance_valid()
function.
func _physics_process(delta: float) -> void:
# If the target has not been set, or has been destroyed, we destroy this
# rocket.
if not target or not is_instance_valid(target):
explode()
return
Then we obtain the target through the function we explored earlier:
func _physics_process(delta: float) -> void:
# ...
# Calculate the direction to the target
var direction := global_position.direction_to(target.global_position)
All we have to do now is reuse the code we wrote for the steering ship before.
func _physics_process(delta: float) -> void:
# ...
# steer to the target
var desired_velocity := speed * direction
var steering_vector := desired_velocity - velocity
+= steering_vector * drag_factor
velocity += velocity * delta
position = velocity.angle() rotation
We’re almost done!
The only remaining small issue is that, as it is currently, the rocket would start with no velocity and directly rotate towards the target mob.
That isn’t how we expect homing rockets to work. We want it to already have a velocity in some direction at launch but then steer towards its target.
So we will give it an initial velocity:
# The turret that shoots this rocket should call this function to give the
# rocket an initial impulse.
func set_initial_velocity() -> void:
= transform.x * speed velocity
This takes the transform.x
, which is the orientation of
the sprite and multiplies it by speed
.
All we need now is a turret that shoots our homing rocket instead of the regular rocket.
Right-click the TowerDefense/Turret.tscn
scene, and
select New Inherited Scene.
Rename the top node to TurretHoming, and save it as
TowerDefense/TurretHoming.tscn
.
Then create a new script, name it TurretHoming.gd
, and
assign it to the TurretHoming node.
We only need to override _on_Timer_timeout()
, as this is
where the turret shoots rockets.
Here are the differences we’ll code:
Rocket.tscn
, we’ll use the
HomingRocket.tscn
we just created.target
.set_initial_velocity
function we created.As we’re only rewriting one function and most of the code is familiar
by now, here’s the complete TurretHoming.gd
script.
extends "Turret.gd"
func _on_Timer_timeout() -> void:
if not target:
return
# We instance the homing rocket instead of the regular rocket.
var rocket := preload("HomingRocket.tscn").instance()
add_child(rocket)
# Like before, we place it and rotate it based on the cannon's tip.
= cannon.global_transform
rocket.global_transform # We give the rocket the target, so it has something to steer to.
= target
rocket.target # And finally, we give the rocket an initial impulse for natural motion.
set_initial_velocity() rocket.
Head back to the TowerDefense scene and instantiate a couple of TurretHoming instances.
You can also place some TurretMultiShot instances for some extra variety.
We’re done! Congratulations on finishing this project. You’ve created:
Now is a good time to review all the code you wrote, and notice how little code you needed to create significant behavioral changes between similar entities.
Feel free to play with the values, move the turrets around, and be proud of yourself!
You may wonder, “How do I write my scripts perfectly the first time over, so that I can easily extend them?” And the answer is: you don’t!
By following this series, it might seem to you like we knew, when
writing Turret.gd
, exactly how to write it so we could make
it and all the other turrets.
Tutorials can make you feel like developers just write code that works and get it right from the start.
But it’s not that easy! We work iteratively and gradually adapt and improve the code to create the final project.
We modify the code multiple times to get to the final result.
We started with the simplest code we could write. Later, when we wanted to build a different turret, we changed the code to allow for easy extension.
We kept going back and forth, changing the original turret and all the others iteratively, until we reached a balance where the original code was extensible enough but not too complex.
This is not easy, and it’s okay if you don’t always manage at first. Don’t get hung up on it.
But whatever you do, don’t try to make your code extensible before you know what you need.
You’re likely to make it more complex than it needs to be, and it’s almost certain you will not make life easier for your future self.
Open the practice Planet defender 2.
In this one, we have a working turret, but it’s too slow to beat all the enemies.
You need to the make the turret fire homing rockets fast enough to kill all the mice.
Here’s the code for this lesson’s scripts.
TurretHoming.gd
.
extends "Turret.gd"
func _on_Timer_timeout() -> void:
if not target:
return
# We instance the homing rocket instead of the regular rocket.
var rocket := preload("HomingRocket.tscn").instance()
add_child(rocket)
# Like before, we place it and rotate it based on the cannon's tip.
= cannon.global_transform
rocket.global_transform # We give the rocket the target, so it has something to steer to.
= target
rocket.target # And finally, we give the rocket an initial impulse for natural motion.
set_initial_velocity() rocket.
Here’s the code for HomingRocket.gd
.
extends "common/Rocket.gd"
var velocity := Vector2(0, 0)
var drag_factor := 0.1
# The turret will provide the rocket with this target, one of the mobs.
var target: PhysicsBody2D = null
func _physics_process(delta: float) -> void:
# If the target has not been set, or has been destroyed, we destroy this
# rocket.
if not target or not is_instance_valid(target):
explode()
return
# Calculate the direction to the target
var direction := global_position.direction_to(target.global_position)
# steer to the target
var desired_velocity := speed * direction
var steering_vector := desired_velocity - velocity
+= steering_vector * drag_factor
velocity += velocity * delta
position = velocity.angle()
rotation
# The turret that shoots this rocket should call this function to give the
# rocket an initial impulse.
func set_initial_velocity() -> void:
= transform.x * speed velocity