05.refactoring-our-code-to-make-more-turrets

Rewriting our code to make more turrets

You rarely get code right as you first write it. You often figure it out as you type. As you have new ideas, your code needs to change.

This is a process called refactoring. Refactoring is the process of changing your code’s structure without changing its behavior. This makes it easier to write new features. It’s something that we do regularly in computer programming.

In this lesson, we will change the code’s structure to prepare for creating new kinds of turrets.

How to extend scripts

In previous lessons, we’ve attached scripts to nodes. These scripts always started with a line like:

extends Area2D

The extends keyword means that the script adds to existing functionality.

The lines above extend code from the Godot engine, but we can also extend our own scripts.

Imagine a script named Person.gd that only prints a name.

# Person.gd
var name := "Godot"

func say_name() -> void:
    print("Hello, my name is " + name)

We can extend its code by creating a new script and using the Person.gd script’s path after the extends keyword.

# PersonWithAge.gd
extends "Person.gd"

var age := 15

func say_age() -> void:
    prints("my age is", age)

func say_all() -> void:
    say_name()
    say_age()

The PersonWithAge.gd script has its own function, say_age(), but it also has the say_name() function from the script it extends.

We say that PersonWithAge.gd extends or inherits Person.gd.

When a script extends another script, it has access to all the functions and properties of the script it extends.

That’s why, when you write extends Area2D, you get all the functions and properties of Area2D, and you can add your own.

On top of adding functions to a script, extending it lets you replace existing functions. For example:

# ShyPerson.gd
extends "PersonWithAge.gd"

func say_all() -> void:
    prints("I would rather not say, thanks")

ShyPerson’s say_all() function is different from PersonWithAge’s say_all() function. We replace the function of PersonWithAge.

This is called overriding a function. We say that ShyPerson’s say_all() overrides PersonWithAge’s say_all().

We will put this to use in the following lesson to create two new turrets:

  1. One that shoots all mobs in range simultaneously.
  2. One that shoots the weakest mob in range.

Preparing to extend our turret script

Before we extend our turret script to create new kinds of turrets, we need to prepare our scene and make a code change.

Open the scene Variations.tscn in the TowerDefense/ directory. It contains a few mobs and turrets.

We will create two more turrets. Select the Turret node and press Ctrl+D, or right-click and select Duplicate to duplicate it.

Move the new turret to the right. Then duplicate it again, and move it to the right again.

You should end up with a structure similar to the one below.

Let’s rename the turrets to differentiate them more.

Rename Turret2 to TurretMultiShot and Turret3 to TurretWeakestPicker.

Finally, drag the Turret.gd script we wrote onto the first Turret. We will use it to compare the turrets’ behaviors.

You should end up with this node structure.

Save the scene, and let’s create our first variation.

Planning Ahead

Our new turrets will differ from the original ones in a few ways:

We can’t know in advance exactly how many turrets our game will have and what each will do. But we know that we’ll want to experiment with their behaviors.

That’s why we should isolate the parts of code that pick targets, rotate, and shoot to easily override them in subclasses.

Splitting the function that chooses a target

Our new turrets will shoot at multiple targets simultaneously and aim at the mob with the lowest health in range.

Each new turret needs to select targets differently.

Currently, however, the code to select targets is hard-coded in the _on_body_entered() and _on_body_exited() functions. This makes it difficult to override in new turrets.

Changing your code’s structure will often help you override behavior in scripts that extend it.

Here’s the current code that we want to override in new turrets.

func _on_body_exited(body: PhysicsBody2D) -> void:
    # ...
    if target_list:
        # The body that left the area may be the current target, which is why we
        # must update the target if the array isn't empty.
        target = target_list[0]
    else:
        target = null

We extract these four lines into a new function. Rewrite the functions _on_body_entered() and _on_body_exited() like so.

func select_target() -> void:
    if target_list:
        target = target_list[0]
    else:
        target = null


func _on_body_entered(body: PhysicsBody2D) -> void:
    target_list.append(body)
    select_target()


func _on_body_exited(body: PhysicsBody2D) -> void:
    var index := target_list.find(body)
    target_list.remove(index)
    select_target()

This does not change how the existing turret works, but it will make it easier to override target selection in our new turrets’ scripts.

Moving the Physics’ code

A quirk of Godot 3 is that you can’t override some built-in functions. Usually, overriding a function replaces it completely. For some built-in functions, like _process(), _physics_process(), or _ready(), both the original function and your override will run.

For example, let’s imagine we create two script files A.gd and B.gd:

# A.gd
extends Node2D

func _ready() -> void:
    print("Running code in A")
# B.gd
extends "A.gd":

func _ready() -> void:
    print("Running code in B")

If we instantiate B.gd, then Godot will print two lines:

Running code in A
Running code in B

This means that we have no way to prevent A’s _ready() function from running.

Note: This is specific to GDScript and, except for a few edge cases, is not a problem in other languages.

You can always override functions you create yourself.

So the workaround is to extract the code to override and wrap it into a new function.

# A.gd
extends Node2D:

func _ready() -> void:
    # We move the code to a function so that we can override the new function in B.gd.
    _print_message()

func _print_message() -> void:
    print("Running code in A")
# B.gd
extends "A.gd"

func _print_message() -> void:
    print("Running code in B")

In this case, creating an instance of B.gd will only print one line.

Running code in B

Let’s apply this technique to our turrets.

Overriding the turret’s rotation code

We currently have the rotation logic for the turret in _physics_process():

func _physics_process(_delta: float) -> void:
    var target_angle := PI / 2
    if target:
        target_angle = target.global_position.angle_to_point(global_position)
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)

If we try to override it in a child turret script, it won’t work. We need to extract the code and wrap it into a new function, like so.

func _physics_process(_delta: float) -> void:
    _rotate_to_target()


func _rotate_to_target() -> void:
    var target_angle := PI / 2
    if target:
        target_angle = target.global_position.angle_to_point(global_position)
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)

Now, our new turrets can override _rotate_to_target() to rotate differently!

When and how to add functions

You might be wondering why we don’t separate code into small functions like this all the time.

Trying to make everything easy to change or replace in your code is a common trap that even experienced developers fall into.

You don’t want to create additional functions before you know that you need them.

We recommend keeping code as simple and as specific as possible at first. It keeps the code fast to write and easy to read.

And as you saw in this lesson, it’s easy to extract a couple of lines into a new function when we need it.

In the next lesson, we will code a turret that targets multiple enemies simultaneously.

Turret’s Complete code

extends Area2D

# Allow rotation factor to be changed from the editor
export(float, 0.01, 1) var rotation_factor := 0.4

var target: PhysicsBody2D = null
var target_list := []

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


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


func _physics_process(_delta: float) -> void:
    _rotate_to_target()


func _rotate_to_target() -> void:
    var target_angle := PI / 2
    if target:
        target_angle = target.global_position.angle_to_point(global_position)
    sprite.rotation = lerp_angle(sprite.rotation, target_angle, rotation_factor)


func select_target() -> void:
    if target_list:
        target = target_list[0]
    else:
        target = null


func _on_body_entered(body: PhysicsBody2D) -> void:
    target_list.append(body)
    select_target()


func _on_body_exited(body: PhysicsBody2D) -> void:
    var index := target_list.find(body)
    target_list.remove(index)
    select_target()


# shoot a rocket every time the timer goes off
func _on_Timer_timeout() -> void:
    if not target_list:
        return
    var rocket := preload("common/Rocket.tscn").instance()
    add_child(rocket)
    rocket.global_transform = cannon.global_transform