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 # Клиент: на какой мы карте. map_ready — сервер подтвердил, что мы в её комнате. var current_map := "world" var map_ready := false # Преследователи (камень, Верити…): их все считает один клиент (владелец), остальные повторяют var chasers := {} # id -> Node3D; преследователи этой сцены, регистрируют себя сами var chaser_states := {} # id -> {"pos": Vector3, "yaw": float} — что прислал владелец var rock_owner := 0 # peer_id владельца (0 — ещё не назначен) var dead_peers := {} # peer_id -> true, пока игрока поймали var hooked_player: Node = null # Сервер var peer_maps := {} # peer_id -> карта ("" пока клиент не сообщил) var rock_owners := {} # карта -> peer_id владельца камня var rock_seen := {} # карта -> время (сек), когда владелец последний раз присылал камень 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) peer_maps[id] = "" func _server_peer_left(id: int) -> void: print("- игрок ", id) var map: String = peer_maps.get(id, "") peer_maps.erase(id) _leave_room(id, map) # Игроки комнаты map, кроме except func _room(map: String, except := 0) -> Array: var out := [] if map == "": return out for p in multiplayer.get_peers(): if p != except and peer_maps.get(p, "") == map: out.append(p) return out # Игрок id покинул комнату map (вышел или сменил карту) func _leave_room(id: int, map: String) -> void: if map == "": return for other in _room(map, id): recv_left.rpc_id(other, id) if rock_owners.get(map, 0) == id: _set_rock_owner(map, _next_rock_owner(map, id)) # Новый владелец камня — наименьший peer_id в комнате, кроме except (0 — никого) func _next_rock_owner(map: String, except: int) -> int: var next := 0 for p in _room(map, except): if next == 0 or p < next: next = p return next func _set_rock_owner(map: String, id: int) -> void: rock_owners[map] = id rock_seen[map] = Time.get_ticks_msec() / 1000.0 if id != 0: print("[%s] камень считает игрок %d" % [map, id]) for p in _room(map): recv_rock_owner.rpc_id(p, id) # Владелец перестал присылать камень (вкладка в фоне, лаги) — передаём другому func _server_check_rock_owner() -> void: var now := Time.get_ticks_msec() / 1000.0 for map in rock_owners.keys(): var owner: int = rock_owners[map] if owner != 0 and now - rock_seen.get(map, now) > ROCK_TIMEOUT: var next := _next_rock_owner(map, owner) if next != 0: _set_rock_owner(map, next) # Клиент -> сервер: я теперь на этой карте. Подтверждение приходит в recv_map_ack. @rpc("any_peer", "reliable") func submit_map(map: String) -> void: if not is_server: return var id := multiplayer.get_remote_sender_id() var old: String = peer_maps.get(id, "") peer_maps[id] = map if old != map: _leave_room(id, old) recv_map_ack.rpc_id(id, map) if rock_owners.get(map, 0) == 0: _set_rock_owner(map, id) else: recv_rock_owner.rpc_id(id, rock_owners[map]) # Клиент -> сервер: своё состояние. Сервер пересылает остальным на той же карте. @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 _room(peer_maps.get(sender, ""), sender): recv_state.rpc_id(other, sender, pos, yaw, pitch, flags) # Владелец -> сервер: позиции всех преследователей. Сервер пересылает остальным на той же карте. @rpc("any_peer", "unreliable_ordered") func submit_chasers(ids: PackedStringArray, positions: PackedVector3Array, yaws: PackedFloat32Array) -> void: if not is_server: return var sender := multiplayer.get_remote_sender_id() var map: String = peer_maps.get(sender, "") if map == "" or rock_owners.get(map, 0) != sender: return rock_seen[map] = Time.get_ticks_msec() / 1000.0 for other in _room(map, sender): recv_chasers.rpc_id(other, ids, positions, yaws) # Клиент -> сервер: его поймали (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 _room(peer_maps.get(sender, ""), 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 submit_map.rpc_id(1, current_map) func _on_lost() -> void: connected = false _reset_room_state() multiplayer.multiplayer_peer = null _schedule_retry() func _schedule_retry() -> void: get_tree().create_timer(RETRY_DELAY).timeout.connect(_connect_client) # Забываем всё, что знали про комнату: чужих игроков, владельца и позицию камня func _reset_room_state() -> void: for g in ghosts.values(): if is_instance_valid(g): g.queue_free() ghosts.clear() dead_peers.clear() rock_owner = 0 chaser_states.clear() hooked_player = null map_ready = false # Мы перешли на другую карту (вызывает сама карта при загрузке, идемпотентно) func enter_map(map: String) -> void: if map == current_map: return current_map = map _reset_room_state() if connected: submit_map.rpc_id(1, map) # Портал: сообщаем серверу о смене карты и загружаем сцену func go_to_map(map: String, scene_path: String) -> void: enter_map(map) get_tree().change_scene_to_file.call_deferred(scene_path) # Камень считаем сами: если не в сети или мы владелец. Пока сервер не назначил владельца — ждём. func rock_is_local() -> bool: if not connected: return true return rock_owner == multiplayer.get_unique_id() @rpc("authority", "reliable") func recv_map_ack(map: String) -> void: if map == current_map: map_ready = true @rpc("authority", "reliable") func recv_rock_owner(id: int) -> void: if map_ready: rock_owner = id @rpc("authority", "unreliable_ordered") func recv_chasers(ids: PackedStringArray, positions: PackedVector3Array, yaws: PackedFloat32Array) -> void: if not map_ready: return for i in ids.size(): chaser_states[ids[i]] = {"pos": positions[i], "yaw": yaws[i]} @rpc("authority", "reliable") func recv_life(id: int, dead: bool, pos: Vector3) -> void: if not map_ready: return if dead: dead_peers[id] = true else: dead_peers.erase(id) peer_respawned.emit(pos) @rpc("authority", "unreliable_ordered") func recv_state(id: int, pos: Vector3, yaw: float, pitch: float, flags: int) -> void: if not map_ready or get_tree().current_scene == null: return # Идёт смена сцены — следующий пакет придёт через 50 мс 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 _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 and map_ready: 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 or not map_ready: 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 not chasers.is_empty() and rock_is_local(): var ids := PackedStringArray() var positions := PackedVector3Array() var yaws := PackedFloat32Array() for id in chasers: var c: Node3D = chasers[id] if is_instance_valid(c): ids.append(id) positions.append(c.global_position) yaws.append(c.rotation.y) if not ids.is_empty(): submit_chasers.rpc_id(1, ids, positions, yaws)