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.
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 , 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:
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.
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.
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_list[0]
target else:
= null target
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_list[0]
target else:
= null
target
func _on_body_entered(body: PhysicsBody2D) -> void:
append(body)
target_list.select_target()
func _on_body_exited(body: PhysicsBody2D) -> void:
var index := target_list.find(body)
remove(index)
target_list.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.
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:
in A
Running code in B Running code
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.
in B Running code
Let’s apply this technique to our turrets.
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.global_position.angle_to_point(global_position)
target_angle = lerp_angle(sprite.rotation, target_angle, rotation_factor) sprite.rotation
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.global_position.angle_to_point(global_position)
target_angle = lerp_angle(sprite.rotation, target_angle, rotation_factor) sprite.rotation
Now, our new turrets can override _rotate_to_target()
to
rotate differently!
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.
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")
connect("timeout", self, "_on_Timer_timeout")
timer.
func _physics_process(_delta: float) -> void:
_rotate_to_target()
func _rotate_to_target() -> void:
var target_angle := PI / 2
if target:
= target.global_position.angle_to_point(global_position)
target_angle = lerp_angle(sprite.rotation, target_angle, rotation_factor)
sprite.rotation
func select_target() -> void:
if target_list:
= target_list[0]
target else:
= null
target
func _on_body_entered(body: PhysicsBody2D) -> void:
append(body)
target_list.select_target()
func _on_body_exited(body: PhysicsBody2D) -> void:
var index := target_list.find(body)
remove(index)
target_list.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)
= cannon.global_transform rocket.global_transform