-
-
Notifications
You must be signed in to change notification settings - Fork 98
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
Make the $ operator in GDScript cache node references (or add a $$ operator) #996
Comments
A few notes/questions:
|
I don't think it is a good idea. Some users might rely on dynamically changing sub-nodes, so this behavior would lead to undefined references. The thing is that I am not even sure this would lead to a significant performance improvement for several reasons:
Anyway, before trying to improve performance in a given part of the code, we have to make sure this is a bottleneck for some users, or that the gain will be really significant for everyone. So unless someone make a benchmark proving that this part of the code is slow, it's not worth making the engine code significantly more complex. |
I store node references in vars and it does make a performance difference, at least in functions that are running often (e.g. physics_process and process() ). |
If you access the node several times per frame it surely likely does. But I believe such case is an optimization to be done on the user's side, as he can know if the node is eventually going to be replaced or removed from the hierarchy during the processing. |
I feel this is too much magic. If the script is long, you're now adding a lot of stuff on the user's And the more obvious: what do you do when a node is freed? Because if you call Also, the user might remove a node and replace with another using the same name. If you keep a cache, then the It's also possible that you make some cases worse. If the user is using func _ready():
$Button.button_up.connect(_on_Button_button_up) Now it has a cached value that will never be used again. Of course we can be clever about usage, but I do think this will lead to bugs into the user code that will be hard to pinpoint. The only way to get around those would be using If we are going to be clever about usage, then we can consider adding warnings on cases where this is used inside functions called often. Like this: func _process(delta):
$Sprite2D.rotate(PI * delta) # Warning: Consider caching the reference to "$Sprite2D" to avoid getting the node from tree every frame. |
Hm, good point. Could we get a caching equivalent of get_node() (cache_get_node() or some such), or would that require a new shorthand? |
As stated, if you are working with a non-static node tree you would just use |
You are talking about the user point of view, but the problem is not there at all. What is difficult to do is implementing the caching system while guaranteeing that the reference is reset to null when the node is replaced, moved or removed from the tree. This would require adding checks in every move or delete operation to make sure that we cannot have a wrong pointer somewhere in the code (as the engine should not crash at all). This will, as a consequence, slow down those other operations. |
If a node is able to be replaced, moved, or removed, users should use
Conversely: Users who might assume |
I think you don't get the point. This does not changes anything. Even if the user know that, the engine should not crash at all. And this is not negotiable. Even if the user is not supposed to move or remove the node because "it's not supposed to work", we still have to add checks to simply avoid crashes. Checks that are going to slow down everything else. |
If that is true, wouldn't crashes also exist with
|
Ah yes you are right, I did not think about that. I am not sure about how this is handled in the engine code, it's true that variable referencing nodes should not cause the engine crash too. |
onready var x = $X would translate to onready var internal_x = get_node("X")
onready var x = internal_x |
@nathanfranke I meant |
I don't really like this either, notably because caching is only valid if you know your branch will be static. It might not crash if you just move the node elsewhere in the hierarchy so the ref is still valid, but then the code no longer makes sense. Now from my experience, static nodes is very often the case, and I use
Besides, I wouldnt want this to happen, it would make my use case worse. I don't like this kind of magic. Other than that, I feel I'm not the target audience :p |
Here's a thought:
This doesn't break compatibility, adds a shorthand that both new and experienced developers can take advantage of, and still allows for developers to explicitly decide whether or not they want a node cached. |
vnen has a good point I think there are better alternatives. |
I still stand by my original proposal. For any given edge case where the proposed behavior of |
@aaronfranke I want to use |
That's not much different of what you can do today: @onready var label = $Label Edit: just noticed the "shorthand" comment. It's not much of a shorthand if you need to type almost the same amount of tokens. |
I don't see the problem with storing everything in variables. I always store nodes in variables in When using the Edit: |
@BeayemX You can just do |
@aaronfranke and that's precisely not good, because |
@Zylann Interesting, I didn't know that. You can just do EDIT: Just use EDIT 2: In Godot 4, use In the end we both want better performance. Storing references to non-changing nodes is arguably the most common case, which is why I want |
And write even more code, sorry^^"
The case I described isn't dynamic, and I use it in basically every script that accesses child nodes, in all my projects (I'm not the only one doing that). Also demo projects aren't representative of that.
In my case (and some others) this is not just about performance. I also want more maintainable code through decoupling, which happens to be done by explicit caching for fixed nodes, and |
That should be possible to implement if the only usage is in an Also, if removing the existing behavior of |
I agree it's a bit weird but maintaining backwards compatibility is important. Breaking backwards compatibility too frequently can bleed users pretty easily. Breaking compatibility like that might make sense for Godot 4.0, but I also think a lot of people are expecting 4.0 to be largely a "backend" upgrade that just makes their game better without really affecting the way they make it. |
I vaguely recall |
I wonder if it would be a better practice to encourage users to use export nodes (once they are supported) rather than hard-coding node names. export(Label) var my_label
func _process(delta):
my_label.text = "Bad FPS Estimation: " + str(1/delta) |
A change I would like to propose to this system is cache the $ calls BEFORE ready time, so when you instance the node, the $ calls are already cached and available in init or other before ready method. |
I think #3695 is a better way to address the problem at hand for two reasons:
Most programming IDEs that deal with importing packages (Python, Java, PHP, …) also have implemented a similar kind of autocompletion. |
#1048 is another option which doesn't even need to hardcode the path in a script, on top of being meant to run at instancing time. |
This is part of GDScript compile time optimization, it doesn't makes sense for me add a different operation just to cache the node because this feature proposal is chiefly improving the behavior of an existing operator($). However, there may be corner cases where the compiler can't resolve to cache a node (and it can be cached but the code flow is difficult to compute) and the user want to explicit cache it, so in that case it makes sense to add the explicit $$ cache operator. Therefore, for me the best solution is: Enhance the gdscript compiler to cache $ node references where it can resolve and add the explicit $$ operator that [tries to] caches the node regardless whether the compiler optimized it and throw an error if it wasn't possible. |
alright I just read through the entire thread and while the critique made sense to me, I did not see a single comment arguing that absolutely; let the programmer decide where it makes sense to use the current behavior and where caching would be useful. but that doesn't mean we can't have this. it seems like there are two camps of us: by the way, here is an up to date performance test I just ran in the current alpha:
source
as you can see, performance of get_node() with a string passed is poor, however with a constant NodePath (which is always the case when using as for the type hint, couldn't it added implicitly the moment the caching happens? edit: also while I support #1048, I don't agree that it renders this proposal obsolete. again, I don't want to declare variables at class scope unless I have to and I have no desire to expose anything other than things that make sense as configuration to the inspector. |
The caching happens at runtime, since the script can be used in multiple places and may have different children. |
There is the following comment and I agree with it:
If something is used frequently, then you need to store it somewhere (in a variable). This is a universal solution that works not only with
In theory, we can make the GDScript compiler smarter and optimize the |
while I think it's sufficiently explicit when people learn about
the thing is we have no straight forward way of implementing our own explicit caching without using in that sense, it can be seen as more of an syntactic sugar thing than a performance thing.
had the same thought. it's a waste that |
I'm completely new to Godot, so I'm trying to decide if I want to be in the onready/get_node or $ camp. My first concern was the performance implication of the $ operator, so I found this thread. I understand not wanting to break backwards compatibility, but what if there were a source file or (preferably) project-level directive to enable caching (like "option explicit" in JavaScript)? It would let those who know what they're doing take advantage of caching right away without breaking old projects. Then, after a few releases the default value of the option could be flipped to cache by default. |
We prefer not making GDScript behavior dependent on directives by this (or worse, project settings). Scripts may be copy-pasted from a project to another, and this should never cause them to break because a project uses different settings from another. That said, breaking compatibility for 4.0 is not a concern as compatibility was already broken there a long time ago. |
I disagree that #1048 makes this proposal obsolete. I thought this was at least in part about reducing the ugly boiler plate code of
or did you perhaps mean to link the unique node proposal instead? that would make a bit more sense, but still, performance of |
An argument that I'd like to stress further in favor of this is in terms of code readability. Currently, using $ or get_node() to access nodes provides the benefit to the reader of immediately knowing that the object being referenced is 1. A Node 2. Somewhere in the child hierarchy 3. How far down in the child hierarchy it is being directly proportional to the length of the path. This is a ton of information provided to the reader thanks to the use of $ or get_node(). In the case of onready var, the user is now forced to cache the node into a run-of-the-mill variable indistinguishable at a glance from other global scope variables losing the previously described advantages, unless the user specifically breaks variable naming styles to obtain these advantages. This is run of the mill for programming, and there's "no reason" why I should expect one type of variable to be highlighted like this; but the way things are in Godot, I find myself using $ not because I don't want to cache the node, but simply because it makes the code far more readable. I acknowledge that altering behavior for $ would break compatibility in very obscure ways amongst other issues, but I strongly support the implementation of $$ as a cached $. |
I think Godot does have a genuine need for more static scene information, but I don't think this is the way to accomplish it. The better way to solve the issues of node caching is actually with a change to the editor, not with GDScript. The idea would be the editor lets you mark nodes in a scene as static. Then
Furthermore, this approach has no risk of breaking compatibility because all nodes would be marked as dynamic (ie: non-static) by default. |
@nlupugla Keep in mind that a script may be used for multiple scenes, so there is no way to find the static typing information ahead of time. Well, it can be done just in the script editor (it already does this for autocomplete hints, but doing it in GDScript in general at runtime is tricky. @h0lley You are correct. To clarify, with exporting node references, that is a feature I personally would use over the |
Fair point about a script being reused for multiple scenes. I think there are multiple good workarounds though. For example, there could be a way to declare at the top of a script what scene it is statically attached to. extends Node2D in "res://my_scene.tscn" on "%MyNode2D" |
See #1935. But I'm still skeptical about static node validation. The node can still be deleted at runtime, its name and/or path can be changed. |
Thanks for linking to that proposal dalexeev :) In my mind, the whole point of marking a node as static within a scene would be that any attempts to delete, rename, or move it would fail at compile time. The node would be treated like a const. |
This is difficult or even impossible to guarantee. You can always cheat the static analyzer, sometimes unintentionally. Or the forbidden operation can be performed by external code or the engine. var t: Variant = static_node
t.queue_free() |
Fair enough. The program could abort upon trying to delete a static node though, if static analysis isn't enough to catch everything right? By the way, I don't mean to digress too far from the original topic. On the subject of cached node references, my main problem with the idea is that it makes a bunch of implicit assumptions about the structure of your scene, whereas the static scene approach makes these assumptions explicit. In both approaches, there will be a problem if the node in question moves. |
To be a bit more clear on the idea, I wrote up a proposal: #7572 |
Personally, I adhere to the GDScript Style Guide and use an underscore prefix combined with PascalCase to distinguish node reference variables from regular variables. I'm not sure if others appreciate this style, but if it's possible to implement highlighting for _PascalCase variables in green, it would be quite good for me. |
This is about $ and/or caching, what does PascalCase have anything to do with it? |
Sorry for my misunderstanding. |
EDIT:
If #1048 is implemented, it may make this proposal obsolete.EDIT 2: As @h0lley noted, #1048 doesn't actually make this proposal obsolete. It provides a clean alternative but it still results in boilerplate and a lack of green syntax highlighting unlike this proposal.
Describe the project you are working on: https://github.com/godotengine/godot-demo-projects/ but this applies to any project made in GDScript.
Describe the problem or limitation you are having in your project:
Here is a code snippet from Dodge the Creeps:
The problem is that this code looks elegant, but it isn't very performant. Every time this method is called, it has to call
get_node()
6 times, and 2 of those times are getting the same node (HUD). We can reduce this from 6 to 5 by caching the HUD reference like this:However, this method is still very inefficient, since it calls
get_node()
5 times every time the method is ran. The best simple option that I can see is to useonready var
like this:This means that
get_node()
is only called 5 times once when this node is created, and this method does not have to callget_node()
at all when the method is ran. However, now this code is ugly, since it is much longer than it could be and we've lost the green syntax highlighting innew_game()
that tells us we're working with node children.Describe the feature / enhancement and how it helps to overcome the problem or limitation:
The proposal is to make the
$
operator cache references on ready, so that user code looks like the top image, but behaves like the bottom image.For projects currently caching references via
onready var
, this would make code more elegant.For projects currently using
$
everywhere, this would increase performance.In cases where
get_node()
needs to be called every time, such as if nodes are created and deleted, users can just useget_node()
instead of$
. As such,$
would have different behavior fromget_node()
.Another option, described below, would be to add a
$$
operator with this behavior.Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
See above for examples of user code, but @vnen would be the one handling the implementation.
If this enhancement will not be used often, can it be worked around with a few lines of script?:
It can be worked around, but this is something that will be used often.
Is there a reason why this should be core and not an add-on in the asset library?:
Yes, because it will be used often.
The text was updated successfully, but these errors were encountered: