Skip to main content

🤖 Source Generators

🔄 Lifecycle Hooks

Earlier, we mentioned that you can declare lifecycle hooks on a SuperNode. In SuperNodes lingo, a "lifecycle hook" is just the name of a method that should be invoked whenever a node lifecycle event occurs, such as Ready, Process, EnterTree, etc.

Let's explain by showing an example!

namespace LifecycleExample;

using Godot;
using SuperNodes.Types;

[SuperNode("MyLifecycleHook")]
public partial class MySuperNode : Node {
public override partial void _Notification(int what);
}

The MySuperNode class has declared a lifecycle hook named MyLifecycleHook. Because we've declared this method, SuperNodes will know to invoke it from its generated implementation of _Notification.

Here's what the generated code looks like.

#nullable enable
using Godot;
using SuperNodes.Types;

namespace LifecycleExample {
partial class MySuperNode {
public override partial void _Notification(int what) {
// Invoke declared lifecycle method handlers.
MyLifecycleHook(what);
}
}
}
#nullable disable

Now imagine that we have another source generator that generates a method named MyLifecycleHook on our MySuperNode class.

// Pretend this implementation is created by another source generator
public partial class MySuperNode {
public void MyLifecycleHook(int what) {
if (what == NotificationReady) {
GD.Print($"{Name} is ready.");
}
}
}

Even though SuperNodes has no way of knowing about the other source generator, it can still invoke the declared lifecycle hook method!

We can even declare multiple lifecycle method hooks and PowerUps.

[SuperNode("MyLifecycleHook", typeof(MyPowerUp), "MyOtherLifecycleHook")]
public partial class MySuperNode : Node {
caution

SuperNodes will invoke lifecycle method hooks and PowerUps in the order that they're declared. In the example above, the invocations will be as follows:

// Code generated by SuperNodes
public override partial void _Notification(int what) {
// Invoke declared lifecycle method handlers.
MyLifecycleHook(what);
MyPowerUp(what);
MyOtherLifecycleHook(what);
}

Lifecycle hooks and PowerUps are always invoked before user-defined lifecycle handlers like OnReady, OnProcess, OnEnterTree, etc.

😥 Source Generator Problems

If you've tried to use a third party source generator alongside Godot's official source generators, you may have encountered some of the following limitations:

  1. Since C# source generators don't know each other (by design), the Godot source generators can't generate GDScript bindings for any members added to scripts by other source generators.

    Likewise, properties added to script implementations by other source generators will not be exported using the [Export] attribute since their generated code is not available to the official Godot source generators.

    info

    Because the order that source generators run is not configurable in .NET, there is no easy workaround. See godotengine/godot#66597 for more details about the perils of source generator support.

  2. Generated code will be invalid if more than one third-party source generator add the same lifecycle method implementation, such as _Notification. Source generators have no way to know if another source generator will implement the same method, so they can't avoid generating duplicate code.

tip

We cannot solve problem #1, but we can live with it. Essentially, any members added to a class by a third-party source generator will not be visible from Godot or GDScript, but will work just fine in C#. If you're writing code primarily in C#, this will not cause you any issues.

💖 Source Generator Solutions

Theoretically, we can solve problem #2.

Imagine two source generators that each want to implement _Notification to perform actions in response to a node script's lifecycle events. If both generators implement _Notification and are added to the game developer's project, the code won't even compile since there will be two duplicate implementations of the _Notification method in the same class.

So, how do we solve this problem? The same way your parents solved it when you were a kid: by sharing.

🙋 Mediators to the Rescue

SuperNodes can act as a mediator between source generators. If each generator that wants to tap into a node's lifecycle creates an implementation containing a lifecycle hook method, SuperNodes can invoke each generator's method in the generated _Notification method. Afterwards, users can simply add the name of the generator's lifecycle method hook to their [SuperNode] attribute to take advantage of the other source generator.

For example, imagine we've created a source generator named PrintOnReady that generates a method named PrintOnReady for each node script. We can then add the PrintOnReady lifecycle method name to our [SuperNode] attribute to take advantage of the PrintOnReady source generator.

// Hypothetical PrintOnReady source generator output.
public partial class MySuperNode {
public void PrintOnReady(int what) {
if (what == NotificationReady) {
GD.Print($"{Name} is ready.");
}
}
}

In our node script, we can annotate the class with the PrintOnReady lifecycle hook method name.

[SuperNode("PrintOnReady")]
public partial class MySuperNode : Node {
public override partial void _Notification(int what);
}

That works, but we can do better. Let's use nameof to make sure we don't accidentally misspell the lifecycle hook method name!

[SuperNode(nameof(MySuperNode.PrintOnReady))]
public partial class MySuperNode : Node {
public override partial void _Notification(int what);
}

Eww, that's kind of long. What if we were using multiple generators?

[SuperNode(nameof(MySuperNode.GeneratedMethod1, MySuperNode.GeneratedMethod2))]
public partial class MySuperNode : Node {

Hmm, that's not so fun.

What if the third party source generators injected a class that had the same name as the method it adds to each node script?

// Somewhere in the generated code for PrintOnReady
public class PrintOnReady {
// Nothing to see here — this only exists to help with nameof!
}

Then we could use nameof to get the name of the class, which would be the same as the method.

[SuperNode(nameof(PrintOnReady))]
public partial class MySuperNode : Node {

Ah, perfect. We've essentially developed a convention that third party source generators can follow if they want to work together in harmony by allowing SuperNodes to mediate for them.

🏪 Guidelines for Source Generator Authors

Are you a source generator author? Do you want to make a compatible source generator that taps into a Godot node's lifecycle and plays nicely with other generators using SuperNodes? If so, here are Chickensoft's official guidelines to help you get started:

  • ☑️ Source generators should inject a class with the same name as the lifecycle hook method it intends to add to a node script.

    If a source generator wants to add different kinds of lifecycle hook methods (depending on the node), it can inject a class for each method name it might add. Injecting a class name is helpful to allow users to easily reference the name of the method in the [SuperNode] attribute using nameof in a type-safe way.

  • ☑️ Prefer lifecycle method hook names that match the name of the generator so that users can more easily discover the method name via autocomplete.

  • ☑️ Don't implement other lifecycle methods, like _Notification, _Ready, _Process, etc. The generator's lifecycle method hook will be invoked whenever any event occurs, allowing the generator to filter out the events it cares about.

  • ☑️ In the documentation/README for the source generator, please include a note that explains their users must also reference SuperNodes for the generator to be invoked in response to lifecycle events.

info

If you're working on a source generator, get in touch with us on Discord! We'd love to answer any questions you have, as well as give you a place to share your tool with the community!