godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
89.86k stars 20.99k forks source link

Custom Inspector Grouped Nodepath properties cannot be accessed, assigned #94010

Closed DemonBismuth closed 2 months ago

DemonBismuth commented 3 months ago

Tested versions

System information

Godot v4.2.2.stable - Windows 10.0.22621 - GLES3 (Compatibility) - NVIDIA GeForce RTX 4070 Ti SUPER (NVIDIA; 31.0.15.5123) - AMD Ryzen 7 7700X 8-Core Processor (16 Threads)

Issue description

I am currently trying to create custom exported NodePath properties so that I may dynamically connect signals to AudioStreamPlayers. From a custom script that extends Panel, I have _get_property_list() to create these properties. Here is a snippet:

const _CONTAINER_AUDIO = {
    clicked=["card_clicked",true],
    r_clicked=["card_right_clicked",true],
    paged=["page_changed",true],
    drag_started=["card_drag_started",false],
    drag_stopped=["card_drag_stopped",false],
    card_dropped=["card_dropped",false],
    updated=["container_updated",false],
}
const AUDIO = "AudioStreamPlayer,AudioStreamPlayer2D,AudioStreamPlayer3D"

func _get_property_list():
    var property_list := []
    property_list.append({
        "name": "Container Audio",
        "type": TYPE_NIL,
        "usage": PROPERTY_USAGE_GROUP,
        "hint_string": "_caudio_"
    })

    for c in _CONTAINER_AUDIO.keys():
        property_list.append({
            "name": "_caudio_%s" % c,
            "type": TYPE_NODE_PATH,
            "hint": PROPERTY_HINT_NODE_PATH_VALID_TYPES,
            "usage": PROPERTY_USAGE_DEFAULT,
            "hint_string": AUDIO
        })
    return property_list

_CONTAINER_AUDIO is a const dictionary that contains the names of the properties I want exported as keys, and values are their corresponding signals [0], and whether it takes an argument [1]. These NodePath properties do appear in the inspector correctly:

Screenshot 2024-07-06 160925

However, when trying to assign these properties, nothing happens. I cannot assign AudioStreamPlayer, AudioStreamPlayer2D, or AudioStreamPlayer3D. When trying to access these properties with get(), such as get("_caudio_clicked") or get("clicked"), I get this error: Invalid type in function 'get_node' in base 'Panel (field_container.gd)'. Cannot convert argument 1 from Nil to NodePath. I've never tried accessing fully custom variables before, and not ones from a group, so possibly I may have be in error in how I'm attempting to access the property. However, will little documentation on the matter and inability to find this particular issue (custom grouped properties and how to access them) elsewhere online, I'm not sure what else to do.

Steps to reproduce

Copy and paste the following code block into a @tool script. Then, attempt to assign it from inspector or access it via the method get().

const _CONTAINER_AUDIO = {
    clicked=["card_clicked",true],
    r_clicked=["card_right_clicked",true],
    paged=["page_changed",true],
    drag_started=["card_drag_started",false],
    drag_stopped=["card_drag_stopped",false],
    card_dropped=["card_dropped",false],
    updated=["container_updated",false],
}
const AUDIO = "AudioStreamPlayer,AudioStreamPlayer2D,AudioStreamPlayer3D"

func _get_property_list():
    var property_list := []
    property_list.append({
        "name": "Container Audio",
        "type": TYPE_NIL,
        "usage": PROPERTY_USAGE_GROUP,
        "hint_string": "_caudio_"
    })

    for c in _CONTAINER_AUDIO.keys():
        property_list.append({
            "name": "_caudio_%s" % c,
            "type": TYPE_NODE_PATH,
            "hint": PROPERTY_HINT_NODE_PATH_VALID_TYPES,
            "usage": PROPERTY_USAGE_DEFAULT,
            "hint_string": AUDIO
        })
    return property_list

Minimal reproduction project (MRP)

Here is an MRP TestMRP.zip

AThousandShips commented 3 months ago

Please upload an MRP to make this easier to test:

DemonBismuth commented 2 months ago

@AThousandShips Edited my post to include an MRP.

DemonBismuth commented 2 months ago

So I'm probably gonna close this issue, but I want to say that this led me down a rabbit hole that showed some creative uses for custom properties that I havent seen anywhere else.

The obvious answer to my issue was that I needed to add var _caudio_clicked e.g. to the script. However, manually adding them seemed counter to what I had in mind, after the whole point was to create these properties dynamically. What is the point of _get_property_list() if you still have to add the properties manually? _get_property_list() already has all the functionality needed, and the properties were still being saved in the packed scene even if they couldn't be assigned.

Another thing that had been bugging me is how to use _set() and _get(). I could see the documentation for it, see how it works, but I still didn't really understand the use case. So in the MRP I started doing some tests. Lets say I add var apple to the script, and have _get_property_list() add an exported property orange. Could I use _set() and _get() to have orange assign apple? The answer is yes! In fact, doing so seemed to assign both properties to the same value.

"So what?", you may ask, dear Godot user. What I came to discover is that this could be used to assign dictionary and array entries!

var dict : Dictionary

func _get_property_list():
    var p = []
    p.append({
        "name":"entry_1"
        "type": TYPE_NODE_PATH,
        "hint": PROPERTY_HINT_NODE_PATH_VALID_TYPES,
        "usage": PROPERTY_USAGE_DEFAULT,
        "hint_string": "AudioStreamPlayer"
    })
    return p

func _set(property, value):
    if property == "entry_1":
        if !dict.has(property) || dict[property] != value:
            dict[property] = value
        elif dict.has(property) && value == null:
            dict.erase(property)

func _get(property):
    if property == "entry_1":
        return dict.get(property)

I tested this, both in editor and in a build. The property saves to the packed scene correctly, saving as both entry_1 and as an item in dict. That's one way to edit a dictionary! Works easiest if you want String/StringName only keys, but you can get creative. I can easily see looping through the values of an enum to set the property's name in _get_property_list() and using _set() to convert it back to an enum key in the dictionary. Also, this is likely useful for editing individual properties within inner resources.

Why does nobody talk about this? Sorry if this post is inappropriate for this thread, but I needed to document this somewhere, since I never knew godot was capable of this. Hope other users will find this useful.