diff --git a/Net/net.gd b/Net/net.gd index 05c16fd..ef0292c 100644 --- a/Net/net.gd +++ b/Net/net.gd @@ -6,17 +6,30 @@ 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: @@ -34,14 +47,50 @@ func _start_server() -> void: get_tree().quit(1) return multiplayer.multiplayer_peer = peer - multiplayer.peer_connected.connect(func(id): print("+ игрок ", id)) + 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(): - recv_left.rpc_id(other, id) + 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") @@ -53,6 +102,29 @@ func submit_state(pos: Vector3, yaw: float, pitch: float, flags: int) -> void: 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: @@ -86,6 +158,9 @@ func _on_lost() -> void: 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() @@ -108,9 +183,49 @@ func recv_left(id: int) -> void: 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 or not connected: + if is_server: + _server_check_rock_owner() + return + if not connected: return send_timer += delta if send_timer < 1.0 / SEND_RATE: @@ -119,4 +234,7 @@ func _process(delta: float) -> void: 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) diff --git a/models/Bochi-Rock/rock-follow.gd b/models/Bochi-Rock/rock-follow.gd index 54576e5..2bbbda3 100644 --- a/models/Bochi-Rock/rock-follow.gd +++ b/models/Bochi-Rock/rock-follow.gd @@ -1,11 +1,16 @@ extends Node3D -# Камень преследует локального игрока, обходя препятствия по навмешу, и ловит его. +# Камень преследует ближайшего живого игрока, обходя препятствия по навмешу, и ловит его. +# В сети камень считает один клиент (владелец, его назначает сервер), остальные +# плавно повторяют его позицию — поэтому у всех игроков камень один и тот же. @export var speed := 15.0 # Скорость движения @export var turn_speed := 6.0 # Как быстро разворачивается к игроку @export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока @export var catch_height := 4.0 # Игрока выше этого над камнем не достать @export var repath_distance := 1.0 # Пересчитываем путь, когда игрок сместился на столько +@export var respawn_safe_radius := 25.0 # Игрок возродился ближе этого к камню — камень уходит на старт +@export var sync_smoothing := 12.0 # Плавность повтора чужого камня +@export var snap_distance := 20.0 # Дальше этого чужой камень не едет, а прыгает (сброс на старт) @onready var agent: NavigationAgent3D = $NavigationAgent3D @onready var start_transform := global_transform @@ -16,33 +21,70 @@ func _ready() -> void: agent.target_desired_distance = 1.0 agent.path_desired_distance = 1.5 agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола + Net.rock = self + Net.peer_respawned.connect(_on_respawned) + +func _exit_tree() -> void: + if Net.rock == self: + Net.rock = null func _physics_process(delta: float) -> void: if player == null: player = get_tree().get_first_node_in_group("local_player") as Node3D if player == null: return - if player.has_signal("respawned"): - player.respawned.connect(_on_player_respawned) + player.respawned.connect(func(): _on_respawned(player.global_position)) - # Пока игрок лежит — стоим над ним + if Net.rock_is_local(): + _chase(delta) + else: + _follow_owner(delta) + _try_catch() + +# Своего игрока ловит его собственный клиент — по тому камню, который он видит +func _try_catch() -> void: if player.get("is_dead"): return - var flat := player.global_position - global_position var dy := flat.y flat.y = 0.0 if flat.length() <= catch_distance and absf(dy) <= catch_height: - _face(flat, delta) - if player.has_method("die"): - player.die() + player.die() + +# Ближайший по горизонтали живой игрок: свой или чужой +func _pick_target() -> Node3D: + var best: Node3D = null + var best_d := INF + var candidates: Array[Node3D] = [] + if not player.get("is_dead"): + candidates.append(player) + for id in Net.ghosts: + var g: Node3D = Net.ghosts[id] + if is_instance_valid(g) and not Net.dead_peers.has(id): + candidates.append(g) + for c in candidates: + var d := Vector2(c.global_position.x - global_position.x, c.global_position.z - global_position.z).length_squared() + if d < best_d: + best_d = d + best = c + return best + +func _chase(delta: float) -> void: + var target := _pick_target() + if target == null: + return # Все лежат — стоим + + var flat := target.global_position - global_position + flat.y = 0.0 + if flat.length() <= catch_distance: + _face(flat, delta) # Стоим над жертвой, пока её клиент не засчитает поимку return var step: Vector3 var map := agent.get_navigation_map() if NavigationServer3D.map_get_iteration_id(map) > 0: - if agent.target_position.distance_to(player.global_position) > repath_distance: - agent.target_position = player.global_position + if agent.target_position.distance_to(target.global_position) > repath_distance: + agent.target_position = target.global_position if not agent.is_navigation_finished(): step = agent.get_next_path_position() - global_position @@ -56,10 +98,24 @@ func _physics_process(delta: float) -> void: global_position += step / d * min(speed * delta, d) _face(Vector3(step.x, 0.0, step.z), delta) -# Игрок возродился — возвращаемся на старт, чтобы не поймать его сразу снова -func _on_player_respawned() -> void: - global_transform = start_transform - agent.target_position = global_position +# Камень считает другой клиент — плавно идём к тому, что он прислал +func _follow_owner(delta: float) -> void: + if not Net.rock_has_state: + return + if global_position.distance_to(Net.rock_pos) > snap_distance: + global_position = Net.rock_pos + else: + global_position = global_position.lerp(Net.rock_pos, 1.0 - exp(-sync_smoothing * delta)) + rotation.y = lerp_angle(rotation.y, Net.rock_yaw, 1.0 - exp(-sync_smoothing * delta)) + +# Кто-то (мы или другой игрок) возродился. Если камень рядом с точкой возрождения — +# уводим его на старт, чтобы не поймал снова сразу. Решает только владелец камня. +func _on_respawned(pos: Vector3) -> void: + if not Net.rock_is_local(): + return + if global_position.distance_to(pos) < respawn_safe_radius: + global_transform = start_transform + agent.target_position = global_position # Плавный разворот: модель смотрит вперёд по -Z func _face(dir: Vector3, delta: float) -> void: