# 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**; выход из карт обратно в хаб | | ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) | | ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, **комнаты по картам** (в разных картах не пересекаются) | | ✅ | Преследователи: **камень** и **Верити**; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока | | ✅ | Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!» | | ✅ | Паркур: спринт, рывок, подкат, **присед (C)**, бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ) | | ✅ | Управление **не зависит от раскладки** (русская и любая другая): действия `InputMap` на физические клавиши | | ✅ | Деплой: CI из `grid-world` на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера | | ⏳ | Телефон: **пока не поддерживается** (нет сенсорного управления, не проверена производительность) — план в разделе «Дорожная карта» | | ❌ | Чата, ников, настроек (чувствительность мыши, громкость), меню паузы | | ❌ | Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает | | ❌ | Сохранения прогресса, аккаунтов | | ❌ | Автоматических тестов в CI (есть боты для ручного прогона, см. «Тесты без окна») | | ✅ | Чужой игрок — модель киберниндзя со скелетом (`player/character/character.glb`, 752 треугольника, 17 костей, 14 анимаций); свечение красится в цвет игрока от его id. Своё тело — та же модель, спрятанная от своей камеры | ## Быстрый старт ```bash # запуск игры локально (стартует в хабе; без сервера — одиночная игра, клиент переподключается каждые 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`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются). ## Управление Клавиши привязаны к **физическому расположению** (так работает и на русской раскладке). Действия лежат в `project.godot → [input]`: `move_forward/back/left/right`, `jump`, `sprint`, `crouch`, `dash`. | Клавиша | Действие | |---|---| | W A S D, стрелки | движение | | Shift | спринт | | Пробел | прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок | | C (держать) | **присед** (скорость 3,5, капсула вдвое ниже, пролезает под низкое) | | Shift + C на бегу | **подкат** | | Q | рывок | | ЛКМ (зажать) | зум | | 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/ (здания, башня, обелиск, деревья, Эмпайр-стейт…) 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`), играет по кругу | **Правка и пересборка 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}` — портал появится сам. ## Преследователи `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` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов. ## Экспорт, тесты и деплой ### Экспорт ```bash 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`. ### Проверка сборки как на сервере ```bash 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` — локальный сервер в боевой раскладке. ```bash 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`). ```bash 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 ГБ**, хотя на десктопе сцена занимает ~115 МБ. Причина не найдена (нужны данные Chrome: `Shift+Esc`). Гипотезы: рост 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 смотри только геометрию. ## Дорожная карта ### Цель: играть с телефона Сейчас игра рассчитана на клавиатуру и мышь. Что нужно сделать, по порядку: 1. **Сенсорное управление.** Основа уже есть: ввод игрока сведён к действиям `InputMap`, поэтому сенсорные кнопки просто вызывают `Input.action_press/release("move_forward", strength)` и т. д. Нужна сцена `ui/touch_controls.tscn` (CanvasLayer): виртуальный стик слева (движение), зона перетаскивания справа (обзор), кнопки прыжка, спринта (переключатель), приседа (удержание), рывка, зума. Показывать только на сенсорных устройствах (`DisplayServer.is_touchscreen_available()` и feature-теги `web_android`/`web_ios`). В `player/player.gd` обзор сейчас считается из `InputEventMouseMotion` при захваченной мыши — добавить обработку `InputEventScreenDrag`; зум по ЛКМ заменить действием `zoom`. 2. **Ориентация и экран:** принудительно альбомная (`display/window/handheld/orientation`), полноэкранный режим по первому касанию (браузеры требуют жест), масштаб интерфейса (надписи «ПОЙМАЛ!», экран загрузки сейчас рассчитаны на десктоп), безопасные зоны. 3. **Звук:** браузер не стартует `AudioContext` без касания. Добавить экран «Нажми, чтобы начать» перед хабом (заодно решает и предупреждение на десктопе). 4. **Текстуры (главное узкое место):** мобильные GPU не поддерживают S3TC/BPTC, которыми сейчас сжаты текстуры (`vram_texture_compression/for_desktop=true`, `for_mobile=false`). Нужно включить `for_mobile` (ETC2/ASTC) — это **удваивает размер пакетов**, если держать оба формата в одном `.pck`. Варианты: отдельные пакеты `*.mobile.pck`, выбираемые по feature-тегу, и уменьшенные текстуры (27 текстур Backrooms по 2К — тяжёлые). 5. **Производительность и память:** на телефоне отключить SSAO/glow (в Compatibility они дорогие), уменьшить `Viewport.scaling_3d_scale` (0,6–0,75), меньше деревьев и птиц, теней нет у деревьев уже сейчас. Мобильный Safari убивает вкладки около 1–1,5 ГБ, а на десктопе она уже занимала 1,6 ГБ — **сначала разобраться с памятью** (см. выше). Проверить размер `.wasm` и распаковку (на сотовой сети 40 МБ чувствительны; `index.wasm` уже жмётся gzip). 6. **Сеть:** вкладка уходит в фон и замирает — владельца преследователей сервер уже передаёт другому через 3 с; клиенту при возвращении стоит показывать «переподключение» и заново входить в комнату (`submit_map`), если соединение сброшено. 7. **Тестирование:** режим устройства в Chrome DevTools; на реальном телефоне — `tools/serve_web.py` по локальной сети (`http://:8080`, привязать сервер к `0.0.0.0`). Минимальная планка: 30 FPS в хабе и песочнице на среднем Android. ### Другое - Ники по выбору игрока, текстовый чат, анимация приземления, свои руки/тело от первого лица, настройки (чувствительность, громкость), меню паузы. - Серверные проверки (антиспуфинг позиций), если игроков станет много. - Новые карты (добавляются по инструкции выше) и новые преследователи. - Автоматизировать прогон ботов в CI и проверку «пакет не пустой» после экспорта. ## Персонаж Модель и анимации **генерирует скрипт** `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. ## Лицензии и авторство - Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), **CC-BY 4.0** (`maps/backrooms/model/CREDITS.md`). При публикации нужно указывать автора. - Остальные модели и звуки — материалы владельца проекта.