diff --git a/Containers/container.gd b/Containers/container.gd index 6f9c462..e31bc35 100644 --- a/Containers/container.gd +++ b/Containers/container.gd @@ -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 diff --git a/Containers/plate.tscn b/Containers/plate.tscn index 131c757..bcfefe2 100644 --- a/Containers/plate.tscn +++ b/Containers/plate.tscn @@ -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")] diff --git a/Items/Charcoal.tscn b/Items/Charcoal.tscn index 1444a75..f37c52a 100644 --- a/Items/Charcoal.tscn +++ b/Items/Charcoal.tscn @@ -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") diff --git a/Items/PickupCube.tscn b/Items/PickupCube.tscn index 0ee4783..3e22def 100644 --- a/Items/PickupCube.tscn +++ b/Items/PickupCube.tscn @@ -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") diff --git a/Items/burger.tscn b/Items/burger.tscn index 32b1e23..a71249f 100644 --- a/Items/burger.tscn +++ b/Items/burger.tscn @@ -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") diff --git a/Net/join_panel.gd b/Net/join_panel.gd new file mode 100644 index 0000000..016da2a --- /dev/null +++ b/Net/join_panel.gd @@ -0,0 +1,48 @@ +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" + +@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 diff --git a/Net/join_panel.gd.uid b/Net/join_panel.gd.uid new file mode 100644 index 0000000..b9ba103 --- /dev/null +++ b/Net/join_panel.gd.uid @@ -0,0 +1 @@ +uid://dvivyrspjcjgm diff --git a/Net/join_panel.tscn b/Net/join_panel.tscn new file mode 100644 index 0000000..b3f2129 --- /dev/null +++ b/Net/join_panel.tscn @@ -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" diff --git a/Net/network_manager.gd b/Net/network_manager.gd new file mode 100644 index 0000000..f49ddf6 --- /dev/null +++ b/Net/network_manager.gd @@ -0,0 +1,269 @@ +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: + if multiplayer.multiplayer_peer: + multiplayer.multiplayer_peer.close() + multiplayer.multiplayer_peer = null + log_line("Session ended") + session_ended.emit() + + +# --- 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 requests authority over an item it just grabbed. Runs on the server. +@rpc("any_peer", "reliable") +func request_item_authority(item_path: NodePath) -> void: + if not is_server(): + return + var sender := multiplayer.get_remote_sender_id() + _set_item_authority.rpc(item_path, sender) + + +## A client releases an item, forwarding its throw velocity so the server can +## resume simulating it. Runs on the server. +@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 + + +# 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 + + +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") + multiplayer.multiplayer_peer = null + connection_failed.emit() + +func _on_server_disconnected() -> void: + log_line("server_disconnected") + multiplayer.multiplayer_peer = null + 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 line := "[NET %d] %s" % [multiplayer.get_unique_id() if multiplayer.multiplayer_peer else 0, s] + print(line) + if _log_file: + _log_file.store_line(line) + _log_file.flush() diff --git a/Net/network_manager.gd.uid b/Net/network_manager.gd.uid new file mode 100644 index 0000000..eb73e4b --- /dev/null +++ b/Net/network_manager.gd.uid @@ -0,0 +1 @@ +uid://deefoory0vqmm diff --git a/Net/network_menu.gd b/Net/network_menu.gd new file mode 100644 index 0000000..81a5dd5 --- /dev/null +++ b/Net/network_menu.gd @@ -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) diff --git a/Net/network_menu.gd.uid b/Net/network_menu.gd.uid new file mode 100644 index 0000000..f27a267 --- /dev/null +++ b/Net/network_menu.gd.uid @@ -0,0 +1 @@ +uid://1o2nca75po1f diff --git a/Net/network_menu.tscn b/Net/network_menu.tscn new file mode 100644 index 0000000..0675a82 --- /dev/null +++ b/Net/network_menu.tscn @@ -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) diff --git a/Player/Player.tscn b/Player/Player.tscn new file mode 100644 index 0000000..e31ddcb --- /dev/null +++ b/Player/Player.tscn @@ -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") diff --git a/Player/player.gd b/Player/player.gd new file mode 100644 index 0000000..ef5f69e --- /dev/null +++ b/Player/player.gd @@ -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 diff --git a/Player/player.gd.uid b/Player/player.gd.uid new file mode 100644 index 0000000..5c798d2 --- /dev/null +++ b/Player/player.gd.uid @@ -0,0 +1 @@ +uid://ndrfa2xle033 diff --git a/Prefabs/combine_zone.gd b/Prefabs/combine_zone.gd index 6dd1543..c06bb70 100644 --- a/Prefabs/combine_zone.gd +++ b/Prefabs/combine_zone.gd @@ -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]. diff --git a/Prefabs/net_pickable.gd b/Prefabs/net_pickable.gd new file mode 100644 index 0000000..c1e11fb --- /dev/null +++ b/Prefabs/net_pickable.gd @@ -0,0 +1,64 @@ +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() + + +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) diff --git a/Prefabs/net_pickable.gd.uid b/Prefabs/net_pickable.gd.uid new file mode 100644 index 0000000..51a0d47 --- /dev/null +++ b/Prefabs/net_pickable.gd.uid @@ -0,0 +1 @@ +uid://6n784li8n5ck diff --git a/Stations/Hob.tscn b/Stations/Hob.tscn index 6f63fba..f59a348 100644 --- a/Stations/Hob.tscn +++ b/Stations/Hob.tscn @@ -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") @@ -40,3 +45,6 @@ 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") diff --git a/Stations/hob.gd b/Stations/hob.gd index c1467d4..f40fff5 100644 --- a/Stations/hob.gd +++ b/Stations/hob.gd @@ -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 diff --git a/Stations/item_dispenser.gd b/Stations/item_dispenser.gd index 22794eb..80e98e8 100644 --- a/Stations/item_dispenser.gd +++ b/Stations/item_dispenser.gd @@ -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) diff --git a/TODO.md b/TODO.md index a307ff3..c77e3c3 100644 --- a/TODO.md +++ b/TODO.md @@ -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` \ No newline at end of file +- Plates have a property `is_dirty` they get after a customer clears the plate, and is disabled after being processed in `Sink` diff --git a/XROrigin.tscn b/XROrigin.tscn index 3a1ee5a..99968e7 100644 --- a/XROrigin.tscn +++ b/XROrigin.tscn @@ -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 diff --git a/kanban_tasks_data.kanban b/kanban_tasks_data.kanban index 3a614ff..2810657 100644 --- a/kanban_tasks_data.kanban +++ b/kanban_tasks_data.kanban @@ -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": [ { diff --git a/main.gd b/main.gd index 287efec..274be2e 100644 --- a/main.gd +++ b/main.gd @@ -1,12 +1,109 @@ 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) + _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() + + +func _on_connection_failed() -> void: + _menu.set_status("Connection failed — check the IP") + _spawn_player(1) # restore embodiment + _menu.show_menu() + + +# --- 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))) diff --git a/main.tscn b/main.tscn index 88dac01..e41de7b 100644 --- a/main.tscn +++ b/main.tscn @@ -1,14 +1,13 @@ [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://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="9_kek77"] +[ext_resource type="PackedScene" path="res://Net/network_menu.tscn" id="10_menu"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] size = Vector3(5, 0.1, 5) @@ -30,8 +29,15 @@ sdfgi_enabled = true [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="."] +spawn_path = NodePath("..") +_spawnable_scenes = PackedStringArray("res://Player/Player.tscn") + +[node name="ItemsSpawner" type="MultiplayerSpawner" parent="."] +spawn_path = NodePath("..") + +[node name="NetworkMenu" parent="." 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) @@ -52,12 +58,6 @@ 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="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") diff --git a/project.godot b/project.godot index b1ff58b..bb780b6 100644 --- a/project.godot +++ b/project.godot @@ -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"