Карта Backrooms, порталы, комнаты по картам и зрение камня

- Backrooms на готовой модели (CC-BY, VTX): отдельный пакет backrooms.pck,
  качается при первом входе в портал (World/map_loader.gd), навмеш запечён заранее.
- Порталы между картами (World/portal.gd); выход из Backrooms возвращает в мир.
- Мультиплеер: игроки и общий камень делятся на комнаты по картам (Net/net.gd).
- Камень видит игрока только вблизи или в поле зрения, может потерять и бродит
  по навмешу (sight_limited); в мире и в Backrooms включено.
- Экспорт: пресет Backrooms для пакета карты, основной пресет без неё.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 16:15:00 +05:00
co-authored by Claude Sonnet 5.5
parent b7ca18ae3d
commit 97fafe6a30
71 changed files with 1920 additions and 97 deletions
+146 -73
View File
@@ -1,6 +1,9 @@
extends Node
# Автолоад "Net". Один и тот же скрипт работает и на клиенте, и на выделенном сервере
# (RPC требуют одинаковый путь узла и скрипт). Сервер — это headless-запуск Godot.
#
# Игроки делятся на «комнаты» по картам: видят друг друга и общий камень-преследователь
# только те, кто сейчас на одной карте. Сервер ничего не знает об уровнях — он лишь пересылает.
const PORT = 9080
const DEFAULT_URL = "ws://127.0.0.1:9080"
@@ -20,16 +23,24 @@ var ghosts := {} # peer_id -> Ghost
var send_timer := 0.0
var connected := false
# Клиент: на какой мы карте. map_ready — сервер подтвердил, что мы в её комнате.
var current_map := "world"
var map_ready := false
# Камень-преследователь: его считает один клиент (владелец), остальные повторяют
var rock: Node3D = null # Камень этой сцены (регистрирует сам)
var rock_owner := 0 # peer_id владельца (0 — ещё не назначен); на сервере тоже
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
# Сервер
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:
@@ -53,67 +64,96 @@ func _start_server() -> void:
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)
peer_maps[id] = ""
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)
var map: String = peer_maps.get(id, "")
peer_maps.erase(id)
_leave_room(id, map)
# Новый владелец камня — наименьший peer_id среди оставшихся, кроме except (0 — никого)
func _next_rock_owner(except: int) -> int:
var next := 0
# Игроки комнаты 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 (next == 0 or p < next):
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(id: int, gone := 0) -> void:
rock_owner = id
rock_last_seen = Time.get_ticks_msec() / 1000.0
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("камень считает игрок ", id)
for p in multiplayer.get_peers():
if p != gone:
recv_rock_owner.rpc_id(p, id)
print("[%s] камень считает игрок %d" % [map, id])
for p in _room(map):
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)
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 multiplayer.get_peers():
if other != sender:
recv_state.rpc_id(other, sender, pos, yaw, pitch, flags)
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_rock(pos: Vector3, yaw: float) -> void:
if not is_server:
return
var sender := multiplayer.get_remote_sender_id()
if sender != rock_owner:
var map: String = peer_maps.get(sender, "")
if map == "" or rock_owners.get(map, 0) != sender:
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)
rock_seen[map] = Time.get_ticks_msec() / 1000.0
for other in _room(map, sender):
recv_rock.rpc_id(other, pos, yaw)
# Клиент -> сервер: его поймали (dead) или он возродился в точке pos.
@rpc("any_peer", "reliable")
@@ -121,9 +161,8 @@ 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)
for other in _room(peer_maps.get(sender, ""), sender):
recv_life.rpc_id(other, sender, dead, pos)
# ---------- Клиент ----------
@@ -152,23 +191,81 @@ func _connect_client() -> void:
func _on_connected() -> void:
connected = true
submit_map.rpc_id(1, current_map)
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
_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
rock_has_state = false
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_rock(pos: Vector3, yaw: float) -> void:
if not map_ready:
return
rock_pos = pos
rock_yaw = yaw
rock_has_state = true
@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()
@@ -185,30 +282,6 @@ func recv_left(id: int) -> void:
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:
@@ -218,14 +291,14 @@ func _hook_player(player: Node) -> void:
player.respawned.connect(func(): _send_life(false, player))
func _send_life(dead: bool, player: Node) -> void:
if connected:
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:
if not connected or not map_ready:
return
send_timer += delta
if send_timer < 1.0 / SEND_RATE: