Skip to content
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

Add cascading Node properties to auto-populate values and statically check ancestral dependencies #3147

Open
willnationsdev opened this issue Aug 16, 2021 · 0 comments

Comments

@willnationsdev
Copy link
Contributor

willnationsdev commented Aug 16, 2021

Describe the project you are working on

Writing code that involves interactive node hierarchies (my game, plugins, potential core features, answering questions online, etc.).

Describe the problem or limitation you are having in your project

In the above situations, I've encountered problems where I'm designing a certain kind of node hierarchy interaction that varies across many use cases but has a common pattern of boilerplate code/expectations with just enough similarities that it feels like it should just be a core feature.

In these situations, there is some context node with some cascading property (named whatever). Then have some target descendant node that automatically has access to whatever cascading property they specifically are looking to receive from their above context (usually by having a local parameter populated with the value).

The ideal, loosely-coupled relationship is where some descendant says, "I want x", and some ancestor says, "I'm gonna pass along x to whoever wants it.", and then all of the "work" of connecting the two is handled automatically.

  1. Whether the cascading property on the target requires initialization varies between use cases.
  2. Whether the cascading property is statically typed varies between use cases.
  3. Whether the cascading property's availability can be statically evaluated at editor-time varies between use cases (but usually, it can).
    • If so, and it is required, then the target node should have a configuration warning if the value is inaccessible, sometimes even across scene boundaries.
    • If so, and it is statically typed, then the target node should have a configuration warning if the provided value would not match the type correctly.
  4. Usually, you want the ancestor to automatically populate the cascading property immediately after the target node's instantiation, i.e. before the target's descendants have been created. Sometimes though, the context itself may need to delay initializing the cascading property until after it is ready (leads to targets using the ready signal or maybe the context doing a propagate_call to set the value, etc.).
  5. Whether the cascading property should be assigned only after the subtree has been attached to the SceneTree varies between use cases. That is, a scene may be instantiated manually (do it now?) and then later attached to the SceneTree (or now?).
  6. The cascading property automatically updates if the target node is reparented and the new hierarchy does not fall under the same context.
    • If reparented to a hierarchy without a context that can provide the value, and if the cascading property is required, then the application should log an error.

The most common solutions to this problem are...

  1. Let the context be responsible for adding/removing target nodes and updating cascading properties.
  2. Have a singleton track contexts, instantiate targets, and mediate updates for cascading properties.
  3. Have a group of which context nodes are a member. Upon entering the tree, have target nodes scan ancestors until one in the group is found and cache the reference. Then get whatever data is needed from that node manually.

There are a lot of different approaches to it not even mentioned above (thanks to Godot's flexibility), but there are consistent issues with all of them.

  1. Often, the logic has to be tied to very specific data types (custom logic for (re)parenting) that should be shared between many classes (annoying workaround: delegate to external pure functions and reuse across many small class declarations).
  2. Tooling is complicated.
    1. Configuration warnings for different target node types result in problem 1.
    2. Most strategies above involve customizing/wrapping the instantiation of the node, but that would preclude adding nodes the traditional way (not desirable).
    3. You could alternatively use an EditorPlugin that constantly monitors the state of all open scene tabs and reacts to changes by re-validating all the relevant nodes, but Godot doesn't really have a scriptable property metadata/annotation system, so you'd have to hack things with set_meta/get_meta and it would be pretty finicky/difficult to maintain.

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

I envision a built-in cascading properties feature that would automatically perform the above operations on behalf of users to bypass any need for re-implementation across many types and simplify common boilerplate use cases. This could be handled via annotations (now that GDScript and C# have them). A context-key could be associated with the property metadata to link up which values should be passed along. This would prevent mixed contextual hierarchies from attempting to populate values related to other types of contextual relationships.

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

Have an @GlobalScope flags enum to define the settings:

enum {
    CASCADE_REQUIRED = 1, # must be passed along
    CASCADE_STRICT = 2, # must be the same data type
    CASCADE_TARGET_INIT = 4, # assign after child instantiation
    CASCADE_SOURCE_READY = 8, # assign after ancestor is ready
    CASCADE_DEFAULT = CASCADE_REQUIRED | CASCADE_TARGET_INIT
}

Have the following two annotations:

# target: property
# - Declares that the property can be cascaded to descendant cascade parameters
# - If `p_key` is empty, then `p_key` is inferred from the property name.
@cascade(p_key: string = "")

# target: property
# - Declares that the property can be initialized by the first ancestor with a matching `p_key`.
# - The annotation's bool params merely configure the cascade flag enum options.
# - If `p_key` is empty, then `p_key` is inferred from the property name.
# - If `p_is_required` is true, then if the property is empty by the `_ready` callback, log a config warning / runtime error.
# - If `p_is_strict` is true, then if an identified cascade property is not the same type, log a config warning / runtime error.
# - If `p_is_strict` is true and the parameter property is statically typed as `bool`, then ignore (no Nullable<bool>, so no meaning).
@cascade_parameter(p_key: string = "", p_is_required: bool = false, p_is_strict: bool = true)

# Alt format where `p_key` is omitted and inferred
@cascade_parameter(p_is_required: bool = false, p_is_strict: bool = true)

# If you use @onready with it, it automatically
# disables `CASCADE_TARGET_INIT` and enables `CASCADE_SOURCE_READY`
@onready
@cascade_parameter

Example syntax where key is implied by property name, it is optional, and data type doesn't matter:

# ancestor.gd
extends Node

@cascade
var category_name := ""

# descendant.gd
extends Node

@cascade_param(true, false)
var category_name: StringName

Manually defined contextual keys, value supplied from scene export.
Might have many different types of "display" containers that provide a category value, etc.
Might also have other kinds of values passed around hierarchically that you wouldn't want category-related stuff to conflict with.

# category.gd
class_name CategoryDisplay
extends PanelContainer

@export
@cascade("category_name")
var name := ""

# descendant.gd
extends Node

@cascade_param("category_name")
@cascade_param("category_name", true) # optional second bool param for an `is_required` flag
var container_category: String

func _init():
    pass # container_category is still null

func _ready():
    pass # container_category is populated

It would need to be a system that would be capable of working at runtime without the Editor (just to pass along the value). The naive approach (but probably the best one?) would be to just make nodes with a cascade_param annotation search up through their ancestors to find one that declares a cascade annotation with the same key and then get the value to assign it to its own property. And then do the same thing any time it is reparented.

Once in the Editor though, the EditorData, to perform static evaluations, would likely need to cache which nodes in which scenes declare/request cascading properties and provide helper functions to perform validation checks. The SceneDock can then generically raise configuration warnings on any relevant node as needed.

Considering the ramifications of cascading properties on node relationships, I think it would also be important to add a separate list of cascading properties in the documentation, in between properties and signals, for each class so that they are clearly distinguished from other properties.

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

As has been described, there are several different steps and details to the pattern, along with complex editor logic, and the logic is often something that occurs across many different types of hierarchical relationships in node trees, so the "few lines of script" that do get written are often duplicated across many different classes with slight alterations.

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

I could imagine some parts of this could be emulated in an addon/plugin combination if an annotation API were added to the core Object and/or Script interface(s) and if a few extra signals were added to the SceneDock to make tracking changes to it easier. But this is also a pattern that is central to node trees in the first place since every node has the potential to become a context for other nodes below it. It just converts what would otherwise have been a lot of boilerplate, tooling, and/or explicit searching/assigning into an implicit, configuration-based relationship that reduces boilerplate.

@Calinou Calinou changed the title Add cascading Node properties to auto-populate values and statically check ancestral dependencies. Add cascading Node properties to auto-populate values and statically check ancestral dependencies Aug 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants