Общий камень-преследователь в мультиплеере
Камень считает один клиент (владелец, назначает сервер), остальные повторяют его позицию. Цель — ближайший живой игрок; события смерти/возрождения пересылаются через сервер. Сервер передаёт камень другому, если владелец вышел или замолчал. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+121
-3
@@ -6,17 +6,30 @@ const PORT = 9080
|
|||||||
const DEFAULT_URL = "ws://127.0.0.1:9080"
|
const DEFAULT_URL = "ws://127.0.0.1:9080"
|
||||||
const SEND_RATE = 20.0
|
const SEND_RATE = 20.0
|
||||||
const RETRY_DELAY = 3.0
|
const RETRY_DELAY = 3.0
|
||||||
|
const ROCK_TIMEOUT = 3.0 # Владелец камня молчит дольше (свернул вкладку?) — камень берёт другой
|
||||||
|
|
||||||
const FLAG_SLIDING = 1
|
const FLAG_SLIDING = 1
|
||||||
const FLAG_WALLRUN = 2
|
const FLAG_WALLRUN = 2
|
||||||
const FLAG_DASH = 4
|
const FLAG_DASH = 4
|
||||||
const FLAG_MANTLE = 8
|
const FLAG_MANTLE = 8
|
||||||
|
|
||||||
|
signal peer_respawned(pos: Vector3) # Чужой игрок возродился в точке pos
|
||||||
|
|
||||||
var is_server := false
|
var is_server := false
|
||||||
var ghosts := {} # peer_id -> Ghost
|
var ghosts := {} # peer_id -> Ghost
|
||||||
var send_timer := 0.0
|
var send_timer := 0.0
|
||||||
var connected := false
|
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:
|
func _ready() -> void:
|
||||||
is_server = DisplayServer.get_name() == "headless"
|
is_server = DisplayServer.get_name() == "headless"
|
||||||
if is_server:
|
if is_server:
|
||||||
@@ -34,14 +47,50 @@ func _start_server() -> void:
|
|||||||
get_tree().quit(1)
|
get_tree().quit(1)
|
||||||
return
|
return
|
||||||
multiplayer.multiplayer_peer = peer
|
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)
|
multiplayer.peer_disconnected.connect(_server_peer_left)
|
||||||
print("Сервер запущен на порту ", PORT)
|
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:
|
func _server_peer_left(id: int) -> void:
|
||||||
print("- игрок ", id)
|
print("- игрок ", id)
|
||||||
for other in multiplayer.get_peers():
|
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")
|
@rpc("any_peer", "unreliable_ordered")
|
||||||
@@ -53,6 +102,29 @@ func submit_state(pos: Vector3, yaw: float, pitch: float, flags: int) -> void:
|
|||||||
if other != sender:
|
if other != sender:
|
||||||
recv_state.rpc_id(other, sender, pos, yaw, pitch, flags)
|
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:
|
func _server_url() -> String:
|
||||||
@@ -86,6 +158,9 @@ func _on_lost() -> void:
|
|||||||
for g in ghosts.values():
|
for g in ghosts.values():
|
||||||
g.queue_free()
|
g.queue_free()
|
||||||
ghosts.clear()
|
ghosts.clear()
|
||||||
|
dead_peers.clear()
|
||||||
|
rock_owner = 0
|
||||||
|
rock_has_state = false
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
_schedule_retry()
|
_schedule_retry()
|
||||||
|
|
||||||
@@ -108,9 +183,49 @@ func recv_left(id: int) -> void:
|
|||||||
if ghost:
|
if ghost:
|
||||||
ghost.queue_free()
|
ghost.queue_free()
|
||||||
ghosts.erase(id)
|
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:
|
func _process(delta: float) -> void:
|
||||||
if is_server or not connected:
|
if is_server:
|
||||||
|
_server_check_rock_owner()
|
||||||
|
return
|
||||||
|
if not connected:
|
||||||
return
|
return
|
||||||
send_timer += delta
|
send_timer += delta
|
||||||
if send_timer < 1.0 / SEND_RATE:
|
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")
|
var player := get_tree().get_first_node_in_group("local_player")
|
||||||
if player == null:
|
if player == null:
|
||||||
return
|
return
|
||||||
|
_hook_player(player)
|
||||||
submit_state.rpc_id(1, player.global_position, player.rotation.y, player.head.rotation.x, player.get_net_flags())
|
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)
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
# Камень преследует локального игрока, обходя препятствия по навмешу, и ловит его.
|
# Камень преследует ближайшего живого игрока, обходя препятствия по навмешу, и ловит его.
|
||||||
|
# В сети камень считает один клиент (владелец, его назначает сервер), остальные
|
||||||
|
# плавно повторяют его позицию — поэтому у всех игроков камень один и тот же.
|
||||||
|
|
||||||
@export var speed := 15.0 # Скорость движения
|
@export var speed := 15.0 # Скорость движения
|
||||||
@export var turn_speed := 6.0 # Как быстро разворачивается к игроку
|
@export var turn_speed := 6.0 # Как быстро разворачивается к игроку
|
||||||
@export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока
|
@export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока
|
||||||
@export var catch_height := 4.0 # Игрока выше этого над камнем не достать
|
@export var catch_height := 4.0 # Игрока выше этого над камнем не достать
|
||||||
@export var repath_distance := 1.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 agent: NavigationAgent3D = $NavigationAgent3D
|
||||||
@onready var start_transform := global_transform
|
@onready var start_transform := global_transform
|
||||||
@@ -16,33 +21,70 @@ func _ready() -> void:
|
|||||||
agent.target_desired_distance = 1.0
|
agent.target_desired_distance = 1.0
|
||||||
agent.path_desired_distance = 1.5
|
agent.path_desired_distance = 1.5
|
||||||
agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола
|
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:
|
func _physics_process(delta: float) -> void:
|
||||||
if player == null:
|
if player == null:
|
||||||
player = get_tree().get_first_node_in_group("local_player") as Node3D
|
player = get_tree().get_first_node_in_group("local_player") as Node3D
|
||||||
if player == null:
|
if player == null:
|
||||||
return
|
return
|
||||||
if player.has_signal("respawned"):
|
player.respawned.connect(func(): _on_respawned(player.global_position))
|
||||||
player.respawned.connect(_on_player_respawned)
|
|
||||||
|
|
||||||
# Пока игрок лежит — стоим над ним
|
if Net.rock_is_local():
|
||||||
|
_chase(delta)
|
||||||
|
else:
|
||||||
|
_follow_owner(delta)
|
||||||
|
_try_catch()
|
||||||
|
|
||||||
|
# Своего игрока ловит его собственный клиент — по тому камню, который он видит
|
||||||
|
func _try_catch() -> void:
|
||||||
if player.get("is_dead"):
|
if player.get("is_dead"):
|
||||||
return
|
return
|
||||||
|
|
||||||
var flat := player.global_position - global_position
|
var flat := player.global_position - global_position
|
||||||
var dy := flat.y
|
var dy := flat.y
|
||||||
flat.y = 0.0
|
flat.y = 0.0
|
||||||
if flat.length() <= catch_distance and absf(dy) <= catch_height:
|
if flat.length() <= catch_distance and absf(dy) <= catch_height:
|
||||||
_face(flat, delta)
|
player.die()
|
||||||
if player.has_method("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
|
return
|
||||||
|
|
||||||
var step: Vector3
|
var step: Vector3
|
||||||
var map := agent.get_navigation_map()
|
var map := agent.get_navigation_map()
|
||||||
if NavigationServer3D.map_get_iteration_id(map) > 0:
|
if NavigationServer3D.map_get_iteration_id(map) > 0:
|
||||||
if agent.target_position.distance_to(player.global_position) > repath_distance:
|
if agent.target_position.distance_to(target.global_position) > repath_distance:
|
||||||
agent.target_position = player.global_position
|
agent.target_position = target.global_position
|
||||||
if not agent.is_navigation_finished():
|
if not agent.is_navigation_finished():
|
||||||
step = agent.get_next_path_position() - global_position
|
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)
|
global_position += step / d * min(speed * delta, d)
|
||||||
_face(Vector3(step.x, 0.0, step.z), delta)
|
_face(Vector3(step.x, 0.0, step.z), delta)
|
||||||
|
|
||||||
# Игрок возродился — возвращаемся на старт, чтобы не поймать его сразу снова
|
# Камень считает другой клиент — плавно идём к тому, что он прислал
|
||||||
func _on_player_respawned() -> void:
|
func _follow_owner(delta: float) -> void:
|
||||||
global_transform = start_transform
|
if not Net.rock_has_state:
|
||||||
agent.target_position = global_position
|
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
|
# Плавный разворот: модель смотрит вперёд по -Z
|
||||||
func _face(dir: Vector3, delta: float) -> void:
|
func _face(dir: Vector3, delta: float) -> void:
|
||||||
|
|||||||
Reference in New Issue
Block a user