GodotVR/components/persistent/persistent_item.gd
2025-10-08 11:10:13 +02:00

231 lines
6.6 KiB
GDScript

@tool
class_name PersistentItem
extends XRToolsPickable
## Persistent Item Instance Node
##
## The [PersistentItem] type is for instances of items managed by the
## persistence system. The [PersistentItem] type extends from
## [XRToolsPickable] because most items will be moved and carried by the user,
## however picking up and moving may be disabled by editing the
## [XRToolsPickable] and [RigidBody3D] settings.
##
## [PersistentItem] objects may be placed in persistent zones extending from
## [PersistentZone] and will have their state information (such as zone and
## position) managed in the [PersistentWorld] store. That data can be saved to
## file and loaded back.
##
## Extending from [PersistentItem] allows objects to extend the information
## persisted to the [PersistentWorld] store by overriding the
## [method _load_world_state] and [method _save_world_state] methods.
# Group for world-data properties
@export_group("World Data")
## This property specifies the unique ID (or base ID) for this item
@export var item_id : String
## This property specifies the [PersistentItemType] of this item
@export var item_type : PersistentItemType
## This property indicates whether this object was dynamically created
@export var item_dynamic := false
# Group for auto-return properties
@export_group("Auto Return")
## Automatically return to the last pocket when dropped
@export var auto_return := false
## Timeout for auto-return
@export var auto_return_timeout := 2.0
# Destroyed flag
var _destroyed := false
# Last pocket this object was in
var _last_pocket : PersistentPocket
# Auto-return timer node
var _auto_return_timer : Timer
# Add support for is_xr_class
func is_xr_class(p_name : String) -> bool:
return p_name == "PersistentItem" or super(p_name)
# Called when the node enters the scene tree for the first time.
func _ready():
super()
# Subscribe to picked_up and dropped signals
picked_up.connect(_on_picked_up)
dropped.connect(_on_dropped)
# Get configuration warnings
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Verify item ID is set
if not item_id:
warnings.append("PersistentItem ID not zet")
# Verify the item type is set
if not item_type:
warnings.append("PersistentItem Type not set")
# Verify item is in persistent group
if not is_in_group("persistent"):
warnings.append("PersistentItem not in 'persistent' group")
# Return warnings
return warnings
# Handle notifications
func _notification(what : int) -> void:
# Ignore notifications on freeing objects
if is_queued_for_deletion():
return
match what:
Persistent.NOTIFICATION_LOAD_STATE:
_load_state()
Persistent.NOTIFICATION_SAVE_STATE:
_save_state()
Persistent.NOTIFICATION_DESTROY:
_destroy()
## This method is called when the [PersistentItem] is dropped and freed
func drop_and_free():
super()
# Propagate destruction to this node and all children
propagate_notification(Persistent.NOTIFICATION_DESTROY)
# This method loads the item state from [PersistentWorld]. If the
# [PersistentWorld] indicates this item is destroyed then it queues itself
# and all children for destruction, otherwise it restores the items state.
func _load_state() -> void:
# Restore the item state
var state = PersistentWorld.instance.get_value(item_id)
if not state is Dictionary:
return
# If the item is recorded as having been destroyed then destroy it
if state.get("destroyed", false):
propagate_notification(Persistent.NOTIFICATION_DESTROY)
return
# Restore the item state
_load_world_state(state)
# This method saves the state of the item to [PersistentWorld]. If the item
# is destroyed then the destroyed state is saved to the [PersistentWorld].
func _save_state() -> void:
# Handle saving destroyed state
if _destroyed:
# Dynamic items can just have their ID cleared
if item_dynamic:
PersistentWorld.instance.clear_value(item_id)
return
# Design-time items must be saved with the destroyed state
PersistentWorld.instance.set_value(item_id, { destroyed = true })
return
# Populate the state information
var state := {}
_save_world_state(state)
PersistentWorld.instance.set_value(item_id, state)
# This method destroys the item by marking it as destroyed, saving the
# destroyed state to the [PersistentWorld], and queueing the instance for
# destruction.
func _destroy() -> void:
# Mark the item as destroyed and save state
_destroyed = true
_save_state()
# Ensure the item is queued for destruction
queue_free()
## This method restores item state from the [param state] world data. The
## base implementation just restores the location. Classes extending from
## [PersistentItem] can override this method to load additional item state by
## calling super() to load the basic information and then reading additional
## state information from the dictionary.
func _load_world_state(state : Dictionary) -> void:
# Restore the location
var location = state.get("location")
if location is Transform3D:
global_transform = location
## This method saves item state to the [param state] world data. The base
## implementation just saves the type and location. Classes extending from
## [PersistentItem] can override this method to save additional item state by
## calling super() to save the basic information and then writing additional
## state information to the dictionary.
func _save_world_state(state : Dictionary) -> void:
# Save the type and location
state["type"] = item_type.type_id
state["location"] = global_transform
# Start the auto-return timer
func _start_auto_return_timer() -> void:
# Construct the auto-return timer on first use
if not _auto_return_timer:
_auto_return_timer = Timer.new()
_auto_return_timer.one_shot = true
_auto_return_timer.timeout.connect(_on_auto_return)
add_child(_auto_return_timer)
# Start the auto-return timer
_auto_return_timer.start(auto_return_timeout)
# Called when this object is picked up
func _on_picked_up(_pickable) -> void:
# Save the last pocket
if get_picked_up_by() is PersistentPocket:
_last_pocket = get_picked_up_by()
# Stop any auto-return timer
if _auto_return_timer:
_auto_return_timer.stop()
# Called when this object is dropped
func _on_dropped(_pickable) -> void:
# Start the auto-return timer if possible
if auto_return and _last_pocket:
_start_auto_return_timer()
# Called when the auto-return timer expires
func _on_auto_return() -> void:
# Skip if the last pocket is invalid
if not is_instance_valid(_last_pocket):
return
# Skip if the last pocket is already holding an object
if is_instance_valid(_last_pocket.picked_up_object):
return
# Instruct the pocket to pick us up
_last_pocket.pick_up_object.call_deferred(self)