This commit is contained in:
algodoogle
2026-07-25 19:26:43 +01:00
parent f7024db98d
commit 596d777fae
32 changed files with 832 additions and 257 deletions
+98 -43
View File
@@ -24,7 +24,11 @@ func _ready() -> void:
push_error("XRPickable node not found in container.gd") push_error("XRPickable node not found in container.gd")
# Absorbing items is a server decision (the container's holder is server-
# snapped into a station, matching table.gd/hob.gd's convention).
func _on_body_entered(body: Node3D) -> void: func _on_body_entered(body: Node3D) -> void:
if not NetworkManager.owns_world():
return
print("Container enabled: ", enabled) print("Container enabled: ", enabled)
if not enabled: if not enabled:
print("Container disabled in _on_body_entered body") print("Container disabled in _on_body_entered body")
@@ -34,67 +38,118 @@ func _on_body_entered(body: Node3D) -> void:
print("Container _on_body_entered body: ", body) print("Container _on_body_entered body: ", body)
var picked_by = xr_pickable.get_picked_up_by() var picked_by = xr_pickable.get_picked_up_by()
if picked_by and picked_by.is_in_group("station"): if picked_by and picked_by.is_in_group("station"):
# If compatible and enough space, add item # If compatible and enough space, add item
var food_item = body.get_node("FoodItem") var food_item = body.get_node("FoodItem")
print("Container in station found %s of type %s: %s" % [target_group, FoodItem.Type.keys()[food_item.type], body.name]) print("Container in station found %s of type %s: %s" % [target_group, FoodItem.Type.keys()[food_item.type], body.name])
if food_item.type == FoodItem.Type.MEAL and _meal_container.get_child_count() < meal_positions.size(): var meal_count := contained_items.filter(func(f): return f.type == FoodItem.Type.MEAL).size()
var side_count := contained_items.filter(func(f): return f.type == FoodItem.Type.SIDE).size()
if food_item.type == FoodItem.Type.MEAL and meal_count < meal_positions.size():
print("Container adding meal") print("Container adding meal")
_add_item(body, meal_positions, _meal_container) _add_item(body)
if food_item.type == FoodItem.Type.SIDE and _side_container.get_child_count() < side_positions.size(): if food_item.type == FoodItem.Type.SIDE and side_count < side_positions.size():
print("Container adding side") print("Container adding side")
_add_item(body, side_positions, _side_container) _add_item(body)
func _add_item(item: Node3D, positions: Array[Node3D], container_root: Node3D) -> void: # Contents are synced as data (ids on the plate's PlateController), not
# reparented nodes: reparenting a MultiplayerSpawner-tracked item out of
# WorldContent would despawn it on every client the instant it happened.
func _add_item(item: Node3D) -> void:
var pickable = item as XRToolsPickable var pickable = item as XRToolsPickable
var rigidbody = item as RigidBody3D if pickable and pickable.is_picked_up():
pickable.drop()
# Drop item
if pickable: var food_node := item.get_node_or_null("FoodItem") as FoodItem
if pickable.is_picked_up(): if not food_node:
pickable.drop() return
pickable.enabled = false
# Copy the data out before despawning the real item.
# Freeze the rigid body and disable its collisions so it doesn't fight the container var data := FoodItem.new()
if rigidbody: data.id = food_node.id
rigidbody.freeze = true data.type = food_node.type
rigidbody.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC data.sell_value = food_node.sell_value
rigidbody.process_mode = PROCESS_MODE_DISABLED contained_items.append(data)
# Optional: Disable collision layer/mask so it doesn't bump into other food
rigidbody.collision_layer = 0 var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
rigidbody.collision_mask = 0 if plate_controller:
plate_controller.contained_ids.append(food_node.id)
item.reparent(container_root, false) # false = discard global transform
NetworkManager.despawn_item(item)
# Get target position index
var target_slot_index = container_root.get_child_count() - 1
if target_slot_index < positions.size():
item.position = positions[target_slot_index].position
item.rotation = positions[target_slot_index].rotation
# Add to list
var food_node = item.get_node_or_null("FoodItem")
if food_node:
contained_items.append(food_node as FoodItem)
func erase_item(item: FoodItem) -> void: func erase_item(item: FoodItem) -> void:
for container_root in [_meal_container, _side_container]: contained_items.erase(item)
for child in container_root.get_children(): var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
var food_item = child.get_node_or_null("FoodItem") as FoodItem if plate_controller:
if food_item.id == item.id: plate_controller.contained_ids.erase(item.id)
contained_items.erase(item)
child.queue_free()
func clear() -> void: func clear() -> void:
# delete all nodes in containers and contained_items contained_items.clear()
var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
if plate_controller:
plate_controller.contained_ids = []
## Rebuilds the purely-cosmetic visual representation of the plate's contents
## from a synced id list. Runs on every peer (called from PlateController
## whenever contained_ids changes, whether set locally or by the network).
func refresh_visuals(ids: Array[String]) -> void:
for child in _meal_container.get_children(): for child in _meal_container.get_children():
child.queue_free() child.queue_free()
for child in _side_container.get_children(): for child in _side_container.get_children():
child.queue_free() child.queue_free()
contained_items.clear()
var meal_idx := 0
var side_idx := 0
for id in ids:
var scene := RecipeManager.get_item_scene(id)
if not scene:
continue
var visual := scene.instantiate()
var food_node := visual.get_node_or_null("FoodItem") as FoodItem
var container_root: Node3D
var positions: Array[Node3D]
var idx: int
if food_node and food_node.type == FoodItem.Type.MEAL and meal_idx < meal_positions.size():
container_root = _meal_container
positions = meal_positions
idx = meal_idx
meal_idx += 1
elif food_node and food_node.type == FoodItem.Type.SIDE and side_idx < side_positions.size():
container_root = _side_container
positions = side_positions
idx = side_idx
side_idx += 1
else:
visual.queue_free()
continue
_make_cosmetic(visual)
container_root.add_child(visual)
visual.position = positions[idx].position
visual.rotation = positions[idx].rotation
# Strip interactivity/networking from a display-only copy: it's not spawned
# through NetworkManager, so it must never try to sync (its NetPickable child,
# if any, would have no corresponding replicated identity on other peers) or
# be grabbable/collidable.
func _make_cosmetic(visual: Node3D) -> void:
var net_pickable := visual.get_node_or_null("NetPickable")
if net_pickable:
net_pickable.queue_free()
if visual is RigidBody3D:
visual.freeze = true
visual.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
visual.collision_layer = 0
visual.collision_mask = 0
if visual is XRToolsPickable:
visual.enabled = false
visual.set_process(false)
visual.set_physics_process(false)
#plate (Pickalbe) #plate (Pickalbe)
+23
View File
@@ -8,6 +8,7 @@
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"] [ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
height = 0.0635376 height = 0.0635376
@@ -26,6 +27,23 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
albedo_color = Color(0.36656043, 0.16690676, 0.12531222, 1) albedo_color = Color(0.36656043, 0.16690676, 0.12531222, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_plate"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath("PlateController:contained_ids")
properties/3/spawn = true
properties/3/replication_mode = 1
properties/4/path = NodePath("PlateController:is_dirty")
properties/4/spawn = true
properties/4/replication_mode = 1
[node name="Plate" type="RigidBody3D" unique_id=190487773] [node name="Plate" type="RigidBody3D" unique_id=190487773]
collision_layer = 4 collision_layer = 4
collision_mask = 196615 collision_mask = 196615
@@ -134,3 +152,8 @@ transform = Transform3D(0.21189868, 0, 0, 0, -9.2623855e-09, 0.21189868, 0, -0.2
polygon = PackedVector2Array(-0.053057775, 0.2586278, 0.09469998, 0.40791017, 0.31746316, 0.26044407, 0.4514293, -0.05809097, 0.105977096, -0.11683442, -4.3913722e-05, 0.02302903) polygon = PackedVector2Array(-0.053057775, 0.2586278, 0.09469998, 0.40791017, 0.31746316, 0.26044407, 0.4514293, -0.05809097, 0.105977096, -0.11683442, -4.3913722e-05, 0.02302903)
depth = 0.02 depth = 0.02
material = SubResource("StandardMaterial3D_vlqg6") material = SubResource("StandardMaterial3D_vlqg6")
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_plate")
script = ExtResource("20_netpk")
+20 -1
View File
@@ -5,7 +5,14 @@ extends Node
@onready var container: ItemContainer = $"../Container" @onready var container: ItemContainer = $"../Container"
@export var is_dirty: bool = false @export var is_dirty: bool = false
## Synced plate contents (item ids), replacing reparented child nodes so
## contents survive replication (a MultiplayerSpawner-tracked item would
## despawn on every client the instant it was reparented out of WorldContent).
## Server writes it via ItemContainer; every peer (server included) renders
## the cosmetic result via the setter below.
@export var contained_ids: Array[String] = []: set = _set_contained_ids
func _ready() -> void: func _ready() -> void:
if not dirty_node: if not dirty_node:
@@ -20,3 +27,15 @@ func _process(_delta: float) -> void:
else: else:
container.enabled = true container.enabled = true
dirty_node.visible = false dirty_node.visible = false
func _set_contained_ids(value: Array[String]) -> void:
contained_ids = value
# Deferred: this can be written by the replicated spawn payload before
# this node's own @onready vars (container) have resolved.
_refresh_visuals.call_deferred()
func _refresh_visuals() -> void:
if container:
container.refresh_visuals(contained_ids)
+17
View File
@@ -8,6 +8,7 @@
[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"] [ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"] [ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="8_t9y3x"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="8_t9y3x"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
height = 0.10708985 height = 0.10708985
@@ -26,6 +27,17 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1) albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_buns"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")] [node name="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
@@ -72,3 +84,8 @@ id = "burger_buns"
type = 2 type = 2
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")] [node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")]
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_buns")
script = ExtResource("20_netpk")
+17
View File
@@ -8,6 +8,7 @@
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_mde49"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_mde49"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"] [ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
custom_solver_bias = 0.1 custom_solver_bias = 0.1
@@ -33,6 +34,17 @@ script = ExtResource("4_mgacb")
closed_pose = ExtResource("6_cp3eg") closed_pose = ExtResource("6_cp3eg")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_charcoal"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="PickableObject" unique_id=1675596942 instance=ExtResource("1_r73y2")] [node name="PickableObject" unique_id=1675596942 instance=ExtResource("1_r73y2")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
@@ -54,3 +66,8 @@ id = "charcoal"
type = 2 type = 2
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")] [node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")]
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_charcoal")
script = ExtResource("20_netpk")
+17
View File
@@ -8,6 +8,7 @@
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"] [ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"] [sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
size = Vector3(0.1, 0.1, 0.1) size = Vector3(0.1, 0.1, 0.1)
@@ -29,6 +30,17 @@ script = ExtResource("4_hc3f7")
closed_pose = ExtResource("6_bdp75") closed_pose = ExtResource("6_bdp75")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_cube"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")] [node name="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
@@ -50,3 +62,8 @@ id = "cube"
type = 1 type = 1
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")] [node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")]
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_cube")
script = ExtResource("20_netpk")
+17
View File
@@ -8,6 +8,7 @@
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_yxtxs"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_yxtxs"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.0338974 height = 0.0338974
@@ -26,6 +27,17 @@ script = ExtResource("5_w8sii")
closed_pose = ExtResource("7_wqxjj") closed_pose = ExtResource("7_wqxjj")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_burger"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="raw_burger" unique_id=1675596942 instance=ExtResource("1_fco8w")] [node name="raw_burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
@@ -49,3 +61,8 @@ hand_pose = SubResource("Resource_qyiot")
[node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("10_yxtxs")] [node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("10_yxtxs")]
id = "raw_burger" id = "raw_burger"
type = 2 type = 2
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_burger")
script = ExtResource("20_netpk")
+17
View File
@@ -8,6 +8,7 @@
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"] [ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_zz42p"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_zz42p"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.04777527 height = 0.04777527
@@ -26,6 +27,17 @@ script = ExtResource("5_ychvb")
closed_pose = ExtResource("7_rl64h") closed_pose = ExtResource("7_rl64h")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_cookedburger"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")] [node name="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
@@ -51,3 +63,8 @@ id = "cooked_burger"
type = 2 type = 2
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")] [node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")]
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_cookedburger")
script = ExtResource("20_netpk")
+17
View File
@@ -7,6 +7,7 @@
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_dunns"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_dunns"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_fh0f6"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_fh0f6"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_yy3y8"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_yy3y8"]
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.10392761 height = 0.10392761
@@ -28,6 +29,17 @@ albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
albedo_color = Color(0.29, 0.101500005, 0, 1) albedo_color = Color(0.29, 0.101500005, 0, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_hamburger"]
properties/0/path = NodePath(".:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("NetPickable:net_held_by")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")] [node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
gravity_scale = 0.04 gravity_scale = 0.04
@@ -81,3 +93,8 @@ material = SubResource("StandardMaterial3D_6l01i")
id = "hamburger" id = "hamburger"
type = 0 type = 0
sell_value = 4 sell_value = 4
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_hamburger")
script = ExtResource("20_netpk")
+74
View File
@@ -0,0 +1,74 @@
extends MultiplayerSynchronizer
## Networked sync component for a pickable item. Added as a child literally
## named "NetPickable" (network_manager.gd's authority RPCs already expect
## this) of every net-synced pickable, replicating its transform and held
## state. Only the current multiplayer authority (the server while loose, or
## whichever peer is holding it) actually simulates physics for the item;
## every other peer freezes their local copy and just follows the synced
## transform.
## 0 = loose/server-simulated; otherwise the peer id currently holding it.
## Replicated at spawn and on change so a late joiner sees the current holder.
var net_held_by: int = 0: set = _set_net_held_by
var _pickable: XRToolsPickable
func _ready() -> void:
_pickable = get_parent() as XRToolsPickable
if not _pickable:
push_error("NetPickable must be a child of an XRToolsPickable")
return
_pickable.picked_up.connect(_on_picked_up)
_pickable.dropped.connect(_on_dropped)
# Deferred: the pickable root captures its own original_collision_mask/
# original_collision_layer via @onready, which runs AFTER this child's
# _ready() but BEFORE the root's _ready() body. Calling apply_held_state
# synchronously here would freeze/mask the item before that capture runs,
# permanently corrupting the "restore on drop" values.
apply_held_state.call_deferred()
func _set_net_held_by(value: int) -> void:
net_held_by = value
apply_held_state()
## Puts the item in the right physics state for whether this peer currently
## owns it. Called locally after net_held_by changes, and directly by
## NetworkManager._set_item_authority right after an authority handoff.
func apply_held_state() -> void:
if not _pickable:
return
if not NetworkManager.is_online() or is_multiplayer_authority():
# We own this item's simulation (offline, loose+server, or currently
# holding it): leave physics alone, XRToolsPickable manages the rest.
return
# Someone else owns it: stop simulating locally, just follow the sync.
if _pickable.is_picked_up():
_pickable.drop()
_pickable.freeze = true
_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
_pickable.collision_mask = 0
_pickable.enabled = (net_held_by == 0)
## Local hand grab (not a station snap zone, which is server-only): request
## authority immediately so the throw/drop can be reconciled, but let the grab
## happen instantly here rather than waiting on the round trip.
func _on_picked_up(_p) -> void:
var by := _pickable.get_picked_up_by()
if not (by is XRToolsFunctionPickup):
return
NetworkManager.request_item_authority.rpc_id(1, _pickable.get_path())
func _on_dropped(_p) -> void:
# Only forward if we're actually still the authority — a drop caused by
# apply_held_state() losing authority (see above) must not re-report.
if not is_multiplayer_authority():
return
NetworkManager.release_item_authority.rpc_id(
1, _pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity
)
+1
View File
@@ -0,0 +1 @@
uid://bwd0pe2udb5xo
+99 -13
View File
@@ -24,9 +24,15 @@ signal connection_failed()
var _world: Node = null var _world: Node = null
var _players_spawner: MultiplayerSpawner = null var _players_spawner: MultiplayerSpawner = null
var _items_spawner: MultiplayerSpawner = null var _items_spawner: MultiplayerSpawner = null
var _content_root: Node = null
var _log_file: FileAccess var _log_file: FileAccess
## Reason the session ended, shown by the menu on the next _ready() (see
## take_status). Avoids depending on a live signal connection to a panel that
## doesn't exist yet at the moment the session actually ends.
var last_status := ""
func _ready() -> void: func _ready() -> void:
_open_log() _open_log()
@@ -80,26 +86,43 @@ func _go_offline() -> void:
if multiplayer.multiplayer_peer: if multiplayer.multiplayer_peer:
multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
unregister_world()
# --- Item spawning --------------------------------------------------------- # --- Item spawning ---------------------------------------------------------
## Spawn a networked item. Server-only when online (replicates to all peers via ## Spawn a networked item. Server-only when online (replicates to all peers via
## the ItemsSpawner); works directly when offline. Returns the new node on the ## the ItemsSpawner, including late joiners); works directly when offline.
## machine that owns spawning, else null. ## node_name gives the spawned node a deterministic, identical name on every
func spawn_item(scene_path: String, xform: Transform3D) -> Node: ## peer (needed for NodePath-based RPCs to resolve it); props are applied to
## the instance before it enters the tree, so exported vars land correctly.
## Returns the new node on the machine that owns spawning, else null.
func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "", props: Dictionary = {}) -> Node:
if is_online() and not is_server(): if is_online() and not is_server():
return null return null
var data := {"scene": scene_path, "xform": xform} var data := {"scene": scene_path, "xform": xform, "name": node_name, "props": props}
if is_online() and _items_spawner: if is_online() and _items_spawner:
return _items_spawner.spawn(data) return _items_spawner.spawn(data)
# Offline: instantiate directly under the world. # Offline: instantiate directly under the registered content root, or (for
# scenes that never call register_world, e.g. the offline menu/dev scenes)
# the current scene, so this keeps working without every offline scene
# needing to opt in.
var inst := _spawn_item_from_data(data) var inst := _spawn_item_from_data(data)
if _world and inst: if inst:
_world.add_child(inst) var parent: Node = _content_root if _content_root else get_tree().current_scene
if parent:
parent.add_child(inst)
return inst return inst
## Despawn a server-spawned item. MultiplayerSpawner broadcasts a despawn to
## every peer when a tracked node exits the tree on the authority, so this is
## the single seam for destroying spawned items (works offline too).
func despawn_item(node: Node) -> void:
if owns_world() and is_instance_valid(node):
node.queue_free()
# MultiplayerSpawner custom spawn function: runs on every peer to build the node # MultiplayerSpawner custom spawn function: runs on every peer to build the node
# from the replicated payload. # from the replicated payload.
func _spawn_item_from_data(data: Variant) -> Node: func _spawn_item_from_data(data: Variant) -> Node:
@@ -110,9 +133,32 @@ func _spawn_item_from_data(data: Variant) -> Node:
var inst := scene.instantiate() var inst := scene.instantiate()
if inst is Node3D: if inst is Node3D:
inst.transform = data["xform"] inst.transform = data["xform"]
if data.get("name", "") != "":
inst.name = data["name"]
for key in data.get("props", {}):
inst.set(key, data["props"][key])
if not owns_world():
_gate_station(inst)
return inst return inst
# Stations run their own logic and auto-grab (XRToolsSnapZone with
# snap_mode=RANGE) identically on every peer by default, which would let each
# peer independently grab/simulate the same shared object. Disable both on
# every peer except the one that owns world logic; the server-authoritative
# item-authority RPCs are what let clients still grab a server-held item by
# hand. Runs before the node enters the tree, so its own _ready() sees the
# final (disabled) state.
func _gate_station(node: Node) -> void:
if not (node is StaticBody3D):
return
for child in node.get_children():
if child is XRToolsSnapZone:
child.enabled = false
child.set_process(false)
node.set_process(false)
# --- Item grab-authority transfer ----------------------------------------- # --- Item grab-authority transfer -----------------------------------------
## A client (or host) requests authority over an item it just grabbed. Runs on ## A client (or host) requests authority over an item it just grabbed. Runs on
@@ -123,10 +169,17 @@ func request_item_authority(item_path: NodePath) -> void:
if not is_server(): if not is_server():
return return
var sender := multiplayer.get_remote_sender_id() var sender := multiplayer.get_remote_sender_id()
var item := get_node_or_null(item_path)
if item:
var np := item.get_node_or_null("NetPickable")
if np and np.net_held_by != 0 and np.net_held_by != sender:
# Already legitimately held by a different live peer: reject the
# requester's optimistic client-side grab instead of stealing it.
force_release_item.rpc_id(sender, item_path)
return
# Assign authority + held state first (disables the item on the server so its # Assign authority + held state first (disables the item on the server so its
# snap zone won't re-grab it), then release it from any station. # snap zone won't re-grab it), then release it from any station.
_set_item_authority.rpc(item_path, sender) _set_item_authority.rpc(item_path, sender)
var item := get_node_or_null(item_path)
if item: if item:
_release_from_snap_zones(item) _release_from_snap_zones(item)
@@ -147,13 +200,14 @@ func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) ->
_try_snap_into_station(item) _try_snap_into_station(item)
# All station snap zones in the world (nodes in the "station" group). # All station snap zones in the world (every XRToolsSnapZone child of a node in
# the "station" group — some stations, e.g. Table, have more than one).
func _station_snap_zones() -> Array: func _station_snap_zones() -> Array:
var zones := [] var zones := []
for station in get_tree().get_nodes_in_group("station"): for station in get_tree().get_nodes_in_group("station"):
var zone = station.get_node_or_null("XRToolsSnapZone") for child in station.get_children():
if zone: if child is XRToolsSnapZone:
zones.append(zone) zones.append(child)
return zones return zones
@@ -190,6 +244,15 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void:
np.apply_held_state() np.apply_held_state()
## Server tells a specific client that its optimistic grab was rejected (the
## item was already legitimately held by someone else). The client drops it.
@rpc("authority", "reliable")
func force_release_item(item_path: NodePath) -> void:
var item := get_node_or_null(item_path)
if item and item.has_method("drop"):
item.drop()
func is_server() -> bool: func is_server() -> bool:
return is_online() and multiplayer.is_server() return is_online() and multiplayer.is_server()
@@ -223,16 +286,37 @@ func submit_work(station_path: NodePath, amount: float) -> void:
station.add_work(multiplayer.get_remote_sender_id(), amount) station.add_work(multiplayer.get_remote_sender_id(), amount)
## Called by main.gd once the world scene is ready, passing its spawners. ## Called by the world scene once it's ready, passing its spawners. Must run
## before world_ready()/host()/join() on every peer so the custom spawn
## function is installed before any spawn packet can arrive.
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void: func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
_world = world _world = world
_players_spawner = players_spawner _players_spawner = players_spawner
_items_spawner = items_spawner _items_spawner = items_spawner
_content_root = items_spawner.get_node(items_spawner.spawn_path) if items_spawner else world
if _items_spawner: if _items_spawner:
_items_spawner.spawn_function = _spawn_item_from_data _items_spawner.spawn_function = _spawn_item_from_data
log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)]) log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)])
## Called when the world scene goes away (disconnect, leaving the session) so
## the autoload doesn't hold stale/freed references across a scene reload.
func unregister_world() -> void:
_world = null
_content_root = null
_players_spawner = null
_items_spawner = null
## Returns the reason the last session ended (if any) and clears it. The menu
## pulls this on its own _ready() rather than depending on a live signal
## connection to a panel that doesn't exist yet when the session ends.
func take_status() -> String:
var s := last_status
last_status = ""
return s
## Called by main.gd after it has registered the world and connected its ## Called by main.gd after it has registered the world and connected its
## player_joined/left listeners. Kicks off any menu- or command-line-driven ## player_joined/left listeners. Kicks off any menu- or command-line-driven
## session so that session signals never fire before the world is listening. ## session so that session signals never fire before the world is listening.
@@ -286,11 +370,13 @@ func _on_connected_to_server() -> void:
func _on_connection_failed() -> void: func _on_connection_failed() -> void:
log_line("connection_failed") log_line("connection_failed")
_go_offline() _go_offline()
last_status = "Could not connect"
connection_failed.emit() connection_failed.emit()
func _on_server_disconnected() -> void: func _on_server_disconnected() -> void:
log_line("server_disconnected") log_line("server_disconnected")
_go_offline() _go_offline()
last_status = "Host disconnected"
session_ended.emit() session_ended.emit()
+81
View File
@@ -0,0 +1,81 @@
class_name WorldLayout
## Data-driven description of what's in the multiplayer world. The server (or
## the single machine, when offline) spawns every entry through
## NetworkManager.spawn_item() instead of baking these into the scene file, so
## a joining client receives them from the server rather than assuming its own
## copy of the scene matches. Swapping the contents of these two functions is
## the only change needed for a future varying/procedural layout.
##
## Positions below are transcribed verbatim from the previous baked layout in
## Scenes/multiPlayer.tscn so the starting world is unchanged.
static func get_stations() -> Array[Dictionary]:
return [
{"scene": "res://Stations/Hob.tscn", "name": "Hob",
"xform": Transform3D(Basis(), Vector3(0.29336345, 0.8981018, -1.484)), "props": {}},
{"scene": "res://Stations/BurgerBunsDispenser.tscn", "name": "BurgerBunsDispenser",
"xform": Transform3D(Basis(), Vector3(-1.832131, 0.40028095, -1.4813508)), "props": {}},
{"scene": "res://Stations/sink.tscn", "name": "Sink",
"xform": Transform3D(Basis(), Vector3(1.3140475, 0.9061539, -1.4941733)), "props": {}},
{"scene": "res://Stations/dirt_station.tscn", "name": "DirtStation",
"xform": Transform3D(Basis(), Vector3(2.0864775, 0.8981018, -1.244947)), "props": {}},
{"scene": "res://Stations/Counter.tscn", "name": "Counter",
"xform": Transform3D(Basis(), Vector3(-0.7124918, 0.90304357, -1.4886917)), "props": {}},
{"scene": "res://Stations/Counter.tscn", "name": "Counter2",
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, -0.4458799)), "props": {}},
{"scene": "res://Stations/Counter.tscn", "name": "Counter3",
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, 0.55367994)), "props": {}},
{"scene": "res://Stations/Counter.tscn", "name": "Counter4",
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, 1.5539298)), "props": {}},
{"scene": "res://Stations/table.tscn", "name": "Table",
"xform": Transform3D(Basis(), Vector3(1.633146, 0.9030438, 1.1343781)),
"props": {
"initial_thinking_time": 8.0,
"initial_primary_time": 40.0,
"initial_friend_time": 3.0,
"initial_eating_time": 3.0,
}},
]
static func get_items() -> Array[Dictionary]:
var items: Array[Dictionary] = []
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns", Vector3(-1.8162017, 1.6081157, -1.4714175)))
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns2", Vector3(-1.3596323, 1.4110342, -0.22482127)))
items.append(_item("res://Containers/plate.tscn", "Plate", Vector3(-1.5717233, 1.5454081, 1.5373346)))
items.append(_item("res://Containers/plate.tscn", "Plate2", Vector3(-1.5730225, 1.499024, 0.59790254)))
items.append(_item("res://Containers/plate.tscn", "Plate3", Vector3(-1.5818124, 1.499024, -0.4634577)))
items.append(_item("res://Items/burger.tscn", "burger", Vector3(0.6888188, 1.4195822, -1.7110313)))
items.append(_item("res://Items/burger.tscn", "burger2", Vector3(0.6931299, 1.5300478, -1.71225)))
items.append(_item("res://Items/burger.tscn", "burger3", Vector3(0.69027674, 1.4969791, -1.7210286)))
items.append(_item("res://Items/burger.tscn", "burger4", Vector3(0.69134104, 1.4543622, -1.7210286)))
items.append(_item("res://Items/hamburger.tscn", "Hamburger", Vector3(-1.9352558, 1.623975, 1.2447833)))
items.append(_item("res://Items/hamburger.tscn", "Hamburger2", Vector3(-1.9857153, 1.5329368, 0.9472374)))
items.append(_item("res://Items/hamburger.tscn", "Hamburger3", Vector3(-1.7201865, 1.5329367, 0.14773655)))
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger", Vector3(-0.30671906, 1.4131018, -1.0928738)))
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger2", Vector3(-0.30671906, 1.4496142, -1.0928738)))
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger3", Vector3(-1.3344773, 1.4398065, -0.7096845)))
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger4", Vector3(-0.30671906, 1.525444, -1.0928738)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject", Vector3(0.6225724, 1.4792972, -1.0473135)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject2", Vector3(0.6359743, 1.4639391, -1.1673055)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject3", Vector3(0.5142721, 1.4792972, -1.0473135)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject4", Vector3(0.5276739, 1.4639391, -1.1673055)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject5", Vector3(0.73287535, 1.4792972, -1.0473135)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject6", Vector3(0.7462772, 1.4639391, -1.1673055)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject7", Vector3(-1.3556751, 1.4792972, 0.28881657)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject8", Vector3(-1.3422732, 1.4639391, 0.16882455)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject9", Vector3(-1.4639754, 1.4792972, 0.28881657)))
items.append(_item("res://Items/PickupCube.tscn", "PickableObject10", Vector3(-1.4505737, 1.4639391, 0.16882455)))
return items
static func _item(scene: String, name: String, pos: Vector3) -> Dictionary:
return {"scene": scene, "name": name, "xform": Transform3D(Basis(), pos), "props": {}}
+1
View File
@@ -0,0 +1 @@
uid://donvkica3drtx
+50
View File
@@ -0,0 +1,50 @@
extends Node3D
## Networked representation of one connected player. The owning peer's local
## XR rig (found via the "local_xr_origin" group) drives Head/LeftHand/
## RightHand each frame; the MultiplayerSynchronizer replicates those
## transforms so every other peer sees a matching avatar.
@onready var _head: Node3D = $Head
@onready var _left_hand: Node3D = $LeftHand
@onready var _right_hand: Node3D = $RightHand
var _local_camera: Node3D
var _local_left_hand: Node3D
var _local_right_hand: Node3D
func _enter_tree() -> void:
# MultiplayerSynchronizer resolves authority when this node enters the
# tree, so authority must be set here rather than in _ready(), or the
# first synced frames go the wrong direction.
set_multiplayer_authority(str(name).to_int())
func _ready() -> void:
if is_multiplayer_authority():
var origin := get_tree().get_first_node_in_group("local_xr_origin")
if origin:
_local_camera = origin.get_node_or_null("XRCamera3D")
_local_left_hand = origin.get_node_or_null("XRControllerLeftHand")
_local_right_hand = origin.get_node_or_null("XRControllerRightHand")
# Every peer's rig is baked at the same spot in the scene; spread
# joiners out along X by peer id so they don't start stacked on
# top of each other (host/peer 1 keeps the original spot).
var peer_id := str(name).to_int()
origin.position += Vector3((peer_id - 1) * 1.5, 0, 0)
# Don't render your own floating head/hands from the inside.
_head.visible = false
_left_hand.visible = false
_right_hand.visible = false
else:
set_process(false)
func _process(_delta: float) -> void:
if _local_camera:
_head.global_transform = _local_camera.global_transform
if _local_left_hand:
_left_hand.global_transform = _local_left_hand.global_transform
if _local_right_hand:
_right_hand.global_transform = _local_right_hand.global_transform
+1
View File
@@ -0,0 +1 @@
uid://bncuxh7j7ix5q
+44
View File
@@ -0,0 +1,44 @@
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://Player/net_player.gd" id="1_np001"]
[ext_resource type="PackedScene" uid="uid://bq86r4yll8po" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/left_fullglove_low.tscn" id="2_np002"]
[ext_resource type="PackedScene" uid="uid://xqimcf20s2jp" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/right_fullglove_low.tscn" id="3_np003"]
[sub_resource type="CapsuleMesh" id="CapsuleMesh_np004"]
radius = 0.09
height = 0.22
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np005"]
properties/0/path = NodePath("Head:position")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath("Head:quaternion")
properties/1/spawn = false
properties/1/replication_mode = 1
properties/2/path = NodePath("LeftHand:position")
properties/2/spawn = false
properties/2/replication_mode = 1
properties/3/path = NodePath("LeftHand:quaternion")
properties/3/spawn = false
properties/3/replication_mode = 1
properties/4/path = NodePath("RightHand:position")
properties/4/spawn = false
properties/4/replication_mode = 1
properties/5/path = NodePath("RightHand:quaternion")
properties/5/spawn = false
properties/5/replication_mode = 1
[node name="NetPlayer" type="Node3D"]
script = ExtResource("1_np001")
[node name="Head" type="MeshInstance3D" parent="."]
mesh = SubResource("CapsuleMesh_np004")
[node name="LeftHand" parent="." instance=ExtResource("2_np002")]
[node name="RightHand" parent="." instance=ExtResource("3_np003")]
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np005")
replication_interval = 0.033
+9 -9
View File
@@ -49,7 +49,10 @@ func _on_item_dropped(_item: Node3D) -> void:
set_deferred("monitoring", false) set_deferred("monitoring", false)
# If the other body is a FoodItem, check for a recipe and combine if one exists. # If the other body is a FoodItem, check for a recipe and combine if one exists.
# Combining is a server decision (the base item is server-snapped into a zone).
func _on_body_entered(body: Node3D) -> void: func _on_body_entered(body: Node3D) -> void:
if not NetworkManager.owns_world():
return
if _combining or body == _pickable: if _combining or body == _pickable:
print("CombinableItem _on_body_entered: other is our own pickable") print("CombinableItem _on_body_entered: other is our own pickable")
return return
@@ -78,19 +81,16 @@ func _combine(other_body: Node3D, result: PackedScene) -> void:
_combining = false _combining = false
return return
# Spawn the result at the base item's location, in world space. # Spawn the result through the server (so it replicates to every peer,
# including late joiners) at the base item's location, in world space.
var base_transform := _pickable.global_transform var base_transform := _pickable.global_transform
var result_instance: Node3D = result.instantiate() var result_instance: Node3D = NetworkManager.spawn_item(result.resource_path, base_transform)
_pickable.get_tree().current_scene.add_child(result_instance)
result_instance.global_transform = base_transform
# Free the pickable / root of this item and consume the incoming item. # Free the pickable / root of this item and consume the incoming item.
snap_zone.drop_object() snap_zone.drop_object()
_pickable.queue_free() NetworkManager.despawn_item(_pickable)
if other_body.has_method("drop_and_free"): # XRToolsPickable has this method other_body.drop()
other_body.drop_and_free() NetworkManager.despawn_item(other_body)
else:
other_body.queue_free()
# Snap the result into the now-empty zone. # Snap the result into the now-empty zone.
snap_zone.pick_up_object(result_instance) snap_zone.pick_up_object(result_instance)
+1 -1
View File
@@ -19,7 +19,7 @@
[ext_resource type="PackedScene" path="res://UI/main_menu_panel.tscn" id="17_panel"] [ext_resource type="PackedScene" path="res://UI/main_menu_panel.tscn" id="17_panel"]
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
size = Vector3(5, 0.1, 5) size = Vector3(115, 0.1, 15)
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"] [sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
material = ExtResource("3_m56cs") material = ExtResource("3_m56cs")
+9 -119
View File
@@ -3,22 +3,10 @@
[ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Scenes/multiplayer_world.gd" id="1_kdan8"] [ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Scenes/multiplayer_world.gd" id="1_kdan8"]
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_g30gi"] [ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_g30gi"]
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_75ecy"] [ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_75ecy"]
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_y6cjq"]
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"] [ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"]
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="6_urxur"]
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="7_3a3tq"]
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="8_8xmyt"]
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="9_6lx3u"]
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="10_p5bqy"]
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="11_jl027"]
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_j3sw5"]
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_sbg0f"]
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="14_lt5wx"]
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_7pgth"]
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_8yyag"]
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
size = Vector3(5, 0.1, 5) size = Vector3(15, 0.1, 15)
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"] [sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
material = ExtResource("3_75ecy") material = ExtResource("3_75ecy")
@@ -36,7 +24,7 @@ sdfgi_enabled = true
[node name="Main" type="Node3D" unique_id=1312265607] [node name="Main" type="Node3D" unique_id=1312265607]
script = ExtResource("1_kdan8") script = ExtResource("1_kdan8")
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_g30gi")] [node name="XROrigin3D" parent="." unique_id=2055526621 groups=["local_xr_origin"] instance=ExtResource("2_g30gi")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
@@ -52,114 +40,16 @@ shape = SubResource("BoxShape3D_vlqg6")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
mesh = SubResource("BoxMesh_24d3s") mesh = SubResource("BoxMesh_24d3s")
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_y6cjq")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -1.484)
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250] [node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
environment = SubResource("Environment_bvwq1") environment = SubResource("Environment_bvwq1")
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("6_urxur")] [node name="WorldContent" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175)
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_3a3tq")] [node name="Players" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.40028095, -1.4813508)
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("8_8xmyt")] [node name="ItemsSpawner" type="MultiplayerSpawner" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5717233, 1.5454081, 1.5373346) spawn_path = NodePath("../WorldContent")
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("9_6lx3u")] [node name="PlayersSpawner" type="MultiplayerSpawner" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6888188, 1.4195822, -1.7110313) _spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
spawn_path = NodePath("../Players")
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("9_6lx3u")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6931299, 1.5300478, -1.71225)
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("9_6lx3u")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69027674, 1.4969791, -1.7210286)
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("9_6lx3u")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69134104, 1.4543622, -1.7210286)
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("10_p5bqy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9352558, 1.623975, 1.2447833)
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.6225724, 1.4792972, -1.0473135)
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.6359743, 1.4639391, -1.1673055)
[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5142721, 1.4792972, -1.0473135)
[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.5276739, 1.4639391, -1.1673055)
[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("10_p5bqy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9857153, 1.5329368, 0.9472374)
[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.73287535, 1.4792972, -1.0473135)
[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.7462772, 1.4639391, -1.1673055)
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_j3sw5")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3140475, 0.9061539, -1.4941733)
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_sbg0f")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.0864775, 0.8981018, -1.244947)
[node name="Plate2" parent="." unique_id=356790445 instance=ExtResource("8_8xmyt")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5730225, 1.499024, 0.59790254)
[node name="CookedBurger" parent="." unique_id=1127807542 instance=ExtResource("14_lt5wx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4131018, -1.0928738)
[node name="CookedBurger2" parent="." unique_id=160088590 instance=ExtResource("14_lt5wx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4496142, -1.0928738)
[node name="CookedBurger3" parent="." unique_id=992183367 instance=ExtResource("14_lt5wx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344773, 1.4398065, -0.7096845)
[node name="CookedBurger4" parent="." unique_id=1837607086 instance=ExtResource("14_lt5wx")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.525444, -1.0928738)
[node name="BurgerBuns2" parent="." unique_id=1210358958 instance=ExtResource("6_urxur")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3596323, 1.4110342, -0.22482127)
[node name="PickableObject7" parent="." unique_id=641653545 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3556751, 1.4792972, 0.28881657)
[node name="PickableObject8" parent="." unique_id=1733819363 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.3422732, 1.4639391, 0.16882455)
[node name="PickableObject9" parent="." unique_id=12419746 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4639754, 1.4792972, 0.28881657)
[node name="PickableObject10" parent="." unique_id=313160217 instance=ExtResource("11_jl027")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.4505737, 1.4639391, 0.16882455)
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("15_7pgth")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7124918, 0.90304357, -1.4886917)
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_7pgth")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, -0.4458799)
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_7pgth")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 0.55367994)
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_7pgth")]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 1.5539298)
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_8yyag")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.633146, 0.9030438, 1.1343781)
initial_thinking_time = 8.0
initial_primary_time = 40.0
initial_friend_time = 3.0
initial_eating_time = 3.0
[node name="Hamburger3" parent="." unique_id=369573848 instance=ExtResource("10_p5bqy")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.5329367, 0.14773655)
[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("8_8xmyt")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.499024, -0.4634577)
+61 -4
View File
@@ -1,13 +1,21 @@
extends Node3D extends Node3D
## World script for the multiplayer scene. Consumes the host/join request set ## World script for the multiplayer scene. Consumes the host/join request set
## by the main menu, and returns to the menu if the session ends. ## by the main menu, spawns the world's stations/items on whichever machine
## owns the world (server, or the local player when offline), spawns/despawns
## a player avatar per connected peer, and returns to the menu if the session
## ends.
## ##
## Connection-level only for now: no PlayersSpawner/ItemsSpawner here since the ## The world's actual content (stations, items) is NOT baked into this scene —
## networked Player/NetPickable scenes aren't ported yet (out of scope for this ## it's spawned at runtime from Net/world_layout.gd via NetworkManager, so a
## change) — peers connect, but avatars/items don't sync. ## joining client receives it from the server (MultiplayerSpawner replays
## existing spawns to late joiners) instead of relying on its own local copy
## matching.
const PLAYER_SCENE := preload("res://Player/net_player.tscn")
var xr_interface: XRInterface var xr_interface: XRInterface
var _populated := false
func _ready() -> void: func _ready() -> void:
@@ -16,11 +24,60 @@ func _ready() -> void:
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
get_viewport().use_xr = true get_viewport().use_xr = true
NetworkManager.register_world(self, $PlayersSpawner, $ItemsSpawner)
NetworkManager.player_joined.connect(_on_player_joined)
NetworkManager.player_left.connect(_on_player_left)
NetworkManager.session_started.connect(_on_session_started)
NetworkManager.session_ended.connect(_on_session_ended) NetworkManager.session_ended.connect(_on_session_ended)
NetworkManager.connection_failed.connect(_on_connection_failed) NetworkManager.connection_failed.connect(_on_connection_failed)
# Offline (single player / dev run): owns_world() is already true, so
# populate immediately. Online host case is handled by session_started.
_populate_world_if_owner()
NetworkManager.world_ready() NetworkManager.world_ready()
func _exit_tree() -> void:
NetworkManager.unregister_world()
func _on_session_started(_is_server: bool) -> void:
_populate_world_if_owner()
## Spawns the world's stations/items exactly once, on the machine that owns
## world logic (server or offline). Safe to call multiple times/entry points.
func _populate_world_if_owner() -> void:
if not NetworkManager.owns_world() or _populated:
return
_populated = true
GameManager.meals_in_play = ["hamburger"]
for d in WorldLayout.get_stations():
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
for d in WorldLayout.get_items():
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
## Only the server (or the single offline machine) materialises player
## avatars; MultiplayerSpawner replicates the result to everyone else,
## including late joiners.
func _on_player_joined(peer_id: int) -> void:
if not NetworkManager.owns_world() or $Players.has_node(str(peer_id)):
return
var p := PLAYER_SCENE.instantiate()
p.name = str(peer_id)
$Players.add_child(p, true)
func _on_player_left(peer_id: int) -> void:
if not NetworkManager.owns_world():
return
var p := $Players.get_node_or_null(str(peer_id))
if p:
p.queue_free()
func _on_session_ended() -> void: func _on_session_ended() -> void:
get_tree().change_scene_to_file("res://Scenes/mainMenu.tscn") get_tree().change_scene_to_file("res://Scenes/mainMenu.tscn")
-1
View File
@@ -22,7 +22,6 @@ collision_layer = 65536
collision_mask = 65536 collision_mask = 65536
script = ExtResource("1_p30r1") script = ExtResource("1_p30r1")
snap_mode = 1 snap_mode = 1
initial_object = NodePath("../../BurgerBuns")
metadata/_custom_type_script = "uid://cquqe4f1m1sw6" metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1104626493] [node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1104626493]
+12
View File
@@ -8,6 +8,14 @@
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"] [sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
radius = 0.3 radius = 0.3
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_hob"]
properties/0/path = NodePath(".:time_cooked")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:cooking_result_time")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]] [node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]]
script = ExtResource("1_7jc4g") script = ExtResource("1_7jc4g")
@@ -103,3 +111,7 @@ size = Vector3(0.9842529, 0.095687866, 0.14019775)
transform = Transform3D(0.94465846, 0, 0, 0, 0.94465846, 0, 0, 0, 0.94465846, -0.003479004, -0.08253479, 0.5616675) transform = Transform3D(0.94465846, 0, 0, 0, 0.94465846, 0, 0, 0, 0.94465846, -0.003479004, -0.08253479, 0.5616675)
operation = 2 operation = 2
size = Vector3(0.9842529, 0.8349304, 0.072387695) size = Vector3(0.9842529, 0.8349304, 0.072387695)
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_hob")
+5 -1
View File
@@ -7,9 +7,13 @@ func _ready() -> void:
snap_zone.has_picked_up.connect(_makeDirty) snap_zone.has_picked_up.connect(_makeDirty)
func _makeDirty(item) -> void: func _makeDirty(item) -> void:
if not NetworkManager.owns_world():
return
var plate: PlateController = item.get_node_or_null("PlateController") as PlateController var plate: PlateController = item.get_node_or_null("PlateController") as PlateController
if not plate:
print("Dirt Station: held object is not a Plate")
return
if not plate.is_dirty: if not plate.is_dirty:
plate.is_dirty = true plate.is_dirty = true
print("Dirt Station could not find Dirty in: ", item)
+9 -8
View File
@@ -38,23 +38,24 @@ func _on_object_dropped() -> void:
func convert_held_to_item(_item: PackedScene) -> void: func convert_held_to_item(_item: PackedScene) -> void:
if not NetworkManager.owns_world():
return
print("Hob converting ", _item) print("Hob converting ", _item)
var old_pickable = snap_zone.picked_up_object var old_pickable = snap_zone.picked_up_object
if not old_pickable: if not old_pickable:
push_warning("Hob finished cooking _item, but snap zone is missing its reference") push_warning("Hob finished cooking _item, but snap zone is missing its reference")
return return
# Create new item as child of root in the same position # Spawn the new item through the server so it replicates to every peer
# (including late joiners), in the old item's position.
var original_transform = old_pickable.global_transform var original_transform = old_pickable.global_transform
var new_scene_instance = _item.instantiate() var new_scene_instance = NetworkManager.spawn_item(_item.resource_path, original_transform)
get_tree().get_root().add_child(new_scene_instance)
new_scene_instance.global_transform = original_transform
# Drop and free the old item and pick up the new one # Drop and free the old item and pick up the new one
print("Hob freeing old_pickable ", old_pickable) print("Hob freeing old_pickable ", old_pickable)
snap_zone.drop_object() snap_zone.drop_object()
old_pickable.queue_free() NetworkManager.despawn_item(old_pickable)
snap_zone.pick_up_object(new_scene_instance) snap_zone.pick_up_object(new_scene_instance)
+3 -5
View File
@@ -13,10 +13,8 @@ func _ready() -> void:
# Called every frame. 'delta' is the elapsed time since the previous frame. # Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not NetworkManager.owns_world():
return
if not snap_zone.picked_up_object: if not snap_zone.picked_up_object:
var new_item = item_scene.instantiate() var new_item = NetworkManager.spawn_item(item_scene.resource_path, snap_zone.global_transform)
# Add the new item to the hob's parent (so it lives in the main world space)
get_parent().add_child(new_item)
new_item.global_transform = snap_zone.transform
snap_zone.pick_up_object(new_item) snap_zone.pick_up_object(new_item)
+9
View File
@@ -9,6 +9,11 @@
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"] [sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
radius = 0.3 radius = 0.3
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_sink"]
properties/0/path = NodePath(".:progress")
properties/0/spawn = true
properties/0/replication_mode = 1
[node name="Sink" type="StaticBody3D" unique_id=2055277359 groups=["station"]] [node name="Sink" type="StaticBody3D" unique_id=2055277359 groups=["station"]]
script = ExtResource("1_7hh4b") script = ExtResource("1_7hh4b")
plate_scene = ExtResource("2_ai6d4") plate_scene = ExtResource("2_ai6d4")
@@ -75,3 +80,7 @@ size = Vector3(1.4033203, 1.2053223, 0.352417)
transform = Transform3D(1.3113414e-07, 0.12955962, 0.99157166, 8.8817837e-16, 0.9915716, -0.1295596, -0.9999999, 1.6989695e-08, 1.3002891e-07, 0.6035124, 0.019589934, -0.20879287) transform = Transform3D(1.3113414e-07, 0.12955962, 0.99157166, 8.8817837e-16, 0.9915716, -0.1295596, -0.9999999, 1.6989695e-08, 1.3002891e-07, 0.6035124, 0.019589934, -0.20879287)
operation = 2 operation = 2
size = Vector3(1.4033203, 1.2053223, 0.352417) size = Vector3(1.4033203, 1.2053223, 0.352417)
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_sink")
+70 -43
View File
@@ -25,9 +25,16 @@ enum TableState {
WAITING_FRIEND, # Waiting for friends food to arrive WAITING_FRIEND, # Waiting for friends food to arrive
EATING EATING
} }
var _state = TableState.EMPTY # use _setState(), never set directly
var _state_time: float = 0.0 ## State is server-authoritative and synced (see table.tscn's Sync node);
var _unsatisfied_orders: Array[String] ## state transitions only ever run where NetworkManager.owns_world() is true
## (the whole station's _process is gated off elsewhere for non-owners, see
## NetworkManager._gate_station). The setters below just refresh the display,
## so both the server (via _setState) and clients (via incoming sync) show
## the same text. Use _setState(), never assign _state directly.
var _state: TableState = TableState.EMPTY: set = _set_state
var _state_time: float = 0.0: set = _set_state_time
var _unsatisfied_orders: Array[String] = []: set = _set_unsatisfied_orders
func _ready() -> void: func _ready() -> void:
if not label_3d: if not label_3d:
@@ -59,8 +66,8 @@ func _on_object_picked_up(_item) -> void:
func _on_object_dropped() -> void: func _on_object_dropped() -> void:
print("Table: object dropped ") print("Table: object dropped ")
func _get_plate_from_item(_item: Node) -> PlateController: func _get_plate_from_item(_item: Node) -> PlateController:
return _item.get_children().filter(func(c): return c is PlateController).front() as PlateController return _item.get_children().filter(func(c): return c is PlateController).front() as PlateController
@@ -70,9 +77,9 @@ func _absorb_item_if_correct(_item: Node) -> void:
var plate = _get_plate_from_item(_item) var plate = _get_plate_from_item(_item)
if not plate: if not plate:
print("Table: held object is not a Plate") print("Table: held object is not a Plate")
return return
print("Table: held a Plate!") print("Table: held a Plate!")
# Absorm items from the plate we want # Absorm items from the plate we want
for food_item in plate.container.contained_items: for food_item in plate.container.contained_items:
print("Table: held a plate with FoodItem: ", food_item.id) print("Table: held a plate with FoodItem: ", food_item.id)
@@ -81,12 +88,11 @@ func _absorb_item_if_correct(_item: Node) -> void:
_unsatisfied_orders.erase(food_item.id) _unsatisfied_orders.erase(food_item.id)
GAME_MANAGER.money += food_item.sell_value GAME_MANAGER.money += food_item.sell_value
_setState(TableState.WAITING_FRIEND) _setState(TableState.WAITING_FRIEND)
_update_order_text()
# Table has everything it wants. Start eating # Table has everything it wants. Start eating
if _unsatisfied_orders.is_empty(): if _unsatisfied_orders.is_empty():
_setState(TableState.EATING) _setState(TableState.EATING)
func place_order() -> void: func place_order() -> void:
print("Table: place_order()") print("Table: place_order()")
@@ -94,56 +100,78 @@ func place_order() -> void:
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal()) _unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
func _update_order_text() -> void: ## Server-only state transition: sets the new state's timer and assigns
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)] ## _state (whose setter refreshes the display on every peer).
func _setState(newState: TableState) -> void: func _setState(newState: TableState) -> void:
print("Table set _state: ", TableState.keys()[newState]) print("Table set _state: ", TableState.keys()[newState])
if newState == TableState.EMPTY: match newState:
label_3d.text = "empty" TableState.EMPTY:
_state_time = 0.0
if newState == TableState.THINKING: TableState.THINKING:
_state_time = initial_eating_time _state_time = initial_eating_time
label_3d.text = lbl_thinking TableState.WAITING_PRIMARY:
_state_time = initial_primary_time
if newState == TableState.WAITING_PRIMARY: TableState.WAITING_FRIEND:
_state_time = initial_primary_time _state_time = initial_friend_time
_update_order_text() TableState.EATING:
_state_time = initial_eating_time
if newState == TableState.WAITING_FRIEND:
_state_time = initial_friend_time
_update_order_text()
if newState == TableState.EATING:
_state_time = initial_eating_time
label_3d.text = lbl_eating
_state = newState _state = newState
## Pure presentation, driven off the current (locally authoritative or
## synced-from-server) state. Runs on every peer.
func _refresh_display() -> void:
if not label_3d:
return
match _state:
TableState.EMPTY:
label_3d.text = "empty"
TableState.THINKING:
label_3d.text = lbl_thinking
TableState.WAITING_PRIMARY, TableState.WAITING_FRIEND:
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
TableState.EATING:
label_3d.text = lbl_eating
if label_3d_time:
label_3d_time.text = "%.1f" % _state_time
func _set_state(value: TableState) -> void:
_state = value
_refresh_display()
func _set_state_time(value: float) -> void:
_state_time = value
_refresh_display()
func _set_unsatisfied_orders(value: Array[String]) -> void:
_unsatisfied_orders = value
_refresh_display()
func _process(delta: float) -> void: func _process(delta: float) -> void:
# Wait for state to finish # Wait for state to finish
if _state_time > 0: if _state_time > 0:
_state_time -= delta _state_time -= delta
label_3d_time.text = "%.1f" % _state_time
return return
# When state timer is finished, do this stuff before moving to next state # When state timer is finished, do this stuff before moving to next state
match _state: match _state:
TableState.EMPTY: TableState.EMPTY:
_setState(TableState.THINKING) _setState(TableState.THINKING)
TableState.THINKING:
_setState(TableState.WAITING_PRIMARY)
place_order()
_update_order_text()
TableState.THINKING:
place_order()
_setState(TableState.WAITING_PRIMARY)
TableState.WAITING_PRIMARY: TableState.WAITING_PRIMARY:
label_3d.text = lbl_gameover label_3d.text = lbl_gameover
TableState.WAITING_FRIEND: TableState.WAITING_FRIEND:
label_3d.text = lbl_gameover label_3d.text = lbl_gameover
@@ -158,4 +186,3 @@ func _process(delta: float) -> void:
plate.container.clear() plate.container.clear()
plate.is_dirty = true plate.is_dirty = true
_setState(TableState.EMPTY) _setState(TableState.EMPTY)
+15
View File
@@ -11,6 +11,17 @@ radius = 0.3
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
albedo_color = Color(0.31, 0.21576, 0.1333, 1) albedo_color = Color(0.31, 0.21576, 0.1333, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_table"]
properties/0/path = NodePath(".:_state")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:_state_time")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:_unsatisfied_orders")
properties/2/spawn = true
properties/2/replication_mode = 1
[node name="Table" type="StaticBody3D" unique_id=1863572470 groups=["station"]] [node name="Table" type="StaticBody3D" unique_id=1863572470 groups=["station"]]
script = ExtResource("1_2vcpj") script = ExtResource("1_2vcpj")
@@ -155,3 +166,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3306234, 0)
pixel_size = 0.003 pixel_size = 0.003
billboard = 2 billboard = 2
text = "20.1s" text = "20.1s"
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath("..")
replication_config = SubResource("SceneReplicationConfig_np_table")
+25
View File
@@ -7,6 +7,7 @@ extends Control
const JON_SCENE := "res://Scenes/JonScene.tscn" const JON_SCENE := "res://Scenes/JonScene.tscn"
const MULTIPLAYER_SCENE := "res://Scenes/multiPlayer.tscn" const MULTIPLAYER_SCENE := "res://Scenes/multiPlayer.tscn"
const SETTINGS_PATH := "user://vryhungry_settings.cfg"
var _ip := "127.0.0.1" var _ip := "127.0.0.1"
@@ -33,8 +34,15 @@ func _ready() -> void:
_join_menu_btn.pressed.connect(_show_join_view) _join_menu_btn.pressed.connect(_show_join_view)
_join_btn.pressed.connect(_on_join_pressed) _join_btn.pressed.connect(_on_join_pressed)
_back_btn.pressed.connect(_show_root_view) _back_btn.pressed.connect(_show_root_view)
_load_ip()
_show_root_view() _show_root_view()
_refresh_ip() _refresh_ip()
# Show why we're back here, if the last session ended unexpectedly
# (dropped connection, failed join). Pulled rather than pushed, since
# NetworkManager may have set this before this panel even existed.
var status := NetworkManager.take_status()
if not status.is_empty():
set_status(status)
## --server / --join <ip> on the command line skip straight to the multiplayer ## --server / --join <ip> on the command line skip straight to the multiplayer
@@ -64,6 +72,7 @@ func _show_root_view() -> void:
func _show_join_view() -> void: func _show_join_view() -> void:
_root_view.visible = false _root_view.visible = false
_join_view.visible = true _join_view.visible = true
set_status("Enter host IP, then Join")
func _on_key(key: String) -> void: func _on_key(key: String) -> void:
@@ -94,10 +103,26 @@ func _on_join_pressed() -> void:
if _ip.is_empty(): if _ip.is_empty():
set_status("Enter an IP first") set_status("Enter an IP first")
return return
_save_ip()
NetworkManager.request_join(_ip) NetworkManager.request_join(_ip)
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE) get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
## Remembers the last IP the player tried to join, so the keypad starts
## pre-filled next time instead of always defaulting to 127.0.0.1.
func _load_ip() -> void:
var cfg := ConfigFile.new()
if cfg.load(SETTINGS_PATH) == OK:
_ip = cfg.get_value("network", "last_ip", _ip)
func _save_ip() -> void:
var cfg := ConfigFile.new()
cfg.load(SETTINGS_PATH)
cfg.set_value("network", "last_ip", _ip)
cfg.save(SETTINGS_PATH)
## Called by network_manager (e.g. on connection_failed after a bounce back to ## Called by network_manager (e.g. on connection_failed after a bounce back to
## this menu) to show feedback. ## this menu) to show feedback.
func set_status(text: String) -> void: func set_status(text: String) -> void:
+9 -8
View File
@@ -33,6 +33,15 @@ theme_override_font_sizes/font_size = 26
text = "VRyHungry" text = "VRyHungry"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="Status" type="Label" parent="Margin"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 0
theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1)
theme_override_font_sizes/font_size = 16
text = ""
horizontal_alignment = 1
[node name="RootView" type="VBoxContainer" parent="Margin"] [node name="RootView" type="VBoxContainer" parent="Margin"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
@@ -82,14 +91,6 @@ theme_override_font_sizes/font_size = 34
text = "127.0.0.1" text = "127.0.0.1"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="Status" type="Label" parent="Margin/JoinView"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1)
theme_override_font_sizes/font_size = 16
text = "Enter host IP, then Join"
horizontal_alignment = 1
[node name="Keypad" type="GridContainer" parent="Margin/JoinView"] [node name="Keypad" type="GridContainer" parent="Margin/JoinView"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
+1 -1
View File
@@ -8,7 +8,7 @@ var xr_interface :XRInterface
func _ready(): func _ready():
RECIPE_MANAGER.print_all_recipes() RECIPE_MANAGER.print_all_recipes()
GAME_MANAGER.meals_in_play.append("hamburger") GAME_MANAGER.meals_in_play = ["hamburger"]
xr_interface = XRServer.find_interface("OpenXR") xr_interface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized(): if xr_interface and xr_interface.is_initialized():