Files
my-world/Net/net.gd
T
aibrashandClaude Sonnet 5.5 b7ca18ae3d Общий камень-преследователь в мультиплеере
Камень считает один клиент (владелец, назначает сервер), остальные повторяют его позицию.
Цель — ближайший живой игрок; события смерти/возрождения пересылаются через сервер.
Сервер передаёт камень другому, если владелец вышел или замолчал.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 15:22:19 +05:00

241 lines
8.1 KiB
GDScript

extends Node
# Автолоад "Net". Один и тот же скрипт работает и на клиенте, и на выделенном сервере
# (RPC требуют одинаковый путь узла и скрипт). Сервер — это headless-запуск Godot.
const PORT = 9080
const DEFAULT_URL = "ws://127.0.0.1:9080"
const SEND_RATE = 20.0
const RETRY_DELAY = 3.0
const ROCK_TIMEOUT = 3.0 # Владелец камня молчит дольше (свернул вкладку?) — камень берёт другой
const FLAG_SLIDING = 1
const FLAG_WALLRUN = 2
const FLAG_DASH = 4
const FLAG_MANTLE = 8
signal peer_respawned(pos: Vector3) # Чужой игрок возродился в точке pos
var is_server := false
var ghosts := {} # peer_id -> Ghost
var send_timer := 0.0
var connected := false
# Камень-преследователь: его считает один клиент (владелец), остальные повторяют
var rock: Node3D = null # Камень этой сцены (регистрирует сам)
var rock_owner := 0 # peer_id владельца (0 — ещё не назначен); на сервере тоже
var rock_pos := Vector3.ZERO
var rock_yaw := 0.0
var rock_has_state := false
var rock_last_seen := 0.0 # Сервер: когда владелец в последний раз присылал камень (сек)
var dead_peers := {} # peer_id -> true, пока игрока поймали
var hooked_player: Node = null
func _ready() -> void:
is_server = DisplayServer.get_name() == "headless"
if is_server:
_start_server()
else:
_connect_client()
# ---------- Сервер ----------
func _start_server() -> void:
var peer := WebSocketMultiplayerPeer.new()
var err := peer.create_server(PORT)
if err != OK:
push_error("Не удалось открыть порт %d: %s" % [PORT, error_string(err)])
get_tree().quit(1)
return
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_server_peer_joined)
multiplayer.peer_disconnected.connect(_server_peer_left)
print("Сервер запущен на порту ", PORT)
func _server_peer_joined(id: int) -> void:
print("+ игрок ", id)
if rock_owner == 0:
_set_rock_owner(id)
else:
recv_rock_owner.rpc_id(id, rock_owner)
func _server_peer_left(id: int) -> void:
print("- игрок ", id)
for other in multiplayer.get_peers():
if other != id:
recv_left.rpc_id(other, id)
if id == rock_owner:
_set_rock_owner(_next_rock_owner(id), id)
# Новый владелец камня — наименьший peer_id среди оставшихся, кроме except (0 — никого)
func _next_rock_owner(except: int) -> int:
var next := 0
for p in multiplayer.get_peers():
if p != except and (next == 0 or p < next):
next = p
return next
func _set_rock_owner(id: int, gone := 0) -> void:
rock_owner = id
rock_last_seen = Time.get_ticks_msec() / 1000.0
if id != 0:
print("камень считает игрок ", id)
for p in multiplayer.get_peers():
if p != gone:
recv_rock_owner.rpc_id(p, id)
# Владелец перестал присылать камень (вкладка в фоне, лаги) — передаём другому
func _server_check_rock_owner() -> void:
if rock_owner == 0:
return
if Time.get_ticks_msec() / 1000.0 - rock_last_seen > ROCK_TIMEOUT:
var next := _next_rock_owner(rock_owner)
if next != 0:
_set_rock_owner(next)
# Клиент -> сервер: своё состояние. Сервер пересылает остальным.
@rpc("any_peer", "unreliable_ordered")
func submit_state(pos: Vector3, yaw: float, pitch: float, flags: int) -> void:
if not is_server:
return
var sender := multiplayer.get_remote_sender_id()
for other in multiplayer.get_peers():
if other != sender:
recv_state.rpc_id(other, sender, pos, yaw, pitch, flags)
# Владелец камня -> сервер: позиция камня. Сервер пересылает остальным.
@rpc("any_peer", "unreliable_ordered")
func submit_rock(pos: Vector3, yaw: float) -> void:
if not is_server:
return
var sender := multiplayer.get_remote_sender_id()
if sender != rock_owner:
return
rock_last_seen = Time.get_ticks_msec() / 1000.0
for other in multiplayer.get_peers():
if other != sender:
recv_rock.rpc_id(other, pos, yaw)
# Клиент -> сервер: его поймали (dead) или он возродился в точке pos.
@rpc("any_peer", "reliable")
func submit_life(dead: bool, pos: Vector3) -> void:
if not is_server:
return
var sender := multiplayer.get_remote_sender_id()
for other in multiplayer.get_peers():
if other != sender:
recv_life.rpc_id(other, sender, dead, pos)
# ---------- Клиент ----------
func _server_url() -> String:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--server-url="):
return arg.substr("--server-url=".length())
if OS.has_feature("web"):
var proto = JavaScriptBridge.eval("window.location.protocol")
var host = JavaScriptBridge.eval("window.location.host")
return ("wss://" if proto == "https:" else "ws://") + str(host) + "/ws"
return DEFAULT_URL
func _connect_client() -> void:
var peer := WebSocketMultiplayerPeer.new()
var err := peer.create_client(_server_url())
if err != OK:
push_warning("Не удалось подключиться: %s" % error_string(err))
_schedule_retry()
return
multiplayer.multiplayer_peer = peer
if not multiplayer.connected_to_server.is_connected(_on_connected):
multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_lost)
multiplayer.server_disconnected.connect(_on_lost)
func _on_connected() -> void:
connected = true
func _on_lost() -> void:
connected = false
for g in ghosts.values():
g.queue_free()
ghosts.clear()
dead_peers.clear()
rock_owner = 0
rock_has_state = false
multiplayer.multiplayer_peer = null
_schedule_retry()
func _schedule_retry() -> void:
get_tree().create_timer(RETRY_DELAY).timeout.connect(_connect_client)
@rpc("authority", "unreliable_ordered")
func recv_state(id: int, pos: Vector3, yaw: float, pitch: float, flags: int) -> void:
var ghost: Ghost = ghosts.get(id)
if ghost == null:
ghost = Ghost.new()
ghost.setup(id, pos)
ghosts[id] = ghost
get_tree().current_scene.add_child(ghost)
ghost.set_target(pos, yaw, pitch, flags)
@rpc("authority", "reliable")
func recv_left(id: int) -> void:
var ghost = ghosts.get(id)
if ghost:
ghost.queue_free()
ghosts.erase(id)
dead_peers.erase(id)
# Камень считаем сами: если не в сети или мы владелец. Пока сервер не назначил владельца — ждём.
func rock_is_local() -> bool:
if not connected:
return true
return rock_owner == multiplayer.get_unique_id()
@rpc("authority", "reliable")
func recv_rock_owner(id: int) -> void:
rock_owner = id
@rpc("authority", "unreliable_ordered")
func recv_rock(pos: Vector3, yaw: float) -> void:
rock_pos = pos
rock_yaw = yaw
rock_has_state = true
@rpc("authority", "reliable")
func recv_life(id: int, dead: bool, pos: Vector3) -> void:
if dead:
dead_peers[id] = true
else:
dead_peers.erase(id)
peer_respawned.emit(pos)
# Сообщаем остальным о смерти и возрождении своего игрока
func _hook_player(player: Node) -> void:
if hooked_player == player:
return
hooked_player = player
player.died.connect(func(): _send_life(true, player))
player.respawned.connect(func(): _send_life(false, player))
func _send_life(dead: bool, player: Node) -> void:
if connected:
submit_life.rpc_id(1, dead, player.global_position)
func _process(delta: float) -> void:
if is_server:
_server_check_rock_owner()
return
if not connected:
return
send_timer += delta
if send_timer < 1.0 / SEND_RATE:
return
send_timer = 0.0
var player := get_tree().get_first_node_in_group("local_player")
if player == null:
return
_hook_player(player)
submit_state.rpc_id(1, player.global_position, player.rotation.y, player.head.rotation.x, player.get_net_flags())
if rock != null and rock_is_local():
submit_rock.rpc_id(1, rock.global_position, rock.rotation.y)