How to force a Resource to be local to a scene's owner?

Godot Version

4.4.1

Question

My game structure uses many resources. I have a series of reusable scene components that export a Resource variable and react when it changes (via signals).

For example, in my enemy scene, I have the Hurtbox, Destructible and HealthBar components as subscenes. Their script takes an AttributeData custom Resource and when its value changes, all of them adapt to the new value.

The destructible scene will remove the enemy when the health attribute reaches 0.
The Hurtbox component is detected by Hitboxes to subtract the value of the health attribute. The HealthBar component updates the progress property accordingly.

This achieves a very modular and data driven design. I can reuse many of these scenes for different purposes with little modifications and none of them need to communicate with each other or hardcode dependencies directly. It works for enemies and destructible props.

There’s an issue, though. The AttributeData instance should be the exact same in all components (The Hurtbox, HealthBar and Destructible would have a reference to the same resource). Each instance of an enemy should have its own unique health attribute.

I thought Resource.local_to_scene would fix this. The problem is that it also duplicates the resource when it is referenced by any scene instance, including sub scenes. This leads to each component having an isolated instance, so the data is never shared.

Is there a way for a Resource to only be duplicated per owner/root, ignoring any children scenes?

Could you post some of the offending code? You can call .duplicate() on resources to create unique copies instead of references.

I agree that we likely need more code to correctly assist.

Their script takes an AttributeData custom Resource and when its value changes, all of them adapt to the new value.

There’s an issue, though. The AttributeData instance should be the exact same in all components

Ignoring the local to scene issue, how are you CURRENTLY ensuring that all the components share the data? If you @export in each component, then the instances won’t know anything about each other. The exception of course is if you save the resources to disk, and then drag+drop them into place. I guess that’s what you’re doing?

I think probably what you want is to introduce the concept of an ‘Actor’ or ‘ComponentHolder’, which is responsible for exporting this type, and potentially propogating the signal when the data changes. Then each component can still gain a reference to the data (via the parent/actor), but since you’re not exporting it more than once, you can safely use local to scene.

I was sharing the reference by saving the resource to disk. I do export resources in each component.
I like the concept of a ComponentHolder. However, not all resources I want to export are necessarily of the same type or have the same signals.
A ComponentHolder could take an exported resource and pass that instance to other components. Those would take care of connecting the appropriate signals.

In that case, the code would be something like this:

class_name ComponentHolder
extends Node

## Node passing a resource reference to other components.

@export var resource : Resource

@export var components : Array[Node] = []

@export var resource_property : StringName

func _ready() -> void:
    pass_reference()

func pass_reference() -> void:
    for node in components:
        if node and resource_property in node:
            node.set(resource_property, resource)
class_name Destructible
extends Node

## Node freeing another when its health property depletes.

## Health AttributeData to track
@export var health : AttributeData:
	set(value):
		if value == health:
			return
		if health:
			health.depleted.disconnect(destroy)
		health = value
		if health:
			health.depleted.connect(destroy)

## Target node to remove when health is min. It's intended for this to be the scene's owner.
@export var remove_target : Node

signal destroyed

func destroy() -> void:
	if Engine.is_editor_hint():
		return
	destroyed.emit()
	if remove_target and is_instance_valid(remove_target) and remove_target != get_tree().current_scene:
		remove_target.queue_free()

The type safety isn’t perfect, but it should work for different resource types.
Even though, I think I wouldn’t be able to export the health property in the Destructible or other components, right? And signals would have to be connected/disconnected in the setter.