Compare commits
5 Commits
multi2
...
588d11a9ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 588d11a9ff | |||
| 3df2a0af58 | |||
| 832e539a96 | |||
| 4e6c645938 | |||
| 1869dbf054 |
@@ -23,6 +23,11 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
# Container fill mutates world state (reparent/freeze); run only on the
|
||||
# world owner. Item transforms replicate to clients via each item's
|
||||
# MultiplayerSynchronizer (global_transform), so contents follow the plate.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if not body.is_in_group(target_group):
|
||||
return
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
[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="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://Prefabs/net_pickable.gd" id="7_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
|
||||
height = 0.0635376
|
||||
@@ -61,6 +67,12 @@ hand_pose = SubResource("Resource_2hcq0")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.16517048, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_dlngj")
|
||||
|
||||
[node name="NetPickable" type="Node" parent="."]
|
||||
script = ExtResource("7_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
[node name="Container" parent="." unique_id=377793422 node_paths=PackedStringArray("meal_positions", "side_positions") instance=ExtResource("1_s5yhh")]
|
||||
target_group = "platalbe_item"
|
||||
meal_positions = [NodePath("MealPositions/Slot1")]
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_mgacb"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vw7i6"]
|
||||
[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="Script" path="res://Prefabs/net_pickable.gd" id="7_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
|
||||
custom_solver_bias = 0.1
|
||||
@@ -46,3 +52,9 @@ hand_pose = SubResource("Resource_lc22d")
|
||||
[node name="GrabPointHandRight" parent="." index="3" unique_id=514404634 instance=ExtResource("5_vw7i6")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="NetPickable" type="Node" parent="." index="4"]
|
||||
script = ExtResource("7_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="5"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
[ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="10_65ccv"]
|
||||
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="11_recipe"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
|
||||
size = Vector3(0.1, 0.1, 0.1)
|
||||
@@ -63,3 +69,9 @@ recipes = Array[ExtResource("11_recipe")]([SubResource("Resource_i8ixk")])
|
||||
[node name="FoodItem" parent="." index="6" unique_id=63948206 instance=ExtResource("11_vjw41")]
|
||||
id = "cube"
|
||||
type = 1
|
||||
|
||||
[node name="NetPickable" type="Node" parent="." index="7"]
|
||||
script = ExtResource("12_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="8"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
[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://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="9_hwyyd"]
|
||||
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="9_r8jvx"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||
height = 0.0338974
|
||||
@@ -49,3 +55,9 @@ hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="CookableItem" parent="." index="4" unique_id=280820828 instance=ExtResource("9_hwyyd")]
|
||||
turns_into = ExtResource("9_r8jvx")
|
||||
|
||||
[node name="NetPickable" type="Node" parent="." index="5"]
|
||||
script = ExtResource("12_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="6"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
@@ -29,7 +29,7 @@ albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
||||
albedo_color = Color(0.29, 0.101500005, 0, 1)
|
||||
|
||||
[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
|
||||
gravity_scale = 0.04
|
||||
gravity_scale = 0.4
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.02, 0.02, 0)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
extends Control
|
||||
|
||||
## The 2D UI rendered inside the world-space menu (XRToolsViewport2DIn3D).
|
||||
## Lets the player type an IP on a numeric keypad and pick Host / Join / Solo.
|
||||
## Pure UI: it emits intents; main.gd performs the networking.
|
||||
|
||||
signal host_pressed()
|
||||
signal join_pressed(ip: String)
|
||||
signal solo_pressed()
|
||||
|
||||
#var _ip := "127.0.0.1"
|
||||
var _ip := "51.175.10.161"
|
||||
|
||||
@onready var _ip_label: Label = %IPLabel
|
||||
@onready var _status: Label = %Status
|
||||
@onready var _keypad: GridContainer = %Keypad
|
||||
@onready var _host_btn: Button = %HostButton
|
||||
@onready var _join_btn: Button = %JoinButton
|
||||
@onready var _solo_btn: Button = %SoloButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for child in _keypad.get_children():
|
||||
if child is Button:
|
||||
child.pressed.connect(_on_key.bind(child.text))
|
||||
_host_btn.pressed.connect(func(): host_pressed.emit())
|
||||
_join_btn.pressed.connect(func(): join_pressed.emit(_ip))
|
||||
_solo_btn.pressed.connect(func(): solo_pressed.emit())
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_key(key: String) -> void:
|
||||
match key:
|
||||
"DEL":
|
||||
_ip = _ip.substr(0, max(0, _ip.length() - 1))
|
||||
_:
|
||||
if _ip.length() < 21:
|
||||
_ip += key
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
_ip_label.text = _ip if not _ip.is_empty() else "_"
|
||||
|
||||
|
||||
## Called by main.gd (via network_menu) to show connection feedback.
|
||||
func set_status(text: String) -> void:
|
||||
if _status:
|
||||
_status.text = text
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvivyrspjcjgm
|
||||
@@ -0,0 +1,172 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://Net/join_panel.gd" id="1_panel"]
|
||||
|
||||
[node name="JoinPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_panel")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0.09, 0.1, 0.13, 1)
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "VRyHungry — Multiplayer"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="IPLabel" type="Label" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
text = "127.0.0.1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Status" type="Label" parent="Margin/VBox"]
|
||||
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 = 18
|
||||
text = "Enter host IP, then Join"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Keypad" type="GridContainer" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 8
|
||||
theme_override_constants/v_separation = 8
|
||||
columns = 3
|
||||
|
||||
[node name="B1" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "1"
|
||||
|
||||
[node name="B2" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "2"
|
||||
|
||||
[node name="B3" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "3"
|
||||
|
||||
[node name="B4" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "4"
|
||||
|
||||
[node name="B5" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "5"
|
||||
|
||||
[node name="B6" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "6"
|
||||
|
||||
[node name="B7" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "7"
|
||||
|
||||
[node name="B8" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "8"
|
||||
|
||||
[node name="B9" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "9"
|
||||
|
||||
[node name="BDot" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "."
|
||||
|
||||
[node name="B0" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "0"
|
||||
|
||||
[node name="BDel" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "DEL"
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HostButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "HOST"
|
||||
|
||||
[node name="JoinButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "JOIN"
|
||||
|
||||
[node name="SoloButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "SOLO"
|
||||
@@ -0,0 +1,319 @@
|
||||
extends Node
|
||||
|
||||
## Client-server session manager for VRyHungry (listen-server model).
|
||||
##
|
||||
## Registered as the "NetworkManager" autoload. Owns transport (ENet), tracks
|
||||
## the session, and is the single place that reassigns multiplayer authority
|
||||
## (only the server does so). The world scene (main.gd) registers its spawners
|
||||
## here via [method register_world]; higher layers (players, items, stations)
|
||||
## build on top of this in later phases.
|
||||
|
||||
const DEFAULT_PORT := 24565
|
||||
const MAX_CLIENTS := 7
|
||||
|
||||
## Emitted on every peer (including the server for its own local player) when a
|
||||
## player peer joins. On the server this fires for each remote peer; the server
|
||||
## uses it to spawn that peer's player.
|
||||
signal player_joined(peer_id: int)
|
||||
signal player_left(peer_id: int)
|
||||
signal session_started(is_server: bool)
|
||||
signal session_ended()
|
||||
signal connection_failed()
|
||||
|
||||
# World hooks, registered by main.gd once the scene tree exists.
|
||||
var _world: Node = null
|
||||
var _players_spawner: MultiplayerSpawner = null
|
||||
var _items_spawner: MultiplayerSpawner = null
|
||||
|
||||
var _log_file: FileAccess
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_open_log()
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
|
||||
|
||||
# --- Public API ------------------------------------------------------------
|
||||
|
||||
## Start hosting. The host is peer 1 and also plays (listen server).
|
||||
func host(port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_server(port, MAX_CLIENTS)
|
||||
if err != OK:
|
||||
log_line("HOST failed to create_server on port %d: %s" % [port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
|
||||
session_started.emit(true)
|
||||
# The host's own local player joins immediately.
|
||||
_on_player_present(multiplayer.get_unique_id())
|
||||
return OK
|
||||
|
||||
|
||||
## Join an existing host.
|
||||
func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_client(address, port)
|
||||
if err != OK:
|
||||
log_line("JOIN failed to create_client %s:%d: %s" % [address, port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("JOIN connecting to %s:%d ..." % [address, port])
|
||||
return OK
|
||||
|
||||
|
||||
## Leave the session and tear down transport.
|
||||
func leave() -> void:
|
||||
_go_offline()
|
||||
log_line("Session ended")
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer
|
||||
# null), so is_multiplayer_authority()/get_unique_id() keep working while we are
|
||||
# back in single-player / menu state.
|
||||
func _go_offline() -> void:
|
||||
if multiplayer.multiplayer_peer:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
|
||||
|
||||
# --- Item spawning ---------------------------------------------------------
|
||||
|
||||
## 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
|
||||
## machine that owns spawning, else null.
|
||||
func spawn_item(scene_path: String, xform: Transform3D) -> Node:
|
||||
if is_online() and not is_server():
|
||||
return null
|
||||
var data := {"scene": scene_path, "xform": xform}
|
||||
if is_online() and _items_spawner:
|
||||
return _items_spawner.spawn(data)
|
||||
# Offline: instantiate directly under the world.
|
||||
var inst := _spawn_item_from_data(data)
|
||||
if _world and inst:
|
||||
_world.add_child(inst)
|
||||
return inst
|
||||
|
||||
|
||||
# MultiplayerSpawner custom spawn function: runs on every peer to build the node
|
||||
# from the replicated payload.
|
||||
func _spawn_item_from_data(data: Variant) -> Node:
|
||||
var scene: PackedScene = load(data["scene"])
|
||||
if not scene:
|
||||
push_error("spawn_item: could not load scene %s" % str(data.get("scene")))
|
||||
return null
|
||||
var inst := scene.instantiate()
|
||||
if inst is Node3D:
|
||||
inst.transform = data["xform"]
|
||||
return inst
|
||||
|
||||
|
||||
# --- Item grab-authority transfer -----------------------------------------
|
||||
|
||||
## A client (or host) requests authority over an item it just grabbed. Runs on
|
||||
## the server. If the item was snapped into a station, the station releases it
|
||||
## so the grabber cleanly takes ownership.
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_item_authority(item_path: NodePath) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
# 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.
|
||||
_set_item_authority.rpc(item_path, sender)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item:
|
||||
_release_from_snap_zones(item)
|
||||
|
||||
|
||||
## A player releases an item, forwarding its throw velocity so the server can
|
||||
## resume simulating it. Runs on the server. If released next to a station, the
|
||||
## server snaps it in (server-authoritative placement).
|
||||
@rpc("any_peer", "reliable")
|
||||
func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
_set_item_authority.rpc(item_path, 1)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item is RigidBody3D:
|
||||
item.freeze = false
|
||||
item.linear_velocity = lin
|
||||
item.angular_velocity = ang
|
||||
_try_snap_into_station(item)
|
||||
|
||||
|
||||
# All station snap zones in the world (nodes in the "station" group).
|
||||
func _station_snap_zones() -> Array:
|
||||
var zones := []
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
var zone = station.get_node_or_null("XRToolsSnapZone")
|
||||
if zone:
|
||||
zones.append(zone)
|
||||
return zones
|
||||
|
||||
|
||||
# If the item is snapped into any station, drop it from that station.
|
||||
func _release_from_snap_zones(item: Node) -> void:
|
||||
for zone in _station_snap_zones():
|
||||
if zone.picked_up_object == item:
|
||||
zone.drop_object()
|
||||
|
||||
|
||||
# Snap the item into the nearest empty station snap zone within grab range.
|
||||
func _try_snap_into_station(item: Node) -> void:
|
||||
if not (item is Node3D):
|
||||
return
|
||||
for zone in _station_snap_zones():
|
||||
if is_instance_valid(zone.picked_up_object):
|
||||
continue
|
||||
if zone.global_position.distance_to(item.global_position) <= zone.grab_distance:
|
||||
zone.pick_up_object(item)
|
||||
return
|
||||
|
||||
|
||||
# Server broadcasts an authority assignment so every peer agrees on who owns the
|
||||
# item (set_multiplayer_authority is a local call and must run everywhere).
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _set_item_authority(item_path: NodePath, peer: int) -> void:
|
||||
var item := get_node_or_null(item_path)
|
||||
if not item:
|
||||
return
|
||||
item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable
|
||||
var np := item.get_node_or_null("NetPickable")
|
||||
if np:
|
||||
np.net_held_by = 0 if peer == 1 else peer
|
||||
np.apply_held_state()
|
||||
|
||||
|
||||
func is_server() -> bool:
|
||||
return is_online() and multiplayer.is_server()
|
||||
|
||||
|
||||
## True only when a real ENet session is active. Godot installs a default
|
||||
## OfflineMultiplayerPeer, so a non-null peer alone does not mean "online".
|
||||
func is_online() -> bool:
|
||||
var p := multiplayer.multiplayer_peer
|
||||
return p != null and not (p is OfflineMultiplayerPeer)
|
||||
|
||||
|
||||
## True on the machine that owns authoritative world logic: the server when
|
||||
## online, or the single player when offline. Station logic and spawning should
|
||||
## only run where this is true, so state has one source of truth.
|
||||
func owns_world() -> bool:
|
||||
return not is_online() or is_server()
|
||||
|
||||
|
||||
# --- Station work-progress seam -------------------------------------------
|
||||
|
||||
## Reusable entry point for a client to contribute work to a station (e.g. a
|
||||
## future chopping/gesture station). The client detects the gesture locally and
|
||||
## calls this; the server validates and accumulates. Timer-driven stations like
|
||||
## the Hob don't need it, but it is the drop-in seam for input-driven ones.
|
||||
@rpc("any_peer", "reliable")
|
||||
func submit_work(station_path: NodePath, amount: float) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
var station := get_node_or_null(station_path)
|
||||
if station and station.has_method("add_work"):
|
||||
station.add_work(multiplayer.get_remote_sender_id(), amount)
|
||||
|
||||
|
||||
## Called by main.gd once the world scene is ready, passing its spawners.
|
||||
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
|
||||
_world = world
|
||||
_players_spawner = players_spawner
|
||||
_items_spawner = items_spawner
|
||||
if _items_spawner:
|
||||
_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)])
|
||||
|
||||
|
||||
## Called by main.gd after it has registered the world and connected its
|
||||
## player_joined/left listeners. Kicks off any command-line driven session so
|
||||
## that session signals never fire before the world is listening.
|
||||
func world_ready() -> void:
|
||||
_handle_cmdline()
|
||||
|
||||
|
||||
# --- Session signal handlers ----------------------------------------------
|
||||
|
||||
func _on_peer_connected(peer_id: int) -> void:
|
||||
log_line("peer_connected: %d" % peer_id)
|
||||
# Only the server reacts by materialising that peer's player.
|
||||
if is_server():
|
||||
_on_player_present(peer_id)
|
||||
|
||||
func _on_peer_disconnected(peer_id: int) -> void:
|
||||
log_line("peer_disconnected: %d" % peer_id)
|
||||
if is_server():
|
||||
_on_player_absent(peer_id)
|
||||
|
||||
func _on_connected_to_server() -> void:
|
||||
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
|
||||
session_started.emit(false)
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
log_line("connection_failed")
|
||||
_go_offline()
|
||||
connection_failed.emit()
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
log_line("server_disconnected")
|
||||
_go_offline()
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Player materialise/dematerialise. Phase 2 wires these to the PlayersSpawner;
|
||||
# for now they announce presence so the transport layer is independently testable.
|
||||
func _on_player_present(peer_id: int) -> void:
|
||||
log_line("player_present: %d" % peer_id)
|
||||
player_joined.emit(peer_id)
|
||||
|
||||
func _on_player_absent(peer_id: int) -> void:
|
||||
log_line("player_absent: %d" % peer_id)
|
||||
player_left.emit(peer_id)
|
||||
|
||||
|
||||
# --- Command-line driven test bootstrap -----------------------------------
|
||||
|
||||
func _handle_cmdline() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.has("--server"):
|
||||
log_line("cmdline: --server")
|
||||
host()
|
||||
elif args.has("--join"):
|
||||
var idx := args.find("--join")
|
||||
var addr := "127.0.0.1"
|
||||
if idx + 1 < args.size():
|
||||
addr = args[idx + 1]
|
||||
log_line("cmdline: --join %s" % addr)
|
||||
join(addr)
|
||||
|
||||
|
||||
# --- Logging ---------------------------------------------------------------
|
||||
|
||||
func _open_log() -> void:
|
||||
var dir := OS.get_environment("TEMP")
|
||||
if dir.is_empty():
|
||||
dir = OS.get_environment("TMPDIR")
|
||||
if dir.is_empty():
|
||||
dir = "user://"
|
||||
var path := dir.path_join("vryhungry_net_%d.log" % OS.get_process_id())
|
||||
_log_file = FileAccess.open(path, FileAccess.WRITE)
|
||||
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
|
||||
|
||||
func log_line(s: String) -> void:
|
||||
var id := 0
|
||||
var p := multiplayer.multiplayer_peer
|
||||
if p != null and p.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
id = multiplayer.get_unique_id()
|
||||
var line := "[NET %d] %s" % [id, s]
|
||||
print(line)
|
||||
if _log_file:
|
||||
_log_file.store_line(line)
|
||||
_log_file.flush()
|
||||
@@ -0,0 +1 @@
|
||||
uid://deefoory0vqmm
|
||||
@@ -0,0 +1,37 @@
|
||||
extends Node3D
|
||||
|
||||
## World-space main menu: hosts the 2D join_panel on an XRToolsViewport2DIn3D so
|
||||
## the player can point at it with a controller laser. Re-emits the panel's
|
||||
## intents; main.gd drives the actual networking.
|
||||
|
||||
signal host_pressed()
|
||||
signal join_pressed(ip: String)
|
||||
signal solo_pressed()
|
||||
|
||||
@onready var _vp: XRToolsViewport2DIn3D = $Viewport2Din3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# The viewport instantiates its 2D scene during its own _ready; connect once
|
||||
# that has happened.
|
||||
_connect_scene.call_deferred()
|
||||
|
||||
|
||||
func _connect_scene() -> void:
|
||||
_vp.connect_scene_signal("host_pressed", func(): host_pressed.emit())
|
||||
_vp.connect_scene_signal("join_pressed", func(ip): join_pressed.emit(ip))
|
||||
_vp.connect_scene_signal("solo_pressed", func(): solo_pressed.emit())
|
||||
|
||||
|
||||
func show_menu() -> void:
|
||||
visible = true
|
||||
|
||||
|
||||
func hide_menu() -> void:
|
||||
visible = false
|
||||
|
||||
|
||||
func set_status(text: String) -> void:
|
||||
var scene := _vp.get_scene_instance()
|
||||
if scene and scene.has_method("set_status"):
|
||||
scene.set_status(text)
|
||||
@@ -0,0 +1 @@
|
||||
uid://1o2nca75po1f
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://Net/network_menu.gd" id="1_menu"]
|
||||
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="2_vp"]
|
||||
[ext_resource type="PackedScene" path="res://Net/join_panel.tscn" id="3_panel"]
|
||||
|
||||
[node name="NetworkMenu" type="Node3D"]
|
||||
script = ExtResource("1_menu")
|
||||
|
||||
[node name="Viewport2Din3D" parent="." instance=ExtResource("2_vp")]
|
||||
screen_size = Vector2(0.6, 0.4)
|
||||
scene = ExtResource("3_panel")
|
||||
viewport_size = Vector2(600, 400)
|
||||
@@ -0,0 +1,48 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://cnetplayer0001"]
|
||||
|
||||
[ext_resource type="Script" path="res://Player/player.gd" id="1_player"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_player"]
|
||||
properties/0/path = NodePath(".:head_xform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath(".:left_xform")
|
||||
properties/1/spawn = true
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath(".:right_xform")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 1
|
||||
properties/3/path = NodePath(".:net_heartbeat")
|
||||
properties/3/spawn = true
|
||||
properties/3/replication_mode = 1
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_head"]
|
||||
albedo_color = Color(0.85, 0.7, 0.55, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_head"]
|
||||
material = SubResource("Mat_head")
|
||||
size = Vector3(0.18, 0.24, 0.16)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_hand"]
|
||||
albedo_color = Color(0.2, 0.5, 0.9, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_hand"]
|
||||
material = SubResource("Mat_hand")
|
||||
size = Vector3(0.09, 0.05, 0.12)
|
||||
|
||||
[node name="Player" type="Node3D"]
|
||||
script = ExtResource("1_player")
|
||||
|
||||
[node name="Avatar" type="Node3D" parent="."]
|
||||
|
||||
[node name="Head" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_head")
|
||||
|
||||
[node name="LeftHand" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_hand")
|
||||
|
||||
[node name="RightHand" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_hand")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_player")
|
||||
@@ -0,0 +1,80 @@
|
||||
extends Node3D
|
||||
|
||||
## One instance per connected peer. The owning peer is the multiplayer
|
||||
## authority. The local (authority) player builds the full XR rig from
|
||||
## XROrigin.tscn and feeds its head/hand poses into synced properties; remote
|
||||
## players show a lightweight avatar driven by those synced poses.
|
||||
|
||||
const RIG_SCENE := preload("res://XROrigin.tscn")
|
||||
|
||||
# Synced avatar pose. Authority writes these from the XR trackers each frame;
|
||||
# remotes read them onto the Avatar. net_heartbeat is a monotonic counter used
|
||||
# to confirm replication is live (also handy for debugging).
|
||||
@export var head_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var left_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var right_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var net_heartbeat: int = 0
|
||||
|
||||
var _rig: Node3D
|
||||
var _camera: Node3D
|
||||
var _left: Node3D
|
||||
var _right: Node3D
|
||||
var _is_local := false
|
||||
|
||||
@onready var _avatar: Node3D = $Avatar
|
||||
@onready var _avatar_head: Node3D = $Avatar/Head
|
||||
@onready var _avatar_left: Node3D = $Avatar/LeftHand
|
||||
@onready var _avatar_right: Node3D = $Avatar/RightHand
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# The spawner names each player by its owning peer id.
|
||||
set_multiplayer_authority(str(name).to_int())
|
||||
|
||||
|
||||
# (replication config is authored in Player.tscn's MultiplayerSynchronizer)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_is_local = is_multiplayer_authority()
|
||||
if _is_local:
|
||||
_setup_local()
|
||||
else:
|
||||
_setup_remote()
|
||||
if Engine.has_singleton("NetworkManager") or get_node_or_null("/root/NetworkManager"):
|
||||
NetworkManager.log_line("Player %s ready (authority=%d local=%s)" % [name, get_multiplayer_authority(), str(_is_local)])
|
||||
|
||||
|
||||
func _setup_local() -> void:
|
||||
# We see through the headset; hide our own avatar mesh.
|
||||
_avatar.visible = false
|
||||
_rig = RIG_SCENE.instantiate()
|
||||
add_child(_rig)
|
||||
_camera = _rig.get_node_or_null("XRCamera3D")
|
||||
_left = _rig.get_node_or_null("XRControllerLeftHand")
|
||||
_right = _rig.get_node_or_null("XRControllerRightHand")
|
||||
_enable_xr()
|
||||
|
||||
|
||||
func _enable_xr() -> void:
|
||||
var xr := XRServer.find_interface("OpenXR")
|
||||
if xr and xr.is_initialized():
|
||||
get_viewport().use_xr = true
|
||||
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
||||
|
||||
|
||||
func _setup_remote() -> void:
|
||||
_avatar.visible = true
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _is_local:
|
||||
if _camera:
|
||||
head_xform = _camera.global_transform
|
||||
left_xform = _left.global_transform
|
||||
right_xform = _right.global_transform
|
||||
net_heartbeat += 1
|
||||
else:
|
||||
_avatar_head.global_transform = head_xform
|
||||
_avatar_left.global_transform = left_xform
|
||||
_avatar_right.global_transform = right_xform
|
||||
@@ -0,0 +1 @@
|
||||
uid://ndrfa2xle033
|
||||
+10
-5
@@ -54,6 +54,12 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
if _combining or body == _item:
|
||||
return
|
||||
|
||||
# Combining is a world-state change: only the server/offline host runs it.
|
||||
# (Clients never have the base item snapped, so their zone won't monitor, but
|
||||
# guard explicitly in case of overlap during authority handoff.)
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
|
||||
var other := _find_combinable(body)
|
||||
if not other:
|
||||
return
|
||||
@@ -75,11 +81,9 @@ func _combine(other_body: Node3D, result: PackedScene) -> void:
|
||||
_combining = false
|
||||
return
|
||||
|
||||
# Spawn the result at the base item's location, in world space.
|
||||
# Spawn the result (networked) at the base item's location, in world space.
|
||||
var base_transform := _item.global_transform
|
||||
var result_instance: Node3D = result.instantiate()
|
||||
_item.get_tree().current_scene.add_child(result_instance)
|
||||
result_instance.global_transform = base_transform
|
||||
var result_instance := NetworkManager.spawn_item(result.resource_path, base_transform)
|
||||
|
||||
# Free the base item and consume the incoming item.
|
||||
snap_zone.drop_object()
|
||||
@@ -90,7 +94,8 @@ func _combine(other_body: Node3D, result: PackedScene) -> void:
|
||||
other_body.queue_free()
|
||||
|
||||
# Snap the result into the now-empty zone.
|
||||
snap_zone.pick_up_object(result_instance)
|
||||
if result_instance:
|
||||
snap_zone.pick_up_object(result_instance)
|
||||
|
||||
|
||||
# Find a CombinableItem among the direct children of [param node].
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
class_name NetPickable
|
||||
extends Node
|
||||
|
||||
## Networks an XRToolsPickable (RigidBody3D). Default authority is the server,
|
||||
## which simulates free items and syncs their transform. Grabbing an item with a
|
||||
## hand transfers authority to the grabbing client (its local kinematic grab
|
||||
## driver then drives the synced transform); releasing returns authority and the
|
||||
## throw velocity to the server. Non-authority peers keep the body frozen and
|
||||
## follow the synced transform, so only one machine ever simulates an item.
|
||||
|
||||
@onready var _item: XRToolsPickable = get_parent() as XRToolsPickable
|
||||
|
||||
var net_held_by := 0 # peer id currently holding (0 = free); kept consistent via NetworkManager
|
||||
var _was_authority := true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not _item:
|
||||
push_error("NetPickable must be a child of an XRToolsPickable")
|
||||
return
|
||||
_item.grabbed.connect(_on_grabbed)
|
||||
_item.released.connect(_on_released)
|
||||
_was_authority = _item.is_multiplayer_authority()
|
||||
_apply_authority_state()
|
||||
|
||||
|
||||
## Enable the item only where it is free or where we are its current holder, so
|
||||
## a peer's snap zone / hand never grabs an item another peer is holding. Called
|
||||
## by NetworkManager whenever net_held_by changes. Note: net_held_by is set per
|
||||
## peer by RPC (not by a synchronizer), so the holder keeps its item enabled
|
||||
## while everyone else disables their copy.
|
||||
func apply_held_state() -> void:
|
||||
if not _item:
|
||||
return
|
||||
_item.enabled = net_held_by == 0 or net_held_by == multiplayer.get_unique_id()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
var mine := _item.is_multiplayer_authority()
|
||||
if mine != _was_authority:
|
||||
_was_authority = mine
|
||||
_apply_authority_state()
|
||||
|
||||
|
||||
# Configure physics for whether we own this item or merely mirror it.
|
||||
func _apply_authority_state() -> void:
|
||||
if not _item:
|
||||
return
|
||||
if _item.is_multiplayer_authority():
|
||||
# We own physics: simulate the item whenever it is not held.
|
||||
if not _item.is_picked_up():
|
||||
_item.freeze = false
|
||||
else:
|
||||
# Someone else owns it: follow the synced transform kinematically.
|
||||
_item.freeze = true
|
||||
_item.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||
|
||||
|
||||
func _on_grabbed(_pickable: Node3D, by: Node3D) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
# Only hand grabs transfer authority; snap-zone grabs are server-driven.
|
||||
if not (by is XRToolsFunctionPickup):
|
||||
return
|
||||
NetworkManager.request_item_authority.rpc_id(1, _item.get_path())
|
||||
|
||||
|
||||
func _on_released(_pickable: Node3D, by: Node3D) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
if not (by is XRToolsFunctionPickup):
|
||||
return
|
||||
NetworkManager.release_item_authority.rpc_id(1, _item.get_path(), _item.linear_velocity, _item.angular_velocity)
|
||||
@@ -0,0 +1 @@
|
||||
uid://6n784li8n5ck
|
||||
@@ -26,7 +26,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_dlkho")
|
||||
snap_mode = 1
|
||||
snap_mode = 0
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=969340130]
|
||||
|
||||
+9
-1
@@ -16,6 +16,11 @@ material = SubResource("StandardMaterial3D_m5xe2")
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_hob"]
|
||||
properties/0/path = NodePath(".:progress")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]]
|
||||
script = ExtResource("1_7jc4g")
|
||||
|
||||
@@ -34,9 +39,12 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("2_er1dp")
|
||||
snap_mode = 1
|
||||
snap_mode = 0
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=370920392]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_vlqg6")
|
||||
|
||||
[node name="ProgressSync" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_hob")
|
||||
|
||||
+32
-29
@@ -1,59 +1,62 @@
|
||||
extends StaticBody3D
|
||||
|
||||
## Cooking station. The cook timer and item conversion run only on the machine
|
||||
## that owns world logic (server or offline host); `progress` is replicated to
|
||||
## clients for UI via a MultiplayerSynchronizer. This is the reference
|
||||
## implementation of the reusable "station work-progress" pattern: a server-owned
|
||||
## progress value advanced by an input source (here, a timer), which converts the
|
||||
## held item once it reaches the threshold and spawns the result over the network.
|
||||
|
||||
@export var cook_speed = 1
|
||||
@export var cook_speed := 1.0
|
||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||
|
||||
var item = null
|
||||
var progress = 0
|
||||
var progress: float = 0.0
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
if snap_zone.initial_object:
|
||||
item = snap_zone.initial_object
|
||||
print("Hob has item: ", item)
|
||||
else:
|
||||
print("Hob is empty")
|
||||
|
||||
|
||||
func convert_item(cookable: CookableItem) -> void:
|
||||
print("Hob converting ", cookable)
|
||||
|
||||
var old_pickable = snap_zone.picked_up_object
|
||||
if not old_pickable:
|
||||
push_warning("Hob finished cooking item, but snap zone is missing its reference")
|
||||
return
|
||||
|
||||
print("picked_up_object ", snap_zone.picked_up_object)
|
||||
|
||||
var original_transform = old_pickable.global_transform
|
||||
var new_scene_instance = cookable.turns_into.instantiate()
|
||||
|
||||
# Add the new item to the hob's parent (so it lives in the main world space)
|
||||
get_parent().add_child(new_scene_instance)
|
||||
new_scene_instance.global_transform = original_transform
|
||||
var original_transform: Transform3D = old_pickable.global_transform
|
||||
|
||||
snap_zone.drop_object()
|
||||
print("old_pickable ", old_pickable)
|
||||
# Spawn the result over the network (server) or locally (offline).
|
||||
var result := NetworkManager.spawn_item(cookable.turns_into.resource_path, original_transform)
|
||||
|
||||
# Remove the cooked item (despawn replicates for spawner-managed items).
|
||||
snap_zone.drop_object()
|
||||
old_pickable.queue_free()
|
||||
snap_zone.pick_up_object(new_scene_instance)
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
# Find the CookableItem in held object
|
||||
# Snap the result into the now-empty zone.
|
||||
if result:
|
||||
snap_zone.pick_up_object(result)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Only the world owner runs cooking logic; clients receive `progress` + the
|
||||
# converted item via replication.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
|
||||
# Find the CookableItem in the held object.
|
||||
var cookable = null
|
||||
if snap_zone.picked_up_object:
|
||||
var matches = snap_zone.picked_up_object.get_children().filter(func(c): return c is CookableItem)
|
||||
if matches.size() > 0:
|
||||
cookable = matches[0]
|
||||
|
||||
# Cook it if exists, then convert item after cooking
|
||||
|
||||
# Advance progress and convert once cooked.
|
||||
if cookable:
|
||||
progress += cook_speed * delta
|
||||
print("Cooking ", progress)
|
||||
if progress >= cookable.cooking_time:
|
||||
progress = 0
|
||||
progress = 0.0
|
||||
convert_item(cookable)
|
||||
else:
|
||||
progress = 0
|
||||
|
||||
progress = 0.0
|
||||
|
||||
@@ -13,10 +13,10 @@ func _ready() -> void:
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
# Only the world owner dispenses, so every client doesn't spawn its own copy.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if not snap_zone.picked_up_object:
|
||||
var new_item = item_scene.instantiate()
|
||||
|
||||
# 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)
|
||||
var new_item := NetworkManager.spawn_item(item_scene.resource_path, snap_zone.global_transform)
|
||||
if new_item:
|
||||
snap_zone.pick_up_object(new_item)
|
||||
|
||||
@@ -53,4 +53,4 @@
|
||||
Plate[`Hamburger`] accepts `Chips` => Plate[`Hamburger`, `Chips`]
|
||||
Plate[`Hamburger`, `Chips`] accepts `Cheese` => Plate[`Hamburger(Cheese=true)`, `Chips`]
|
||||
The logic for combining has to be expanded for plates.
|
||||
- Plates have a property `is_dirty` they get after a customer clears the plate, and is disabled after being processed in `Sink`
|
||||
- Plates have a property `is_dirty` they get after a customer clears the plate, and is disabled after being processed in `Sink`
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://diyu06cw06syv" path="res://addons/godot-xr-tools/player/player_body.tscn" id="4_6xr3x"]
|
||||
[ext_resource type="PackedScene" uid="uid://b6bk2pj8vbj28" path="res://addons/godot-xr-tools/functions/movement_turn.tscn" id="4_rd8py"]
|
||||
[ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="5_rd8py"]
|
||||
[ext_resource type="PackedScene" uid="uid://cqhw276realc" path="res://addons/godot-xr-tools/functions/function_pointer.tscn" id="6_ptr"]
|
||||
|
||||
[node name="XROrigin3D" type="XROrigin3D" unique_id=2055526621]
|
||||
|
||||
@@ -25,6 +26,10 @@ strafe = true
|
||||
[node name="FunctionPickup" parent="XRControllerLeftHand" unique_id=2133415351 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerLeftHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="XRControllerRightHand" type="XRController3D" parent="." unique_id=202756852]
|
||||
tracker = &"right_hand"
|
||||
|
||||
@@ -39,6 +44,10 @@ smooth_turn_speed = 3.5
|
||||
[node name="FunctionPickup" parent="XRControllerRightHand" unique_id=1876210450 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerRightHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="PlayerBody" type="CharacterBody3D" parent="." unique_id=1444632058 groups=["player_body"] instance=ExtResource("4_6xr3x")]
|
||||
process_priority = -100
|
||||
process_physics_priority = -100
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
{
|
||||
"uuid": "d9cebd68-792e-4d01-a916-7df5f128b4a8",
|
||||
"title": "Implement Multiplayer Synchronization (Network Architecture)",
|
||||
"description": "Establish the client-server (or P2P) networking model for the game. Focus on syncing critical game state: physics objects (FoodItems), player actions (gestures/work progress), and station states (Chopping progress).",
|
||||
"description": "Establish the client-server networking model for the game. Focus on syncing critical game state: physics objects (FoodItems), player actions (gestures/work progress), and station states (Chopping progress).",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": [
|
||||
{
|
||||
|
||||
@@ -1,12 +1,126 @@
|
||||
extends Node3D
|
||||
|
||||
var xr_interface :XRInterface
|
||||
## World manager. Owns the multiplayer spawners and the world-space main menu,
|
||||
## and turns NetworkManager's session events into spawned/despawned players and
|
||||
## seeded items.
|
||||
##
|
||||
## Startup:
|
||||
## - Command-line session (--server/--join): skip the menu, behave headlessly.
|
||||
## - Otherwise: embody a local player (so they can point) and show the menu; the
|
||||
## Host/Join/Solo buttons start the actual session.
|
||||
|
||||
func _ready():
|
||||
xr_interface = XRServer.find_interface("OpenXR")
|
||||
if xr_interface and xr_interface.is_initialized():
|
||||
print("OpenXR started :)")
|
||||
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
||||
get_viewport().use_xr = true
|
||||
const PLAYER_SCENE := preload("res://Player/Player.tscn")
|
||||
const CUBE_SCENE := "res://Items/PickupCube.tscn"
|
||||
|
||||
@onready var _players_spawner: MultiplayerSpawner = $PlayersSpawner
|
||||
@onready var _items_spawner: MultiplayerSpawner = $ItemsSpawner
|
||||
@onready var _menu: Node3D = $NetworkMenu
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
NetworkManager.register_world(self, _players_spawner, _items_spawner)
|
||||
NetworkManager.player_joined.connect(_on_player_joined)
|
||||
NetworkManager.player_left.connect(_on_player_left)
|
||||
NetworkManager.session_started.connect(_on_session_started)
|
||||
NetworkManager.connection_failed.connect(_on_connection_failed)
|
||||
NetworkManager.session_ended.connect(_on_session_ended)
|
||||
_menu.host_pressed.connect(_on_menu_host)
|
||||
_menu.join_pressed.connect(_on_menu_join)
|
||||
_menu.solo_pressed.connect(_on_menu_solo)
|
||||
|
||||
# Start any command-line driven session now that we are listening.
|
||||
NetworkManager.world_ready()
|
||||
|
||||
if NetworkManager.is_online():
|
||||
# Command-line session: no menu.
|
||||
_menu.hide_menu()
|
||||
if _owns_spawning():
|
||||
_spawn_starting_items()
|
||||
else:
|
||||
print("OpenXR failed to start. Check your headset...")
|
||||
# Show the menu; embody a local player so they can point at it.
|
||||
_spawn_player(1)
|
||||
_menu.show_menu()
|
||||
|
||||
|
||||
# --- Menu button handlers --------------------------------------------------
|
||||
|
||||
func _on_menu_host() -> void:
|
||||
_menu.hide_menu()
|
||||
NetworkManager.host() # keeps the already-spawned local player (id 1)
|
||||
_spawn_starting_items()
|
||||
|
||||
|
||||
func _on_menu_solo() -> void:
|
||||
_menu.hide_menu()
|
||||
_spawn_starting_items()
|
||||
|
||||
|
||||
func _on_menu_join(ip: String) -> void:
|
||||
_menu.set_status("Connecting to %s ..." % ip)
|
||||
# Our placeholder id "1" belongs to the host once we join; drop it and let
|
||||
# the server spawn our real player.
|
||||
_despawn_player(1)
|
||||
NetworkManager.join(ip)
|
||||
|
||||
|
||||
func _on_session_started(is_server: bool) -> void:
|
||||
if not is_server:
|
||||
# Connected to a host as a client.
|
||||
_menu.hide_menu()
|
||||
# Stations are server-authoritative: clients must not run snap zones, or
|
||||
# they create divergent local copies of snapped items.
|
||||
_disable_client_snap_zones()
|
||||
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
_menu.set_status("Connection failed — check the IP")
|
||||
_spawn_player(1) # restore embodiment
|
||||
_menu.show_menu()
|
||||
|
||||
|
||||
# The client lost the server (or left): reload the scene to return to a clean
|
||||
# main-menu state.
|
||||
func _on_session_ended() -> void:
|
||||
get_tree().reload_current_scene.call_deferred()
|
||||
|
||||
|
||||
func _disable_client_snap_zones() -> void:
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
var zone = station.get_node_or_null("XRToolsSnapZone")
|
||||
if zone:
|
||||
zone.enabled = false
|
||||
|
||||
|
||||
# --- Player / item spawning ------------------------------------------------
|
||||
|
||||
func _on_player_joined(peer_id: int) -> void:
|
||||
if NetworkManager.is_server():
|
||||
_spawn_player(peer_id)
|
||||
|
||||
|
||||
func _on_player_left(peer_id: int) -> void:
|
||||
if NetworkManager.is_server():
|
||||
_despawn_player(peer_id)
|
||||
|
||||
|
||||
func _spawn_player(peer_id: int) -> void:
|
||||
if has_node(str(peer_id)):
|
||||
return
|
||||
var p := PLAYER_SCENE.instantiate()
|
||||
p.name = str(peer_id)
|
||||
add_child(p)
|
||||
|
||||
|
||||
func _despawn_player(peer_id: int) -> void:
|
||||
var n := get_node_or_null(str(peer_id))
|
||||
if n:
|
||||
n.queue_free()
|
||||
|
||||
|
||||
func _owns_spawning() -> bool:
|
||||
return not NetworkManager.is_online() or NetworkManager.is_server()
|
||||
|
||||
|
||||
func _spawn_starting_items() -> void:
|
||||
NetworkManager.spawn_item(CUBE_SCENE, Transform3D(Basis(), Vector3(-0.705, 1.576, -1.032)))
|
||||
NetworkManager.spawn_item(CUBE_SCENE, Transform3D(Basis(), Vector3(0.283, 1.561, -1.066)))
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
[gd_scene format=3 uid="uid://clw8ai6kngqxb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_0xm2m"]
|
||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_h2yge"]
|
||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_272bh"]
|
||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="3_h2yge"]
|
||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="3_lquwl"]
|
||||
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="4_7mycd"]
|
||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="6_7mycd"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="8_5vw27"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="8_vivmo"]
|
||||
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="9_2cqfq"]
|
||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="9_kek77"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(5, 0.1, 5)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||
material = ExtResource("3_272bh")
|
||||
size = Vector3(5, 0.1, 5)
|
||||
[ext_resource type="PackedScene" path="res://Net/network_menu.tscn" id="10_menu"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="12_yaehf"]
|
||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="13_074og"]
|
||||
|
||||
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||
background_mode = 2
|
||||
@@ -27,49 +23,104 @@ ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_cegan"]
|
||||
size = Vector3(5, 0.1, 5)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_82xsv"]
|
||||
material = ExtResource("3_272bh")
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_0xm2m")
|
||||
|
||||
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_h2yge")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8820953, 0)
|
||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=1626417783]
|
||||
_spawnable_scenes = PackedStringArray("uid://d3obr5ldhyiuq")
|
||||
spawn_path = NodePath("..")
|
||||
|
||||
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="." unique_id=462360509]
|
||||
spawn_path = NodePath("..")
|
||||
|
||||
[node name="NetworkMenu" parent="." unique_id=1067581818 instance=ExtResource("10_menu")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -0.8)
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||
|
||||
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="." unique_id=1088862514]
|
||||
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||
|
||||
[node name="Floor" type="StaticBody3D" parent="." unique_id=1667851334]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
|
||||
shape = SubResource("BoxShape3D_vlqg6")
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=1242150722]
|
||||
shape = SubResource("BoxShape3D_cegan")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=2074025214]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
|
||||
mesh = SubResource("BoxMesh_24d3s")
|
||||
mesh = SubResource("BoxMesh_82xsv")
|
||||
|
||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("3_lquwl")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -1.484)
|
||||
|
||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("4_7mycd")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.73186195, 0.8981018, -1.484)
|
||||
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("8_vivmo")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175)
|
||||
|
||||
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("3_h2yge")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.70505303, 1.5761534, -1.0317149)
|
||||
|
||||
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("3_h2yge")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.28315204, 1.5607953, -1.0657519)
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
[node name="OmniLight3D" type="OmniLight3D" parent="." unique_id=812168256]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2856425, 1.4939771, -1.4959787)
|
||||
visible = false
|
||||
light_color = Color(1, 0.38431373, 0, 1)
|
||||
light_energy = 1.784
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="burger" parent="." unique_id=1554047278 instance=ExtResource("8_5vw27")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.482068, -1.2252241)
|
||||
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("9_2cqfq")]
|
||||
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("9_kek77")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.22971058, 1.699144, -1.8499806)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.72025335, 1.4458435, -1.5107731)
|
||||
|
||||
[node name="burger" parent="." unique_id=1675596942 instance=ExtResource("8_5vw27")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1439226, 1.4195822, -1.2004558)
|
||||
|
||||
[node name="burger2" parent="." unique_id=2005944088 instance=ExtResource("8_5vw27")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1396115, 1.5300478, -1.2016745)
|
||||
|
||||
[node name="burger3" parent="." unique_id=1397498409 instance=ExtResource("8_5vw27")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1424646, 1.4969791, -1.210453)
|
||||
|
||||
[node name="burger4" parent="." unique_id=185317851 instance=ExtResource("8_5vw27")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1414003, 1.4543622, -1.210453)
|
||||
|
||||
[node name="Hamburger" parent="." unique_id=1652598956 instance=ExtResource("12_yaehf")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6108264, 1.5438088, -1.4718918)
|
||||
|
||||
[node name="PickableObject" parent="." unique_id=1736331465 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4133378, 1.4792972, -0.8079578)
|
||||
|
||||
[node name="PickableObject2" parent="." unique_id=1644484469 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.399936, 1.4639391, -0.92794985)
|
||||
|
||||
[node name="PickableObject3" parent="." unique_id=1829014935 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.5216382, 1.4792972, -0.8079578)
|
||||
|
||||
[node name="PickableObject4" parent="." unique_id=77451619 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.5082363, 1.4639391, -0.92794985)
|
||||
|
||||
[node name="Hamburger2" parent="." unique_id=1166243968 instance=ExtResource("12_yaehf")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.63934994, 1.5329368, -1.7388973)
|
||||
|
||||
[node name="PickableObject5" parent="." unique_id=880462791 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3030349, 1.4792972, -0.8079578)
|
||||
|
||||
[node name="PickableObject6" parent="." unique_id=1001033415 instance=ExtResource("13_074og")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.289633, 1.4639391, -0.92794985)
|
||||
|
||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("4_7mycd")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.77353764, 0.8777853, -1.5049882)
|
||||
|
||||
[node name="Counter2" parent="." unique_id=782427350 instance=ExtResource("4_7mycd")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7615445, 0.8777853, -0.47819337)
|
||||
|
||||
[node name="Counter3" parent="." unique_id=601017957 instance=ExtResource("4_7mycd")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7615445, 0.8777853, 0.54801154)
|
||||
|
||||
[node name="Plate2" parent="." unique_id=640350345 instance=ExtResource("9_kek77")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.0154996, 1.4458435, -0.0048529953)
|
||||
|
||||
[node name="Plate3" parent="." unique_id=632801239 instance=ExtResource("9_kek77")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5449673, 1.4458435, -0.0015302896)
|
||||
|
||||
@@ -19,9 +19,12 @@ config/icon="res://icon.svg"
|
||||
|
||||
XRToolsUserSettings="*uid://bqgb8i74tm0t"
|
||||
XRToolsRumbleManager="*uid://by853dk86g1qw"
|
||||
NetworkManager="*res://Net/network_manager.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="expand"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user