My World

Мультиплеерная игра от первого лица в браузере (Godot 4.7.2, Web). Игроки собираются в стартовой комнате-хабе, заходят через порталы на разные карты и убегают там от преследователей — камня и Верити. Паркур-управление: спринт, рывок, подкат, присед, бег по стенам, подтягивание на уступы.

  • Движок: Godot 4.7.2, рендерер GL Compatibility (нужен для Web), физика Jolt
  • Платформа: Web (WebAssembly), без потоков (thread_support=false), без GDExtension (extensions_support=false)
  • Язык комментариев в коде, интерфейса и документации: русский
  • Репозитории (соседние папки в /home/aibrash/Projects/My_World/): my-world — этот проект; grid-world — готовый веб-экспорт, который CI выкладывает на сайт; Blender models — исходники моделей; source-assets — исходные файлы вне игры (например, видео, из которого сделан звук Верити)

Состояние: что есть и чего нет

Что
✅ Хаб с порталами; карты Песочница, Backrooms, Parkour и Race; выход из карт обратно в хаб
✅ Race — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); Parkour — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня»)
✅ Вкладка в браузере занимает ~300 МБ (замер владельца, окт 2026)
✅ Карты качаются отдельными пакетами .pck при первом входе в портал (экран загрузки с прогрессом)
✅ Мультиплеер: выделенный сервер, игроки видят друг друга, комнаты по картам (в разных картах не пересекаются)
✅ Преследователи: камень и Верити; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока
✅ Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!»
✅ Паркур: спринт, рывок, подкат, присед (C), бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ)
✅ Управление не зависит от раскладки (русская и любая другая): действия InputMap на физические клавиши
✅ Деплой: CI из grid-world на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера
❌ Телефон: не планируется (решение владельца, окт 2026). Сенсорного управления нет, мобильных текстур нет
⏳ Ники и текстовый чат — следующая задача (см. «Дорожная карта»)
❌ Настроек (чувствительность мыши, громкость), меню паузы
❌ Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает
❌ Сохранения прогресса, аккаунтов
❌ Автоматических тестов в CI (есть боты для ручного прогона, см. «Тесты без окна»)
✅ Чужой игрок — модель киберниндзя со скелетом (player/character/character.glb, 752 треугольника, 17 костей, 14 анимаций); свечение красится в цвет игрока от его id. Своё тело — та же модель, спрятанная от своей камеры

Быстрый старт

# запуск игры локально (стартует в хабе; без сервера — одиночная игра, клиент переподключается каждые 3 с)
godot --path . --windowed --resolution 960x540

# с локальным сервером (в другом терминале)
tools/test/make_test_server.sh /tmp/test-server && godot --headless --path /tmp/test-server
godot --path .          # можно запустить несколько окон

Карты в редакторе запускаются отдельно (F6 на сцене карты): maps/sandbox/sandbox.tscn, maps/backrooms/backrooms.tscn, maps/parkour/parkour.tscn, maps/race/race.tscn. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются).

Управление

Клавиши привязаны к физическому расположению (так работает и на русской раскладке). Действия лежат в project.godot → [input]: move_forward/back/left/right, jump, sprint, crouch, dash, ready (R — «готов» в гонке).

Клавиша Действие
W A S D, стрелки движение
Shift спринт
Пробел прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок
C (держать) присед (скорость 3,5, капсула вдвое ниже, пролезает под низкое)
Shift + C на бегу подкат
Q рывок
R «готов» (только на карте Race)
ЛКМ (зажать) зум
Esc / клик отпустить / захватить мышь

Бег по стене — в воздухе у стены с зажатым «вперёд» на скорости. Ctrl не используется: в браузере Ctrl+W закрывает вкладку.

Структура проекта

project.godot, export_presets.cfg, icon.svg
net/                 сеть. Net — автолоад: и клиент, и сервер (один скрипт)
  net.gd                 состояние комнат, преследователей, RPC (см. «Сеть»)
  ghost.gd               чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник
player/character/        character.glb — модель и анимации (исходник и скрипт сборки: `../Blender models/character_01/`)
  server.tscn            пустая сцена для запуска выделенного сервера
core/
  map_loader.gd          автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт»)
player/
  player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение
objects/
  portal.gd              портал на другую карту (Area3D, рисуется кодом)
chasers/
  chaser.gd              общий «мозг» преследователя: навмеш, зрение, погоня, блуждание, поимка, синхронизация
  rock/                  камень: rock.tscn, Rock.glb (+ извлечённая текстура), Guitar.ogg
  verity/                Верити: verity.tscn, verity2.glb (+ текстура), verity.ogg (звук из видео)
maps/                    у каждой карты своя папка — она самодостаточна и экспортируется отдельным пакетом
  hub/                   стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS)
  sandbox/               песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd,
                         floor/ sky/ audio/ models/ (здания, башня, обелиск, деревья, Эмпайр-стейт…)
  race/                  race.tscn, race.gd, race_rules.gd (правила гонки), race_data.gd (генерируется), race.glb, sky.png, audio/music.ogg
  parkour/               parkour.tscn, parkour.gd (логика: чекпоинты, лазеры, дождь, звук, окружение), parkour_data.gd (генерируется),
                         baked.gdshader, parkour.glb (из Blender), sky.png (панорама), audio/music.ogg; см. «Заметки для будущего меня»
  backrooms/             backrooms.tscn, backrooms.gd, backrooms_nav.res (запечённый навмеш),
                         model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md)
deploy/                  боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера
tools/                   export_all.sh, serve_web.py, deploy_server.sh, test/ (боты)

Правила, которые стоит соблюдать

  • Папки и файлы — в snake_case. Каждая карта — в maps/<имя>/. Всё, что нужно только ей, лежит внутри. Общее (игрок, преследователи, порталы, сеть) — вне maps/: оно входит в основной пакет игры.
  • Файлы рядом с .glb не переименовывать и не удалять. Это текстуры, которые Godot вынул из модели при импорте (gltf/embedded_image_handling=Extract), их имена зависят от имени .glb. Переименуешь .glb — импорт создаст дубли. Папки с моделями переносить целиком можно (я так и делал).
  • .import и .uid коммитятся и переносятся вместе с файлом (git mv). Кэш .godot/ не коммитится; после переноса файлов вне редактора его удаляют и делают godot --headless --import --path ..
  • Пути в сценах — через res://; ссылки на карты (PORTALS в hub.gd, target_scene у порталов) — строками.

Карты и порталы

Карта Сцена Пакет Что там
Хаб maps/hub/hub.tscn в основном пакете Комната 28×18 м, строится кодом (пол-грид, стены, свет). Порталы SANDBOX и BACKROOMS в ряд по центру, влезает 6. Безопасна, преследователей нет
Песочница maps/sandbox/sandbox.tscn world.pck Киберпанк-мир: пол-шахматка, здания, 60 деревьев (MultiMesh), птицы, парящий обелиск, звук tron_01. Преследователи: камень и Верити. Портал «HUB» возвращает в хаб
Backrooms maps/backrooms/backrooms.tscn backrooms.pck Модель «Backrooms VR» (×1,4). Преследователь: Верити (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом
Parkour maps/parkour/parkour.tscn parkour.pck (7,4 МБ: небо 3,5 + модель 3,4 + музыка 1,1) Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте. Музыка: maps/parkour/audio/music.ogg (Ogg Vorbis, моно 32 кГц, ~1 МБ; делается ffmpeg -i исходник.mp3 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg), играет по кругу
Race maps/race/race.tscn race.pck Гонка на ногах до 8 игроков (кто быстрее до финиша), мрачный город: стартовая площадка → рынок → эстакада → крыши → каньон → ангар. 8 ворот-чекпоинтов, 2 лазера, развилки с обходами. Правила и сеть — раздел «Карта Race» ниже

Правка и пересборка Parkour (всё в Blender models/parkour_02/):

  1. layout.py — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; стенами высоту не набрать — прыжков от стены без бега только 2 за полёт).
  2. build_parkour.py — в Blender (exec): строит коллизию (Collision-colonly — Godot делает из неё StaticBody), детальную геометрию Solid, светящиеся элементы Neon; пишет parkour_data.gd (чекпоинты, лазеры, старт, портал). Сохраняет Parkour_02.blend.
  3. bake.py — blender -b Parkour_02.blend -P bake.py: Cycles запекает прямой+непрямой свет (неон светит сам, плюс холодный/пурпурный/бирюзовый свет) в цвета вершин (сжаты в 8 раз — шейдер умножает обратно) и экспортирует parkour.glb. ~1 мин.
  4. build_sky.py — blender -b -P build_sky.py: панорама дальнего города 3072×1536 → sky.png (в импорте включены мипы — по ним размытые отражения). ~2 мин.
  5. godot --headless --import --path .. Палитра светящихся задана именами материалов из Blender (старые названия сохранены): NeonCyan — холодный белёсый, NeonMagenta — красный, NeonOrange — натриевая лампа. Настроение — в parkour.gd (туман, дождь _add_rain, гул _add_ambience, насыщенность и контраст), в bake.py (силы света), build_sky.py (дымное небо). В Godot: baked.gdshader (unshaded: цвет материала × запечённый свет × «зерно» бетона по мировым координатам + влажный блеск — отражение панорамы на горизонтальных поверхностях); parkour.gd подменяет материалы по именам из Blender (SURFACES, GLOWS), строит ворота-чекпоинты, лазеры (пульсирующие считают фазу по системным часам), окружение и портал.

Добавить карту:

  1. Создай maps/<имя>/<имя>.tscn: корень Node3D, внутри игрок (player/player.tscn), портал обратно (objects/portal.gd, target_map="hub", target_scene="res://maps/hub/hub.tscn"), при необходимости преследователи. В _ready() вызови Net.enter_map("<имя>") (идемпотентно; так сервер знает, в какой ты комнате).
  2. Если нужны преследователи — навмеш: либо NavigationRegion3D + maps/sandbox/nav_bake.gd (запекание при старте, как в песочнице), либо заранее запечённый .res (как у Backrooms: в вебе без потоков запекать на лету долго).
  3. В export_presets.cfg добавь пресет-копию «World» (export_filter="resources", export_files=["res://maps/<имя>/<имя>.tscn"], exclude_filter="player/*, objects/*, chasers/chaser.gd", export_path="../grid-world/<имя>.pck"), а в пресет «Web» — maps/<имя>/* в exclude_filter. Добавь вызов в tools/export_all.sh.
  4. В PORTALS (maps/hub/hub.gd) допиши запись {title, map, scene, pack, color} — портал появится сам.

Карта Race (гонка)

Файлы: maps/race/ — race.tscn, race.gd (внешний вид, ворота, лазеры, срыв, дождь/гул, музыка), race_rules.gd (правила, HUD, сеть), race_data.gd (генерируется из Blender models/race_01/, не править), race.glb, sky.png, baked.gdshader, audio/music.ogg. Пакет race.pck (в нём include_filter: sky.png, baked.gdshader, race_data.gd, race_rules.gd, race.gd, audio/*; без него файлы, которые скрипт грузит по строке, не попадают в пакет).

Правила. ОЖИДАНИЕ → ОТСЧЁТ → ГОНКА → ИТОГИ → ОЖИДАНИЕ.

  • Ожидание: бегаем свободно. R (действие ready) переключает «готов»; подсказка «R — готов (n/m)». Все готовы (хотя бы один) → отсчёт.
  • Отсчёт 6 с (секунда «приготовьтесь», затем 5…1): все телепортируются в слоты (номер = индекс в отсортированном списке id комнаты; больше 8 — рядами позади, шаг 3 м) лицом по трассе и заморожены (player.input_frozen; обзор мышью работает).
  • Гонка: ворота только по порядку (пересечение плоскости ворот по ходу трассы в пределах полуширины+1 м; чужие ворота игнорируются). Последние ворота = финиш: фиксируется время, дальше бежим свободно. Сплит: «ВОРОТА n время ±разница с личным лучшим забегом».
  • Место: финишировавшие — по времени, остальные — по числу ворот, затем по расстоянию до следующих ворот. Конец круга: финишировали все присутствующие или прошло 45 с после первого финиша; игрок вышел — убирается из расчётов (если остался один — бежит до финиша). Итоги ~8 с, затем ожидание; готовность сброшена, игроки стоят где стояли.
  • Зашедший в разгар круга бегает без зачёта, видит «ГОНКА ИДЁТ» (определяется по маякам других игроков) и участвует со следующего круга.
  • Срыв: ниже последних пройденных ворот на 32 м (до первых ворот — ниже стартового слота) или лазер → «СОРВАЛСЯ!», возрождение на последних воротах лицом по трассе (Basis(UP, yaw); знак проверен: rotation.y = yaw_deg из race_data.gd даёт нужное направление: yaw -90 = на +X). Личный рекорд и сплиты лучшего забега — user://race_best.cfg (ConfigFile).
  • Музыка audio/music.ogg (Ogg Vorbis, моно 32 кГц, 0,6 МБ; ffmpeg -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg) включается в момент «СТАРТ!» с начала, играет до возврата в ожидание и плавно (0,8 с) затихает; дождь и гул остаются под ней. Зашедший в разгар круга слышит её, пока идёт чужой круг.

Кто что считает (сервер ничего не знает про гонку). Каждый клиент ведёт свою копию состояния (race_rules.gd) и обменивается событиями Net.RACE_* через submit_race(kind, a, b) / recv_race(id, kind, a, b) (+ сигнал Net.race_event; сервер просто пересылает остальным в комнате):

kind a b Кто шлёт
RACE_READY 1/0 — игрок при нажатии R
RACE_START — unix-время старта (отправитель: now + 6) только наименьший id в комнате, когда видит, что готовы все (поэтому двое «последних готовых» дают один старт; второй START от большего id игнорируется)
RACE_CP индекс ворот время гонки игрок при прохождении ворот
RACE_FINISH — время финиша игрок на последних воротах
RACE_STATE биты: 0..7 пройдено ворот, 8 готов, 9..10 фаза время финиша или -1 «маяк» раз в секунду и при смене фазы; по нему догоняют потерявшие событие и зашедшие позже

Время: на получении START остаток считается как b - unix_now; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = (ticks - start_tick), финиш b пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник и цвет в таблице — как у призрака (Игрок id%1000, Color.from_hsv(id*0.618034…)).

Проверка по существу (всё в tools/test/):

  • race_game_test.sh [папка] — локальный сервер + 2 клиента-бота (A, B) на карте race два круга; третий (C) заходит в разгар круга 1, участвует во втором и выходит посреди него. Проверяет: одинаковые таблицы мест на всех, один START на круг, слоты, синхронность старта, заморозку на отсчёте, музыку после старта, возрождение лицом по трассе (смерть и срыв), зрителя. Сверяет race_game_check.py; боты — race_bot.gd (автозагрузка во временной копии проекта) по маршруту race_route.json. Порт 9080 должен быть свободен; рекорды ботов пишутся в папку теста (--race-best=), настоящий не трогается.
  • race_tour.tscn — кадры для проверки HUD/ворот (--mode=gates|hud, см. заголовок).
  • Пакет: godot --headless --export-pack Race /tmp/x/race.pck, затем strings /tmp/x/race.pck | grep -c для sky.png, baked.gdshader, race_data, race_rules.gd, music.ogg, race.glb (каждое > 0).
  • Пакетная загрузка через портал хаба: tools/test/race_check.txt (по образцу parkour_check.txt).

Выкладка: старый клиент с новым сервером (и наоборот) не работает вообще — Godot сверяет контрольную сумму набора RPC узла /root/Net («rpc node checksum failed»), проверено; поэтому порядок строго по README («Деплой»). net/net.gd изменён (новые RPC submit_race/recv_race) → после пуша grid-world сразу tools/deploy_server.sh root@192.168.10.110.

Преследователи

chasers/chaser.gd — один скрипт для всех. Сцены (rock.tscn, verity.tscn) — это просто модель + звук + NavigationAgent3D.

  • Поведение: гонится за ближайшим живым игроком по навмешу, ловит на catch_distance (по горизонтали, не выше catch_height). После поимки игрок умирает и возрождается через 2,5 с в стартовой точке; если преследователь рядом со стартом — уходит на свой старт.
  • Зрение (sight_limited = true): замечает игрока, если он ближе hearing_radius (слышит даже за стеной) или в конусе sight_fov до sight_range и луч до него не упирается в стену. Потерял из виду — идёт к последнему месту lose_time секунд, потом блуждает (patrol_speed, wander_radius). Без sight_limited знает, где игрок, из любой точки.
  • Параметры (экспорты узла): chaser_id (имя в сети), speed, turn_speed, catch_distance, catch_height, sight_*, hearing_radius, lose_time, patrol_speed, wander_radius, model_scale (масштаб дочернего узла Model), emission_energy (чисто светящийся материал; у Верити 1,0).
  • Значения сейчас: камень в песочнице — скорость 15, зрение 45 м / 130°, слух 8 м, лимит потери 5 с. Верити в песочнице — скорость 12, зрение 55 м / 110°, слух 10 м, потеря 6 с. Верити в Backrooms — скорость 10, масштаб 0,5, зрение по умолчанию (30 м / 120° / слух 6 м / потеря 4 с).
  • Модели — плоские «карточки» вдоль оси X; в сцене повёрнуты на 90° и подняты, чтобы стоять на полу. У verity2 лицо нарисовано с одной стороны (свечением), при погоне оно повёрнуто к игроку.
  • Добавить преследователя: новая сцена по образцу verity.tscn (корень со скриптом chaser.gd, chaser_id="имя", NavigationAgent3D), инстанс в карту. Больше ничего: сеть синхронизирует всех по имени.

Сеть

Архитектура: выделенный сервер на Godot (WebSocket, порт 9080), клиенты — WebSocketMultiplayerPeer. Сервер не знает про уровни и ничего не считает, только пересылает сообщения внутри «комнаты» (карты). Физика, управление, преследователи считаются на клиентах.

  • net/net.gd — автолоад Net. DisplayServer.get_name() == "headless" → сервер, иначе клиент; флаг --client делает клиентом и headless (боты). Клиент переподключается каждые 3 с. Адрес в вебе берётся из window.location (wss://<домен>/ws или ws://), на десктопе — ws://127.0.0.1:9080 или -- --server-url=....
  • Комнаты по картам. Клиент при входе на карту шлёт submit_map(имя), сервер отвечает recv_map_ack. Пока подтверждения нет (Net.map_ready == false), клиент игнорирует чужие сообщения — так при смене карты не просачиваются старые пакеты. Игроки разных карт друг друга не видят.
  • Владелец преследователей. В каждой комнате один клиент (первый вошедший, затем наименьший peer_id из оставшихся) симулирует всех преследователей и шлёт их позиции 20 раз/с; остальные клиенты плавно повторяют. Если владелец молчит ROCK_TIMEOUT = 3 с (вкладка в фоне, лаги) или вышел — сервер передаёт роль другому; преследователь продолжает с того же места. Поимку «своего» игрока всегда решает его собственный клиент по тому, что он видит.
  • Смерть и возрождение шлются отдельно (submit_life), чтобы владелец не гнался за мёртвыми и мог увести преследователя от точки возрождения.
RPC Кто → кому Что
submit_map(map) / recv_map_ack(map) клиент → сервер / обратно вход в комнату
recv_rock_owner(id) сервер → клиенты кто симулирует преследователей
submit_state(pos, yaw, pitch, flags) / recv_state(id, …) клиент → сервер → остальные состояние игрока, 20 Гц, unreliable_ordered
submit_chasers(ids, positions, yaws) / recv_chasers(…) владелец → сервер → остальные позиции всех преследователей, 20 Гц
submit_life(dead, pos) / recv_life(id, dead, pos) клиент → сервер → остальные смерть/возрождение, reliable
recv_left(id) сервер → комната игрок вышел

Флаги состояния (Net.FLAG_*): подкат, бег по стене (+ сторона стены), рывок, подтягивание, «в воздухе». Присед передаётся как подкат; призрак отличает их по скорости. Скорость и двойной прыжок призрак считает сам по смещению позиции — в протокол они не входят.

RPC требуют одинаковые путь узла и скрипт на сервере и клиенте, поэтому net.gd один. Любое изменение RPC — клиент и сервер выкладываются вместе (старый клиент с новым сервером не работает). Имена вроде rock_owner/recv_rock_owner остались от времени, когда преследователь был один.

Загрузка карт пакетами (веб)

Основной пакет игры (index.pck) — только хаб, игрок, сеть и порталы (около 70 КБ). Движок — index.wasm (39,5 МБ, ~10 МБ с gzip). Карты — отдельные .pck: world.pck (13,6 МБ), backrooms.pck (21,3 МБ). Портал вызывает MapLoader.enter(map, scene, pack, title):

  1. если сцена уже есть в проекте (редактор, десктоп, пакет уже скачан) — переход сразу;
  2. иначе игра встаёт на паузу (преследователь не должен убить игрока на экране загрузки), показывается «Загрузка: <имя> N / M МБ»;
  3. пакет качается HTTPRequest с Cache-Control: no-cache (браузер сверяет файл с сервером), записывается в user:// и подключается через ProjectSettings.load_resource_pack(path, false) (false — файлы основной игры в приоритете, устаревший пакет не затрёт код);
  4. при ошибке на экране и в консоли пишется причина, портал можно повторить.

Грабли, на которые уже наступили:

  • HTTPRequest в браузере: accept_gzip = false обязательно. Браузер сам распаковывает gzip, а движок видит Content-Encoding: gzip и пытается распаковать ещё раз (ошибка 8).
  • download_file в вебе не создаёт файл — тело берём в память и пишем в user:// сами.
  • Файлы-«не ресурсы» (.mp4, .md) не экспортируются; потому исходное видео Верити лежит вне проекта (source-assets).
  • Предупреждения invalid UID … using text path instead при загрузке пакета безвредны.
  • Ресурсы, которые скрипт грузит строкой (load("res://…")), в пакет карты не попадают: пресет export_filter=resources берёт только сцену и её прямые зависимости (preload тоже находится). Добавляй такие файлы в include_filter пресета карты (у Parkour: sky.png, baked.gdshader, parkour_data.gd, audio/*). Признак беды: пакет «слишком маленький». Проверка: strings parkour.pck | grep -c sky.png (должно быть > 0). Раз это случилось: карта тихо грузится без неба, шейдера и логики, а проверка «сцена называется Parkour» этого не замечает — проверяй по существу (см. «Тесты»).
  • Кэш зависимостей редактора у недавно созданных/изменённых сцен может устареть, и экспорт пакета карты пропустит модели и звуки (пакет «слишком маленький»). tools/export_all.sh делает touch сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов.

Экспорт, тесты и деплой

Экспорт

tools/export_all.sh            # веб-клиент + backrooms.pck + world.pck в ../grid-world

Три пресета в export_presets.cfg: Web (хаб + общее; из него исключены maps/sandbox, maps/backrooms, chasers/rock, chasers/verity, tools), World и Backrooms (export_filter=resources — сцена карты и всё, на что она ссылается, кроме общего из основного пакета). Шаблоны экспорта — ~/.local/share/godot/export_templates/4.7.2.stable/ (есть только Web). Экспорт при выходе печатает безвредную ошибку Parameter "singleton" is null.

Проверка сборки как на сервере

python3 tools/serve_web.py ../grid-world 8080     # nginx-подобно: COOP/COEP и gzip чанками

Открыть http://127.0.0.1:8080. На десктопном godot --main-pack пакеты карт качаются плоским python3 -m http.server (без gzip) через -- --pack-url=http://127.0.0.1:8777/.

Тесты без окна (боты)

Автотестов в CI нет; для проверок есть боты. tools/test/make_test_project.sh /tmp/tp делает временную копию проекта с автозагрузкой tools/test/bot.gd (телепортирует игрока по расписанию и печатает состояние раз в секунду), tools/test/make_test_server.sh /tmp/ts — локальный сервер в боевой раскладке.

godot --headless --path /tmp/ts &                                   # сервер
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=A &
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=B
# цепочка карт на реальных пакетах (портал SANDBOX в хабе на (-2.25, -7.5), BACKROOMS на (2.25, -7.5)):
/tmp/tp/tools/export_all.sh /tmp/site && (cd /tmp/site && python3 -m http.server 8777 --bind 127.0.0.1 &)
godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:-2.25,1,-7.5

Для картинок: godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/a.png --quit-after 25 res://maps/hub/hub.tscn (окно мелькнёт; кадры — a00000020.png). В браузере: Chrome (flatpak com.google.Chrome) --headless=new --enable-logging=stderr --v=0 http://127.0.0.1:8080 — вывод Godot виден в строках CONSOLE.

Готовые проверки (tools/test/, запускаются как сцены: в режиме --script автозагрузки недоступны при компиляции, поэтому только res://…tscn; -- --client, иначе headless поднимет сервер на порту 9080):

  • measure_movement.tscn — замеры мувсета (прыжки, рывок, стена); mantle_test.tscn — подтягивание на 40 вариантах уступов; parkour_gaps.tscn — проходимость контейнеров; parkour_tour.tscn — облёт карты со снимками (см. заголовок файла).
  • Проверяй по существу, а не по имени сцены. Для карт из пакетов: собери тестовый проект (make_test_project.sh), в его bot.gd добавь печать фактов (есть ли у current_scene скрипт, подменён ли материал, есть ли PanoramaSkyMaterial, играет ли музыка, сколько лазеров) и пройди портал с --pack-url. Готовый код проверки — tools/test/parkour_check.txt (ловит пропавшие sky.png/шейдер/музыку/логику).
  • Ввод ботом: Input.action_press("jump") работает для зажатия и is_action_pressed; рывок и подкат ловятся в _unhandled_input, поэтому для них нужно Input.parse_input_event(InputEventAction) с pressed=true.
  • Грабли оболочки: в этом окружении ls — алиас на eza (используй command ls), cp спрашивает подтверждение (command cp -f), zsh не режет $args по пробелам (передавай аргументы отдельно), echo ====== ломает команду.

Осторожно: pkill -f/pgrep -f с шаблоном, который есть и в твоей же командной строке, убивает собственный шелл; не используй pkill chrome — закроет браузер владельца. Останавливай по PID или по точному признаку.

Деплой

Браузер ──https/wss──▶ Nginx Proxy Manager (SSL, домен на duckdns) ──http──▶ контейнер 192.168.10.110
                                                                          nginx :80
                                                                            ├─ /    → /opt/grid-world (статика игры)
                                                                            └─ /ws  → 127.0.0.1:9080 (игровой сервер)

Сервер: root@192.168.10.110, Debian 12, контейнер LXC в Proxmox, 1 ядро, 1 ГБ RAM, ssh по ключам. Нет curl, rsync, ufw; есть python3. В Nginx Proxy Manager включено Websockets Support (без него /ws не работает). Боевой конфиг nginx: deploy/nginx.conf (/etc/nginx/sites-enabled/godot_game); обязателен gzip для .wasm/.pck (иначе 39,5 + 21 МБ уходят как есть) и заголовки COOP/COEP.

Клиент: tools/export_all.sh, затем коммит и пуш в grid-world (main). Gitea (git.ibrash.duckdns.org, aibrash/grid-world) по пушу запускает .gitea/workflows/deploy.yml: ssh на сервер и git pull в /opt/grid-world. Пакеты карт выкладываются вместе с index.*.

Сервер: отдельный минимальный проект в /opt/my-world-server/ (project.godot + net/ + официальный бинарник Godot 4.7.2 linux x86_64 godot; бинарник из Fedora на Debian не подходит, берётся из github.com/godotengine/godot-builds). Служба systemd my-world.service (deploy/my-world.service, DynamicUser, Restart=always).

tools/deploy_server.sh root@192.168.10.110     # бэкап, копирование net/ и project.godot, пересборка кэша классов, перезапуск
ssh root@192.168.10.110 'journalctl -u my-world -f'   # лог: «+ игрок …», «[карта] камень считает игрок …»

Трогать сервер нужно только при изменении net/*. Порядок при изменении RPC: сначала пуш grid-world, сразу следом deploy_server.sh; игрокам — Ctrl+F5. Откат сервера: бэкап в /opt/backup-my-world-server-<дата>/.

Сейчас на сервере ещё старая раскладка (/opt/my-world-server/Net/, заглавная). deploy_server.sh при следующем запуске переведёт её на net/. Код сети при этом по протоколу не изменился.

Git

  • Репо проекта: git@git.ibrash.duckdns.org:aibrash/my-world.git, основная ветка master; коммиты на русском с трейлером Co-Authored-By.
  • Пушить и выкладывать на сайт — только по прямой просьбе владельца. Коммитить локально можно.

Что выяснено по ходу работы

  • Окружение: в рендерере Compatibility объёмный туман не работает (удалён из песочницы). SSAO и glow работают (неоновое свечение), оставлены. В Backrooms включён обычный туман (Environment.fog_*), он работает.
  • Память: раньше вкладка показывала около 1,6 ГБ; в окт 2026 владелец замерил ~300 МБ (после замены капсул на модель со скелетом, новых карт и всех правок этой сессии). Что именно помогло — неизвестно (гипотезы прежних замеров: несжатые текстуры, HDR-небо, .ogg целиком в памяти). Проблему считаем снятой; повторно измерять при добавлении тяжёлого контента (небо Parkour/Race ~6 МБ в видеопамяти каждое). Гипотезы: рост WASM-памяти, несжатые текстуры, HDR-небо 2k, .ogg, распаковываемый целиком. Измерения с тех пор не повторялись.
  • Материал Верити — две поверхности: жёлтая основа и диск с лицом (текстура свечения на белой основе). Белая основа на ярком свету даёт бледность, поэтому emission_energy делает материал чисто светящимся (чёрная основа).
  • Backrooms: двери ~1,8 м, поэтому преследователь там масштабом 0,5 и навмеш запечён для агента радиусом 0,6 (иначе проход закрыт). Самый дальний зал за узкой дверью недостижим.
  • Ввод: Input.is_key_pressed(KEY_W) проверяет символ, на русской раскладке это «Ц». Только действия InputMap на физические клавиши.
  • Пресеты: при отключённом extensions_support — один index.wasm; включение даёт ещё index.side.wasm 44 МБ.

Заметки для будущего меня

Договорённости с владельцем (важно не нарушать)

  • Коммитить локально можно, пушить и выкладывать — только по прямой просьбе. Сервер выкладывать нужно лишь при правках net/* с изменением RPC.
  • Parkour — мрачная антиутопия (дождь, грязный смог, мокрый бетон, натриевый оранжевый, красный, больной зелёный; надписи-приказы COMPLY/CURFEW). Яркий розово-бирюзовый неон владелец назвал «детским». Туман 0,011 выбран из серии проб (0,02 владелец откатил как слишком густой), цвет тумана светлее фона — тёмный туман лишь затемняет.
  • Сложность Parkour владельцу «нормально»; он просил и «не детский уровень» — если захочет жёстче: зазоры 27–28 м (двойной прыжок + рывок), лазеры в каньоне, быстрее пульсы, реже чекпоинты.
  • Сальто камеры при двойном прыжке делалось и убрано по просьбе (тошнит, не видно куда прыгать). Без просьбы не возвращать.
  • Чекпоинты должны быть заметными (раньше тонкие кольца на полу не нашли): сейчас ворота 5 м с надписью CHECKPOINT.
  • Мой стиль ответов: по-русски, коротко, без перечисления всего подряд; честно говорить, что проверено, а что нет.

Мувсет (замеры ботом, спринт 13 м/с) — нужны при любом проектировании карт: одиночный прыжок 15 м и +1,9 м вверх; двойной 25 м и +3,5 м; прыжок+рывок 19 м; двойной+рывок 28 м; бег по стене 1,8 с (23 м), провал 1,4 м; прыжок от стены ~5 м в сторону. Стенами высоту не набрать: прыжков от стены без бега только MAX_WALL_JUMPS = 2 за полёт, а прыжок с бега по стене почти ничего не даёт — высоту дают двойной прыжок и подтягивание (уступ до ~2,6 м над полом, с двойным прыжком до ~4,5). Шахта-«лифт» из стен невозможна; в Parkour вместо неё башня из уступов.

Подтягивание (player.gd → _try_mantle): цель — центр капсулы на default_capsule_height * 0.5 + 0.05 над поверхностью уступа. Раньше стояло + 0.05 (центр на уровне поверхности): тело оказывалось наполовину в уступе, физика выталкивала назад, и на уступах от 2 м не работало (на всех картах!). Проверка — mantle_test.tscn.

Конвейер Parkour (Blender models/parkour_02/): layout.py (единственный источник раскладки) → build_parkour.py (в Blender: геометрия Solid/Neon/Collision-colonly, пишет parkour_data.gd) → bake.py (blender -b Parkour_02.blend -P bake.py: запекание Cycles в цвета вершин + экспорт GLB, ~1 мин) → build_sky.py (blender -b -P build_sky.py: панорама ~2 мин) → godot --headless --import --path .. Правка раскладки требует всех четырёх шагов; по ходу собирай и смотри картинки (снимки parkour_tour).

  • Освещение — в цветах вершин, а не в динамических источниках. Сетка граней режется на ячейки ≤ 1,6 м (kit.Mesh.grid), поэтому число треугольников растёт быстро: сейчас Solid ~50 тыс. четырёхугольников, Neon ~22 тыс. Фон и дно — грубая сетка (9–80 м). Не раздувай: веб.
  • glTF хранит цвета вершин нормализованными (до 1), поэтому в bake.py значения сжаты ×0.125, а шейдер baked.gdshader умножает на light_scale = 8. Освещённость без этого обрезается на единице.
  • Яркость уровня: сила света в bake.py (Moon/CityGlow/Smog и цвет мира) × exposure в parkour.gd (2,1) × цвет материала (kit.SOLID и SURFACES в parkour.gd — должны совпадать). Первые попытки были чёрными: средняя запечённая освещённость ~0,05 × альбедо 0,11 → почти ноль.
  • Тексты (kit.Mesh.text): без поворота смотрит в −Y (читается, если смотреть вдоль +Y); для видимой с +X грани нужен yaw = +π/2, с −X — −π/2, для видимой со стороны +Y — π. Ошибка с ориентацией даёт зеркальные надписи. Кривые букв огрублены (resolution_u = 2) — иначе десятки тысяч лишних треугольников.
  • Имена в русском интерфейсе Blender переведены: узлы шейдеров/мира искать по type, не по имени ('Background' не найдётся).
  • Экспорт цветов вершин в GLB требует активного атрибута (export_vertex_color='ACTIVE'); Cycles-запекание в target='VERTEX_COLORS'.
  • Панорама неба: мипы включены в sky.png.import (mipmaps/generate=true, compress/mode=2) — по ним размытые отражения в мокрых полах (textureLod в шейдере). Дно города в панораме — отдельный материал: общий шейдер дымки на огромной плоскости давал белое.
  • Не мерялось: нагрузка Parkour в браузере (~145 тыс. треугольников + дождь 2200 частиц, небо ~6 МБ в видеопамяти), звук (шум дождя, гул, песня) на слух.
  • Расходный ресурс: mcp__blender__look очень дорог по токенам (сотни тысяч за сессию) — снимки просматривай из Godot (parkour_tour), в Blender смотри только геометрию.

Race — что выяснено

  • Имя ready у переменной в Node занято сигналом движка (Node.ready): флаги готовности названы flags_ready.
  • Данные ворот — центр капсулы на полу: ворота ставим на pos.y - 1.2; слоты на 1,25 м, при заморозке игрок оседает на ~9 см (это нормально).
  • Маршрут бота route.json проходит ворота ровно по центру; точки маршрута на воротах бот обязан «брать» только после пересечения плоскости (иначе срезает угол, как делал мой первый бот).
  • В --write-movie с canvas_items+expand видимая область шире окна, и подписи у левого края обрезаются; HUD слева отступает на 64 px.
  • Кадры гонки: race_tour.tscn --mode=hud (R, отсчёт, пробег, итоги). Подробности — раздел «Карта Race».

Дорожная карта

Телефон — не планируется

Владелец решил, что телефон не нужен (окт 2026). Если когда-нибудь вернёмся: нужны сенсорный стик и зона обзора (ввод уже сведён к действиям InputMap, достаточно Input.action_press), ориентация и полноэкранный режим по касанию, экран «Нажми, чтобы начать» (аудио в браузере без жеста не стартует), мобильные текстуры ETC2/ASTC (vram_texture_compression/for_mobile; удвоит пакеты — делать отдельными *.mobile.pck), отключение SSAO/glow и scaling_3d_scale 0,6–0,75, восстановление после ухода вкладки в фон (submit_map заново). Сначала измерить память и FPS на реальном устройстве.

Ближайшее

  • Ники и текстовый чат (выбор владельца, окт 2026): ник игрока вместо «Игрок N» над призраком, в итогах гонки и в HUD; строка чата (физическая клавиша, работает на любой раскладке) в комнате карты. Потребует новых RPC → выкладка клиента и сервера вместе, Ctrl+F5 игрокам.

Идеи (оценка независимого консультанта, окт 2026; не обязательства)

  • Parkour/Race: режим «время и призрак» (запись лучшего забега, показ призраком), старт гонки по таймеру при зависшем «готов» (сейчас один не нажавший R блокирует всех), развилки Race с большим выигрышем, спектейт для мёртвых, приватные комнаты по ссылке.
  • Мувсет и ощущение: анимация приземления, звуки шагов/приземления/подката/бега по стене/рывка (самый заметный эффект при малых затратах), настройки и меню паузы (чувствительность, громкость, FOV).
  • Контент и социальное: игрок в роли преследователя, голос по близости, подбираемые предметы/двери, гроза со вспышкой молнии на картах, кастомизация персонажа.
  • Инструменты: номер версии протокола (чтобы старый клиент получал «обнови страницу»), автопроверка пакетов в CI, одна команда «собрать карту», экран credits (CC-BY Backrooms VR).
  • Серверные проверки (антиспуфинг позиций), если игроков станет много.

Персонаж

Модель и анимации генерирует скрипт Blender models/character_01/build_character.py (Blender 5.x: exec в окне или blender -b -P). Он строит меш из гранёных сегментов (жёсткие веса: одна кость на сегмент), арматуру из 17 костей, считает позы и пишет 14 анимаций (idle walk run crouch_idle crouch_walk slide jump fall air_jump dash wallrun_R wallrun_L mantle death), ноги ставятся через IK. Экспорт: GLB, export_animation_mode=NLA_TRACKS, в player/character/character.glb. Материалы: Suit (тёмный) и Glow (свечение; в игре заменяется цветом игрока).

  • Выбор анимации — Ghost._choose(): смерть > подтягивание > рывок > бег по стене > воздух > присед/подкат > бег/ходьба/стоит. Скорость шага подгоняется speed_scale (REF_SPEED), между анимациями смешивание 0,15 с. Голова повторяет взгляд вверх-вниз.
  • Своё тело: та же модель стоит в player/player.tscn (видна в редакторе, отбрасывает тень), но спрятана от своей камеры слоем 2 (player.gd → _ready). Не анимируется.
  • Выкладка сервера: ghost.gd лежит и на сервере, где модели нет, поэтому модель грузится load() внутри setup(), а не preload.
  • Изменить позы: правь функции анимаций в скрипте, перезапусти его в Blender, заново экспортируй GLB и импортируй в Godot.

Журнал изменений (окт 2026)

Сделано одной серией работ (хронологически):

  1. Персонаж: модель киберниндзя со скелетом (752 треугольника, 14 анимаций, IK ног) вместо капсул; цвет свечения от id игрока; свой персонаж спрятан от своей камеры. Сальто камеры при двойном прыжке делалось и убрано по просьбе.
  2. Подтягивание на уступы починено (не работало от ~2 м на всех картах), замеры мувсета и тестовые сцены в tools/test/.
  3. Parkour: подъём по мегабашне (крыши, каньон, башня, лазерный зал, контейнеры, спираль, вершина), запечённый свет, панорама неба, мокрый блеск, дождь, туман, песня; мрачная антиутопия.
  4. Race: гонка на ногах (трасса 633 м из Blender, три развилки, 8 ворот), правила и HUD, новые сетевые события, песня на «СТАРТ!»; сделано агентами, проверено двухклиентным тестом на локальном сервере.
  5. Инфраструктура: пресеты экспорта карт (с include_filter для файлов, грузимых по строке), проверка пакетов «по существу», выкладка сайта и сервера по порядку из «Деплой».
  6. Замеры: вкладка в браузере ~300 МБ.

Лицензии и авторство

  • Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), CC-BY 4.0 (maps/backrooms/model/CREDITS.md). При публикации нужно указывать автора.
  • Остальные модели и звуки — материалы владельца проекта.
S
Description
No description provided
Readme
134 MiB
Languages
GDScript 87.6%
Python 8.8%
Shell 1.9%
GDShader 1.7%