I’ve created an Item(Resource) class to represent all the items in my game, it has name, icon and reference to a scene for the visuals. I started with having interaction components in the visuals scene for the item (Area3D for interaction) but then ran into issue because the interaction required a reference to the item creating circular dependencies.
My solution has been to create a WorldItem class that has the Area3D for interaction and an item. When an item is assign it is added as a child for the visuals and its collision shapes are duplicated to the WorldItem and the interaction component. I started with a scene for the nodes, but I ditched that and made it a code only node.
I’m looking for feedback on this approach, if there’s anything you would suggest changing or a pitfall I might be missing.
@tool
class_name WorldItem
extends RigidBody3D
@export var item: Item: # Resource holding item data
set(value):
item = value
_connect_item()
var visual_root: Node3D # The container for the item model
var interactable: Interactable # Area3D
var pickupable: Pickupable = Pickupable.new()
func _ready() -> void:
pickupable.item_picked_up.connect(func(_item: Item): queue_free())
# We do this because duplicating keeps the nodes, but not the reference in the variable
visual_root = get_node_or_null("VisualRoot")
if not visual_root:
visual_root = Node3D.new()
visual_root.name = "VisualRoot"
add_child(visual_root)
# We do this because duplicating keeps the nodes, but not the reference in the variable
interactable = get_node_or_null("Interactable")
if not interactable:
interactable = Interactable.new()
interactable.name = "Interactable"
add_child(interactable)
interactable.interacted.connect(pickupable.interact)
_connect_item()
func _reset() -> void:
for child in visual_root.get_children():
child.queue_free()
# Remove old collision shapes
for child in get_children():
if child is CollisionShape3D:
child.queue_free()
# Remove old collision shapes from interactable
if interactable:
for child in interactable.get_children():
if child is CollisionShape3D:
child.queue_free()
pickupable.item = null
func _connect_item() -> void:
if not is_node_ready():
return
_reset()
if item == null or !item.prefab_path or item.prefab_path == "":
return
pickupable.item = item
var visual_scene = item.create_scene()
# Add the item scene with the mesh
visual_root.add_child(visual_scene)
_add_collision_shapes(visual_scene)
func _add_collision_shapes(node: Node) -> void:
for child in node.get_children():
if child is CollisionShape3D:
# duplicate the collision shapes from the item scene to here
add_child(child.duplicate())
# duplicate the collision shapes for the interactable area (Area3D)
interactable.add_child(child.duplicate())
# disable collision on the item scene to prevent double collision
# we do this instead of removing the original shape from the visual scene
# so the item scene tree is intact otherwise duplicating generates errors
child.disabled = true