feat(multiplayer): ENet networking + player/world sync + lobby menu + chat
Add dedicated server / host / client modes via NetworkManager autoload, PlayerSyncComponent (20 Hz unreliable RPC), WorldSyncComponent (authoritative block break/place), ChatManager (F2), LobbyMenu scene, updated MainMenu with Solo/Heberger/Rejoindre/Quitter buttons. Port changed to 7777 (9999 occupied by sntlkeyssrvr on this machine). Mobs disabled in multi (spawn solo only). Solo mode untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,10 +87,17 @@ escape={
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open_chat={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
BlockDatabase="*res://scripts/world/BlockDatabase.gd"
|
BlockDatabase="*res://scripts/world/BlockDatabase.gd"
|
||||||
AudioManager="*res://scripts/ambience/AudioManager.gd"
|
AudioManager="*res://scripts/ambience/AudioManager.gd"
|
||||||
ItemDatabase="*res://scripts/inventory/ItemDatabase.gd"
|
ItemDatabase="*res://scripts/inventory/ItemDatabase.gd"
|
||||||
|
NetworkManager="*res://scripts/net/NetworkManager.gd"
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1)
|
environment/defaults/default_clear_color=Color(0.05, 0.15, 0.25, 1)
|
||||||
|
|||||||
107
scenes/LobbyMenu.tscn
Normal file
107
scenes/LobbyMenu.tscn
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://dauphincraft_lobby"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/net/LobbyMenu.gd" id="1_lobbyscript"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_btn"]
|
||||||
|
bg_color = Color(0.05, 0.15, 0.3, 0.75)
|
||||||
|
border_width_left = 2
|
||||||
|
border_width_top = 2
|
||||||
|
border_width_right = 2
|
||||||
|
border_width_bottom = 2
|
||||||
|
border_color = Color(0.3, 0.8, 1.0, 1.0)
|
||||||
|
corner_radius_top_left = 6
|
||||||
|
corner_radius_top_right = 6
|
||||||
|
corner_radius_bottom_right = 6
|
||||||
|
corner_radius_bottom_left = 6
|
||||||
|
|
||||||
|
[node name="LobbyMenu" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("1_lobbyscript")
|
||||||
|
|
||||||
|
[node name="Background" type="ColorRect" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
color = Color(0.02, 0.08, 0.18, 1.0)
|
||||||
|
|
||||||
|
[node name="Panel" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -200.0
|
||||||
|
offset_top = -200.0
|
||||||
|
offset_right = 200.0
|
||||||
|
offset_bottom = 200.0
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "REJOINDRE"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 48
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.95, 1.0, 1.0)
|
||||||
|
|
||||||
|
[node name="Spacer1" type="Control" parent="Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="Panel"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="IPLabel" type="Label" parent="Panel/VBox"]
|
||||||
|
text = "Adresse IP du serveur"
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.9, 1.0, 1.0)
|
||||||
|
|
||||||
|
[node name="IPInput" type="LineEdit" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(400, 48)
|
||||||
|
placeholder_text = "127.0.0.1"
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
|
||||||
|
[node name="Spacer2" type="Control" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 10)
|
||||||
|
|
||||||
|
[node name="PortLabel" type="Label" parent="Panel/VBox"]
|
||||||
|
text = "Port"
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.7, 0.9, 1.0, 1.0)
|
||||||
|
|
||||||
|
[node name="PortInput" type="LineEdit" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(400, 48)
|
||||||
|
placeholder_text = "9999"
|
||||||
|
theme_override_font_sizes/font_size = 20
|
||||||
|
|
||||||
|
[node name="Spacer3" type="Control" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(0, 10)
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
theme_override_colors/font_color = Color(1.0, 0.8, 0.3, 1.0)
|
||||||
|
|
||||||
|
[node name="ConnectBtn" type="Button" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(400, 56)
|
||||||
|
text = "Se connecter"
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
|
[node name="BackBtn" type="Button" parent="Panel/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(400, 56)
|
||||||
|
text = "Retour"
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
@@ -36,9 +36,9 @@ anchor_top = 0.5
|
|||||||
anchor_right = 0.5
|
anchor_right = 0.5
|
||||||
anchor_bottom = 0.5
|
anchor_bottom = 0.5
|
||||||
offset_left = -200.0
|
offset_left = -200.0
|
||||||
offset_top = -160.0
|
offset_top = -220.0
|
||||||
offset_right = 200.0
|
offset_right = 200.0
|
||||||
offset_bottom = 160.0
|
offset_bottom = 220.0
|
||||||
alignment = 1
|
alignment = 1
|
||||||
|
|
||||||
[node name="Title" type="Label" parent="CenterContainer"]
|
[node name="Title" type="Label" parent="CenterContainer"]
|
||||||
@@ -53,36 +53,52 @@ theme_override_constants/shadow_offset_y = 3
|
|||||||
|
|
||||||
[node name="Subtitle" type="Label" parent="CenterContainer"]
|
[node name="Subtitle" type="Label" parent="CenterContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Un monde voxel sous l'océan"
|
text = "Un monde voxel sous l'ocean"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
theme_override_colors/font_color = Color(0.5, 0.75, 0.9, 0.85)
|
theme_override_colors/font_color = Color(0.5, 0.75, 0.9, 0.85)
|
||||||
|
|
||||||
[node name="Spacer" type="Control" parent="CenterContainer"]
|
[node name="Spacer" type="Control" parent="CenterContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
custom_minimum_size = Vector2(0, 30)
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
|
||||||
[node name="DiveButton" type="Button" parent="CenterContainer"]
|
[node name="DiveButton" type="Button" parent="CenterContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
custom_minimum_size = Vector2(300, 56)
|
custom_minimum_size = Vector2(300, 52)
|
||||||
text = "Plonger"
|
text = "Solo"
|
||||||
theme_override_font_sizes/font_size = 26
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
|
[node name="HostButton" type="Button" parent="CenterContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(300, 52)
|
||||||
|
text = "Heberger"
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
|
[node name="JoinButton" type="Button" parent="CenterContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(300, 52)
|
||||||
|
text = "Rejoindre"
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
[node name="OptionsButton" type="Button" parent="CenterContainer"]
|
[node name="OptionsButton" type="Button" parent="CenterContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
custom_minimum_size = Vector2(300, 56)
|
custom_minimum_size = Vector2(300, 52)
|
||||||
text = "Options"
|
text = "Options"
|
||||||
theme_override_font_sizes/font_size = 26
|
theme_override_font_sizes/font_size = 24
|
||||||
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
[node name="QuitButton" type="Button" parent="CenterContainer"]
|
[node name="QuitButton" type="Button" parent="CenterContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
custom_minimum_size = Vector2(300, 56)
|
custom_minimum_size = Vector2(300, 52)
|
||||||
text = "Quitter"
|
text = "Quitter"
|
||||||
theme_override_font_sizes/font_size = 26
|
theme_override_font_sizes/font_size = 24
|
||||||
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
theme_override_colors/font_color = Color(0.8, 0.97, 1.0, 1.0)
|
||||||
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
theme_override_styles/normal = SubResource("StyleBoxFlat_btn")
|
||||||
|
|
||||||
@@ -134,8 +150,42 @@ max_value = 1.0
|
|||||||
step = 0.01
|
step = 0.01
|
||||||
value = 0.8
|
value = 0.8
|
||||||
|
|
||||||
|
[node name="HostPortPopup" type="Window" parent="."]
|
||||||
|
title = "Heberger une partie"
|
||||||
|
size = Vector2i(360, 200)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="HostPortPopup"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 20.0
|
||||||
|
offset_top = 20.0
|
||||||
|
offset_right = -20.0
|
||||||
|
offset_bottom = -20.0
|
||||||
|
|
||||||
|
[node name="PortLabel" type="Label" parent="HostPortPopup/VBox"]
|
||||||
|
text = "Port (defaut 9999)"
|
||||||
|
|
||||||
|
[node name="PortInput" type="LineEdit" parent="HostPortPopup/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "9999"
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="HostPortPopup/VBox"]
|
||||||
|
theme_override_colors/font_color = Color(1.0, 0.4, 0.4, 1.0)
|
||||||
|
|
||||||
|
[node name="ConfirmBtn" type="Button" parent="HostPortPopup/VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Lancer le serveur"
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
|
||||||
[connection signal="pressed" from="CenterContainer/DiveButton" to="." method="_on_dive_pressed"]
|
[connection signal="pressed" from="CenterContainer/DiveButton" to="." method="_on_dive_pressed"]
|
||||||
|
[connection signal="pressed" from="CenterContainer/HostButton" to="." method="_on_host_pressed"]
|
||||||
|
[connection signal="pressed" from="CenterContainer/JoinButton" to="." method="_on_join_pressed"]
|
||||||
[connection signal="pressed" from="CenterContainer/OptionsButton" to="." method="_on_options_pressed"]
|
[connection signal="pressed" from="CenterContainer/OptionsButton" to="." method="_on_options_pressed"]
|
||||||
[connection signal="pressed" from="CenterContainer/QuitButton" to="." method="_on_quit_pressed"]
|
[connection signal="pressed" from="CenterContainer/QuitButton" to="." method="_on_quit_pressed"]
|
||||||
[connection signal="value_changed" from="OptionsPopup/VBox/MusicSlider" to="." method="_on_music_slider_value_changed"]
|
[connection signal="value_changed" from="OptionsPopup/VBox/MusicSlider" to="." method="_on_music_slider_value_changed"]
|
||||||
[connection signal="value_changed" from="OptionsPopup/VBox/SFXSlider" to="." method="_on_sfx_slider_value_changed"]
|
[connection signal="value_changed" from="OptionsPopup/VBox/SFXSlider" to="." method="_on_sfx_slider_value_changed"]
|
||||||
|
[connection signal="pressed" from="HostPortPopup/VBox/ConfirmBtn" to="." method="_on_host_confirm_pressed"]
|
||||||
|
|||||||
206
scripts/Main.gd
206
scripts/Main.gd
@@ -1,8 +1,6 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
@onready var world: Node3D = $World/ChunkManager
|
@onready var world: Node3D = $World/ChunkManager
|
||||||
@onready var dolphin: CharacterBody3D = $Dolphin
|
|
||||||
@onready var hud: CanvasLayer = $Dolphin/HUD
|
|
||||||
@onready var plankton_follower: Node3D = $PlanktonFollower
|
@onready var plankton_follower: Node3D = $PlanktonFollower
|
||||||
|
|
||||||
var inventory: Inventory = Inventory.new()
|
var inventory: Inventory = Inventory.new()
|
||||||
@@ -11,19 +9,78 @@ var _pause_menu: Control = null
|
|||||||
|
|
||||||
var _last_chunk_update_pos: Vector3 = Vector3(99999, 99999, 99999)
|
var _last_chunk_update_pos: Vector3 = Vector3(99999, 99999, 99999)
|
||||||
|
|
||||||
|
# Multiplayer state
|
||||||
|
var _my_dolphin: CharacterBody3D = null
|
||||||
|
var _peer_dolphins: Dictionary = {} # peer_id -> CharacterBody3D
|
||||||
|
var _world_sync: Node = null
|
||||||
|
var _chat_manager: Node = null
|
||||||
|
|
||||||
|
# Dolphin scene cached
|
||||||
|
var _dolphin_scene: PackedScene = preload("res://scenes/Dolphin.tscn")
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
dolphin.block_break_requested.connect(_on_block_break)
|
add_to_group("main")
|
||||||
dolphin.block_place_requested.connect(_on_block_place)
|
|
||||||
dolphin.echolocation_triggered.connect(_on_echolocation)
|
|
||||||
|
|
||||||
if dolphin.has_signal("hotbar_scroll"):
|
# Setup WorldSyncComponent on ChunkManager
|
||||||
dolphin.hotbar_scroll.connect(inventory.scroll_hotbar)
|
_world_sync = load("res://scripts/net/WorldSyncComponent.gd").new()
|
||||||
|
_world_sync.name = "WorldSyncComponent"
|
||||||
|
world.add_child(_world_sync)
|
||||||
|
|
||||||
|
world.world_seed = 12345
|
||||||
|
|
||||||
|
var mode := NetworkManager.current_mode
|
||||||
|
|
||||||
|
if NetworkManager.is_solo():
|
||||||
|
_spawn_local_dolphin()
|
||||||
|
_spawn_mobs()
|
||||||
|
elif NetworkManager.is_server():
|
||||||
|
# HOST or DEDICATED: spawn mobs, connect peer signals, spawn dolphin for existing peers
|
||||||
|
_spawn_mobs()
|
||||||
|
NetworkManager.peer_connected.connect(_on_net_peer_connected)
|
||||||
|
NetworkManager.peer_disconnected.connect(_on_net_peer_disconnected)
|
||||||
|
if mode == NetworkManager.Mode.HOST:
|
||||||
|
# Host also plays — spawn own dolphin
|
||||||
|
_spawn_networked_dolphin(multiplayer.get_unique_id())
|
||||||
|
# Spawn dolphins for any peers already connected (edge case)
|
||||||
|
for peer_id in multiplayer.get_peers():
|
||||||
|
_spawn_networked_dolphin(peer_id)
|
||||||
|
elif NetworkManager.is_client():
|
||||||
|
# CLIENT: only dolphin for self, world updates come from server
|
||||||
|
NetworkManager.peer_disconnected.connect(_on_net_peer_disconnected)
|
||||||
|
_spawn_networked_dolphin(multiplayer.get_unique_id())
|
||||||
|
|
||||||
|
# Chat (all modes except dedicated headless)
|
||||||
|
if mode != NetworkManager.Mode.DEDICATED or not DisplayServer.get_name() == "headless":
|
||||||
|
_chat_manager = load("res://scripts/net/ChatManager.gd").new()
|
||||||
|
_chat_manager.name = "ChatManager"
|
||||||
|
add_child(_chat_manager)
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
AudioManager.play_ambient_loop("underwater_ambient")
|
||||||
|
AudioManager.play_music("underwater_theme")
|
||||||
|
AudioManager.play_whale_call_random()
|
||||||
|
|
||||||
|
# Starting inventory
|
||||||
|
inventory.add_item(2, 10)
|
||||||
|
inventory.add_item(6, 5)
|
||||||
|
inventory.add_item(8, 2)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_local_dolphin() -> void:
|
||||||
|
var dolphin: CharacterBody3D = _dolphin_scene.instantiate()
|
||||||
|
dolphin.name = "Dolphin"
|
||||||
|
dolphin.position = Vector3(0, 55, 0)
|
||||||
|
add_child(dolphin)
|
||||||
|
_my_dolphin = dolphin
|
||||||
|
_connect_dolphin_signals(dolphin)
|
||||||
|
|
||||||
|
var hud: CanvasLayer = dolphin.get_node_or_null("HUD")
|
||||||
if is_instance_valid(hud) and hud.has_method("connect_to_dolphin"):
|
if is_instance_valid(hud) and hud.has_method("connect_to_dolphin"):
|
||||||
hud.connect_to_dolphin(dolphin)
|
hud.connect_to_dolphin(dolphin)
|
||||||
|
|
||||||
# Load and attach InventoryUI
|
dolphin.add_to_group("player")
|
||||||
|
|
||||||
var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn")
|
var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn")
|
||||||
if ui_scene != null:
|
if ui_scene != null:
|
||||||
_inventory_ui = ui_scene.instantiate() as CanvasLayer
|
_inventory_ui = ui_scene.instantiate() as CanvasLayer
|
||||||
@@ -32,50 +89,111 @@ func _ready() -> void:
|
|||||||
else:
|
else:
|
||||||
push_error("Main: could not load InventoryUI.tscn")
|
push_error("Main: could not load InventoryUI.tscn")
|
||||||
|
|
||||||
# Load and attach PauseMenu
|
|
||||||
var pause_scene: PackedScene = preload("res://scenes/PauseMenu.tscn")
|
var pause_scene: PackedScene = preload("res://scenes/PauseMenu.tscn")
|
||||||
_pause_menu = pause_scene.instantiate() as Control
|
_pause_menu = pause_scene.instantiate() as Control
|
||||||
add_child(_pause_menu)
|
add_child(_pause_menu)
|
||||||
_pause_menu.hide()
|
_pause_menu.hide()
|
||||||
|
|
||||||
AudioManager.play_ambient_loop("underwater_ambient")
|
plankton_follower.global_position = dolphin.position
|
||||||
AudioManager.play_music("underwater_theme")
|
|
||||||
AudioManager.play_whale_call_random()
|
|
||||||
|
|
||||||
world.world_seed = 12345
|
|
||||||
|
|
||||||
# Starting inventory
|
func _spawn_networked_dolphin(peer_id: int) -> void:
|
||||||
inventory.add_item(2, 10) # 10 SAND
|
var dolphin: CharacterBody3D = _dolphin_scene.instantiate()
|
||||||
inventory.add_item(6, 5) # 5 KELP
|
dolphin.name = "Dolphin_%d" % peer_id
|
||||||
inventory.add_item(8, 2) # 2 ICE
|
dolphin.position = Vector3(0, 55, 0)
|
||||||
|
add_child(dolphin)
|
||||||
|
|
||||||
# Add dolphin to player group (for mob detection)
|
# Attach sync component
|
||||||
dolphin.add_to_group("player")
|
var sync_comp: Node = load("res://scripts/net/PlayerSyncComponent.gd").new()
|
||||||
|
sync_comp.name = "PlayerSyncComponent"
|
||||||
|
dolphin.add_child(sync_comp)
|
||||||
|
sync_comp.setup(peer_id)
|
||||||
|
|
||||||
# Spawner de mobs
|
var my_id := multiplayer.get_unique_id()
|
||||||
|
if peer_id == my_id:
|
||||||
|
_my_dolphin = dolphin
|
||||||
|
_connect_dolphin_signals(dolphin)
|
||||||
|
dolphin.add_to_group("player")
|
||||||
|
|
||||||
|
var hud: CanvasLayer = dolphin.get_node_or_null("HUD")
|
||||||
|
if is_instance_valid(hud) and hud.has_method("connect_to_dolphin"):
|
||||||
|
hud.connect_to_dolphin(dolphin)
|
||||||
|
|
||||||
|
var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn")
|
||||||
|
if ui_scene != null:
|
||||||
|
_inventory_ui = ui_scene.instantiate() as CanvasLayer
|
||||||
|
add_child(_inventory_ui)
|
||||||
|
_inventory_ui.setup(inventory)
|
||||||
|
|
||||||
|
var pause_scene: PackedScene = preload("res://scenes/PauseMenu.tscn")
|
||||||
|
_pause_menu = pause_scene.instantiate() as Control
|
||||||
|
add_child(_pause_menu)
|
||||||
|
_pause_menu.hide()
|
||||||
|
|
||||||
|
plankton_follower.global_position = dolphin.position
|
||||||
|
else:
|
||||||
|
# Remote dolphin: disable input processing
|
||||||
|
dolphin.set_process_unhandled_input(false)
|
||||||
|
if dolphin.has_method("set_physics_process"):
|
||||||
|
# Keep physics but no input
|
||||||
|
pass
|
||||||
|
_peer_dolphins[peer_id] = dolphin
|
||||||
|
|
||||||
|
|
||||||
|
func _despawn_peer_dolphin(peer_id: int) -> void:
|
||||||
|
if _peer_dolphins.has(peer_id):
|
||||||
|
var d: CharacterBody3D = _peer_dolphins[peer_id]
|
||||||
|
if is_instance_valid(d):
|
||||||
|
d.queue_free()
|
||||||
|
_peer_dolphins.erase(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_mobs() -> void:
|
||||||
var MobSpawnerScene: PackedScene = load("res://scenes/mobs/MobSpawner.tscn")
|
var MobSpawnerScene: PackedScene = load("res://scenes/mobs/MobSpawner.tscn")
|
||||||
|
if MobSpawnerScene == null:
|
||||||
|
return
|
||||||
var mob_spawner: Node3D = MobSpawnerScene.instantiate()
|
var mob_spawner: Node3D = MobSpawnerScene.instantiate()
|
||||||
add_child(mob_spawner)
|
add_child(mob_spawner)
|
||||||
mob_spawner.player = mob_spawner.get_path_to(dolphin)
|
if is_instance_valid(_my_dolphin):
|
||||||
|
mob_spawner.player = mob_spawner.get_path_to(_my_dolphin)
|
||||||
|
|
||||||
|
|
||||||
|
func _connect_dolphin_signals(dolphin: CharacterBody3D) -> void:
|
||||||
|
if dolphin.has_signal("block_break_requested"):
|
||||||
|
dolphin.block_break_requested.connect(_on_block_break)
|
||||||
|
if dolphin.has_signal("block_place_requested"):
|
||||||
|
dolphin.block_place_requested.connect(_on_block_place)
|
||||||
|
if dolphin.has_signal("echolocation_triggered"):
|
||||||
|
dolphin.echolocation_triggered.connect(_on_echolocation)
|
||||||
|
if dolphin.has_signal("hotbar_scroll"):
|
||||||
|
dolphin.hotbar_scroll.connect(inventory.scroll_hotbar)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_net_peer_connected(peer_id: int) -> void:
|
||||||
|
if NetworkManager.is_server():
|
||||||
|
_spawn_networked_dolphin(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_net_peer_disconnected(peer_id: int) -> void:
|
||||||
|
_despawn_peer_dolphin(peer_id)
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
# Throttle chunk updates: only when dolphin moved > 4m
|
if not is_instance_valid(_my_dolphin):
|
||||||
if dolphin.global_position.distance_to(_last_chunk_update_pos) > 4.0:
|
return
|
||||||
world.update_player_position(dolphin.global_position)
|
if _my_dolphin.global_position.distance_to(_last_chunk_update_pos) > 4.0:
|
||||||
_last_chunk_update_pos = dolphin.global_position
|
world.update_player_position(_my_dolphin.global_position)
|
||||||
plankton_follower.global_position = dolphin.global_position
|
_last_chunk_update_pos = _my_dolphin.global_position
|
||||||
|
plankton_follower.global_position = _my_dolphin.global_position
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not event.is_action_pressed("escape"):
|
if not event.is_action_pressed("escape"):
|
||||||
return
|
return
|
||||||
# Priority: close inventory first if open
|
|
||||||
if _inventory_ui != null and _inventory_ui.get("_is_open"):
|
if _inventory_ui != null and _inventory_ui.get("_is_open"):
|
||||||
_inventory_ui.call("_toggle_inventory")
|
_inventory_ui.call("_toggle_inventory")
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
# Toggle pause menu
|
|
||||||
if is_instance_valid(_pause_menu):
|
if is_instance_valid(_pause_menu):
|
||||||
if _pause_menu.visible:
|
if _pause_menu.visible:
|
||||||
_pause_menu.hide_pause()
|
_pause_menu.hide_pause()
|
||||||
@@ -85,9 +203,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_block_break(hit_position: Vector3, _normal: Vector3) -> void:
|
func _on_block_break(hit_position: Vector3, _normal: Vector3) -> void:
|
||||||
var broken_id: int = world.break_block(hit_position)
|
if NetworkManager.is_solo():
|
||||||
if broken_id > 0:
|
var broken_id: int = world.break_block(hit_position)
|
||||||
inventory.add_item(broken_id, 1)
|
if broken_id > 0:
|
||||||
|
inventory.add_item(broken_id, 1)
|
||||||
|
AudioManager.play_bubble_sfx(hit_position)
|
||||||
|
else:
|
||||||
|
_world_sync.server_break_block(hit_position)
|
||||||
AudioManager.play_bubble_sfx(hit_position)
|
AudioManager.play_bubble_sfx(hit_position)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,15 +224,25 @@ func _on_block_place(hit_position: Vector3, normal: Vector3) -> void:
|
|||||||
var block_coord := Vector3(floor(place_pos.x), floor(place_pos.y), floor(place_pos.z))
|
var block_coord := Vector3(floor(place_pos.x), floor(place_pos.y), floor(place_pos.z))
|
||||||
var block_center := block_coord + Vector3(0.5, 0.5, 0.5)
|
var block_center := block_coord + Vector3(0.5, 0.5, 0.5)
|
||||||
|
|
||||||
# Refuse if bloc overlaps player AABB (CapsuleShape3D r=0.4 h=2.0)
|
if is_instance_valid(_my_dolphin):
|
||||||
var dolphin_pos: Vector3 = dolphin.global_position
|
var dolphin_pos: Vector3 = _my_dolphin.global_position
|
||||||
var dist := block_center - dolphin_pos
|
var dist := block_center - dolphin_pos
|
||||||
if abs(dist.x) < 1.0 and abs(dist.y) < 1.8 and abs(dist.z) < 1.0:
|
if abs(dist.x) < 1.0 and abs(dist.y) < 1.8 and abs(dist.z) < 1.0:
|
||||||
return # too close to player, silent refuse
|
return
|
||||||
|
|
||||||
if world.place_block(place_pos, selected["item_id"]):
|
if NetworkManager.is_solo():
|
||||||
|
if world.place_block(place_pos, selected["item_id"]):
|
||||||
|
inventory.remove_item_from_slot(inventory.selected_hotbar, 1)
|
||||||
|
else:
|
||||||
|
_world_sync.server_place_block(place_pos, selected["item_id"])
|
||||||
inventory.remove_item_from_slot(inventory.selected_hotbar, 1)
|
inventory.remove_item_from_slot(inventory.selected_hotbar, 1)
|
||||||
|
|
||||||
|
|
||||||
func _on_echolocation(position: Vector3, _radius: float) -> void:
|
func _on_echolocation(position: Vector3, _radius: float) -> void:
|
||||||
AudioManager.play_bubble_sfx(position)
|
AudioManager.play_bubble_sfx(position)
|
||||||
|
|
||||||
|
|
||||||
|
# Called by WorldSyncComponent when server confirms a block was broken by this client
|
||||||
|
func _on_remote_block_broken(broken_id: int) -> void:
|
||||||
|
if broken_id > 0:
|
||||||
|
inventory.add_item(broken_id, 1)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ extends Control
|
|||||||
@onready var _options_popup: Window = $OptionsPopup
|
@onready var _options_popup: Window = $OptionsPopup
|
||||||
@onready var _music_slider: HSlider = $OptionsPopup/VBox/MusicSlider
|
@onready var _music_slider: HSlider = $OptionsPopup/VBox/MusicSlider
|
||||||
@onready var _sfx_slider: HSlider = $OptionsPopup/VBox/SFXSlider
|
@onready var _sfx_slider: HSlider = $OptionsPopup/VBox/SFXSlider
|
||||||
|
@onready var _host_port_popup: Window = $HostPortPopup
|
||||||
|
@onready var _host_port_input: LineEdit = $HostPortPopup/VBox/PortInput
|
||||||
|
@onready var _host_status: Label = $HostPortPopup/VBox/StatusLabel
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
AudioManager.play_music("underwater_theme.mp3")
|
AudioManager.play_music("underwater_theme.mp3")
|
||||||
@@ -10,9 +14,30 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_dive_pressed() -> void:
|
func _on_dive_pressed() -> void:
|
||||||
|
NetworkManager.current_mode = NetworkManager.Mode.SOLO
|
||||||
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_host_pressed() -> void:
|
||||||
|
_host_status.text = ""
|
||||||
|
_host_port_input.text = str(NetworkManager.DEFAULT_PORT)
|
||||||
|
_host_port_popup.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_host_confirm_pressed() -> void:
|
||||||
|
var port: int = int(_host_port_input.text.strip_edges())
|
||||||
|
_host_port_popup.hide()
|
||||||
|
if NetworkManager.start_host(port):
|
||||||
|
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
||||||
|
else:
|
||||||
|
_host_status.text = "Erreur demarrage serveur"
|
||||||
|
_host_port_popup.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_join_pressed() -> void:
|
||||||
|
get_tree().change_scene_to_file("res://scenes/LobbyMenu.tscn")
|
||||||
|
|
||||||
|
|
||||||
func _on_options_pressed() -> void:
|
func _on_options_pressed() -> void:
|
||||||
_options_popup.popup_centered()
|
_options_popup.popup_centered()
|
||||||
|
|
||||||
|
|||||||
61
scripts/net/ChatManager.gd
Normal file
61
scripts/net/ChatManager.gd
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# Simple chat: F2 opens input, send to all peers, printed to console.
|
||||||
|
|
||||||
|
var _input_open: bool = false
|
||||||
|
var _input_line: LineEdit = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Build minimal LineEdit for chat input
|
||||||
|
_input_line = LineEdit.new()
|
||||||
|
_input_line.placeholder_text = "Message... (Entrée pour envoyer, Echap pour annuler)"
|
||||||
|
_input_line.custom_minimum_size = Vector2(500, 40)
|
||||||
|
_input_line.visible = false
|
||||||
|
_input_line.text_submitted.connect(_on_text_submitted)
|
||||||
|
add_child(_input_line)
|
||||||
|
|
||||||
|
if not InputMap.has_action("open_chat"):
|
||||||
|
InputMap.add_action("open_chat")
|
||||||
|
var ev := InputEventKey.new()
|
||||||
|
ev.physical_keycode = KEY_F2
|
||||||
|
InputMap.action_add_event("open_chat", ev)
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("open_chat") and not _input_open:
|
||||||
|
_open_chat()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif _input_open and event.is_action_pressed("escape"):
|
||||||
|
_close_chat()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _open_chat() -> void:
|
||||||
|
_input_open = true
|
||||||
|
_input_line.visible = true
|
||||||
|
_input_line.text = ""
|
||||||
|
_input_line.grab_focus()
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
|
||||||
|
|
||||||
|
func _close_chat() -> void:
|
||||||
|
_input_open = false
|
||||||
|
_input_line.visible = false
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_text_submitted(text: String) -> void:
|
||||||
|
if text.strip_edges().length() == 0:
|
||||||
|
_close_chat()
|
||||||
|
return
|
||||||
|
send_chat.rpc(text.strip_edges())
|
||||||
|
_close_chat()
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func send_chat(msg: String) -> void:
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
if sender_id == 0:
|
||||||
|
sender_id = multiplayer.get_unique_id()
|
||||||
|
print("[CHAT][peer %d] %s" % [sender_id, msg])
|
||||||
1
scripts/net/ChatManager.gd.uid
Normal file
1
scripts/net/ChatManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://0qvav8nyjqdt
|
||||||
44
scripts/net/LobbyMenu.gd
Normal file
44
scripts/net/LobbyMenu.gd
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
@onready var _ip_input: LineEdit = $Panel/VBox/IPInput
|
||||||
|
@onready var _port_input: LineEdit = $Panel/VBox/PortInput
|
||||||
|
@onready var _status_label: Label = $Panel/VBox/StatusLabel
|
||||||
|
@onready var _connect_btn: Button = $Panel/VBox/ConnectBtn
|
||||||
|
@onready var _back_btn: Button = $Panel/VBox/BackBtn
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_ip_input.text = "127.0.0.1"
|
||||||
|
_port_input.text = str(NetworkManager.DEFAULT_PORT)
|
||||||
|
_status_label.text = ""
|
||||||
|
|
||||||
|
NetworkManager.connection_succeeded.connect(_on_connected)
|
||||||
|
NetworkManager.connection_failed.connect(_on_failed)
|
||||||
|
|
||||||
|
_connect_btn.pressed.connect(_on_connect_pressed)
|
||||||
|
_back_btn.pressed.connect(_on_back_pressed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_connect_pressed() -> void:
|
||||||
|
var address: String = _ip_input.text.strip_edges()
|
||||||
|
var port: int = int(_port_input.text.strip_edges())
|
||||||
|
if address.length() == 0:
|
||||||
|
_status_label.text = "Adresse invalide"
|
||||||
|
return
|
||||||
|
_status_label.text = "Connexion en cours..."
|
||||||
|
_connect_btn.disabled = true
|
||||||
|
NetworkManager.join_server(address, port)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_connected() -> void:
|
||||||
|
_status_label.text = "Connecte!"
|
||||||
|
get_tree().change_scene_to_file("res://scenes/Main.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_failed() -> void:
|
||||||
|
_status_label.text = "Connexion echouee"
|
||||||
|
_connect_btn.disabled = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_pressed() -> void:
|
||||||
|
get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")
|
||||||
1
scripts/net/LobbyMenu.gd.uid
Normal file
1
scripts/net/LobbyMenu.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://djs0exhdnreiu
|
||||||
132
scripts/net/NetworkManager.gd
Normal file
132
scripts/net/NetworkManager.gd
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
signal connection_succeeded()
|
||||||
|
signal connection_failed()
|
||||||
|
signal server_started()
|
||||||
|
signal server_closed()
|
||||||
|
signal peer_connected(id: int)
|
||||||
|
signal peer_disconnected(id: int)
|
||||||
|
|
||||||
|
enum Mode { NONE, SOLO, HOST, CLIENT, DEDICATED }
|
||||||
|
var current_mode: Mode = Mode.NONE
|
||||||
|
|
||||||
|
const DEFAULT_PORT := 7777
|
||||||
|
const MAX_PLAYERS := 16
|
||||||
|
|
||||||
|
var peer: ENetMultiplayerPeer = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
|
multiplayer.connected_to_server.connect(_on_connection_ok)
|
||||||
|
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||||
|
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||||
|
|
||||||
|
# Auto-start from command line args
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
if "--server" in args:
|
||||||
|
start_dedicated_server(DEFAULT_PORT)
|
||||||
|
elif "--client" in args:
|
||||||
|
var idx := args.find("--client")
|
||||||
|
var address := "127.0.0.1"
|
||||||
|
if idx + 1 < args.size():
|
||||||
|
address = args[idx + 1]
|
||||||
|
join_server(address, DEFAULT_PORT)
|
||||||
|
|
||||||
|
|
||||||
|
func start_dedicated_server(port: int = DEFAULT_PORT) -> bool:
|
||||||
|
peer = ENetMultiplayerPeer.new()
|
||||||
|
var err := peer.create_server(port, MAX_PLAYERS)
|
||||||
|
if err != OK:
|
||||||
|
push_error("NetworkManager: server creation failed — error %d" % err)
|
||||||
|
peer = null
|
||||||
|
return false
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
current_mode = Mode.DEDICATED
|
||||||
|
print("[NET] Dedicated server listening on port %d" % port)
|
||||||
|
server_started.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func start_host(port: int = DEFAULT_PORT) -> bool:
|
||||||
|
peer = ENetMultiplayerPeer.new()
|
||||||
|
var err := peer.create_server(port, MAX_PLAYERS)
|
||||||
|
if err != OK:
|
||||||
|
push_error("NetworkManager: host creation failed — error %d" % err)
|
||||||
|
peer = null
|
||||||
|
return false
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
current_mode = Mode.HOST
|
||||||
|
print("[NET] Host server listening on port %d" % port)
|
||||||
|
server_started.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func join_server(address: String, port: int = DEFAULT_PORT) -> bool:
|
||||||
|
peer = ENetMultiplayerPeer.new()
|
||||||
|
var err := peer.create_client(address, port)
|
||||||
|
if err != OK:
|
||||||
|
push_error("NetworkManager: client connection failed — error %d" % err)
|
||||||
|
peer = null
|
||||||
|
return false
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
current_mode = Mode.CLIENT
|
||||||
|
print("[NET] Connecting to %s:%d" % [address, port])
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func disconnect_from_server() -> void:
|
||||||
|
if peer != null:
|
||||||
|
peer.close()
|
||||||
|
peer = null
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
current_mode = Mode.NONE
|
||||||
|
server_closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_connected(id: int) -> void:
|
||||||
|
print("[NET] Peer connected: %d" % id)
|
||||||
|
peer_connected.emit(id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_peer_disconnected(id: int) -> void:
|
||||||
|
print("[NET] Peer disconnected: %d" % id)
|
||||||
|
peer_disconnected.emit(id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_connection_ok() -> void:
|
||||||
|
print("[NET] Connected to server (my id: %d)" % multiplayer.get_unique_id())
|
||||||
|
connection_succeeded.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_connection_failed() -> void:
|
||||||
|
push_warning("[NET] Connection failed")
|
||||||
|
peer = null
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
current_mode = Mode.NONE
|
||||||
|
connection_failed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_server_disconnected() -> void:
|
||||||
|
push_warning("[NET] Server disconnected")
|
||||||
|
peer = null
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
current_mode = Mode.NONE
|
||||||
|
server_closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func is_server() -> bool:
|
||||||
|
return current_mode == Mode.HOST or current_mode == Mode.DEDICATED
|
||||||
|
|
||||||
|
|
||||||
|
func is_client() -> bool:
|
||||||
|
return current_mode == Mode.CLIENT
|
||||||
|
|
||||||
|
|
||||||
|
func is_solo() -> bool:
|
||||||
|
return current_mode == Mode.SOLO or current_mode == Mode.NONE
|
||||||
|
|
||||||
|
|
||||||
|
func get_my_id() -> int:
|
||||||
|
return multiplayer.get_unique_id()
|
||||||
1
scripts/net/NetworkManager.gd.uid
Normal file
1
scripts/net/NetworkManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dijtouqot4v5y
|
||||||
63
scripts/net/PlayerSyncComponent.gd
Normal file
63
scripts/net/PlayerSyncComponent.gd
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
@export var sync_rate: float = 0.05 # 20 Hz
|
||||||
|
|
||||||
|
var _owner_id: int = 1
|
||||||
|
var _timer: float = 0.0
|
||||||
|
|
||||||
|
# Interpolation buffer for remote dolphins
|
||||||
|
var _remote_pos: Vector3 = Vector3.ZERO
|
||||||
|
var _remote_yaw: float = 0.0
|
||||||
|
var _remote_pitch: float = 0.0
|
||||||
|
var _has_remote_data: bool = false
|
||||||
|
var _interp_speed: float = 12.0
|
||||||
|
|
||||||
|
|
||||||
|
func setup(owner_peer_id: int) -> void:
|
||||||
|
_owner_id = owner_peer_id
|
||||||
|
set_multiplayer_authority(owner_peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if NetworkManager.is_solo():
|
||||||
|
return
|
||||||
|
|
||||||
|
var parent_node := get_parent()
|
||||||
|
if not is_instance_valid(parent_node):
|
||||||
|
return
|
||||||
|
|
||||||
|
if multiplayer.get_unique_id() == _owner_id:
|
||||||
|
# Send our transform at sync_rate
|
||||||
|
_timer += delta
|
||||||
|
if _timer >= sync_rate:
|
||||||
|
_timer = 0.0
|
||||||
|
var pos: Vector3 = parent_node.global_position
|
||||||
|
var yaw: float = parent_node.rotation.y
|
||||||
|
var pitch: float = 0.0
|
||||||
|
if parent_node.has_node("CameraPivot"):
|
||||||
|
pitch = parent_node.get_node("CameraPivot").rotation.x
|
||||||
|
var boosting: bool = false
|
||||||
|
if "is_boosting" in parent_node:
|
||||||
|
boosting = parent_node.is_boosting
|
||||||
|
receive_transform.rpc(pos, yaw, pitch, boosting)
|
||||||
|
else:
|
||||||
|
# Interpolate toward remote state
|
||||||
|
if _has_remote_data:
|
||||||
|
parent_node.global_position = parent_node.global_position.lerp(
|
||||||
|
_remote_pos, _interp_speed * delta
|
||||||
|
)
|
||||||
|
parent_node.rotation.y = lerp_angle(parent_node.rotation.y, _remote_yaw, _interp_speed * delta)
|
||||||
|
if parent_node.has_node("CameraPivot"):
|
||||||
|
var pivot: Node3D = parent_node.get_node("CameraPivot")
|
||||||
|
pivot.rotation.x = lerp_angle(pivot.rotation.x, _remote_pitch, _interp_speed * delta)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "unreliable")
|
||||||
|
func receive_transform(pos: Vector3, rot_yaw: float, rot_pitch: float, boost: bool) -> void:
|
||||||
|
_remote_pos = pos
|
||||||
|
_remote_yaw = rot_yaw
|
||||||
|
_remote_pitch = rot_pitch
|
||||||
|
_has_remote_data = true
|
||||||
|
var parent_node := get_parent()
|
||||||
|
if is_instance_valid(parent_node) and "is_boosting" in parent_node:
|
||||||
|
parent_node.is_boosting = boost
|
||||||
1
scripts/net/PlayerSyncComponent.gd.uid
Normal file
1
scripts/net/PlayerSyncComponent.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c3ojrbvoxtbrp
|
||||||
74
scripts/net/WorldSyncComponent.gd
Normal file
74
scripts/net/WorldSyncComponent.gd
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# Attached to ChunkManager node.
|
||||||
|
# Server: intercepts block changes and broadcasts to clients.
|
||||||
|
# Client: receives and applies block changes locally.
|
||||||
|
|
||||||
|
var _chunk_manager: Node = null
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_chunk_manager = get_parent()
|
||||||
|
|
||||||
|
|
||||||
|
# Called by Main.gd instead of world.break_block / world.place_block when in multiplayer.
|
||||||
|
func server_break_block(world_pos: Vector3) -> int:
|
||||||
|
if not NetworkManager.is_server():
|
||||||
|
# Client sends request to server
|
||||||
|
request_break.rpc_id(1, world_pos)
|
||||||
|
return 0
|
||||||
|
var broken_id: int = _chunk_manager.break_block(world_pos)
|
||||||
|
if broken_id > 0:
|
||||||
|
broadcast_block_change.rpc(world_pos, 0)
|
||||||
|
return broken_id
|
||||||
|
|
||||||
|
|
||||||
|
func server_place_block(world_pos: Vector3, block_id: int) -> bool:
|
||||||
|
if not NetworkManager.is_server():
|
||||||
|
# Client sends request to server
|
||||||
|
request_place.rpc_id(1, world_pos, block_id)
|
||||||
|
return false
|
||||||
|
var placed: bool = _chunk_manager.place_block(world_pos, block_id)
|
||||||
|
if placed:
|
||||||
|
broadcast_block_change.rpc(world_pos, block_id)
|
||||||
|
return placed
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func request_break(pos: Vector3) -> void:
|
||||||
|
if not NetworkManager.is_server():
|
||||||
|
return
|
||||||
|
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||||
|
var broken_id: int = _chunk_manager.break_block(pos)
|
||||||
|
if broken_id > 0:
|
||||||
|
broadcast_block_change.rpc(pos, 0)
|
||||||
|
# Notify requesting peer of item pickup
|
||||||
|
notify_break_result.rpc_id(sender_id, broken_id)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func request_place(pos: Vector3, block_id: int) -> void:
|
||||||
|
if not NetworkManager.is_server():
|
||||||
|
return
|
||||||
|
var placed: bool = _chunk_manager.place_block(pos, block_id)
|
||||||
|
if placed:
|
||||||
|
broadcast_block_change.rpc(pos, block_id)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func broadcast_block_change(world_pos: Vector3, new_id: int) -> void:
|
||||||
|
# Applied on all peers including server via call_local
|
||||||
|
if multiplayer.is_server():
|
||||||
|
return # already applied on server
|
||||||
|
if new_id == 0:
|
||||||
|
_chunk_manager.break_block(world_pos)
|
||||||
|
else:
|
||||||
|
_chunk_manager.place_block(world_pos, new_id)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func notify_break_result(broken_id: int) -> void:
|
||||||
|
# Signal to Main.gd to add item to local inventory
|
||||||
|
var main := get_tree().get_first_node_in_group("main")
|
||||||
|
if is_instance_valid(main) and main.has_method("_on_remote_block_broken"):
|
||||||
|
main._on_remote_block_broken(broken_id)
|
||||||
1
scripts/net/WorldSyncComponent.gd.uid
Normal file
1
scripts/net/WorldSyncComponent.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://eo80c8lq8qr1
|
||||||
Reference in New Issue
Block a user