Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future of GodotOnReady in Godot 4.0+? #49

Open
31 opened this issue Sep 15, 2022 · 18 comments
Open

Future of GodotOnReady in Godot 4.0+? #49

31 opened this issue Sep 15, 2022 · 18 comments

Comments

@31
Copy link
Owner

31 commented Sep 15, 2022

I believe Godot 4.0 now lets you [Export] public Node n;. Providing [OnReadyGet] public Node n; is the main reason I wrote this library, so I'm thinking it might now be obsolete.

https://godotengine.org/article/dev-snapshot-godot-4-0-alpha-11
Exporting Node pointers as NodePaths (@export var some_node: Node)

I've still got to get my hands on the 4.0 beta to confirm that works how I expect, and see if there's other feature gaps I'm forgetting that I want to use GodotOnReady to plug. GodotOnReady has evolved to support more cases (like defining a default NodePath in the attribute that can be overridden in the editor) but personally I'm not sure if those would justify using the library in my own projects.

Now that Godot has built-in source generators and analyzers, ideally any new features can be added to Godot directly through a proposal rather than a library like GodotOnReady.

Opening this issue for comments in case anyone else has some thoughts. 🙂

@31
Copy link
Owner Author

31 commented Sep 18, 2022

A few things:

  • Private non-exported values. Often scripts are written for a specific scene, so it can make sense to require a specific arrangement of nodes. Making a generalized script that can accept any node paths (and work without certain nodes being present!) can be a waste of time, or even introduce new bugs with implementation.
  • Fetching scene-unique node names. Do these work with [Export] public Node n;? Will need to check, but I'm not sure how it would.
    • Similar: OnReadyFind.
  • Built-in null check (and optional null allowance).

@31
Copy link
Owner Author

31 commented Sep 24, 2022

Waiting for the final state of 4.0 to see how it ends up. But so far, it does look like Godot's adding the bare minimum and some people will still find GodotOnReady useful.

@31
Copy link
Owner Author

31 commented Sep 29, 2022

It turns out 4.0 broke _Ready method generation by having a source generator that scans the list of methods: https://github.com/godotengine/godot/blob/master/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators/ScriptMethodsGenerator.cs.

If a _Ready method isn't on that list, it isn't called, and since source generators run independently and don't (can't!) read each other's sources, the method generated by GodotOnReady won't be found, and won't be called.

The obvious workaround is that you need to write a _Ready method in each script to get it to call GodotOnReady's setup:

public override void _Ready()
{
  OnReadySetup();
}

Could use a partial method to let Godot's generator find the method and then fill it in with GodotOnReady:

partial override void _Ready();

I prefer the former because:

  • Now you don't need to use [OnReady] to do things on ready, like connect to signals from the nodes you've just set up. (Although you still could use [OnReady].)
  • If other source generators also want to generate code that runs on ready, you can simply add the call. You don't need to worry about two generators both trying to generate a partial _Ready method and conflicting.

In either case, analyzers could make sure that when [OnReadyGet] is being used, one of these is implemented. This would hopefully mean you don't get surprise null reference exceptions. But no matter what, the boilerplate regression is frustrating.

(I haven't tried any of this yet, this is from discussion in the Discord C# channel.)

@31
Copy link
Owner Author

31 commented Sep 30, 2022

I just tried it out in 4.0 and there's a more fundamental problem: GodotOnReady generates [Export] members, and there's now a Godot source generator that detects exported members. So, the generated members aren't included and don't show up in the inspector.

Any new version of GodotOnReady that works with 4.x will contain significantly more boilerplate than it previously did, and need a significantly different API because of that, so I don't see how it would be possible to continue the project in any recognizable form in 4.x+.

As for myself, I'm going to go forward with only the new [Export] public Node Foo { get; set; } feature and see what happens. I've edited the readme to indicate 3.x compatibility only.

@31
Copy link
Owner Author

31 commented Sep 30, 2022

If Roslyn implements something like this, then GodotOnReady would have a way forward:

@firebelley
Copy link

Hello! I am super interested in using source generation to achieve what you've done here in Godot 4. I have a similar library where I use reflection to find Nodes with matching names instead of source generation: https://github.com/firebelley/GodotUtilities/blob/master/GodotUtilities/src/ChildNodeAttribute.cs

The reason I bring this up is I am wondering if there's an alternate approach to generating the [Export] attribute. Is there a way to use a hybrid of source generation and name-guessing?

For instance, I define like so:

[OnReadyGet]
private MyNode myNode;

Under the hood, this tool could generate the following code (psuedocode example):

myNode = GetNodeOrNull<MyNode>("MyNode") ?? GetNodeOrNull<MyNode>("myNode") ?? GetNodeOrNull<MyNode>("my_node")

Basically generate every reasonable string that could be valid as a node name. Additionally the ability to provide a node path in the OnReadyGet attribute could be preserved. I realize that the user loses control over specifying exact node paths with this proposal but there are a couple of benefits in my opinion:

  • Most nodes defined in scenes don't change and have a relatively static structure. I think it's a little redundant to have to then configure the node paths after using [OnReadyGet]
  • Godot 4 now supports [Export]-ing Nodes rather than node paths, which is what users can use for uncertain or variable node paths

I'm sure calling some kind of setup method would still be necessary. In my reflection code I still require the user to call WireNodes.

Anyway, apologies if any of this is under-informed. But I am super curious about this project and how it could be used in Godot 4. Would appreciate your thoughts on anything I've suggested here! I'm thinking about potentially forking this code and trying my hand at implementing this.

@31
Copy link
Owner Author

31 commented Dec 17, 2022

myNode = GetNodeOrNull<MyNode>("MyNode") ?? GetNodeOrNull<MyNode>("myNode") ?? GetNodeOrNull<MyNode>("my_node")

How would the user specify a node that's nested somewhat deeply in the node hierarchy? (How would you represent / in the field name, or would you?) This is frequent in UI, where scene structure defines behavior.

  • Most nodes defined in scenes don't change and have a relatively static structure.

In my experience, nodes defined in scenes rarely have a static structure, and change frequently. 😄 I suppose this depends on the nature of the project being worked on.

Conceptually, I admit I prefer the purity of a node script defining its dependencies (which set of nodes it needs to be able to fetch, for what reasons), then hooking them up in the scene editor. This is probably why [Export] public Node Foo { get; set; } has been working fine for me.

This part of GodotOnReady isn't addressed here yet, and it's probably the part I'll actually miss:

[OnReadyGet("AnimationTree", Property = "parameters/playback")]
private AnimationNodeStateMachinePlayback _playback;

I'm sure calling some kind of setup method would still be necessary.

Yeah, this is my conclusion in #49 (comment). I think including an analyzer with a fixup is critical to make this approach user-friendly (whether for a reflection or source generation backed implementation).


Thanks for the interest. 🙂 I don't think those solutions are right for GodotOnReady in 4.x. (Although I would support adding [OnReadyGuess] for the member-name multi-lookup suggestion!) I would prefer to wait for a solution that lets the library keep the [OnReadyGet] semantics (by generating [Export] members), or leave this library behind if that never becomes possible.

No argument from me about making a fork for 4.x with different semantics. It's MIT licensed, after all. 😄 I'm just happy it seems like reasonable enough code to fork.

@firebelley
Copy link

How would the user specify a node that's nested somewhat deeply in the node hierarchy? (How would you represent / in the field name, or would you?) This is frequent in UI, where scene structure defines behavior.

The new scene unique nodes are perfect for this, and could be used like so:

[OnReadyGet("%MyNodeSomewhereDeep")]
private Node node;

I think where we differ is I think many scenes (like a player scene) would have a large number of static nodes that all make up the player's behavior. I agree that when a node has external dependencies those should be configured using [Export]. But for all of the nodes that are sort of "internal" to the scene file itself I personally would prefer to not have to configure those paths.

I definitely see where you're coming from though! Thanks for the response, it's helpful👍

@31
Copy link
Owner Author

31 commented Dec 17, 2022

For what it's worth, here's a design that I believe would work in 4.x without dropping features, but significantly clogs up the syntax:

public partial class Foo : Node
{
  [Export, GodotOnReady<Player>] public NodePath playerPath = "somewhere/is/my/player";

  public override void _Ready()
  {
    base._Ready();
    OnReadySetup();
  }

  public override void _Process(double dt)
  {
    player.Happy();
  }
}

Note: player is a generated member made by chopping Path off playerPath. I'm not too happy about using a variable that isn't declared in this class. It functions, but I want the generator to be invisible. (Also, this means Godot can't access it, e.g. for GDScript interop.)

@31
Copy link
Owner Author

31 commented Dec 17, 2022

The new scene unique nodes are perfect for this, and could be used like so:

[OnReadyGet("%MyNodeSomewhereDeep")]
private Node node;

The name of the field wouldn't realistically be node though, right? Rather, myNodeSomewhereDeep. That question was about the name convention in particular, avoiding that name duplication.

@31
Copy link
Owner Author

31 commented Dec 17, 2022

I guess it's not a big deal--here's what I came up with a little later, which I think I'd actually be happy with:

[OnReadyGet(In = "my/node/somewhere")] private Node deep;
[OnReadyGet(Unique = true)] private Node myNodeSomewhereDeep;

@firebelley
Copy link

I see what you're saying regarding generated members. If I understand right, I think the approach I'm thinking of is that the user would specify the Node members (like private Node node), rather than NodePath members, and then those Node members could just be assigned in the generated code.

The name of the field wouldn't realistically be node though, right? Rather, myNodeSomewhereDeep. That question was about the name convention in particular, avoiding that name duplication.

Ah, I don't think I'm describing myself very well. Essentially I would expect that if a node path/name is provided, then there would be no need to make a best-guess effort. The guess would only be necessary if no node path information was made available.

So for example:

[OnReadyGet("%MyNodeSomewhereDeep")]
private Node node;

Would generate the following code before doing any guessing:

node = GetNode<Node>("%MyNodeSomewhereDeep");

So in essence, there would be multi-step assignment logic that first checks if there is an supplied node path. If not, then move on to trying assignment via guessing the node name.

@31
Copy link
Owner Author

31 commented Dec 18, 2022

If I understand right, I think the approach I'm thinking of is that the user would specify the Node members (like private Node node), rather than NodePath members, and then those Node members could just be assigned in the generated code.

Yep, I understand the suggestion, it just isn't possible to make all the current features work with it.

Ah, I don't think I'm describing myself very well. Essentially I would expect that if a node path/name is provided, then there would be no need to make a best-guess effort. The guess would only be necessary if no node path information was made available.

I just wasn't expecting the guessing to stop working after any more than one-level-deep nodes. 😄 Getting direct children isn't a particularly common scenario for me.

I assumed the point was to optionally remove the (near-)duplication of the member name in [OnReadyGet("MyNode")] private Node myNode; so my first thought was to ask about additional common cases.

@cphillips83
Copy link

Just a side note to the _Ready issue, my workaround was instead of doing an override of _Ready, I created a ctor for the class and hooked to TreeEntered to execute my own source gen/node wire up logic.

I ended up using TreeEntered instead of Ready because _Ready is called before the event Ready is fired and I wanted to make sure that if I had custom logic in _Ready that the nodes would be gathered already.

@31
Copy link
Owner Author

31 commented Jan 6, 2023

Well, well! I never considered generating a constructor. From there, the EnterTree or Ready signal makes sense. I don't actually have to pick EnterTree to preserve GodotOnReady functionality, because creating your own _Ready method was already forbidden while using GodotOnReady on a node script.

(I personally prefer Ready, because descendant node Ready methods/receivers might add additional nodes to the tree or cause other edge cases like that. EnterTree certainly works as its own thing, but as far as keeping GodotOnReady the same as it was in 3.x, it could cause unexpected behavior for some people.)

Thanks, I'll give that a try. I'm hoping I can end up creating one NuGet package that picks the strategy based on the version of Godot it's being used in.

@31
Copy link
Owner Author

31 commented Jan 6, 2023

Oh, never mind: I forgot that _Ready is only one part of the problem.

I just tried it out in 4.0 and there's a more fundamental problem: GodotOnReady generates [Export] members, and there's now a Godot source generator that detects exported members. So, the generated members aren't included and don't show up in the inspector.

@TacticalLaptopBag
Copy link

What's the status of updating to Godot 4? I'd love to make use of this as I currently have several variables being initialized in my _Ready() functions

@31
Copy link
Owner Author

31 commented Sep 2, 2023

What's the status of updating to Godot 4?

No change, here's a summary:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants