In languages with strong types, you can discriminate between different elements based on their type. For example, in Java, you can have a method that receives an object of type Animal
, and then you can check if the object is an instance of Dog
or Cat
to perform different actions.
In GDScript, you can also do that. If you have an Animal
class, and two sub-classes Dog
and Cat
, you could write the below:
extends Node
func determine_animal_type(animal: Animal):
if animal is Dog:
print("It's a dog!")
elif animal is Cat:
print("It's a cat!")
else:
print("It's an animal, but we don't know which")
However, this system only works if the elements have a common parent. If you have two classes that don't share a common parent, you can't use this method. This is where duck typing comes in.
Duck typing is the process of guessing what a variable is based on its methods or properties: "If it walks like a duck and quacks like a duck, it's a duck".
This is commonly used in Godot to apply damage to enemies. For example, in a game, the Player
class, and the various enemies, might all descend from different classes. However, if they all have a take_damage()
method, you can call that method without knowing the exact type of the object. For example, here's a BirdEnemy
class with a take_damage()
method:
class_name BirdEnemy extends Area3D
var health := 100
func take_damage(damage: int) -> void:
health -= damage
if health <= 0:
queue_free()
And this bullet will check if an object has a take_damage()
method:
func on_area_entered(target: Area3D) -> void:
if target.has_method("take_damage"):
target.take_damage(10)
This assumes you respect a contract where all objects that have a take_damage()
method use the same funtion signature. In this example above, take_damage()
receives an int
or a float
as a parameter.
Other ways to check for an object type is to add a special property. Let's assume you have a bird enemy, based on Area3d
, and a bull enemy, based on CharacterBody3d
. You may add an is_enemy
property to both classes:
class_name BirdEnemy extends Area3D
var is_enemy := true
var health := 5
func take_damage(damage: int) -> void:
health -= damage
class_name BullEnemy extends CharacterBody3D
var is_enemy := true
var health := 15
func take_damage(damage: int) -> void:
health -= damage
Then, you can check if an object is an enemy by checking if it has the is_enemy
property:
func on_area_or_body_entered(target: Node) -> void:
if target.is_enemy:
target.take_damage(10)
That's a different contract, more complicated: you're making a contract that if you add is_enemy
property, you will also always add the take_damage()
method.
As you can see, while duck typing is practical, it is also error-prone. For that reason, at GDQuest we prefer to use composition when possible. For example, we could have a HitBox
class that handles damage. For example, this hitbox could be added to any object that needs to take damage. When its health reaches 0
, it deletes its parent:
class_name HitBox extends Area3D
@export var health := 10
func take_damage(damage: int) -> void:
health -= damage
if health <= 0:
get_parent().queue_free()
Then, you can add a HitBox
to any object that needs to take damage. This simplifies your code and you can avoid duck typing:
extends Node
func on_area_entered(target: Area3D):
if target is HitBox:
target.take_damage(10)