godotengine / godot-proposals

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

Add ability to set additional scripts to nodes/objects #2296

Open me2beats opened 3 years ago

me2beats commented 3 years ago

Pls note: this is not for having multiple full-fledged scripts on a node/object like this https://github.com/godotengine/godot/issues/3205

Describe the project you are working on

Plugins

Describe the problem or limitation you are having in your project

I am creating a complex plugin that has many pieces of code which could (and should) be separated into independent classes and reused.

Multilevel inheritance only partially solves the problem and is not a flexible solution.

Example

For example, I have 3 independent script behaviors: x.gd: ```gdscript extends Object func _to_string(): print("x") ``` y.gd: ```gdscript extends Node func _ready(): print("y") ``` z.gd: ```gdscript extends BaseButton func _pressed(): print("z") ``` And for example I have a node `MyButton`, I want it to have all three behaviors `X`, `Y`, `Z` For this I have to create a script `xyz.gd` which inherits from `xy.gd`, which inherits from `x.gd` Thus, I created 2 new intermediate scripts `xy.gd` and `xyz.gd`, which I may not need at all. Also, I cannot easily dynamically enable/disable individual behaviors for a node, for example disable `Y` (for this I need to create new scripts like `xz.gd`, for all cases, which is definitely not handy. ---

Thus, I miss a convenient way to reuse scripts-behaviors.

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

Adding ability to set additional ("fake"?) scripts to nodes could solve the problem.

So, the problem above could be solved with additional scripts this way: In the inspector for the MyNode node, we could just specify three scripts: x.gd, y.gd, z.gd and that's it!

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

The idea is a node/object may (or may not) still have a "main" script, but besides this, it may have several additional/extra ("fake"?) scripts.

I call them "fake" because they are not usual, full-fledged scripts:

These additional/extra scripts could work like this:

Adding and removing additional scripts to a node could be done through the inspector, or using the code:

New methods (Object):

This proposal does not compete with the forthcoming (hopefully) trait system like https://github.com/godotengine/godot/issues/23101, they may well exist together.

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

I don't see an easy way to wotk-round this. Child nodes could possibly be a relatively simple way, but adding behaviors as child nodes:

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

It cannot be an add-on, since it involves adding new functionality to the script system.

YuriSizov commented 3 years ago

Child nodes could possibly be a relatively simple way, but adding behaviors as child nodes:

  • could cause confusion and inconvenience, especially for containers.
  • less efficient
  • less flexible (since you can add only nodes to nodes)
  • still needs the code that manages this, that is not handy

This is how the most of Godot exists, so I don't see this as valid criticism and shortcoming. Yes, composition is how you solve your problems. If you don't want additional nodes, you can always load and store an instance of a plain script on your object that would abstract a part of its logic and can be reusable.

This, in fact, is a more clear way to handle your problem rather than mixing several scripts into one. Sure, in some cases you'd have to write some boilerplate/proxy code, if you want to expose behaviors of your helper scripts, but it will be explicit and clear what the intention of the code is, how it functions and where to look for the implementation.

Zireael07 commented 3 years ago

In addition to what @pycbouh you can also use inner classes to separate your script into tidy little parts.

me2beats commented 3 years ago

@pycbouh writing boilerplate/proxy is the main problem for me, I need a simplier solution. This is also related to handling export vars, I don't want to create Inspector plugins for that.

@Zireael07 inner classes don't solve reusability problem

Zireael07 commented 3 years ago

@me2beats: For export vars, there is a proposal about arranging them into subgroups.

And for reusability, child nodes are the way to go - your proposal is basically Unity style multiple scripts, just with some minor limitations slapped on top reminiscent of built-in scripts (which seem on the way out), and multiple scripts on single nodes as well as multiple inheritance were rejected by the team multiple times on grounds of creating special cases and being hard to maintain.

YuriSizov commented 3 years ago

I need a simplier solution.

You may need to take a step back then and take a look at what you're proposing, because this is a very complicated solution for a problem that is solved perfectly well by composition. And not just in Godot. 😕

me2beats commented 3 years ago

this is a very complicated solution for a problem that is solved perfectly well by composition.

perhaps we need to consider the most simple solution to this problem using composition or/and child nodes, because it seems we are talking about different things.

As for the complexity of the implementation - what could be the main difficulty? I do not see any obstacles. Due to the limitations for these extra scripts, imo this could be implemented much easier than traits (so we could see this in godot earlier than traits to make comlex things easier and not wait long)

Also afaik, Godot tried implementing multiple scripts before, but later refused it for some reason. This may have led to multiple inheritance problems.

The proposed solution does not have these problems and would be great for reusing at least independent behaviors.

me2beats commented 3 years ago

I think the children-as-behaviors approach is the easiest, so let's take a look at it.

Here is the simplest example I could come up with.

In the scene we have a button node named MyButton this is where I add behaviors (as child nodes). Say when the delegator node is ready (it is MyButton in this case), one behavior should print "hello" and another one should print "world"

So I create a Delegator class, which should be a base class for MyButton that has behaviors property, that is Array of child nodes - behaviors.

delegator1

And we override _ready() because we want to call all its behaviors ready() methods when MyButton is ready:

delegator.gd:

extends Node
class_name Delegator
var behaviors = []

func _ready():
    for bhv in behaviors:
        bhv.ready()

Also I create Behavior class - base class/script for all behaviors. I find this handy because we need to distinguish behavior nodes from other nodes and have an access to methods like _process() to be able to delegate it as well (some time later) .

bhv.gd:

extends Node
class_name Behavior

func _enter_tree():
    var behaviors:Array= get_parent().behaviors
    behaviors.append(self)

delegator2

Here, when a behavior enters the tree, it gets the parent node, that will be MyButton in this case. Then it appends itself to the behaviors array.

And here are behaviors-scripts

hello.gd

extends Behavior

func ready():
    print('hello')

world.gd

extends Behavior

func ready():
    print('world')

delegator4

delegator5

As you may have noticed I am using ready() instead of _ready(). I think there is no need to explain why.

And finally MyButton script: my_button.gd:

extends Delegator

func _ready():
    print("do stuff")

delegator3

[If there is no need to have "main" script (my_button.gd), then delegate.gd script could be set to MyButton directly, but I think it's a rare case]

And done.

When running the scene, we could see in the Output:

hello
world
do stuff

But there are some problems here.

Now I'm stuck on that my_button.gd does not have an autocomplete for the Button class because the script extends Delegator that inherits from Node. I can't just change extends Node to extends Button in the Delegator, since I don't know in advance what class is needed. So it's something like: I'd like to script to extend Node but at the same I say "Hey autocomletion! treat this script as Button. Is this kinda interfaces system?

Now it seems like the only simple (though not very convenient) solution is to drop the "main" script when you have behavior scripts, and write all the logic in behavior scripts.

An alternative is to abandon the Delegate class, but then the logic will become more complicated and flexibility will be lost [for example, you will not be able to delegate functions such as _process(), etc.].

Another problem is that I can't seem to be able to get an instant call to enter_tree() for behaviors in this way, it seems at least I need to wait for the next frame, this is due to the peculiarities of the node's entry into the tree, but maybe I need to study this problem in more detail.

Btw most likely I explain some of the generally accepted terms in a complex way, in that case, correct me pls.

And probably the most interesting thing for me is - could it be implemented easier?

SysError99 commented 1 year ago

At first I kind got confused of what of the purpose of this proposal, but upon my own research on my own Godot development framework, I found out that doing extra nodes indeed decrease performance and many cases it's very significant (> 20% of performance drops, compared to trying to write everything in single script). Plus, it's very difficult to construct clean code that only consists of single script per node/scene, without sacrificing performance for ease of maintainability.

I think the only reason that this proposal is required is for performance reasons, and GDNative/C# isn't feasible for all use cases (especially on HTML5).

Calinou commented 1 year ago

but upon my own research on my own Godot development framework, I found out that doing extra nodes indeed decrease performance and many cases it's very significant (> 20% of performance drops, compared to trying to write everything in single script). Plus, it's very difficult to construct clean code that only consists of single script per node/scene, without sacrificing performance for ease of maintainability.

If you have a lot of nodes, it makes sense to use a single set of parent nodes that controls all the children (even if this involves creating a handful of nodes). This is expected to be faster than using a lot of child nodes, each with their own script.