-
-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Allow registering "runtime classes" from GDExtension #82554
Allow registering "runtime classes" from GDExtension #82554
Conversation
4c4635c
to
b30073a
Compare
Actually, this seems to be mostly working? I haven't managed to get classes switching back and forth between "gameplay" and "not gameplay" on a hot reload, but otherwise the basics are working. The implementation feels kinda messy, and I'm not sure if this is the right approach in general, so this'll need some more testing (to see if it meets expectations) and discussion. |
I've been thinking about this PR over the weekend. I think what it's doing at a high-level is probably the right overall approach (ie. making placeholders when gameplay classes are instantiated in the editor). However, the implementation is kinda messy, and I'm not crazy about it. Perhaps the way that Perhaps if we wrapped at least the extension instance in a light-weight class, the interactions could be made cleaner and easier to maintain? Since performance is important in GDExtension, we'd probably want to make sure that all the hot-path functions are In any case, I may experiment with some ways to refactor how |
As prior art, I worked around this issue in godot-rust: godot-rust/gdext#365 Basically, what you call gameplay classes is the default, and a I haven't built a larger game with this yet, so there's some research to be done on how useful that behavior is in practice. But so far, community feedback has been quite positive. For a Godot-based solution to this problem, it would be good to outline the exact semantics that one would expect. For example, configuring exported properties is often still desired inside the editor. |
Thanks for sharing your approach from the Rust bindings! I think we want to go further than just not executing virtual methods, though. In the implementation in this PR, we don't actually create the real class (except for once, in order to get the property defaults), and all the placeholder can do is get/set property values (so you can still edit the object in the editor inspector). This fairly closely matches how non- |
Would this still allow for preventing Use case: I often want to load certain external resources already at construction time, so that the class can consider "all external resources are loaded" as an invariant, i.e., not having to deal with partial initialization. If I remember correctly |
Yes. With the current implementation in this PR, each "real" class needs to be created exactly once in order to get the default values for its properties, but it's destroyed right after that, and all subsequent instances of the class are just "placeholders". The placeholders don't create an instance of your real class, they just have a hash map of the properties, so they can be edited in the inspector. |
Question regarding that, what if the class cannot be constructed (GDExtension doesn't provide a |
Hm. I think the current PR doesn't correctly handle that situation, but I think what it should do, is also not allow creating a placeholder if the class can't normally be created. I'll add a fix for that when I come back to working on this one. :-) |
b30073a
to
fbb2cf2
Compare
I've finally finished my big refactor of how Previously, This makes it easy for the The main difference between what I wrote in my comment and what I implement has to do with this:
I originally did try using There's only a handful of things left to do on this (see the PR description) before it's ready for review! |
cdf7f22
to
7882bef
Compare
I just pushed the following changes:
I also tested using hot-reload with a gameplay class, and it seemed to work fine! Assuming tests pass, this should finally be ready for review :-) |
Actually, thinking about it some more... I think I know a way that I could do this without the refactor into |
@Ansraer brought up some interesting cases on RocketChat that I haven't accounted for:
|
In C# at least when you create a Tool class and try to reference or GetNode() a Non-Tool class, it creates an InvalidCastException because there is no actual Non-Tool class running in the editor. Although in GDScript it works perfectly fine which is a little weird to me but I don't know the inner workings of the C# or GDScript bindings. There are multiple people that have run into this problem and its something that is pretty annoying because you then have to mark everything as [Tool] or just use the inherited class which loses some information about the class I attached a MRP showing an example of what I mean if I didn't explain it properly! don't know if this necessarily applies to this new gameplay classes but worth bringing up :D Hopefully its something that can be fixed in GDExtensions or C# since my preferred use case is to have a [Tool] script that can automatically get references with GetNode() or just perform some operations on its children that are NonTool classes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good, makes sense, haven't tested ATM though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general it looks good, save for missing unlikely()
or likely()
to help the optimizer. I marked a few but in the more critical functions it would be good to ensure they are there.
I also feel there is some overlap with MissingResource and MissingNode, but I am not sure if this is fine or intended (I guess its not exactly the same thing).
Other than that it looks great!
So, here is a thought about this. If you are extending the engine and using GDExtension, then from your perspective anything that runs in the game will be gameplay. But if you are using a GDExtension language (say eventually C# or Rust), you will most likely not want to extend the engine and use all classes for gameplay. As such, I am not entirely convinced that this is something we should expose as "gameplay" rather than "tool". At least, as example, I am pretty sure that when C# is moved to GDExtension, it will still want to retain the "tool" concept (given again 99% of the people uses it to create gameplay), so it may want to do this in a flipped up way. Has this been discussed? Maybe an alternative could be, at extension registration time (and this is mostly on the extension side?) have to specify what the default is (tool or gameplay), and then those that do not meet the default have to be specified. |
0c2127e
to
78f74e8
Compare
@reduz Thanks for the review! All your code comments should be addressed in my latest push :-)
It's not really the same thing. There's some high-level similarities in the implementation, but with placeholders for "gameplay classes" (at least if we want to match the behavior of non-
We could still use "tool" for the naming, if we said all classes are "tool classes" by default, and you're opting out of being a "tool class" (and I guess call these "non-tool classes"?). However, I personally feel it's kinda awkward when it's the "non-tool classes" that are the special case, especially in verbal/written discussions and in godot-cpp (we already have So, personally, I still prefer using the "gameplay classes" naming, however, I'd be happy to go with whatever alternative gets the community/contributor consensus. |
@dsnopek at this point I wonder, why not making the tool stuff the non-default route? That's probably what reduz is proposing. |
Hm, well, even if we change the default for classes registered from GDExtension, we still can't really call them "tool classes" and "normal classes" (at least without some confusion) because "normal classes" means something different (in fact, the complete opposite) for classes registered in the engine. From a naming perspective I think we'd still call them "tool classes" and "nontool classes", or at least I would. :-) So, to me, the default is a separate issue - unless we're talking about changing the default for classes registered in engine too? |
@dsnopek, uh, so classes by default are always run in the editor? Makes sense, after all the editor is a game. Rightly you noted that in the OP too (sorry, been a while since I read it). This might require lots of further discussion, as this would definitely imply some deeper change in the way extension classes are handled. Right now, since the extension API is mostly "low-level", I think that the most important thing would be to just expose a way of making "non-tool" classes. We can then discuss on making the default behavior different somehow. If the naming is an issue, what about calling those those "inert", "project", or just "placeholder" classes? After all, we're supposed to mimic modules more than scripting, right? We want them to work like normal module classes. |
It seems like the primary remaining issue is what to name the different types of classes :-) These seem to be the most popular options based on previous discussions here and on RocketChat:
I'll ask again on RocketChat too... |
78f74e8
to
811e029
Compare
Based on an informal poll on RocketChat, it appears that "runtime class" is the most popular name. So, in my latest push, I've re-named everything. Please let me know what you think! |
811e029
to
8090243
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at this slightly harder than usual and, outside of a small nitpick, this actually looks fine! :D
Also, as this part of the engine is one I'd like to know a bit better, I made a bunch of questions regarding how this works, as there are some details that I'm missing. Hope you don't mind.
8090243
to
a74720a
Compare
@Riteo Thanks for the review! ❤️ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested this but code wise looks great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there more naming changes to be expected, like the "tool/non-tool" discussion? I'd like to give this a try but each API change comes with quite a bit of overhead (fetch again, recompile Godot, generate bindings, adapt code) 🙂
a74720a
to
ea75307
Compare
I really hope not :-) |
Thanks! |
Classes registered via GDExtension normally run in the editor (just like module classes) which is sort of similar to
@tool
scripts. This is the number 2 complaint from people who attempt to use GDExtension to write gameplay code (as opposed to people who are using GDExtension to extend the engine).This PR allows registering "runtime classes" from GDExtension, where the real code doesn't run in the editor, similar to how non-
@tool
scripts work.The idea is that when running in the editor, these special classes would have "placeholders" created, which can store the classes properties, so that the inspector and saving/loading still works (which is similar to how non-
@tool
scripts are implement too).In order to do this, this PR refactors howObject
handles extension instances. Previously, this was just avoid *
and everything inObject
and elsewhere had to interact with the GDExtension interface directly. This PR adds aObjectGDExtensionInstance
class to centralize this work, so now we have theObjectGDExtension
andObjectGDExtensionInstance
classes which are analogous to theScript
andScriptInstance
classes.This makes it easy for theObjectGDExtensionInstance
class to switch over to "placeholder mode" when necessary.UPDATE: I ended up rolling back the refactor, because I came up with a clean way to handle placeholders without it.
For this PR to work with godot-cpp, you need this companion PR godotengine/godot-cpp#1256
Still TODO
Get the default values for the real classes properties (we may need to create a single instance the real class to do this - I think this should be possible to do)Set/get property values and ensure saving and loading worksCorrectly handle reverting properties via the inspectorSee ifObjectGDExtensionInterface
can do something smart for theGDVIRTUAL*()
macrosEnsure that this works with hot reload (it should)Investigate the interaction between runtime classes and tool GDScriptsInvestigate the interaction between runtime and non-runtime classes from the same GDExtensionFixes #54999
Production edit: closes godotengine/godot-roadmap#36