CraterCrash / godot-orchestrator

Orchestrator: Unleashing Creativity with Visual Scripting

Home Page:https://www.cratercrash.com/orchestrator

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Component based design?

Shadowblitz16 opened this issue · comments

Description

Sorry if this is brief I am on my phone.

Is there a way we can get component support in this?

My main issue is being able to attach multiple scripts to a the root of the scene and being able to add and remove components and edit properties at the root of the scene

Implementation ideas

I was thinking of a script type called orcatrator root that isn't a editable script but instead allows us to add and remove orcatrator scripts of the same type as the node the orcatrator root script is on, as well as edit the components exported properties.

I am open to suggestions I just desire the following

  • enforcing the type on a node to script based
  • editable properties from the root
  • grouped properties based on the component
  • the ability to add and remove scripts while enforcing the type of the node and script

Now I know that this may not fall under the immediate scope of orcatrator but I think it would improve the workflow since we could start using composition over just inheritance

Can you elaborate on what you mean by enforcing the type on a node to script based @Shadowblitz16 ?

Composition is something you can already do in Godot, and their philosophy has always been to do this using the Scene Tree and multiple nodes rather than trying to get into the scripting layer to handle this specific aspect.

Can you elaborate on what you mean by enforcing the type on a node to script based @Shadowblitz16 ?

I mean if you make a orchestrator script and you try to add it as an component to a orchestrator-root then it must be the same type as the node it's attached to.

Composition is something you can already do in Godot, and their philosophy has always been to do this using the Scene Tree and multiple nodes rather than trying to get into the scripting layer to handle this specific aspect.

The main issue with this is you can't edit properties at the root of the scene.
You also can't add components from the root of the scene either.

Also If you don't want components to be able to be added to a incompatible node type, you can't do that with nodes as there is no way to validate whether a node can be a child of other node.

You can pass a reference to the node you want to operate on but then that can't be done from the root of the scene.

The main issue with this is you can't edit properties at the root of the scene.
You also can't add components from the root of the scene either.

Sure you can.

I suspect your issue may have been you were trying to do this inside the _ready() function, and in this case, it makes sense that Godot would block you because of how the _ready() callback is fired bottom up.

Our PrintString node works very much like this: When the node is called, it attaches a set of custom UI widgets at the top of the scene. When you use the PrintString in the _ready() function, we can't attach the nodes immediately; the call must be deferred, but it's most definitely doable.

Also If you don't want components to be able to be added to a incompatible node type, you can't do that with nodes as there is no way to validate whether a node can be a child of other node.

You can't prevent that, sure, but if the script is a @tool script and implements the _get_configuration_warnings() virtual method, then this information can at least be shown to the user to inform them of such situations.

You can pass a reference to the node you want to operate on but then that can't be done from the root of the scene.

Why can't you pass a NodePath into the root and then lookup the node during an @onready or during the _ready callback to resolve the node from the scene?

I'm sorry for all the questions, but I believe you're talking about a very specific use case that either I haven't encountered or one which can be mitigated by using the Godot API as it was intended. If you can provide a concrete example, perhaps that can certainly make it easier to follow.

The main issue with this is you can't edit properties at the root of the scene.
You also can't add components from the root of the scene either.

Sure you can.

How so?
Godot properties are part of the node if you have a component on a child node (lets say a health components)
You can't easily access the health component from the root of the scene.

You have to either...

  • A expand the scene exposing things you may not want to expose.
  • B attach another script on the scene root and redirect the properties (buggy)

I suspect your issue may have been you were trying to do this inside the _ready() function, and in this case, it makes sense that Godot would block you because of how the _ready() callback is fired bottom up.

It has nothing to do with the _ready function it has to do with how godot is designed.

Our PrintString node works very much like this: When the node is called, it attaches a set of custom UI widgets at the top of the scene. When you use the PrintString in the _ready() function, we can't attach the nodes immediately; the call must be deferred, but it's most definitely doable.

I haven't tested Print Screen but the idea isn't about moving nodes around it's about the inspector.

Also If you don't want components to be able to be added to a incompatible node type, you can't do that with nodes as there is no way to validate whether a node can be a child of other node.

You can't prevent that, sure, but if the script is a @tool script and implements the _get_configuration_warnings() virtual method, then this information can at least be shown to the user to inform them of such situations.

That's not good.
This introduces bugs and makes selecting components a nightmare since you don't know what components are supported by what types.

You can pass a reference to the node you want to operate on but then that can't be done from the root of the scene.

Why can't you pass a NodePath into the root and then lookup the node during an @onready or during the _ready callback to resolve the node from the scene?

  • Type safety
  • It breaks the encapsulation of the scene.

I'm sorry for all the questions, but I believe you're talking about a very specific use case that either I haven't encountered or one which can be mitigated by using the Godot API as it was intended. If you can provide a concrete example, perhaps that can certainly make it easier to follow.

It's not about your api it's about improving workflow.
Your system has the potential to get around some of godot's bad design choices aka (inheritance over composition)
The godot devs refuse to introduce a component oriented designed around nodes.

If implemented, this can potentially prove to godot devs that we are in dire need of a proper behavior system.

The point of this wouldn't necessarily be to modify existing orchestrator behavior.

The point of it would be to..

  1. Provide a way to export orchestrator properties to the scene root
  2. Allow attaching multipule orchestrator scripts to a single node
  3. Ensure orchestrator scripts would not be attached to nodes of a wrong type.

All of these together are basically what I am suggesting.

  1. could be done by just attaching a orchestrator script on the root of the scene but then orchestrator scripts would need to be able to handle multiple sub orchestrator scripts

  2. could also be done with my idea.

My idea was to provide something called orchestrator root or orchestrator entity in which you don't have to modify existing orchestrator behavior, instead users could just choose whether they want a single script or a multi script

I wonder if this belongs in Orchestrator.

First, your description is overgeneralized, and you still need to consider the Godot API and its limitations. Regardless of what you call it, OrchestratorEntity or what-not, the point is the Godot API is limited to a singular Ref<Script> on an Object. In short, to even support the attachment of multiple scripts would be a complete rewrite of Orchestrator from the ground up for its script to act like a container of other scripts.

The next concern is the order of operations. For example, when two scripts override the same virtual method or expose the same method name, what do you do? What's the precedence? What if multiple scripts export the same variable name?

To add a technical example, I'm going to assume we have some container that allows us to attach 2 GDScripts, that are written as follows:

# Script 1
func say_hello() -> void:
  print("Hello From Script1")

# Script 2
func say_hello() -> void:
  print("Hello From Script2")

Then we have a third script on another node that gets a reference to the Node with our "container" script and it calls node.call("say_hello"). Which script do I call, what order do I call them?

The answers to all these questions fundamentally impact the logic inside many of the plug-in's classes and architecture. This isn't something that can just be bolted onto the existing logic without significant internal changes.

@Naros you couldn't just delegate signals and events down to a collection of nodes and then rexport exported properties?

for example some mockup gd script code...

class_name Entity inherits Script
@export var scripts : Script

func _get(name:string):
    for script in scripts:
        for property in script.get_properties():
              if property.has_annotation("entity_export") and property.name == name:
                  return property
     return null
                  
                  
func notification(what):
    for script in scripts:
        script.notification(what)

then you just support exporting properties with @entity_export

you couldn't just delegate signals and events down to a collection of nodes and then rexport exported properties?

There are many things you could "just do", but just because you can doesn't mean it won't be risky. It could be plagued with all sorts of complications both during implementation and afterward. You also have to weigh the risks involved in creating a solution that goes against the Godot design pattern and ideals. While it may work today, a change in Godot could render that solution inoperable tomorrow. That's why I am very picky about specifics; details are essential when you want to design something that goes against expected standards.

The delegation pattern may work well for some standard functions like notification or _ready, which by their nature would have the same argument list as their Godot functions, but what happens when two scripts define the same user-defined function but with different argument lists? Godot doesn't allow the same function to be defined with differing argument lists.

It's not only a question of how exactly outside calls come into the Script and are handled but also how the data is exposed contextually to the engine. Godot needs to know about all exposed methods, signals, and variables, and if you have conflicts between the collection of Script objects, you have a problem. You have to account for how this gets managed inside the ScriptInstance and PlaceholderScriptInstance types of the scripting system to have the correct instance state managed.

As for exported properties, regardless of how you flag them for export, you still need to answer my question about variable conflicts. What about two scripts that are interested the same value with the same name, i.e. Health or what if two scripts name a property the same, but they're two different data types and represent two different concepts?

What I am picturing is this "God Node" that holds a collection of scripts, that to the outside world, is simply a complete amalgamation of all the child scripts that are attached, so we have to consider whether that makes developing the game significantly more rigid or problematic vs the Godot preferred way of using multiple nodes in a hierarchy.

I would say if you can think about the design in a much lower level detail and share how to integrate that detail into the existing Godot API to satisfy all the ScriptExtension and ScriptLanguageExtension requirements, we can certainly discuss this more.

However, regardless, what you're proposing is something better suited for a separate plug-in. What you're ultimately proposing is a completely altered way that Script instances interact with the scene tree. When developed as a standalone plug-in, it would invite the ability to use any Scripting API implementation, including GDScript or Orchestrator.

ok I will think about it more.
I am closing this issue for now.