This is THE way to implement interfaces in Godot
At least before Godot Traits System releases
Ever since I started using Godot, I had been try to find the best implementation for interfaces. There are many tutorials on YouTube, discussions on GitHub but we all do agree to one thing - the lack of a interface mechanic makes the development process clunky (in GDScript atleast).
The simplest implementation we have now is:
func on_do_something(other) -> void:
if other.has_method("foo"):
other.foo()
Don’t get me wrong there is nothing wrong with this implementation, it just presents a few inconveniences especially when you are working on a big project. Let’s take an example on an interactable object that ideally inherits IInteractable interface:
class InteractableObj
func interact(interactor) -> void:
pass
func can_interact(interactor) -> void:
pass
class Interactor
func on_area_enter(area) -> void:
var can_interact: bool = false
if area.has_method("can_interact"):
can_interact = area.can_interact(self)
if can_interact and area.has_method("interact"):
area.interact(self)
The amount of
has_method
checks increases, as you have more functions in the supposed interface. As shown above,interact
andcan_interact
requires 2 checks. Some interact mechanic might have hover/unhover, select/deselect that will raise the amount of checks.Due to all of the individual function checks, human errors are more likely to happen as there isn’t a centralized access point to access the object as an interactable.
Doing it this way also meant that we could not enforce the contract of IInteractable towards the interactable object. We could not assert if the concrete object didn’t implement certain functions.
Because we couldn’t address the object as IInteractable, we will lose code suggestion/autocomplete.
A the time of writing, there is active development on Traits System that works similar as an interface. However, it is a big feature and we still don’t know the release date of it. Follow this link to keep in touch with its development. While waiting for that, I have came up with a solution mostly solves the issue mentioned above.
The solution uses a very underrated feature in Godot - Metadata. The official documentation explains Metadata as:
…
Lastly, every object can also contain metadata (data about data).
set_meta()
can be useful to store information that the object itself does not depend on. To keep your code clean, making excessive use of metadata is discouraged.…
InteractableObj can work standalone just fine without IInteractable, but IInteractable depends on InteractableObj. That makes it the perfect candidate for Interface. Now let’s get into action.
The Solution
1. Create the Interface
Let’s create a class named IInteractable that extends from Node. This classs should contains all the interface function to facilitate interaction. In this example, we will have can_interact
and interact
.
This class will then insert itself into the concrete object as a metadata through NOTIFICATION_PARENTED and will remove itself from it through NOTIFICATION_UNPARENTED.
On _ready
, the class will set the parent as its owner. You can do assertions here to enforce the contract upon the parent. Lastly, create the relevant interface functions in the concrete object’s class.
The script should look like this:
extends Node
class_name IInteractable
const INTERACTABLE: StringName = &"Interactable"
func _notification(what: int) -> void:
match what:
NOTIFICATION_PARENTED:
## Insert itself into parent as a metadata
get_parent().set_meta(INTERACTABLE, self)
NOTIFICATION_UNPARENTED:
## Remove itself from parent as a metadata
get_parent().set_meta(INTERACTABLE)
pass
func _ready() -> void:
## setting parent as owner
owner = get_parent()
## enforcing contract onto parent
assert(owner.has_method("can_interact"))
assert(owner.has_method("interact"))
pass
func can_interact(interactor: Node) -> bool:
return owner.can_interact(interactor) ## proxying the call to parent
func interact(interactor: Node) -> void:
owner.interact(interactor) ## proxying the call to parent
pass
Now, add the IInteractable to object as a child.
If you start play, you will see that the IInteractable is added as metadata to the parent object.
That’s the interface part done. Let’s move onto the interactor.
2. Create the Interactor
I chose to use Area2D as the interactor in this example. This is the example script I use:
extends Area2D
func _on_area_entered(area: Area2D) -> void:
if area.has_meta(IInteractable.INTERACTABLE):
var i := area.get_meta(IInteractable.INTERACTABLE) as IInteractable
if i.can_interact(self):
i.interact(self)
pass
Note: Remember to cast the metadata into its class to get code suggestion/autocomplete.
Notice the syntax on how we call an interface function is very similar to we do in Unity and Unreal.
## Unity(C#)
private void OnTriggerEnter(Collider other)
{
IInteractable i = other.GetComponent<IInteractable>();
if (i != null && i.CanInteract(this))
{
i.Interact(this);
}
}
## Unreal(C++)
void AInteractableObj::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
IInteractable* I = Cast<IInteractable>(OtherActor);
if (I && IInteractable::Execute_CanInteract(OtherActor, this))
{
IInteractable::Execute_Interact(OtherActor, this);
}
}
Using this architecture, it solves the 4 issues mentioned above though, it is a bit inconvenient having to proxy the call from the interface node into its parent. For that, we would have wait and see what the Traits System has to offer. The beauty of this solution is metadata being a feature situated in Object class, meant that it can also be used on non-Node classes too.
Anyway, that’s all I have for today. Hopefully this proves useful for all of you. Sub ’n share if you find it helpful~~