add vr template project

This commit is contained in:
Guillaume Vern
2025-10-08 11:10:13 +02:00
parent 2cda32f24c
commit 97f183e59f
613 changed files with 37826 additions and 1499 deletions
+86
View File
@@ -0,0 +1,86 @@
@tool
class_name Scatter
extends Node3D
## Scatter Meshes Script
##
## This code allows you to quickly place a number of instances of a mesh at
## random locations.
@export var extend : Vector3 = Vector3(1.0, 0.0, 1.0): set = set_extend
@export var instance_count : int = 10: set = set_instance_count
@export var min_scale : float = 1.0: set = set_min_scale
@export var max_scale : float = 1.0: set = set_max_scale
@export var mesh : Mesh: set = set_mesh
@export var material_override : Material: set = set_material_override
var dirty : bool = true
var multi_mesh : MultiMesh
var multi_mesh_instance : MultiMeshInstance3D
func set_extend(new_value):
extend = new_value
_set_dirty()
func set_instance_count(new_value):
instance_count = new_value
_set_dirty()
func set_min_scale(new_value):
min_scale = new_value
_set_dirty()
func set_max_scale(new_value):
max_scale = new_value
_set_dirty()
func set_mesh(new_mesh):
mesh = new_mesh
if multi_mesh:
multi_mesh.mesh = mesh
func set_material_override(new_material):
material_override = new_material
if multi_mesh_instance:
multi_mesh_instance.material_override = material_override
func _set_dirty():
if !dirty:
dirty = true
call_deferred("_update_multimesh")
func _update_multimesh():
if !dirty:
return
multi_mesh.instance_count = instance_count
for i in instance_count:
var t := Transform3D()
var s := randf_range(min_scale, max_scale)
t.basis = Basis() \
.rotated(Vector3.UP, randf_range(-PI, PI)) \
.scaled(Vector3(s, s, s))
t.origin = Vector3(
randf_range(-extend.x, extend.x),
randf_range(-extend.y, extend.y),
randf_range(-extend.z, extend.z))
multi_mesh.set_instance_transform(i, t)
dirty = false
# Called when the node enters the scene tree for the first time.
func _ready():
multi_mesh = MultiMesh.new()
multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
multi_mesh.mesh = mesh
multi_mesh_instance = MultiMeshInstance3D.new()
multi_mesh_instance.multimesh = multi_mesh
multi_mesh_instance.material_override = material_override
add_child(multi_mesh_instance)
# First time creating our multimesh
_update_multimesh()
+1
View File
@@ -0,0 +1 @@
uid://dn6lkhk3gbq4n
+19
View File
@@ -0,0 +1,19 @@
extends Node3D
@export var saved_material : Material
# We only allow saving once after our scene loads
var is_saved = false
func _on_save_button_button_pressed(_button):
# Skip if already saved
if is_saved:
return
# Auto-save the game
if not GameState.auto_save_game():
return
# Change button to saved
$Pole/SaveButton/Button.set_surface_override_material(0, saved_material)
is_saved = true
@@ -0,0 +1 @@
uid://l4ifqkq17ax
+78
View File
@@ -0,0 +1,78 @@
[gd_scene load_steps=13 format=3 uid="uid://dm2e4qfiy0v0a"]
[ext_resource type="Script" path="res://components/helpers/zone_save_point.gd" id="1_hwwjp"]
[ext_resource type="Texture2D" uid="uid://b28f4me1ublg4" path="res://assets/prototype_materials/pattern.png" id="2_2gaip"]
[ext_resource type="Material" uid="uid://c386ygo86nqfc" path="res://assets/prototype_materials/wood_brown.tres" id="3_nemau"]
[ext_resource type="PackedScene" uid="uid://cme66uldrjl2i" path="res://addons/godot-xr-tools/interactables/interactable_area_button.tscn" id="4_8b5h5"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_5lqxg"]
albedo_color = Color(0.00784314, 0.615686, 0, 1)
albedo_texture = ExtResource("2_2gaip")
[sub_resource type="CylinderMesh" id="CylinderMesh_7f0ok"]
top_radius = 0.05
bottom_radius = 0.05
height = 1.4
radial_segments = 8
rings = 1
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cxops"]
albedo_texture = ExtResource("2_2gaip")
metallic = 0.59
roughness = 0.41
uv1_scale = Vector3(1, 4, 1)
[sub_resource type="BoxMesh" id="BoxMesh_0273w"]
size = Vector3(0.4, 0.04, 0.3)
[sub_resource type="BoxShape3D" id="BoxShape3D_0tokh"]
size = Vector3(0.4, 0.04, 0.3)
[sub_resource type="SphereShape3D" id="SphereShape3D_62pep"]
radius = 0.15
[sub_resource type="CylinderMesh" id="CylinderMesh_wjqor"]
top_radius = 0.075
bottom_radius = 0.1
height = 0.05
radial_segments = 16
rings = 1
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lql4b"]
albedo_color = Color(0.760784, 0.164706, 0.0823529, 1)
albedo_texture = ExtResource("2_2gaip")
[node name="ZoneSavePoint" type="Node3D"]
script = ExtResource("1_hwwjp")
saved_material = SubResource("StandardMaterial3D_5lqxg")
[node name="Pole" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7, -0.75)
mesh = SubResource("CylinderMesh_7f0ok")
surface_material_override/0 = SubResource("StandardMaterial3D_cxops")
[node name="Table" type="StaticBody3D" parent="Pole"]
transform = Transform3D(1, 0, 0, 0, 0.929358, -0.369179, 0, 0.369179, 0.929358, 0, 0.702817, 0)
[node name="Mesh" type="MeshInstance3D" parent="Pole/Table"]
mesh = SubResource("BoxMesh_0273w")
skeleton = NodePath("../..")
surface_material_override/0 = ExtResource("3_nemau")
[node name="CollisionShape3D" type="CollisionShape3D" parent="Pole/Table"]
shape = SubResource("BoxShape3D_0tokh")
[node name="SaveButton" parent="Pole" instance=ExtResource("4_8b5h5")]
transform = Transform3D(1, 0, 0, 0, 0.929358, -0.369179, 0, 0.369179, 0.929358, 0, 0.702817, 0)
button = NodePath("Button")
[node name="CollisionShape3D" type="CollisionShape3D" parent="Pole/SaveButton"]
shape = SubResource("SphereShape3D_62pep")
[node name="Button" type="MeshInstance3D" parent="Pole/SaveButton"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0436053, -1.19209e-07)
mesh = SubResource("CylinderMesh_wjqor")
skeleton = NodePath("../../Table/Mesh")
surface_material_override/0 = SubResource("StandardMaterial3D_lql4b")
[connection signal="button_pressed" from="Pole/SaveButton" to="." method="_on_save_button_button_pressed"]
+39
View File
@@ -0,0 +1,39 @@
@tool
extends Area3D
## Zone scene file
@export_file('*.tscn') var zone_scene : String = ""
## If true the zone switcher is enabled
@export var enabled : bool = true
## Name of the spawn-point node name in the target scene
@export var spawn_node_name := ""
# Called when the node enters the scene tree for the first time.
func _ready():
connect("body_entered", Callable(self, "_on_body_entered"))
# Called when a body enters this area
func _on_body_entered(body : Node3D):
# Ignore if not enabled
if not enabled:
return
# Skip if it wasn't the player entering
if not body.is_in_group("player_body"):
return
# Find our scene base
var scene_base : XRToolsSceneBase = XRTools.find_xr_ancestor(self, "*", "XRToolsSceneBase")
if not scene_base:
return
# Fire the load_scene signal
if zone_scene == "":
scene_base.reset_scene(spawn_node_name)
else:
scene_base.load_scene(zone_scene, spawn_node_name)
@@ -0,0 +1 @@
uid://d28e8mu0f8dx4
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://vm423o6hdlya"]
[ext_resource type="Script" path="res://components/helpers/zone_switch_area.gd" id="1"]
[node name="ZoneSwitchArea" type="Area3D"]
collision_layer = 0
collision_mask = 524288
monitorable = false
script = ExtResource("1")
+16
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://elh6ahwi62r4
+230
View File
@@ -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
+179
View File
@@ -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")
+258
View File
@@ -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
+239
View File
@@ -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