GodotVR/components/persistent/persistent_zone.gd
Guillaume Vern a2385360db 18.5
2025-10-10 15:46:35 +02:00

325 lines
9.8 KiB
GDScript

@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
signal numberOfFiresSignal(number: int)
var numberOfFires = 0;
# 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:
var fire_scene = preload("res://assets/fire/fire.tscn")
var ground = $World/Ground/MeshInstance3D
for i in range(30):
spawn_on_surface(ground, fire_scene)
numberOfFiresSignal.emit(numberOfFires)
# 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
func spawn_on_surface(target_mesh_instance: MeshInstance3D, scene_to_spawn: PackedScene):
var original_mesh = target_mesh_instance.mesh
if original_mesh == null:
print("Mesh is missing.")
return
var mesh : ArrayMesh
# Handle QuadMesh or other primitive Meshes
if original_mesh is PrimitiveMesh:
mesh = ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, original_mesh.get_mesh_arrays())
elif original_mesh is ArrayMesh:
mesh = original_mesh
else:
print("Unsupported mesh type.")
return
# Get surface data (assuming surface 0)
if mesh.get_surface_count() == 0:
print("Mesh has no surfaces.")
return
var arrays = mesh.surface_get_arrays(0)
var vertices = arrays[Mesh.ARRAY_VERTEX]
var indices = arrays[Mesh.ARRAY_INDEX]
if vertices.is_empty() or indices.is_empty():
print("Mesh has no usable vertices or indices.")
return
# Try finding a face pointing mostly upwards
var found_point = false
var point = Vector3.ZERO
var max_attempts = 100 # prevent infinite loops
var attempts = 0
while not found_point and attempts < max_attempts:
attempts += 1
# Choose a random triangle
var tri_index = randi() % (indices.size() / 3)
var i0 = indices[tri_index * 3]
var i1 = indices[tri_index * 3 + 1]
var i2 = indices[tri_index * 3 + 2]
var a = vertices[i0]
var b = vertices[i1]
var c = vertices[i2]
# Calculate the face normal
var normal = ((b - a).cross(c - a)).normalized()
# Check if face normal points mostly up (adjust threshold as needed)
if normal.dot(Vector3.UP) > 0.7:
# Get random point in triangle (barycentric coordinates)
var r1 = randf()
var r2 = randf()
if r1 + r2 >= 1.0:
r1 = 1.0 - r1
r2 = 1.0 - r2
point = a + (b - a) * r1 + (c - a) * r2
found_point = true
numberOfFires += 1
if not found_point:
print("No upward facing face found after", max_attempts, "attempts.")
return
# Transform to world space
point = target_mesh_instance.global_transform * point
# Instance and place your object
var new_object = scene_to_spawn.instantiate()
new_object.global_transform.origin = point
new_object.scale = Vector3(3.0, 3.0, 3.0)
get_tree().current_scene.add_child(new_object)
# 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