godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.1k stars 69 forks source link

Add node binding to scenes #3056

Closed timothyqiu closed 2 years ago

timothyqiu commented 3 years ago

Describe the project you are working on

GUI which have lots of nested containers.

Describe the problem or limitation you are having in your project

Having to write long node paths in order to reference the node. This is also a nightmare when refactoring the naming / the tree structure.

onready var label2 = $VBoxContainer/MarginContainer/HBoxContainer/MarginContainer/MarginContainer/HBoxContainer/Label2

There are workarounds, but feels repetitive, as you have to type the same word multiple times:

# A
onready var my_fancy_node = find_node("my_fancy_node")

# B
export var my_fancy_node_path: NodePath
onready var my_fancy_node = get_node(my_fancy_node_path)

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Allow the user to set which node they want to bind in Inspector.

A node that is bound will be directly accessible in code, without the need to define a variable and initialize it by $Path, get_node, or find_node.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

This is inspired by the Android view binding.

Mark the node you want to bind

There are two possible ways to implement, I don't have a conclusion which path we should take.

  1. By enabling the boolean "Use In Bind" property, and the corresponding variable will be a snake_cased node name.
    • Pros:
      • Simple & intuitive
      • No need to come up with a variable name
    • Cons:
      • You have to change the code once the node is renamed
      • You have to use snake_cased variable in C#
  2. By filling the string "Bind Name" property, and the corresponding variable will be named accordingly.
    • Pros:
      • No need to change the code once the node is renamed
      • You can use CamelCase for C# and snake_case for GDScript
    • Cons:
      • You still have to change the code if you want to rename the variable
      • You have to come up with a reasonable variable name

Binding

For a PackedScene like this:

- Player
    - CollisionShape2D
    - Sprite (bind: sprite)
    - AnimationPlayer (bind: animation_player)

A separate anonymous class is generated when loading the PackedScene like this:

# This is not necessarily a real script. Creating the object on the fly is also okay.
extends Reference

# these are typed, and are guaranteed to be not null (a compile error otherwise)
var sprite: Sprite  # will be assigned when instantiating the scene
var animation_player: AnimationPlayer  # will be assigned when instantiating the scene

During the instantiation of the scene, an object of the anonymous class is created with members assigned. The object is then assigned to the instanced scene's root node.

To get the object, call get_binding() at the owner/root node of the PackedScene:

extends KinematicBody2D

func _process():
    var binding = get_binding()
    if Input.is_action_just_pressed("ui_cancel"):
        binding.sprite.flip_h = true
        binding.animation_player.play("animation")

get_binding() will return null when

Common Questions

What if the node is moved at runtime?

The variables in binding are assigned at load time. Moving a node won't affect the reference.

How are nested scenes processed?

It's okay to call get_binding at the root of the nested scene.

If the instanced scene has Editable Children on, variable names inside the instanced scene also appear in its owner's binding.

What if there are duplicates?

The generated anonymous class fails to compile. We can also give the scene a configuration warning in editor.

If a scene is added to another dynamically, binding won't change as they are generated at load/compile time.

Is it possible to change bindings at runtime?

No, everything is done at load/compile time.

What about C#?

It's the same. You get the binding by calling GetBinding().

If this enhancement will not be used often, can it be worked around with a few lines of script?

Adding a custom property & showing it in the Inspector is okay. (meta & EditorInspectorPlugin)

But I think it's not allowed to injecting logic into the loader or to the PackedScene.instantiate() function.

Is there a reason why this should be core and not an add-on in the asset library?

The same reason as above.

zinnschlag commented 3 years ago

A workaround is to use find_node, but it seems that the use of it is discouraged

I don't think this statement is true. If you look at the proposal you linked, there is strong opposition and the chances for it ever to be implemented should be rather slim.

Nodes already have names (see find_node). Adding a second name seems redundant. It adds unnecessary complexity.

Calinou commented 3 years ago

Nodes already have names (see find_node). Adding a second name seems redundant. It adds unnecessary complexity.

find_node() still has the issue that it's stringly-typed and leads to run-time errors (as opposed to compile-time errors). It also lacks autocompletion as far as I know.

Also, using find_node() every frame has a performance impact. Depending on how it's implemented, this node binding proposal could potentially remove the need to cache node references since the caching would be internal to each node, especially since https://github.com/godotengine/godot-proposals/issues/996 may not be accepted. On the other hand, static typing remains an open question.

timothyqiu commented 3 years ago

Nodes already have names (see find_node). Adding a second name seems redundant. It adds unnecessary complexity.

The point is, it'll remain stable across node renaming & tree restructuring.

Giving the node an identifier name also marks the need to register it in the scene tree (the 2nd approach), so that not every node has to do it.

zinnschlag commented 3 years ago

find_node() still has the issue that it's stringly-typed and leads to run-time errors (as opposed to compile-time errors). It also lacks autocompletion as far as I know.

True. But that has nothing to do with the Node getting a second name.

Also, using find_node() every frame has a performance impact.

Again, true. But I don't see that as an argument. If we want to remove or replace every bit of functionality that has a significant performance impact when used every frame, then we don't have a working engine any more. Yes, this statement is a bit hyperbolic. I still don't see "feature can be used in a bad way by an inexperienced user-> feature is problematic" as a reasonable argument.

zinnschlag commented 3 years ago

The point is, it'll remain stable across node renaming & tree restructuring.

Is that a good thing? I mean specially the first part (renaming). I can see your point in regards to restructuring. But if you rename a node shouldn't that break things, because it is now a different node/node with a different role or purpose? Why else would you rename it?

The whole issue could be solved by adding a variant of $ that takes only the leaf name instead of the whole path. This can be checked in the editor in the same way as ordinary $ and it does not involve making the already complex Node class even more complex.

timothyqiu commented 3 years ago

Why else would you rename it?

It's the same reason why one would keep the variable name while renaming a node. For example:

var buttons = $GridContainer

I setup the scene quickly without changing the name. And later thinks it's better to rename it to "ButtonContainer" or "Buttons".

You can just think the new name is the variable name, they are interchangeable in this proposal. You specify the variable name in inspector, and the name is accessible by code (through the bindings proxy object).

The whole issue could be solved by adding a variant of $ that takes only the leaf name instead of the whole path.

zinnschlag commented 3 years ago

Well, then we have to agree to disagree. I don't see node renames as a vulnerability issue. And I find the onready method combined with $ to be supremely clean (with the exception of tree restructuring): You explicitly declare the nodes from the tree that you want to use.

Edit: Okay, I agree that we could use better error checking with onready/$. But that sounds like an orthogonal issue.

timothyqiu commented 3 years ago

I think it's quite Repeat-Yourself™ to write

onready var my_fancy_node = find_node("my_fancy_node")

It's the same problem Android developers faced years ago

TextView hello = (TextView) findViewById(R.id.hello);
// with view binding, one just use binding.hello directly
golddotasksquestions commented 3 years ago

This proposal sounds like it would offer exactly the same functionality and ease of use groups already do, or am I missing something here?

dalexeev commented 3 years ago

Another workaround is to export the node path, but it seems repetitive

1048 Ability to export Node types instead of just NodePaths

See also:

1776 Add shorthand expr$NodePath for expr.get_node("NodePath")

2812 Allow to register nodes globally with a unique identifier

func _ready():
    assert(self.bindings.ok_button == $Path/To/That/Button)
    # much like a dictionary, bindings.ok_button is equivalent to bindings.get("ok_button")

Similar to #2812, but at least not global identifiers, but local to object/scene/branch. However, it seems to me that #1048 is the most optimal solution to the problem.

Unique (at least for a branch) node names + find_node + onready vars is also a good approach, I don't see the need to create a duplicate system. In both cases, you need to come up with unique names and track their renaming.

Either we limit ourselves to one name property, or we end up with complex CSS-like selectors (with ids, classes, attributes, etc.).

timothyqiu commented 3 years ago

@golddotasksquestions Nope. This is about setting the variable name in Inspector and use it directly without the need to declare the variable or finding nodes manually.

# with groups
var my_node = get_tree().get_nodes_in_group("my_node")[0]
my_node.do_something()

# with node binding
bindings.my_node.do_something()
timothyqiu commented 3 years ago

In both cases, you need to come up with unique names and track their renaming.

@dalexeev The bind identifier (i.e. the identifier to bind) is the variable name. You have to come up with a variable name anyway.

Yeah, export node type can make things right without introducing much change. But I think it cumbersome having to

  1. first switch to script editor and define the export variable
  2. then switch to 2d/3d editor and assign the value (by dragging or opening another dialog to pick one)

With node binding, you do all this in the Inspector by filling in a variable name, without the need to switch back and forth.


Node binding can be changed to use the node name directly, with "Bind Identifier" property changing to a "Should Bind Variable" toggle. But in this case:

dalexeev commented 3 years ago
  1. first switch to script editor and define the export variable
  2. then switch to 2d/3d editor and assign the value (by dragging or opening another dialog to pick one)

It's not necessary to switch to the 2d/3d editor to assign a node.

https://user-images.githubusercontent.com/47700418/127664396-bedb4285-48d3-4fc2-903d-8dc0eab98db8.mp4

Everything will be exactly the same, only one more variable is not required to get Node from NodePath.

timothyqiu commented 3 years ago

@dalexeev If you switch to the script editor by clicking the script icon on the node tree, the Inspector will be inspecting the script itself instead of the node.

dalexeev commented 3 years ago

If you switch to the script editor by clicking the script icon on the node tree, the Inspector will be inspecting the script itself instead of the node.

And what's the problem with clicking on the node in the Scene dock? The inspector will switch to the node; you will not need to switch between editors.

And I usually only click once on the script icon in the node tree, and then use the buttons at the top of the window, because the script is already open in the script editor. In this case, the inspector does not switch to the script.

It seems to me that we need to remove the switching of the inspector to the script when clicking on this icon. This is usually not required, but if anyone needs it they can click here:

timothyqiu commented 3 years ago

And what's the problem with clicking on the node in the Scene dock? The inspector will switch to the node; you will not need to switch between editors.

It's still switching back and forth, isn't it?

We start after adding & naming the node in the scene tree.

With node bindings:

  1. Type the variable name in the inspector, done

With export node type, the shortest path:

  1. Switch to the script editor, and declare the export variable, and save
  2. Drag the node into the inspector slot
dalexeev commented 3 years ago

I just see a lot of questions in the approach itself.

You are suggesting to assign the id at the node, not at the root of the scene. But the bindings themselves will be local to what: only to the root node, to all nodes in the scene, or to a branch (like find_node)?

What will happen if you move nodes with ids at runtime, how are nested scenes processed, what will happen if a node with id is duplicated (in the editor and in runtime), is it possible to change bindings at runtime, what will happen if you copy a node with id to another scene in the editor, etc.?

For example, we have this scene:

What happens if you do

self.bindings.node_2
self.bindings.node_4
self.bindings.node_5

$Node2.bindings.node_2
$Node2.bindings.node_4
$Node2.bindings.node_5

$Node2/Node3.bindings.node_2
$Node2/Node3.bindings.node_4
$Node2/Node3.bindings.node_5

?

Sorry, at first I wanted to write in more detail, but I cannot clearly formulate all my objections in English.

dalexeev commented 3 years ago

As for me, creating export variables and assigning them in the editor is fast enough. But we can add a button and a dialog to quickly generate such variables, similar to the signal connection dialog.

timothyqiu commented 3 years ago

The OP listed several apporaches implementing this feature.

Naively, bindings is a local cache. node.bindings.identifier searches inside the node branch, and it'll be an error / debugger break if no found.

Ultimately, bindings is an object with properly typed members injected when loading the PackedScene. Using an undefined identifer is... well, an undefined identifer error when loading/compiling the script. node.bindings.identifier is just a normal member access for nodes with script attached. And for nodes without script attached, it can search for such node in its branch first (but why?)

self.bindings.node_2  # Node2
self.bindings.node_4  # Node4
self.bindings.node_5  # Node5

$Node2.bindings.node_2  # Node2
$Node2.bindings.node_4  # Node4
$Node2.bindings.node_5  # error

$Node2/Node3.bindings.node_2  # error
$Node2/Node3.bindings.node_4  # Node4
$Node2/Node3.bindings.node_5  # error

What will happen if you move nodes with ids at runtime

how are nested scenes processed

it does not matter where the branches come from

what will happen if a node with id is duplicated (in the editor and in runtime)

If you add one packed scene to another as a child, and they contain the same id:

is it possible to change bindings at runtime

Not possible. You can't rename variables at runtime either.

what will happen if you copy a node with id to another scene in the editor

It's a valid action, and the name is kept.

dalexeev commented 3 years ago

Thank you, now it is clear that you are offering an analogue of find_node, but with some differences.

Note that by induction, identifiers must be unique throughout the scene tree (since $"/root/".bindings.some_id is also allowed). In this case, the question arises why the restriction is needed:

$Node2/Node3.bindings.node_2  # error
$Node2/Node3.bindings.node_4  # Node4
$Node2/Node3.bindings.node_5  # error

It should be:

$Node2/Node3.bindings.node_2  # Node2
$Node2/Node3.bindings.node_4  # Node4
$Node2/Node3.bindings.node_5  # Node5

That is, it again boils down to #2812. Looks like id in HTML.

timothyqiu commented 3 years ago

Note that by induction, identifiers must be unique throughout the scene tree (since $"/root/".bindings.some_id is also allowed). In this case, the question arises why the restriction is needed:

Sorry it was very late into the night when I wrote that.

Again, identifiers are variable names attached to the script. They are not necessarily unique thorughout the scene tree in the ultimate version, but are kept unique in the naive version so that people won't get unexpected result due to implementation limits.

The naive version is like find_node because it's easy to implement. The restriction is added trying to keep the same behavior as the ultimate version.

Ideally, for a scene:

- Player
  - Collider
  - Sprite (bind: sprite)
  - AnimationPlayer (bind: animation)

the script you write on Player is:

extends KinematicBody2D

func _process():
    if Input.is_action_just_pressed("ui_cancel"):
        bindings.sprite.flip_h = true
        bindings.animation.play("that-animation")

but at load time, what actually gets compiled is:

extends KinematicBody2D

class AnonymousBindings:
    var sprite: Sprite = <inject the node>
    var animation: AnimationPlayer = <inject the node>  # compile error if you name the AnimationPlayer "sprite"
    # if an instanced scene contains a name "sprite", its added here at load time, so compile error

    func _get_unknown(name):
        return _try_to_find_in_children(name)

var bindings = AnonymousBindings.new()  # or maybe something similar

func _process():
    if Input.is_action_just_pressed("ui_cancel"):
        bindings.sprite.flip_h = true
        bindings.animation.play("that-animation")

So for this:

image

$Node2/Node3.bindings.node_2  # error
$Node2/Node3.bindings.node_4  # Node4
$Node2/Node3.bindings.node_5  # error

make sense if Node3 (or Node4) has a script attached. But if not, it's better to emit error for all these three expressions.

willnationsdev commented 3 years ago

I'm with dalexeev on this one. I don't like how it's effectively declaring a global variable for a node, but with the added caveat that the global variable itself is stateful because it only exists if you're accessing it from a high-enough branch.

Things are generally cleaner when access points are maintained in data that is constrained to a particular subtree, and completely inaccessible outside of that subtree unless you grab it from the "root" point, just like how scene roots can define variables and internal nodes can use owner to quickly get the root and access its property references.

but at load time, what actually gets compiled is:

class AnonymousBindings:
    # etc.

Also, if you had a "bindings" system, implementing it as a language feature that updates the source code representation just-in-time like a macro doesn't really make any sense imo. You'd end up needing to build similar functionality for all languages, and from what I understand, macros are generally frowned upon anyway by the core devs.

If you don't make it a language feature and instead made it an engine feature, then the serialized data wouldn't be able to be kept inside the scripts. It would need to be stored globally in ProjectSettings or compartmentally within PackedScene resources (the latter makes a lot more sense). But, at that point, it doesn't make sense for the bindings themselves to be defined from individual scripts when that would then cause multiples of the same script in one scene to have node name binding collisions within the scene.

It's just a bad idea all around, practically speaking. In comparison, #1048 seems like the sanest approach since all you are doing is directly tying a specific variable to a statically-type-checked node reference assigned from the EditorInspector. I'm fine with swapping editors to make the node assignment if it means things are kept clean architecturally.

And if you're that concerned about having to lift keys from the keyboard and swap editors, then there's always the option of adding a new autocompletion feature to the ScriptEditor that triggers when you type export my_label: Label or export(Label) var my_label and displays a dropdown of nodepaths from the currently open scene, lets you select one, and then, once selected, automatically assigns the exported value in the scene without modifying the source code (a bit weird, but possible).

timothyqiu commented 3 years ago

The naive approach was meant to help understanding, but it caused more misunderstanding and not-so-necessary awkward APIs apparently.

Based on your feedbacks, I made some adjustments to the proposal. Feedback is much appreciated 😄

willnationsdev commented 3 years ago

@timothyqiu

A separate anonymous class is generated when loading the PackedScene like this:

I think you are ultimately saying that you would like a built-in GDScript resource to be generated/updated inside the PackedScene object whenever a scene is saved and there are changes to the "node bindings" controlled by the Editor GUI.

However, that would cause the engine (/scene/resource/packed_scene.h) and/or the Editor that is generating this GDScript to become dependent on the GDScript language. So this is not an option. You would need to either make it a Dictionary (where type information is completely lost) or make it data that is accessible from the PackedScene resource itself.

This, in turn, would also complicate the get_binding() function since, as a Node function, it wouldn't be able to return the loaded PackedScene resource or access any custom methods inside of it since that would cause Node to have an unnecessary dependency on PackedScene. This is why the Node class's filename property that associates it with a scene is just the String of the filepath for it. This separation of dependencies allows you to have a Node exist in memory without requiring that the scene which spawned it is also loaded in memory.

In order to make this concept actually work, you would need to have the information be stored in both Node and PackedScene, that way the Editor can track the changes, update the info in a non-Script-related C++ data structure within the PackedScene class, and upon instantiation of the scene transfer that same information to an identical data structure present in the Node class which is only populated in "root" nodes. However, while this could work, it would be a kind of software architecture faux pas / noob move. It adds complexity to classes (primarily Node) that have no business being involved in the feature in the first place. Furthermore, it would increase the already-bloated size of every single Node that is ever instantiated which has a larger impact on memory usage/allocation benchmarks.

There are two possible ways to implement...

  • By enabling the boolean "Use In Bind" property, and the corresponding variable will be a snake_cased node name.

  • By filling the string "Bind Name" property, and the corresponding variable will be named accordingly.

Neither of these methods sounds appealing to me. Users either have to pick a second scene-wide name for nodes (which will be confusing to anyone new to the engine), or users have to automatically use whatever naming convention they use for Node names in their scripts (and no, they don't necessarily match. For example, I use UpperCamelCase node names regardless of the scripting language used and its internal naming conventions).

Furthermore, generally speaking, Godot has a tendency to prefer configuration-based approaches over magical approaches. Having variables that procedurally generate themselves based on a special secondary name for a particular instance is definitely teetering into "magical" territory. And having to manually name things doesn't work well for things with names that change since you then have code references in script code that have to deal with the change. You still have refactoring issues just like before; it has simply taken a different form.

instance_from_id(<filled by loader>)

The variables in binding are assigned at load time. Moving a node won't affect the reference.

Just an FYI, these would still be NodePaths passed into a get_node() call. Each Object's instance ID is its memory address in the computer, so if you close and re-open the Editor or spawn a new instance of the engine by, say, running the current scene, then all created objects will have different IDs. The only things that remain consistent are Resource filepaths and Node NodePaths relative to the containing scene. And you have to serialize the "path" of how to get the data somehow, otherwise you won't know which instance IDs to load at "load" time, as you put it. ;-)

To get the class, call get_binding() at the owner/root node of the PackedScene:

I mean, if the purpose of get_binding() is usability, then you may as well just have get_binding() implicitly return owner.get_binding() internally for any Node that has a valid owner and is not the root of a scene itself. Course, that doesn't really get around the root node's PackedScene -> NodeBindings <- Node dependency issue. Just an optimization of the original proposal.

What if there are duplicates?

The generated anonymous class fails to compile. We can also give the scene a configuration warning in editor.

If a scene is added to another dynamically, binding won't change as they are generated at load/compile time.

The fact that duplicates can create an issue of any kind, whether script compilation error or editor configuration warning, is already something that sounds like it's going in the wrong direction to me. For things like global script classes or autoloads in the ProjectSettings, that makes sense. They are inherently global. But we're talking about scene-local node names. There should be zero possibility of any kind of name collision happening in the Godot API imo.

Things become increasingly complicated when you factor in not only scene ownership and Editable Children, but also scene inheritance. Currently, derived scenes cannot change the NodePaths of any nodes defined in base scenes, but the script associated with any node can be changed, and therefore its exposed properties can also change and adapt to changes at runtime. If you keep the symbols in code, then you preserve this flexibility even while granting the additional usability, but if you move the symbols into the Editor, then the benefits come at a cost to the users who might want to combine it with their other feature usage.


With the #1048 approach, none of these issues are present.

timothyqiu commented 3 years ago

@willnationsdev

Sorry, I mean the variables are assigned "when instantiating" actually.

There is only one additional use_in_bind / bind_name property that's serialized & deserialized with the nodes.

I don't think this is magical because you're consciously calling get_binding() to get the bindings. It'll be magical if the variables are created directly inside the node class itself.

I don't think this proposal conflicts with #1048 either. The problems they want to solve have intersections, but one does not contain the other. (Sorry for my English.)

dalexeev commented 3 years ago

@timothyqiu Your proposal turns out to be either too contradictory, or too complex and limited (and, in my subjective opinion, does not fit into the philosophy of Godot).

Are you sure your proposal is important to a significant part of Godot users? Why do you think this should be implemented in the core and not in a plugin or module? For example, I made the following plugin as a POC: dalexeev/godot-global-node-ids-plugin. It implements #2812, not your proposal, but I'm sure if you spend more time you can implement a solution what you want.

timothyqiu commented 3 years ago

@dalexeev Thank you. I didn't know there are meta we can use. But on the other hand, I don't think a plugin is allowed to add logic to the loader and to the PackedScene.instantiate() function.

It's hard to determine whether something is important to a significant part of Godot users. But view binding makes Android developers' life easier.


I think this is like the list comprehension proposal (#2972). Python / Android developers are used to their features and think it's life-changing. But Godot users don't know that feature and there are lots of misunderstandings.

dalexeev commented 3 years ago

But on the other hand, I don't think a plugin is allowed to add logic to the loader and to the PackedScene.instantiate() function.

You can generate variables (or export variables) in a script attached to the root of the scene. In this case, static typing will even be supported and you will not have to use a singleton.

But view binding makes Android developers' life easier.

What works in one situation may not work well in another.

timothyqiu commented 3 years ago

You can generate variables (or export variables) in a script attached to the root of the scene. In this case, static typing will even be supported and you will not have to use a singleton.

Then it's "magical" as described in willnationsdev's comment.

The current proposal is static typing, and is not using a singleton. Please see the updated proposal.

dalexeev commented 3 years ago

Then it's "magical" as described in willnationsdev's comment.

No. Variables are explicitly added to the script (as opposed to your suggestion, where binding.variables are not explicitly declared in the script, but are generated from the fields of the scene nodes implicitly). In the same way, signal callbacks are added using the Connect Signal dialog.

Please see the updated proposal.

I have read.

timothyqiu commented 3 years ago

as opposed to your suggestion, where binding.variables are not explicitly declared in the script, but are generated from the fields of the scene nodes implicitly

I have read.

But it's not binding.variables. Users get the node by get_binding().variable, explicitly calling a function.

Generating the fields is an implementation detail that users don't have to know. All they do is call get_binding() to obtain an object that has the fields added.

get_stack() returns an array with stack info. We can access the current function name by get_stack()[0].name, but we don't say name is implicitly declared in the script.

dalexeev commented 3 years ago

But it's not binding.variables. Users get the node by get_binding().variable, explicitly calling a function.

Node.get_binding() -> Dictionary? Then static typing is lost.

timothyqiu commented 3 years ago

@dalexeev No. Please read the proposal.

Node.get_binding() returns an object. The object is an instance of an anonymous class. The class is generated during scene loading.

dalexeev commented 3 years ago

Node.get_binding() -> Object? Then static typing is lost anyway.

@onready @export var label: Label

func _ready() -> void:
    print(label.text) # Safe and autocompletion works.
func _ready() -> void:
    print(get_binding().label.text) # Unsafe and autocompletion does not work.
timothyqiu commented 3 years ago

Node.get_binding() -> Object? Then static typing is lost anyway.

This is a good question. The return type of get_binding() should be the anonymous class. But I don't know if it's currently possible to do it.

timothyqiu commented 2 years ago

Just found this NodeMapper plugin from AssetLib: https://godotengine.org/asset-library/asset/941

The basic concept is the same as this proposal, but with a few more steps:

  1. Prefix the node name with $ if you want to bind it (the generated variable name will be the part after the $)
  2. Run a script to generate a .gd file that contains all the variable names and get_nodes
  3. Change your script to inherit from that generated script.
fire-forge commented 2 years ago

Is this closed by https://github.com/godotengine/godot/pull/60298? The implementation is different than described here, but it solves the same problem.

timothyqiu commented 2 years ago

Yeah, scene unique node are similar to node binding expect it is not type-safe. I'm closing this proposal since type-safety is not the main concern here.