loading...
Cover image for Using Delegates in Godot

Using Delegates in Godot

rcarlson profile image Robert Carlson ・3 min read

I've started cleaning up some of my Godot code recently and found myself in need of an abstract function (aka delegate).

For those of you who you don't know, an abstract function is a function that you delegate to the child class, however the parent class can assume it exists and call it within it's own code.

In my case my base class Orbital.gd had logic to deal with destroying the object. The logic was common among all types of objects: 1) run an animation and 2) mark the object as destroyed. Rather than having this logic repeated for my asteroids, health canisters and space ships, it made more sense to create a base class to deal with this.

That being said, I needed my spaceship to behave slightly differently when it is destroyed. You see when the ship begins to destroy it puts the game engine in slow-mo mode, and when the animation ends it needs to be put the engine back in normal mode. Here is how I achieved this using a pattern similar to a "delegate".

Defining the Base Implementation

My base implementation of destroy is pretty simply. It includes playing the "Destroy" animation, setting the destroyed variable to 'true' and then setting a delay of .25 seconds to allow the animation to complete before finally calling the _remove() function.

Scripts/Orbital.gd

func destroy():
      destroyed = true
      animation.play("Destroy")

      destroyTimer = Timer.new()        
      destroyTimer.set_wait_time(.25)
      destroyTimer.connect("timeout", self, "_remove")
      add_child(destroyTimer)

      destroyTimer.start()

The _remove() function handles the logic after the animation finishes. The secret to getting this to work is assigning self to a local variable. This allows us to cheat the compiler so that it doesn't check to see if the _destoryed delegate has been defined. Don't worry though, we do the check ourselves using obj.has_method('_destroyed'), and if we have the function defined we call it!

Scripts/Orbital.gd

func _remove():
    var obj = self

    if obj.has_method('_destroyed'):
        obj._destroyed()

    if removeOnDestroy:
        queue_free()

Note: The reason I chose not to have a base implementation of _destroyed() and simply override it is to have more control over when the child's _destoryed() function is called. In the example above I have the ability to add logic before and after the event handler is invoked. Ultimately it boils down to preference though :)

Defining the Ship Specific Logic

Now that we have our delegate implemented and available, we can define our space ship specific death sequence.

Before we can start using the base class we need to extend the Orbital.gd script in our Ship.gd script.

Ship.gd

extends "res://Scripts/Orbital.gd"

In this example, when the hp reaches 0 we enable slow motion by setting Engine.time_scale = .125 then proceed by calling the destory() function defined in the Orbital.gd base class.

Ship.gd

if hp == 0:
    laser.enabled = false
    collider.disabled = true
    Engine.time_scale = .125 

    destroy()

Because we are inheriting Orbital.gd we can assume that by defining the _destroyed() delegate the code in this function will fire once the base class (aka Orbital.gd) has completed its destroy sequence.

Our specific logic here is setting Engine.time_scale = 1 so we can get back to normal-mode and hiding the sprite.

Ship.gd

func _destroyed():
    sprite.visible = false
    Engine.time_scale = 1 

The Result

Now that we have a base implementation of destroy with an optional delegate to handle the spaceship specific death sequence lets take a look at the result.

Base Implementation

Alt Text

Base Implementation + Delegate

Alt Text

What about Signals?

I am aware the Godot supports a feature called Signals to handle communication between objects. The reason I decided against using this feature is because it didn't feel very object oriented. Signals is a great way for two unrelated objects to talk to one another, sort of like observables in JavaScript, however I really wanted a parent/child relationship to exist between the base class and its child.

At the end of the day I just didn't like using this pattern to achieve my desired result, but you might disagree (that's okay!). To learn more about Signals see https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html

Posted on by:

rcarlson profile

Robert Carlson

@rcarlson

Husband, Father, Coder, Gamer, Co-Founder LetsBuild.gg

Discussion

markdown guide