add vr template project
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
class_name Persistent
|
||||
|
||||
|
||||
## Persistent script
|
||||
##
|
||||
## This script defines constants used in the persistence system.
|
||||
|
||||
|
||||
## Notification to trigger loading state
|
||||
const NOTIFICATION_LOAD_STATE := 57001
|
||||
|
||||
## Notification to trigger saving state
|
||||
const NOTIFICATION_SAVE_STATE := 57002
|
||||
|
||||
## Notification to trigger destruction of items
|
||||
const NOTIFICATION_DESTROY := 57003
|
||||
@@ -0,0 +1 @@
|
||||
uid://elh6ahwi62r4
|
||||
@@ -0,0 +1,230 @@
|
||||
@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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c02o55acl0jnm
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cc2akik80xtnb"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c8l60rnugru40" path="res://addons/godot-xr-tools/objects/pickable.tscn" id="1_2d7bx"]
|
||||
[ext_resource type="Script" path="res://components/persistent/persistent_item.gd" id="2_xch64"]
|
||||
|
||||
[node name="ItemInstance" groups=["persistent"] instance=ExtResource("1_2d7bx")]
|
||||
script = ExtResource("2_xch64")
|
||||
item_id = ""
|
||||
item_type = null
|
||||
item_dynamic = false
|
||||
auto_return = false
|
||||
auto_return_timeout = 2.0
|
||||
@@ -0,0 +1,50 @@
|
||||
class_name PersistentItemDatabase
|
||||
extends Resource
|
||||
|
||||
|
||||
## Persistent Item Database Resource
|
||||
##
|
||||
## This resource defines all [PersistentItemType] items supported by the
|
||||
## persistence system. The [PersistentZone] classe use this resource when
|
||||
## creating [PersistentItem] instances.
|
||||
|
||||
|
||||
## This property is the array of supported persistent item types
|
||||
@export var items : Array[PersistentItemType] : set = _set_items
|
||||
|
||||
|
||||
# Items cache
|
||||
var _cache := {}
|
||||
|
||||
# Items cache valid flag
|
||||
var _cache_valid := false
|
||||
|
||||
|
||||
## This method gets a [PersistentItemType] given its [param type_id]. If no
|
||||
## corresponding [PersistentItemType] is found then this function returns null.
|
||||
func get_type(type_id : String) -> PersistentItemType:
|
||||
# Populate the cache if necessary
|
||||
if not _cache_valid:
|
||||
_populate_cache()
|
||||
|
||||
return _cache.get(type_id)
|
||||
|
||||
|
||||
# Handle setting the items
|
||||
func _set_items(p_items : Array[PersistentItemType]) -> void:
|
||||
# Save the new items
|
||||
items = p_items
|
||||
|
||||
# Invalidate the cache
|
||||
_cache_valid = false
|
||||
|
||||
|
||||
# Populate the type cache
|
||||
func _populate_cache() -> void:
|
||||
# Populate the cache
|
||||
_cache = {}
|
||||
for item in items:
|
||||
_cache[item.type_id] = item
|
||||
|
||||
# Indicate the cache is valid
|
||||
_cache_valid = true
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5kgtdbxqss67
|
||||
@@ -0,0 +1,15 @@
|
||||
class_name PersistentItemType
|
||||
extends Resource
|
||||
|
||||
|
||||
## Persistent Item Type Resource
|
||||
##
|
||||
## This resource defines a type of persistent items managed by the
|
||||
## persistence system.
|
||||
|
||||
|
||||
## This property specifies the unique type ID
|
||||
@export var type_id : String
|
||||
|
||||
## This property specifies the scene-file for instancing the [PersistentItem]
|
||||
@export_file('*.tscn') var instance_scene : String
|
||||
@@ -0,0 +1 @@
|
||||
uid://bafk5cgrb7cvx
|
||||
@@ -0,0 +1,179 @@
|
||||
@tool
|
||||
class_name PersistentPocket
|
||||
extends XRToolsSnapZone
|
||||
|
||||
|
||||
## Persistent Pocket Node
|
||||
##
|
||||
## The [PersistentPocket] type holds persistent items managed by the
|
||||
## persistence system. The [PersistentPocket] type extends from
|
||||
## [XRToolsSnapZone] to allow [PersistentItem] objects to be snapped or
|
||||
## removed by the player.
|
||||
|
||||
|
||||
## Enumeration to control pocket behavior when the parent item is held
|
||||
enum HeldBehavior {
|
||||
IGNORE, ## Ignore picked_up/dropped changes
|
||||
ENABLE, ## Enable when picked up
|
||||
DISABLE ## Disable when picked up
|
||||
}
|
||||
|
||||
|
||||
# Group for world-data properties
|
||||
@export_group("World Data")
|
||||
|
||||
## This property specifies the unique ID of this pocket
|
||||
@export var pocket_id : String
|
||||
|
||||
# Group for options
|
||||
@export_group("Options")
|
||||
|
||||
## Pocket behavior when held
|
||||
@export var held_behavior := HeldBehavior.ENABLE : set = _set_held_behavior
|
||||
|
||||
|
||||
# Parent pickable body
|
||||
var _parent_body : XRToolsPickable
|
||||
|
||||
|
||||
# Add support for is_xr_class
|
||||
func is_xr_class(p_name : String) -> bool:
|
||||
return p_name == "PersistentPocket" or super(p_name)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
super()
|
||||
|
||||
# Skip initialization if in editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Search for an ancestor XRToolsPickable
|
||||
_parent_body = XRTools.find_xr_ancestor(self, "*", "XRToolsPickable")
|
||||
if _parent_body:
|
||||
_parent_body.picked_up.connect(_on_picked_up)
|
||||
_parent_body.dropped.connect(_on_dropped)
|
||||
|
||||
# Update the held behavior
|
||||
_update_held_behavior()
|
||||
|
||||
|
||||
# Get configuration warnings
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify pocket ID is set
|
||||
if not pocket_id:
|
||||
warnings.append("Pocket ID not zet")
|
||||
|
||||
# Verify pocket is in persistent group
|
||||
if not is_in_group("persistent"):
|
||||
warnings.append("Pocket 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 loads the pocket state from [PersistentWorld]. If the
|
||||
# [PersistentWorld] indicates this pocket holds an item then the item is
|
||||
# created and picked up by the pocket.
|
||||
func _load_state() -> void:
|
||||
# Queue populating the pocket as new nodes cannot be created inside a
|
||||
# notification handler.
|
||||
_populate_pocket.call_deferred()
|
||||
|
||||
|
||||
# This method saves the state of the pocket to [PersistentWorld].
|
||||
func _save_state() -> void:
|
||||
# Handle pocket not holding on to PersistentItem
|
||||
if not picked_up_object is PersistentItem:
|
||||
# Save that the pocket is empty
|
||||
PersistentWorld.instance.clear_value(pocket_id)
|
||||
return
|
||||
|
||||
# Get the item_id of the PersistentItem in the pocket
|
||||
var item_id : String = picked_up_object.item_id
|
||||
|
||||
# Save that the pocket holds the item
|
||||
PersistentWorld.instance.set_value(pocket_id, item_id)
|
||||
|
||||
|
||||
# This method destroys the pocket and any item inside it.
|
||||
func _destroy() -> void:
|
||||
# Propagate destruction for anything we hold
|
||||
if is_instance_valid(picked_up_object):
|
||||
print(self, " propagating destroy to ", picked_up_object.name)
|
||||
picked_up_object.propagate_notification(Persistent.NOTIFICATION_DESTROY)
|
||||
picked_up_object.queue_free()
|
||||
|
||||
|
||||
# Populate the contents of a pocket
|
||||
func _populate_pocket() -> void:
|
||||
# Get the ID of the item in the pocket
|
||||
var item_id = PersistentWorld.instance.get_value(pocket_id)
|
||||
if not item_id is String:
|
||||
return
|
||||
|
||||
# Construct the item for the pocket
|
||||
var zone = PersistentZone.find_instance(self)
|
||||
var item := zone.create_item_instance(item_id)
|
||||
if not item:
|
||||
return
|
||||
|
||||
# Put the item in the pocket
|
||||
item.global_transform = global_transform
|
||||
pick_up_object.call_deferred(item)
|
||||
|
||||
|
||||
# Called when the parent pickable body is picked up
|
||||
func _on_picked_up(_pickable) -> void:
|
||||
_update_held_behavior()
|
||||
|
||||
|
||||
# Called when the parent pickable body is dropped
|
||||
func _on_dropped(_pickable) -> void:
|
||||
_update_held_behavior()
|
||||
|
||||
|
||||
# Called when the held_behavior property has been modified
|
||||
func _set_held_behavior(p_held_behavior : HeldBehavior) -> void:
|
||||
held_behavior = p_held_behavior
|
||||
if is_inside_tree() and _parent_body:
|
||||
_update_held_behavior()
|
||||
|
||||
|
||||
# Update the pocket enable
|
||||
func _update_held_behavior() -> void:
|
||||
# Skip if no valid parent body
|
||||
if not is_instance_valid(_parent_body):
|
||||
return
|
||||
|
||||
# Test if the parent pickable is held
|
||||
var is_held := _parent_body.is_picked_up()
|
||||
|
||||
# Update the enabled state based on whether the parent body is held and
|
||||
# the desired behavior
|
||||
match held_behavior:
|
||||
HeldBehavior.ENABLE:
|
||||
enabled = is_held
|
||||
|
||||
HeldBehavior.DISABLE:
|
||||
enabled = not is_held
|
||||
@@ -0,0 +1 @@
|
||||
uid://bj7rmwsk1wltm
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://qmejywplaagw"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://ce7vysyvondf8" path="res://addons/godot-xr-tools/objects/snap_zone.tscn" id="1_7o2xi"]
|
||||
[ext_resource type="Script" path="res://components/persistent/persistent_pocket.gd" id="2_5072l"]
|
||||
|
||||
[node name="PersistentPocket" groups=["persistent"] instance=ExtResource("1_7o2xi")]
|
||||
script = ExtResource("2_5072l")
|
||||
pocket_id = ""
|
||||
held_behavior = 1
|
||||
@@ -0,0 +1,20 @@
|
||||
@tool
|
||||
class_name PersistentStaging
|
||||
extends XRToolsStaging
|
||||
|
||||
|
||||
## Persistent Staging instance
|
||||
static var instance : PersistentStaging
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(p_name : String) -> bool:
|
||||
return p_name == "PersistentStaging"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
super()
|
||||
|
||||
# Register ourselves as the persistent stage instances
|
||||
instance = self
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgvef7ymbuq78
|
||||
@@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://c2u2yasyfotxr"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bnqnnnet4dw12" path="res://addons/godot-xr-tools/staging/staging.tscn" id="1_hov0i"]
|
||||
[ext_resource type="Script" path="res://components/persistent/persistent_staging.gd" id="2_hx303"]
|
||||
|
||||
[node name="PersistentStaging" instance=ExtResource("1_hov0i")]
|
||||
script = ExtResource("2_hx303")
|
||||
@@ -0,0 +1,258 @@
|
||||
class_name PersistentWorld
|
||||
extends Node
|
||||
|
||||
|
||||
## Persistent World Data Object
|
||||
##
|
||||
## The [PersistentWorld] object holds information about the world. This
|
||||
## information can be stored to encrypted save-files, and then loaded back at
|
||||
## a later time.
|
||||
##
|
||||
## It's assumed this (or an extended script) is instanced as a singleton,
|
||||
## preferably as an autoloaded script.
|
||||
##
|
||||
## Multiple instances of [PersistentWorld] objects can be created - for example
|
||||
## to inspect other saved games without affecting the main instance.
|
||||
|
||||
|
||||
## Signal invoked before loading world-data
|
||||
signal world_loading
|
||||
|
||||
## Signal invoked after loading world-data
|
||||
signal world_loaded
|
||||
|
||||
## Signal invoked before saving world-data
|
||||
signal world_saving
|
||||
|
||||
## Signal invoked after saving world-data
|
||||
signal world_saved
|
||||
|
||||
|
||||
@export_group("Persistence Settings")
|
||||
|
||||
## Password for encrypted save files
|
||||
@export var save_file_password := ""
|
||||
|
||||
## Database of all persistent zones in the game
|
||||
@export var zone_database : PersistentZoneDatabase
|
||||
|
||||
## Database of all persistent item types in the game
|
||||
@export var item_database : PersistentItemDatabase
|
||||
|
||||
|
||||
# World data dictionary
|
||||
var _data := {}
|
||||
|
||||
# Mutex protecting data
|
||||
var _mutex := Mutex.new()
|
||||
|
||||
|
||||
## Static instance of the world data
|
||||
static var instance : PersistentWorld = null
|
||||
|
||||
|
||||
# Check for configuration issues on this node
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Check for blank password
|
||||
if save_file_password == "":
|
||||
warnings.append("Save password not set - saves will be unencrypted")
|
||||
|
||||
# Check for zone database
|
||||
if not zone_database:
|
||||
warnings.append("Zone database not set")
|
||||
|
||||
# Check for item database
|
||||
if not item_database:
|
||||
warnings.append("Item database not set")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
|
||||
## This method creates a unique ID starting with [param base] follwed by a
|
||||
## random number. The [param value] is stored using this ID, and the ID is
|
||||
## returned to the caller.
|
||||
func set_unique(base : String, value : Variant) -> String:
|
||||
# Lock while trying to create the ID
|
||||
_mutex.lock()
|
||||
|
||||
# Loop generating random IDs until we find a free one
|
||||
var id : String
|
||||
while true:
|
||||
id = base + str(randi() % 999999)
|
||||
if not _data.has(id):
|
||||
break
|
||||
|
||||
# Save the value under the ID, then return the ID
|
||||
_data[id] = value
|
||||
_mutex.unlock()
|
||||
return id
|
||||
|
||||
|
||||
## This method saves the [param value] under the [param id].
|
||||
func set_value(id : String, value : Variant) -> void:
|
||||
_mutex.lock()
|
||||
_data[id] = value
|
||||
_mutex.unlock()
|
||||
|
||||
|
||||
## This method gets the value stored under the [param id]. If the [param id]
|
||||
## does not exist then the [param default] value is returned.
|
||||
func get_value(id : String, default : Variant = null): # -> Variant
|
||||
_mutex.lock()
|
||||
var value = _data.get(id, default)
|
||||
_mutex.unlock()
|
||||
return value
|
||||
|
||||
|
||||
## This method clears a value under the [param id].
|
||||
func clear_value(id : String) -> void:
|
||||
_mutex.lock()
|
||||
_data.erase(id)
|
||||
_mutex.unlock()
|
||||
|
||||
|
||||
## This method clears all values matching the glob [param pattern]. See
|
||||
## [method String.match] for pattern matching rules.
|
||||
func clear_matching(pattern : String) -> void:
|
||||
_mutex.lock()
|
||||
for _key in _data.keys():
|
||||
var key : String = _key
|
||||
if key.match(pattern):
|
||||
_data.erase(key)
|
||||
_mutex.unlock()
|
||||
|
||||
|
||||
## This method clears all values.
|
||||
func clear_all() -> void:
|
||||
_mutex.lock()
|
||||
_data.clear()
|
||||
_mutex.unlock()
|
||||
|
||||
|
||||
## This method loads the summary information for the saved world-data
|
||||
## associated with the specified [param file_name]. If the world-data does
|
||||
## not exist then this method returns null.
|
||||
func load_summary(file_name : String) -> Variant:
|
||||
# Open the world-data save file for reading
|
||||
var file := _open_file(file_name, FileAccess.READ)
|
||||
if not file:
|
||||
return null
|
||||
|
||||
# Read the summary
|
||||
var summary = file.get_var()
|
||||
file.close()
|
||||
|
||||
# Return the summary
|
||||
return summary
|
||||
|
||||
|
||||
## This method loads the world-data associated with the specified
|
||||
## [param file_name]. If the world-data does not exist or is invalid then
|
||||
## this method returns false.
|
||||
func load_file(file_name : String) -> bool:
|
||||
# Report start of world-data loading
|
||||
world_loading.emit()
|
||||
|
||||
# Open the world-data save file for reading
|
||||
var file := _open_file(file_name, FileAccess.READ)
|
||||
if not file:
|
||||
return false
|
||||
|
||||
# Skip the summary
|
||||
file.get_var()
|
||||
|
||||
# Read the data
|
||||
var new_data = file.get_var()
|
||||
file.close()
|
||||
|
||||
# Skip if not dictionary
|
||||
if not new_data is Dictionary:
|
||||
return false
|
||||
|
||||
# Use the new data
|
||||
_mutex.lock()
|
||||
_data = new_data
|
||||
_mutex.unlock()
|
||||
|
||||
# Report world-data loaded
|
||||
world_loaded.emit()
|
||||
|
||||
# Report success
|
||||
return true
|
||||
|
||||
|
||||
## This method saves the world-data under the specified [param file_name].
|
||||
## If the save fails then this method returns false. The [param file_name]
|
||||
## string must be a legal part of a file name.
|
||||
func save_file(file_name : String, summary : Variant) -> bool:
|
||||
# Report start of world-data saving
|
||||
world_saving.emit()
|
||||
|
||||
# Open the world-data save file for writing
|
||||
var file := _open_file(file_name, FileAccess.WRITE)
|
||||
if not file:
|
||||
return false
|
||||
|
||||
# Write the summary
|
||||
file.store_var(summary)
|
||||
|
||||
# Write the data
|
||||
_mutex.lock()
|
||||
file.store_var(_data)
|
||||
_mutex.unlock()
|
||||
|
||||
# Close the file
|
||||
file.close()
|
||||
|
||||
# Report world-data saved
|
||||
world_saved.emit()
|
||||
return true
|
||||
|
||||
|
||||
## This method deletes the world-data associated with the specified
|
||||
## [param file_name]. If the world-data does not exist then this method
|
||||
## returns false.
|
||||
static func delete_file(file_name : String) -> bool:
|
||||
# Construct the file name
|
||||
var file_path := "user://save_%s.data" % file_name
|
||||
|
||||
# Remove the file
|
||||
return DirAccess.remove_absolute(file_path) == OK
|
||||
|
||||
|
||||
## This method returns a list of the names of all the saved world-data
|
||||
## instances.
|
||||
func list_saves() -> Array[String]:
|
||||
# Construct the return list
|
||||
var ret : Array[String] = []
|
||||
|
||||
# Build a regular expression to match save file names
|
||||
var regex := RegEx.new()
|
||||
regex.compile("^save_(?<name>.*)\\.data$")
|
||||
|
||||
# Process all files in the user folder
|
||||
for file in DirAccess.get_files_at("user://"):
|
||||
var result := regex.search(file)
|
||||
if result:
|
||||
ret.append(result.get_string("name"))
|
||||
|
||||
# Return the save files
|
||||
return ret
|
||||
|
||||
|
||||
# Open a world-data save file.
|
||||
func _open_file(file_name : String, mode : FileAccess.ModeFlags) -> FileAccess:
|
||||
# Construct the file name
|
||||
var file_path := "user://save_%s.data" % file_name
|
||||
|
||||
# Warn about unencrypted save files for debugging
|
||||
if save_file_password == "":
|
||||
push_warning("Unencrypted save file: ", file_path)
|
||||
return FileAccess.open(file_path, mode)
|
||||
|
||||
# Handle encrypted file with password
|
||||
return FileAccess.open_encrypted_with_pass(file_path, mode, save_file_password)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6077evge5htn
|
||||
@@ -0,0 +1,239 @@
|
||||
@tool
|
||||
class_name PersistentZone
|
||||
extends XRToolsSceneBase
|
||||
|
||||
|
||||
## Persistent Zone Node
|
||||
##
|
||||
## The [PersistentNode] class is an extension of [XRToolsSceneBase] which
|
||||
## manages the state of the zones [PersistentItem] objects through the
|
||||
## persistence system.
|
||||
|
||||
|
||||
# Group for world-data properties
|
||||
@export_group("World Data")
|
||||
|
||||
## This property specifies the persistent zone information
|
||||
@export var zone_info : PersistentZoneInfo
|
||||
|
||||
|
||||
# Add support for is_xr_class
|
||||
func is_xr_class(p_name : String) -> bool:
|
||||
return p_name == "PersistentZone" or super(p_name)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
# call the base
|
||||
super()
|
||||
|
||||
|
||||
# Get configuration warnings
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify zone info is set
|
||||
if not zone_info:
|
||||
warnings.append("Zone ID not zet")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
## Handle zone loaded
|
||||
func scene_loaded(user_data = null):
|
||||
super(user_data)
|
||||
|
||||
# Save the current zone
|
||||
GameState.current_zone = self
|
||||
|
||||
# Find all PersistentItem instances designed into the zone
|
||||
var items_in_zone := {}
|
||||
for node in XRTools.find_xr_children(self, "*", "PersistentItem"):
|
||||
items_in_zone[node.item_id] = node
|
||||
|
||||
# Find the zone items the PersistentWorld thinks should be in this zone.
|
||||
var zone_items = PersistentWorld.instance.get_value(zone_info.zone_id)
|
||||
|
||||
# Free items designed into the zone but PersistentWorld thinks should
|
||||
# be removed.
|
||||
if zone_items is Array:
|
||||
for item_id in items_in_zone:
|
||||
if not zone_items.has(item_id):
|
||||
var item : PersistentItem = items_in_zone[item_id]
|
||||
item.get_parent().remove_child(item)
|
||||
item.queue_free()
|
||||
|
||||
# Load world-state for all items in the zone
|
||||
propagate_notification(Persistent.NOTIFICATION_LOAD_STATE)
|
||||
|
||||
# Create items missing from the zone but PersistentWorld thinks should be
|
||||
# present.
|
||||
if zone_items is Array:
|
||||
for item_id in zone_items:
|
||||
if not items_in_zone.has(item_id):
|
||||
create_item_instance(item_id)
|
||||
|
||||
# Create items held by the players left hand
|
||||
var left_pickup := XRToolsFunctionPickup.find_left($XROrigin3D)
|
||||
var left_item_id = PersistentWorld.instance.get_value("player.left_hand")
|
||||
if left_pickup and left_item_id is String:
|
||||
var left_instance := create_item_instance(left_item_id)
|
||||
if left_instance:
|
||||
left_instance.global_transform = left_pickup.global_transform
|
||||
left_pickup._pick_up_object.call_deferred(left_instance)
|
||||
|
||||
# Create items held by the players right hand
|
||||
var right_pickup := XRToolsFunctionPickup.find_right($XROrigin3D)
|
||||
var right_item_id = PersistentWorld.instance.get_value("player.right_hand")
|
||||
if right_pickup and right_item_id is String:
|
||||
var right_instance := create_item_instance(right_item_id)
|
||||
if right_instance:
|
||||
right_instance.global_transform = right_pickup.global_transform
|
||||
right_pickup._pick_up_object.call_deferred(right_instance)
|
||||
|
||||
|
||||
## Handle zone exiting
|
||||
func scene_exiting(user_data = null):
|
||||
super(user_data)
|
||||
|
||||
# Ensure the zone state is saved before exiting the zone
|
||||
save_world_state()
|
||||
|
||||
# Clear the current zone
|
||||
GameState.current_zone = self
|
||||
|
||||
|
||||
## This method saves the state of the zone to the [PersistentWorld]. This gets
|
||||
## called upon exiting the zone; but it should also be called before saving
|
||||
## the game.
|
||||
func save_world_state() -> void:
|
||||
# Save world-state for all items in the zone
|
||||
propagate_notification(Persistent.NOTIFICATION_SAVE_STATE)
|
||||
|
||||
# Identify items held directly by the zone
|
||||
var items_in_zone : Array[String] = []
|
||||
for node in get_tree().get_nodes_in_group("persistent"):
|
||||
if is_item_held_by_zone(node):
|
||||
items_in_zone.append(node.item_id)
|
||||
|
||||
# Save the items held by the zone
|
||||
PersistentWorld.instance.set_value(zone_info.zone_id, items_in_zone)
|
||||
|
||||
# Handle items held in the players left hand
|
||||
var left_pickup := XRToolsFunctionPickup.find_left($XROrigin3D)
|
||||
var left_item := _get_held_persistent_item(left_pickup)
|
||||
if left_item:
|
||||
# The player.left_hand holds the item
|
||||
PersistentWorld.instance.set_value("player.left_hand", left_item.item_id)
|
||||
else:
|
||||
# The player.left_hand is empty
|
||||
PersistentWorld.instance.clear_value("player.left_hand")
|
||||
|
||||
# Handle items held in the players right hand
|
||||
var right_pickup := XRToolsFunctionPickup.find_right($XROrigin3D)
|
||||
var right_item := _get_held_persistent_item(right_pickup)
|
||||
if right_item:
|
||||
# The player.right_hand holds the item
|
||||
PersistentWorld.instance.set_value("player.right_hand", right_item.item_id)
|
||||
else:
|
||||
# The player.right_hand is empty
|
||||
PersistentWorld.instance.clear_value("player.right_hand")
|
||||
|
||||
|
||||
## Find the [PersistentZone] containing a given node
|
||||
static func find_instance(node : Node) -> PersistentZone:
|
||||
return XRTools.find_xr_ancestor(
|
||||
node,
|
||||
"*",
|
||||
"PersistentZone") as PersistentZone
|
||||
|
||||
|
||||
# Create a [PersistentItem] from its [param item_id]. This is used when
|
||||
# loading a scene that contains an item carried by the user from a different
|
||||
# scene.
|
||||
func create_item_instance(item_id : String) -> PersistentItem:
|
||||
# Get the items state information
|
||||
var state = PersistentWorld.instance.get_value(item_id)
|
||||
if not state is Dictionary:
|
||||
push_warning("Item %s not in world-data" % item_id)
|
||||
return null
|
||||
|
||||
# Get the items type_id
|
||||
var item_type_id = state.get("type")
|
||||
if not item_type_id is String:
|
||||
push_warning("Item %s does not define type" % item_id)
|
||||
return null
|
||||
|
||||
# Get the PersistentItemType
|
||||
var item_type := PersistentWorld.instance.item_database.get_type(item_type_id)
|
||||
if not item_type:
|
||||
push_warning("Item type %s not in database" % item_type_id)
|
||||
return null
|
||||
|
||||
# Load the item scene
|
||||
var item_scene : PackedScene = load(item_type.instance_scene)
|
||||
if not item_scene:
|
||||
push_warning("Item scene %s not valid" % item_type.instance_scene)
|
||||
return null
|
||||
|
||||
# Construct the item
|
||||
var item : PersistentItem = item_scene.instantiate()
|
||||
if not item:
|
||||
push_warning("Item scene %s not valid" % item_type.instance_scene)
|
||||
return null
|
||||
|
||||
# Initialize the item
|
||||
item.item_id = item_id
|
||||
item.item_type = item_type
|
||||
item.propagate_notification(Persistent.NOTIFICATION_LOAD_STATE)
|
||||
add_child(item)
|
||||
return item
|
||||
|
||||
|
||||
# This method returns true if the node is an item held by a zone rather than
|
||||
# being held by some sort of persistent object such as a PersistentPocket or
|
||||
# an XRToolsFunctionPickup.
|
||||
static func is_item_held_by_zone(node : Node) -> bool:
|
||||
# Skip if not valid
|
||||
if not is_instance_valid(node):
|
||||
return false
|
||||
|
||||
# Skip if not an PersistentItem
|
||||
if not node is PersistentItem:
|
||||
return false
|
||||
|
||||
# If the node isn't held by anything valid then it's held by the zone
|
||||
if not is_instance_valid(node.get_picked_up_by()):
|
||||
return true
|
||||
|
||||
# If held by a PersistentPocket then it's not held by the zone
|
||||
if node.get_picked_up_by() is PersistentPocket:
|
||||
return false
|
||||
|
||||
# If held by an XRToolsFunctionPickup the it's not held by the zone
|
||||
if node.get_picked_up_by() is XRToolsFunctionPickup:
|
||||
return false
|
||||
|
||||
# Node is held by a non-persistent mechanism in the zone
|
||||
push_warning("Item ", node.item_id, " held by non-persistent ", node.get_picked_up_by())
|
||||
return true
|
||||
|
||||
|
||||
# This method returns the persistent item primarily held by the pickup
|
||||
static func _get_held_persistent_item(pickup : XRToolsFunctionPickup) -> PersistentItem:
|
||||
# Fail if no pickup
|
||||
if not is_instance_valid(pickup):
|
||||
return null
|
||||
|
||||
# Fail if item is not a PersistentItem
|
||||
var item := pickup.picked_up_object as PersistentItem
|
||||
if not item:
|
||||
return null
|
||||
|
||||
# Fail if not active pickup, but merely second-hand grab
|
||||
if item.get_picked_up_by() != pickup:
|
||||
return null
|
||||
|
||||
# Return the item
|
||||
return item
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsavo6x4ekyk0
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://di1bu0tceg332"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://qbmx03iibuuu" path="res://addons/godot-xr-tools/staging/scene_base.tscn" id="1_rr08k"]
|
||||
[ext_resource type="Script" path="res://components/persistent/persistent_zone.gd" id="2_hkrv4"]
|
||||
|
||||
[node name="PersistentZone" instance=ExtResource("1_rr08k")]
|
||||
script = ExtResource("2_hkrv4")
|
||||
zone_info = null
|
||||
item_database = null
|
||||
@@ -0,0 +1,49 @@
|
||||
class_name PersistentZoneDatabase
|
||||
extends Resource
|
||||
|
||||
|
||||
## Persistent Zone Database Resource
|
||||
##
|
||||
## This resource defines all [PersistentZoneInfo] entries for our game.
|
||||
## This is used by our loading system to load the correct zone.
|
||||
|
||||
|
||||
## This property is the array of supported zones
|
||||
@export var zones : Array[PersistentZoneInfo] : set = _set_zones
|
||||
|
||||
|
||||
# Items cache
|
||||
var _cache := {}
|
||||
|
||||
# Items cache valid flag
|
||||
var _cache_valid := false
|
||||
|
||||
|
||||
## This method get an [PersistentZoneInfo] given its [param zone_id]. If no
|
||||
## corresponding [PersistentZoneInfo] is found then this function returns null.
|
||||
func get_zone(zone_id : String) -> PersistentZoneInfo:
|
||||
# Populate the cache if necessary
|
||||
if not _cache_valid:
|
||||
_populate_cache()
|
||||
|
||||
return _cache.get(zone_id)
|
||||
|
||||
|
||||
# Handle setting the items
|
||||
func _set_zones(p_zones : Array[PersistentZoneInfo]) -> void:
|
||||
# Save the new items
|
||||
zones = p_zones
|
||||
|
||||
# Invalidate the cache
|
||||
_cache_valid = false
|
||||
|
||||
|
||||
# Populate the type cache
|
||||
func _populate_cache() -> void:
|
||||
# Populate the cache
|
||||
_cache = {}
|
||||
for zone in zones:
|
||||
_cache[zone.zone_id] = zone
|
||||
|
||||
# Indicate the cache is valid
|
||||
_cache_valid = true
|
||||
@@ -0,0 +1 @@
|
||||
uid://blrrw6ir4dq80
|
||||
@@ -0,0 +1,14 @@
|
||||
class_name PersistentZoneInfo
|
||||
extends Resource
|
||||
|
||||
|
||||
## Persistent Zone Information Resource
|
||||
##
|
||||
## This resource defines a zone
|
||||
|
||||
|
||||
## This property specifies the unique zone ID
|
||||
@export var zone_id : String
|
||||
|
||||
## This property specifies the scene-file for instancing a [PersistentZone]
|
||||
@export_file('*.tscn') var instance_scene : String
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmwyqii4w7wnf
|
||||
Reference in New Issue
Block a user