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:
extendsNodefuncdetermine_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 extendsArea3Dvar health :=100functake_damage(damage:int)->void:
health -= damage
if health <=0:queue_free()
And this bullet will check if an object has a take_damage() method:
This assumes you respect a contract where all objects that have a take_damage() method use the same function signature. In this example above, take_damage() receives an intint or a floatfloat as a parameter.
Another way to check for an object type is to add a special property. Let's assume you have a bird enemy, based on Area3DArea3D, and a bull enemy, based on CharacterBody3DCharacterBody3D. You may add an is_enemy property to both classes:
class_name BirdEnemy extendsArea3Dvar is_enemy := true
var health :=5functake_damage(damage:int)->void:
health -= damage
class_name BullEnemy extendsCharacterBody3Dvar is_enemy := true
var health :=15functake_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:
That's a different contract, more complicated: you're making a contract that if you add the 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. 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 extendsArea3D@exportvar health :=10functake_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:
extendsNodefuncon_area_entered(target: Area3D):if target is HitBox:
target.take_damage(10)
Become an Indie Gamedev with GDQuest!in GDSchool
Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.