09.shooting-homing-rockets

Shooting homing rockets

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.

Creating the homing rocket

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
    direction.x = Input.get_axis("move_left", "move_right")
    direction.y = Input.get_axis("move_up", "move_down")

    if direction.length() > 1.0:
        direction = direction.normalized()

    if Input.is_action_just_pressed("boost"):
        max_speed = boost_speed
        get_node("Timer").start()

    var desired_velocity := max_speed * direction
    var steering_vector := desired_velocity - velocity
    velocity += steering_vector * drag_factor
    position += velocity * delta
    rotation = velocity.angle()

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.

Coding the homing rocket

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:

  1. Check if we have a valid target.
  2. If so, we calculate the direction to the target.
  3. We steer to it as we did in the steering ship project.

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
    velocity += steering_vector * drag_factor
    position += velocity * delta
    rotation = velocity.angle()

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:
    velocity = transform.x * speed

This takes the transform.x, which is the orientation of the sprite and multiplies it by speed.

Making the homing turret

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:

  1. Instead of instancing Rocket.tscn, we’ll use the HomingRocket.tscn we just created.
  2. We will need to give the rocket a target.
  3. We will need to give it an initial impulse using the 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.
    rocket.global_transform = cannon.global_transform
    # We give the rocket the target, so it has something to steer to.
    rocket.target = target
    # And finally, we give the rocket an initial impulse for natural motion.
    rocket.set_initial_velocity()

Testing the turrets

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:

  1. A turret that shoots multiple targets.
  2. A turret that shoots the weakest target.
  3. A turret that slows down enemies.
  4. A turret that shoots homing rockets.

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!

Closing words: how do you get the code right from the start?

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.

Practice: Planet defender 2

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.

The code

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.
    rocket.global_transform = cannon.global_transform
    # We give the rocket the target, so it has something to steer to.
    rocket.target = target
    # And finally, we give the rocket an initial impulse for natural motion.
    rocket.set_initial_velocity()

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
    velocity += steering_vector * drag_factor
    position += velocity * delta
    rotation = velocity.angle()


# The turret that shoots this rocket should call this function to give the
# rocket an initial impulse.
func set_initial_velocity() -> void:
    velocity = transform.x * speed