- Геймпад: раскладка под паркур и гонки, обзор правым стиком, навигация по меню (A/B/крестовина) - Меню паузы (Esc/Start): выход в хаб, настройки (мышь, стик, FOV, громкость, инверсия), справка по управлению - Хаб 48x32x10 м: колонны, окна с городом, голограмма, лавки; музыка «Local Forecast - Elevator» (Kevin MacLeod, CC-BY) - Тесты: pause_test, pad_menu_test, hub_tour; README обновлён Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
108 KiB
My World
Кароче это пишу я, владелец карты
- Управление геймпадом
- Новую карту гонки, стиль тот же киберпанк, надо чтобы было длиньше первой, и упор шел на маневренность и скорость
- Чтобы на Tab показывался список игроков онлайн
Мультиплеерная игра от первого лица в браузере (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 2; выход из карт обратно в хаб |
| 🔶 | Race 2 «Верхние ярусы» — вторая гонка (крыши и мосты над городом, бег по фасадам, лазерные мосты, спуск, башня; три пути на каждом участке; вентиляторы и ускоряющие полосы). Фаза 1 готова (раскладка, геометрия, проходимость ботами, правила); фаза 2 (чистовое запекание, своё небо и музыка) ждёт одобрения владельца — раздел «Карта Race 2» |
| ✅ | Race — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); Parkour — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня») |
| ✅ | Вкладка в браузере занимает ~300 МБ (замер владельца, окт 2026) |
| ✅ | Карты качаются отдельными пакетами .pck при первом входе в портал (экран загрузки с прогрессом) |
| ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, комнаты по картам (в разных картах не пересекаются) |
| ✅ | Преследователи: камень и Верити; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока |
| ✅ | Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!» |
| ✅ | Паркур: спринт, рывок, подкат, присед (C), бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ) |
| ✅ | Управление не зависит от раскладки (русская и любая другая): действия InputMap на физические клавиши |
| ✅ | Деплой: CI из grid-world на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера |
| ✅ | Геймпад Xbox (раскладка под паркур и гонки, обзор правым стиком), меню паузы по Esc/Start (выход в хаб, настройки, справка), настройки (мышь, стик, FOV, громкость, инверсия) — разделы «Управление», «Меню паузы и настройки». В браузере с живым геймпадом не проверялось (только тест без окна) |
| ✅ | Ники (панель по N, подпись над игроками, в итогах гонки) и общий рейтинг лучших времён на сервере (топ-10 на Race/Parkour, таблица по Tab, доски в хабе, таймер и итоги Parkour, сдача в гонке на G) — раздел «Ники и рейтинг» |
| ❌ | Текстовый чат — запланирован, не сделан (план в «Дорожная карта → Ближайшее») |
| ✅ | Хаб увеличен (48×32×10 м) и оформлен, в нём играет музыка «как в лифте» — раздел «Карты и порталы», «Лицензии» |
| ❌ | Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает |
| ❌ | Сохранения прогресса, аккаунтов |
| ❌ | Автоматических тестов в 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 — «готов» в гонке, перезапуск забега в Parkour), nick (N), leaderboard (Tab), giveup (G). Действия, которых нет в project.godot, добавляет core/settings.gd кодом при старте: pause (Esc, Start), zoom (LT, R3), look_left/right/up/down (правый стик) и все кнопки геймпада.
| Клавиша | Действие |
|---|---|
| W A S D, стрелки | движение |
| Shift | спринт |
| Пробел | прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок |
| C (держать) | присед (скорость 3,5, капсула вдвое ниже, пролезает под низкое) |
| Shift + C на бегу | подкат |
| Q | рывок |
| R | «готов» (карта Race); на карте Parkour — забег заново (на старт, таймер сброшен) |
| G (держать 1,5 с) | сдаться в гонке (только пока идёт гонка; отпустил раньше — отмена) |
| N | ввод/смена ника (панель по центру; Enter — подтвердить, Esc — пропустить/отмена); на время ввода игрок стоит |
| Tab | таблица лидеров текущей карты (топ-10 и «Вы: N-й»); в хабе и на картах без рейтинга — вкладки по картам, A/D или стрелки; повторный Tab или Esc закрывают |
| ЛКМ (зажать) | зум |
| Esc | меню паузы (мышь освобождается); «Продолжить» или повторный Esc — назад в игру и захват мыши |
| клик по игре | захватить мышь |
Геймпад Xbox (правый большой палец всегда на стике обзора, поэтому прыжок продублирован на LB, а рывок на RB):
| Кнопка | Действие |
|---|---|
| Левый стик / правый стик | движение / обзор (кривая 1,6: точно у центра, быстро на краю; скорость — в настройках) |
| A или LB | прыжок (у уступа — подтянуться, у стены — от стены, в воздухе — двойной) |
| RT или L3 | спринт (держать) |
| X или RB | рывок |
| B (держать) | присед; RT + B на бегу — подкат |
| LT или R3 | зум |
| Y | «готов» в Race; забег заново в Parkour |
| Back | таблица лидеров (Tab) |
| Крестовина вниз (держать 1,5 с) | сдаться в гонке |
| Start | меню паузы (в меню: крестовина/левый стик, A — выбрать, B — назад) |
Ник по геймпаду меняется в меню паузы. Бег по стене — в воздухе у стены с зажатым «вперёд» на скорости. Ctrl не используется: в браузере Ctrl+W закрывает вкладку.
Структура проекта
project.godot, export_presets.cfg
branding/ splash.png (заставка загрузки), icon.svg (иконка, фавиконка) — «Aman's World»
net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт)
net.gd состояние комнат, преследователей, RPC, профиль и ники, рейтинг на сервере (см. «Сеть», «Ники и рейтинг»)
ghost.gd чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник
server.tscn пустая сцена для запуска выделенного сервера
core/
map_loader.gd автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт»)
settings.gd автолоад Settings: настройки (user://settings.cfg) и раскладка геймпада в InputMap
pause_menu.gd автолоад PauseMenu: меню паузы Esc/Start (см. «Меню паузы и настройки»)
overlay.gd автолоад Overlay: панель ника (N) и таблица лидеров (Tab) поверх любой карты
lb_view.gd LbView: таблица рейтинга карты (панель Tab, итоги Race/Parkour) и общий стиль панелей
player/
player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение
character/ character.glb — модель киберниндзя и 14 анимаций (исходник и скрипт сборки: `../Blender models/character_01/`)
objects/
portal.gd портал на другую карту (Area3D, рисуется кодом)
vent.gd вентиляционный поток (Area3D): тянет игрока вверх, горизонтальная скорость сохраняется (Race 2)
boost_strip.gd ускоряющая полоса (Area3D): добирает скорость до MAX_SPEED вдоль полосы (Race 2)
chasers/
chaser.gd общий «мозг» преследователя: навмеш, зрение, погоня, блуждание, поимка, синхронизация
rock/ камень: rock.tscn, Rock.glb (+ извлечённая текстура), Guitar.ogg
verity/ Верити: verity.tscn, verity2.glb (+ текстура), verity.ogg (звук из видео)
maps/ у каждой карты своя папка — она самодостаточна и экспортируется отдельным пакетом
hub/ стартовая комната: hub.tscn, hub.gd (порталы из списка PORTALS, музыка), hub_decor.gd (оформление), audio/music.ogg + CREDITS.md
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 2**), race_data.gd (генерируется), baked.gdshader, race.glb, sky.png, audio/music.ogg
race2/ race2.tscn, race2.gd (внешний вид, ворота, лазеры, вентиляторы, полосы), race2_data.gd (генерируется), baked.gdshader, race2.glb, sky.png, audio/music.ogg; правила берёт из maps/race/race_rules.gd
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 |
в основном пакете | «Лобби»: зал 48×32×10 м, строится кодом (hub.gd — стены, порталы, доски, музыка; hub_decor.gd — оформление). Порталы в ряд у северной стены с шагом 6 м (влезает 7; SANDBOX x=-12, RACE 2 x=12, z=-15), игрок появляется на (0, 1.2, 11). В центре платформа с вращающейся голограммой, пол с латунными кольцами и дорожкой к порталам, 6 колонн с неоновыми прорезями, потолочные светильники (свечение — glow), в боковых стенах шесть окон с ночным городом (шейдер), лавки (на них можно запрыгнуть), пылинки в воздухе, табло «ЛОББИ» на южной стене. Над порталами карт «на время» — доски с топ-3 (см. «Ники и рейтинг»). Музыка maps/hub/audio/music.ogg («Local Forecast - Elevator», Kevin MacLeod, CC-BY) играет по кругу, плавно нарастая за 3 с, −9 дБ. Безопасна, преследователей нет |
| Песочница | 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). Забег на время (старт — линия на краю стартовой платформы, финиш — вершина; рейтинг на сервере; R — заново). Подъём по мегабашне, ~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» ниже |
| Race 2 | maps/race2/race2.tscn |
race2.pck (9,5 МБ) |
«Верхние ярусы»: гонка по крышам и мостам над городом, 13 ворот, ~1,2 км безопасного пути; на каждом участке безопасный / рискованный / секретный путь; вентиляторы-подбросы и ускоряющие полосы. Правила те же, что у Race (общий код). Раздел «Карта Race 2» |
Правка и пересборка Parkour (всё в Blender models/parkour_02/):
layout.py— единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; стенами высоту не набрать — прыжков от стены без бега только 2 за полёт).build_parkour.py— в Blender (exec): строит коллизию (Collision-colonly— Godot делает из неё StaticBody), детальную геометриюSolid, светящиеся элементыNeon; пишетparkour_data.gd(чекпоинты, лазеры, старт, портал). СохраняетParkour_02.blend.bake.py—blender -b Parkour_02.blend -P bake.py: Cycles запекает прямой+непрямой свет (неон светит сам, плюс холодный/пурпурный/бирюзовый свет) в цвета вершин (сжаты в 8 раз — шейдер умножает обратно) и экспортируетparkour.glb. ~1 мин.build_sky.py—blender -b -P build_sky.py: панорама дальнего города 3072×1536 →sky.png(в импорте включены мипы — по ним размытые отражения). ~2 мин.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), строит ворота-чекпоинты, лазеры (пульсирующие считают фазу по системным часам), окружение и портал.
Добавить карту:
- Создай
maps/<имя>/<имя>.tscn: корень Node3D, внутри игрок (player/player.tscn), портал обратно (objects/portal.gd,target_map="hub",target_scene="res://maps/hub/hub.tscn"), при необходимости преследователи. В_ready()вызовиNet.enter_map("<имя>")(идемпотентно; так сервер знает, в какой ты комнате). - Если нужны преследователи — навмеш: либо
NavigationRegion3D+maps/sandbox/nav_bake.gd(запекание при старте, как в песочнице), либо заранее запечённый.res(как у Backrooms: в вебе без потоков запекать на лету долго). - В
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. - В
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 с, затем ожидание; готовность сброшена, игроки стоят где стояли.
- Зашедший в разгар круга бегает без зачёта, видит «ГОНКА ИДЁТ» (определяется по маякам других игроков) и участвует со следующего круга.
- Сдача (G, удержание 1,5 с): индикатор «СДАЮСЬ…» с полоской внизу, отпустил раньше — отмена. Только в фазе «гонка», у не финишировавшего. Сдавшийся возвращается на стартовую площадку (свободно бегает до конца круга), в итогах стоит ниже всех со строкой «сдался», время не пишется ни в итоги, ни в рейтинг. Сдавшиеся считаются завершившими для конца круга (все финишировали/сдались — итоги сразу; иначе 45 с после первого финиша), места остальных считаются без них.
- Срыв: ниже последних пройденных ворот на 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 фаза, 11 сдался | время финиша или -1 | «маяк» раз в секунду и при смене фазы; по нему догоняют потерявшие событие и зашедшие позже |
RACE_GIVEUP |
— | время гонки на момент сдачи | игрок, сдавшийся (G) |
Время: на получении START остаток считается как b - unix_now; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = (ticks - start_tick), финиш b пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник — Net.nick_of(id) (как над призраком), цвет — Color.from_hsv(id*0.618034…). В итогах справа — общий рейтинг карты (LbView), на финише клиент шлёт Net.submit_my_record(rating_id, время) .
Проверка по существу (всё в 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_giveup_test.py— сдача: A отменяет удержание и сдаётся, B финиширует; итоги одинаковы, в рейтинге только B; во втором круге сдались оба — круг сразу закончился.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 менялся при Race (submit_race/recv_race) и при никах/рейтинге (см. «Ники и рейтинг») → после пуша grid-world сразу tools/deploy_server.sh root@192.168.10.110.
Карта Race 2 («Верхние ярусы»)
Состояние: фаза 1 (раскладка, геометрия, ворота, правила, проходимость ботами, предварительный свет). Фаза 2 — после одобрения владельцем: чистовое запекание (384+ сэмплов), своё небо (build_sky.py), своя музыка. Палитра своя (холодная индиго-ночь, пурпур и ледяная бирюза; ворота тусклые индиго / очередные маджента / пройденные циан), небо перерисовано (build_sky.py), свет запечён на 384 сэмплах. Музыка — «On My Way to Hell» (Poloz), с 1:40 исходника (ffmpeg -ss 100 -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 maps/race2/audio/music.ogg, ~3:40, 1,2 МБ; исходник вне проекта: source-assets/music/); играет с «СТАРТ!» с начала по кругу (loop = true, затухание 0,8 с). Стык петли на слух/по уровню не проверен.
Исходники: Blender models/race_02/ (README там же; репозитория у папки нет — файлы не коммитятся). Концепция согласована с владельцем: вертикальность и поток скорости в духе Ghostrunner, но атмосфера как у Race/Parkour (дождь, смог, мокрый бетон, натриевый/красный/больной зелёный свет, надписи-приказы), туман 0,011.
Файлы: maps/race2/ — race2.tscn, race2.gd (внешний вид, ворота со световыми столбами, лазеры, вентиляторы, полосы, срыв, дождь без дождя в траншее и шахте, гул, портал), race2_data.gd (генерируется build_race.py, не править),
race2.glb, sky.png, baked.gdshader, audio/music.ogg. Правила — общие с Race: race2.gd создаёт тот же maps/race/race_rules.gd и передаёт ему data (= race2_data.gd) и rating_id (= "race2"); race_rules.gd берёт данные у карты
(m.get("data"), у Race — запасной load его race_data.gd). Пакет race2.pck (9,5 МБ) собирается пресетом Race2 (в нём include_filter: maps/race/race_rules.gd и файлы карты); скрипты objects/vent.gd и objects/boost_strip.gd — в основном пакете.
Рейтинг: Net.TIMED_MAPS["race2"] = {"min": 44} (≈ 60 % от лучшего круга ботом по всем секретам ~72 с), Net.MAP_TITLES["race2"] = "Гонка 2"; в хабе портал «RACE 2» пятым в ряду.
Новые объекты (локальный эффект на игроке, сетевой код не тронут; в player/player.gd — только хуки apply_vent() и apply_boost() и константы VENT_ACCEL, VENT_MAX_RISE, BOOST_ACCEL, EFFECT_HOLD; остальная физика и MAX_SPEED = 22 не менялись):
- Вентиляционный поток (
objects/vent.gd): пока игрок в столбе, вертикальная скорость растёт до 16 м/с (ускорение 80 м/с²; раньше 11 и 60), горизонтальная сохраняется, двойной прыжок возвращается. Выход из столба на 16 м/с даёт ещё ~13 м высоты (v²/2g), поэтому ступень башни 12,5 м берётся с перелётом ~3 м; ступени 14–15 м в пробах ботом вели к промахам и оставлены 12,5. Шахта-лифт — скрытый поток (hidden: без столба, щелей и света), над окном знака нет. Визуал: тёмная решётка со светящимися щелями, мерцающий зелёный столб, слабый свет. - Ускоряющая полоса (
objects/boost_strip.gd): набор скорости вдоль полосы до 22 м/с (не вышеMAX_SPEED), потом на земле она гасится как обычно (14 м/с²), в воздухе сохраняется. Измерено: на 22 м/с одиночный прыжок 25 м, двойной 42 м; рывок сбрасывает скорость до 13. Стрелки пурпурно-белые, бегут по ходу (статичные ледяные — подсказки трассы).
Участки и три пути (подробности и координаты — Blender models/race_02/README.md, layout.py): старт на верхней платформе (8 слотов, шаг 2,8 м) → 2) мосты и крыши между башнями → 3) фасады: бег по четырём стенам → 4) площадь с краном → 5) пульсирующие лазерные мосты → 6) спуск по крышам → 7) башня-спираль из четырёх вентиляторов; на крыше ядра — финиш и портал.
Безопасный путь 1085 м до подножия башни + 136 м башня = 1221 м (план ~1100; можно сжать зоны 4 и 6), 13 ворот (ширина до 100 м там, где ветки расходятся широко), круг бота по безопасному пути ~99 с.
Безопасный: зазоры 8–12 м, подъём до 1,5 м, спуск до 9,2 м. Рискованный: зазоры 16–24 м (двойной прыжок + рывок), бег по стенам. Секретный: скрытые тёмные площадки без подсветки и входы без подписей — окно «пустого квартала», траншея под ямой (подкат), техпроход под лазерами, стена w6, шахта-лифт.
Срыв ниже последних ворот на 32 м = «СОРВАЛСЯ!», возрождение на воротах (центр каждых ворот лежит на плите — проверено).
Проверка ботами (Blender models/race_02/tests/, run.sh; в headless с --fixed-fps 60 — быстрее реального времени). Все ветки пройдены (результаты ниже — последний прогон, оценочные времена бота от одной точки зоны до другой, секунды):
| Зона | безопасный | рискованный | секретный | выигрыш секрета |
|---|---|---|---|---|
| 2 мосты | 13,2 | 10,8 | 9,5 | 3,7 |
| 3 фасады | 14,5 | 9,9 (4 стены, 4 из 4 попыток) | 10,3 | 4,2 |
| 4 площадь | 12,8 | 7,6 | 7,8 | 5,0 |
| 5 лазеры | 10,1 | 11,3 | 9,1 | 1,0 (бот не теряет времени на ритм лазеров) |
| 6 спуск | 15,5 | 12,4 | 10,9 (стена w6: 3 из 4 попыток, с запасом в 1,5 м не проходит) | 4,6 |
| 7 башня | 17,4 | — | 9,5 | 7,9 |
Итого все секреты вместе экономят ~26 с из ~99 (оценка бота; человек на секретах рискует сорваться на последние ворота). Правила на race2: MAP=race2 tools/test/race_game_test.sh (бот проводит игроков через ворота телепортами по 0,35 м за кадр, на лазерном мосту выбирает полосу — проверяются отсчёт, места, итоги, один START, возрождение лицом по трассе, зритель; проходимость проверена отдельно). Рейтинг этим тестом не проверяется: круг бота ~60 с — запись возможна, но тест рейтинг не сверяет.
Пакет: godot --headless --export-pack Race2 /tmp/x/race2.pck, затем strings по sky.png, baked.gdshader, race2_data, race_rules.gd, music.ogg, race2.glb, race2.gd (каждое > 0; vent/boost_strip в этом пакете нет — они в основном). Через портал хаба: tools/test/race2_check.txt.
Что не сделано / известно: после смены силы вентиляторов (11 -> 16 м/с) проходимость башни и траншеи проверена ботом только выборочно (cmp7_safe, secret7, cmp4_secret, risky4 прошли), полный прогон и таблица времён выше — до этой правки, не перемерены; подножие башни всё ещё тёмное; race2.glb 8,4 МБ против 5,2 у Race (в фазе 2 прореживать фон); у зон 1 и 7 нет рискованной ветки; зазор для безопасного пути на спуске к lane — 8 м при спуске 9,2 м; не мерялись: нагрузка в браузере и звук. Выкладка: net/net.gd изменён только константами (TIMED_MAPS, MAP_TITLES), RPC те же — но сервер без нового net.gd не примет рекорды race2 (запись отклонит: карты нет в TIMED_MAPS), поэтому после пуша grid-world запустить tools/deploy_server.sh; порядок и бэкап — как в «Деплой».
Ники и рейтинг
Профиль — user://profile.cfg (ConfigFile, секция profile): client_id (случайный 128-битный hex, 32 символа; создаётся при первом запуске, не показывается, опознаёт игрока в рейтинге; потерял файл/очистил данные сайта — стал новым игроком), nick, asked (уже спрашивали ник). В тестах путь задаётся -- --profile=путь.
Ники. При первом запуске в хабе — панель «Введите ник» (≤ 16 символов, Enter — подтвердить, Esc — пропустить: тогда «Игрок N» и больше не спрашиваем). Клавиша N (физическая, действие nick) в любой момент открывает панель смены с текущим ником; вставка и кириллица работают. Нормализация (Net.normalize_nick, один код на клиенте и сервере): \n \r \t -> пробел, прочие управляющие и невидимые символы (в т.ч. направляющие юникода, нулевой пробел) выбрасываются, пробелы по краям обрезаются, ≤ 16 символов, пустой -> «Игрок N» (N = id % 1000). Одинаковые ники в комнате допустимы. Чужой ник везде выводится обычным Label/Label3D (не RichText) — «[b]» оформление не ломает. Единая точка: Net.nick_of(peer_id) (свой — Net.my_nick), сигнал Net.nick_changed(id). Где виден: подпись над призраком (ghost.gd), HUD и итоги гонки, таблицы рейтинга.
- Сеть: после подключения клиент шлёт
submit_profile(client_id, nick)раньшеsubmit_map; при смене ника — снова. Сервер хранитpeer_client(peer -> client_id) иclient_nick; при входе в комнату (submit_map) присылает вошедшемуrecv_nickвсех присутствующих, а им — его; смену ника рассылает комнате; на выходе чистит. Клиент принимаетrecv_nickтолько приmap_readyи очищаетNet.nicksв_reset_room_state()— ники не просачиваются между комнатами (как состояние игроков). - Блокировка ввода на время набора:
Net.input_blocked(ставитOverlay). Игрок читает ввод опросом, поэтомуplayer.gdприinput_blockedстоит как при заморозке и не ловитcrouch/dash, а клик мыши не захватывает курсор. Это отдельно отplayer.input_frozen(его ведёт гонка на отсчёте; закрытие панели его не сбрасывает). R/N/Tab/G в поле — обычные буквы/не действия; открывающая клавиша в поле не попадает (фокус берётся отложенно). После закрытия мышь снова захвачена.
Рейтинг (сервер). Карты «на время» и допустимые времена — константа Net.TIMED_MAPS в net/net.gd:
const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}}
const MAX_RECORD_TIME := 3600.0
Минимумы — страховка от мусора, не античит: время быстрее min или больше 3600 с сервер молча отбрасывает. Подкрутить — поправить числа (и выложить сервер вместе с клиентом, см. «Деплой»; клиент использует тот же список, чтобы знать, где рисовать доски).
submit_record(map_id, time): клиент сообщает свой результат (сервер берётclient_idи ник по peer). Проверки: карта из списка и игрок сейчас в её комнате, время конечное и в пределах, есть профиль, лимит 6 записей в минуту на игрока (лишнее молча отбрасывается). Хранится лучшее время каждогоclient_id(худшее не затирает; ник обновляется при смене ника и/или улучшении), не более 100 записей на карту (худшие вытесняются). Ответ:recv_record_result(map_id, time, rank, personal, map_record)(место, «новый личный рекорд», «новый рекорд карты» = личный и 1-е место) иrecv_leaderboard.request_leaderboard(map_id)->recv_leaderboard(map_id, top, my_rank, my_time):top— до 10 записей[ник, время, unix-ts]по возрастанию (при равенстве — кто раньше),my_rank— место запросившего (0 — нет записи),my_time— его лучшее. Лимит 12 запросов за 10 с на игрока. Клиент хранит ответы вNet.boards[map_id], сигналNet.leaderboard_updated(map_id).- Хранилище:
user://leaderboard.jsonна сервере. На боевом сервереuser://=/var/lib/my-world/.local/share/godot/app_userdata/world2-server/(службаmy-world:StateDirectory=my-world,HOME=/var/lib/my-world, DynamicUser) — папка постоянная, переживает перезапуск службы иtools/deploy_server.sh(тот копирует толькоnet/иproject.godot). Формат:
{"version": 1, "maps": {"race": {"<client_id>": {"nick": "Алиса", "time": 52.318, "ts": 1793250000}}, "parkour": {}}}
Запись атомарная (во временный .tmp, затем переименование), не чаще раза в 2 с (SAVE_EVERY; первая после паузы — сразу). Чтение при старте; отсутствующий файл — пустой рейтинг; испорченный — сервер не падает, файл переименовывается в leaderboard.json.bad, рейтинг начинается заново; при загрузке отбрасываются неизвестные карты, неверные id и времена, лишнее сверх 100 на карту. systemctl stop/restart не сбрасывает отложенную запись (Godot по SIGTERM не присылает уведомление о закрытии): в худшем случае теряются результаты последних ≤ 2 с.
- Бэкап / сброс:
ssh root@192.168.10.110 'cp /var/lib/my-world/.local/share/godot/app_userdata/world2-server/leaderboard.json /root/leaderboard-$(date +%F).json'; сброс — остановить службу (systemctl stop my-world), удалить файл, запустить. Проверить, что файл пишется в постоянную папку: после любого результатаssh root@192.168.10.110 'ls -la /var/lib/my-world/.local/share/godot/app_userdata/world2-server/; journalctl -u my-world -n 5 --no-pager'(в логе строка[рекорд] race <ник> <время> (место N)). Если путь не тот —find /var/lib/my-world -name leaderboard.json.
Таблица лидеров (Tab, действие leaderboard). core/overlay.gd + core/lb_view.gd: справа по центру, тёмная полупрозрачная подложка, место / ник / мм:сс.мс, свой ряд подсвечен, вне топ-10 — строка «Вы: N-й, время»; сверху остаётся таймер гонки, в центре — отсчёт. Игрок при показе не блокируется. Закрывается повторным Tab или Esc.
Доски в хабе (hub.gd, _build_boards): над порталами карт из TIMED_MAPS — Label3D на тёмной подложке с топ-3; запрос при входе в хаб и раз в 60 с; нет ответа — «—». У песочницы и Backrooms досок нет.
Parkour: забег. Таймер стартует при пересечении линии на конце стартовой платформы — Blender y = 14 (slab('start', -12, 12, -14, 14, …) в layout.py), в Godot z = -14: константа START_LINE_Z в maps/parkour/parkour.gd (меняете раскладку — поправьте её). Финиш — вершина: Area3D 14×5×14 м над верхом платформы summit, центр Data.PORTAL + (0, 2.5, 3) (в parkour_data.gd PORTAL = центр summit + 3 м по Blender-y; FINISH_SIZE). Смерть и возрождение таймер не останавливают. На финише — submit_record("parkour", время) и панель итогов ~8 с (время, рейтинг, «НОВЫЙ РЕКОРД»). R — телепорт на старт, таймер и чекпоинты сброшены. Таймер маленький, сверху по центру.
Проверка по существу (всё в tools/test/, порт 9080 свободен):
nicks_lb_test.py [папка]— локальный сервер (егоuser://в папке теста черезXDG_DATA_HOME) и клиентыlb_client.tscn: ники у двоих, у позже зашедшего, смена, изоляция комнат, «[b]»; рейтинг: лучшее время без дублей, сортировка, топ-10 при 14 игроках, неверные записи, лимиты, повторный вход, перезапуск сервера, испорченный файл, обрезка 120 -> 100, подписи над призраками в настоящей сцене хаба; в концеnick_ui_test.tscn(панель ника, блокировка ввода, R/N/Tab/G в поле, Esc, Tab).parkour_run_test.py— забег: линия, таймер, смерть, финиш, отказ слишком быстрому времени, R, запись в рейтинге.ui_tour.tscn --mode=hub|race|parkour— кадры (--write-movie; рейтинг подставлен, сервер не нужен; добавляйте--server-url=ws://127.0.0.1:1, иначе окно подключится к реальному серверу и затрёт подставленное).
Меню паузы и настройки
core/pause_menu.gd (автолоад PauseMenu) + core/settings.gd (автолоад Settings, объявлен раньше Overlay). Меню открывается по Esc / Start (действие pause), игра при этом идёт дальше (мультиплеер), а игрок стоит: Net.input_blocked = true, как при вводе ника; игровые клавиши за меню не работают (меню забирает событие раньше Overlay). Закрывается кнопкой «Продолжить», повторным Esc/Start или B; мышь снова захватывается.
- Главная: Продолжить · Выйти в хаб (только если карта не хаб:
MapLoader.enter("hub", …), хаб в основном пакете) · Таблица лидеров · Сменить ник · Настройки · Управление. - Настройки (
user://settings.cfg, пишется при закрытии меню; кнопка «По умолчанию»): чувствительность мыши ×0,2–3 (кplayer.MOUSE_SENSITIVITY), чувствительность стика ×0,3–2,5 (Settings.STICK_LOOK_SPEED= 3,6 рад/с), поле зрения 60–100° (player.base_fovберётся изSettings.fovкаждый кадр), громкость (шина Master), инверсия вертикали (мышь и стик). Слайдеры и кнопки управляются геймпадом через стандартныеui_*. - Управление: таблица «клавиатура / геймпад».
- Esc в браузере: при захваченной мыши браузер сам снимает захват и событие Esc до игры не доходит. Поэтому
PauseMenu._processоткрывает меню и по факту «мышь была захвачена ≥ 0,5 с и перестала быть» (если не открыт ввод ника и не идёт загрузка карты); после закрытия 0,6 с это не срабатывает (браузер мог отказать в повторном захвате без клика — тогда клик по игре захватит мышь, как раньше). В живом браузере не проверялось. - Геймпад: обзор правым стиком —
player._process(кадр за кадром, не в физике), мышь —player._mouse_look. Движение стиком аналоговое по направлению, но скорость по-прежнему «шаг/спринт» (directionнормализуется), как и раньше с клавиатурой. - Проверка:
godot --headless --path . res://tools/test/pause_test.tscn -- --client --server-url=ws://127.0.0.1:1— раскладка геймпада в InputMap, открытие/закрытие, блокировка, чувствительность и инверсия, FOV, обзор стиком, A и RT, сохранение, «Выйти в хаб». Кадры меню и хаба:hub_tour.tscn(--menu=main|settings|controls).
Преследователи
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) |
сервер → комната | игрок вышел |
submit_profile(client_id, nick) / recv_nick(id, nick) |
клиент → сервер / сервер → комната | профиль и ники (раздел «Ники и рейтинг») |
submit_record(map_id, time) / recv_record_result(map_id, time, rank, personal, map_record) / recv_leaderboard(map_id, top, my_rank, my_time) |
клиент → сервер → клиенту | рекорды |
request_leaderboard(map_id) |
клиент → сервер | запрос таблицы (ответ — recv_leaderboard) |
Флаги состояния (Net.FLAG_*): подкат, бег по стене (+ сторона стены), рывок, подтягивание, «в воздухе». Присед передаётся как подкат; призрак отличает их по скорости. Скорость и двойной прыжок призрак считает сам по смещению позиции — в протокол они не входят.
RPC требуют одинаковые путь узла и скрипт на сервере и клиенте, поэтому net.gd один. Любое изменение RPC — клиент и сервер выкладываются вместе
(старый клиент с новым сервером не работает). Имена вроде rock_owner/recv_rock_owner остались от времени, когда преследователь был один.
Загрузка карт пакетами (веб)
Основной пакет игры (index.pck) — только хаб (с музыкой), игрок, сеть, меню и порталы (около 1,6 МБ, из них 1,2 МБ — музыка хаба). Движок — index.wasm (39,5 МБ, ~10 МБ с gzip).
Карты — отдельные .pck: world.pck (13,6 МБ), backrooms.pck (21,3 МБ). Портал вызывает MapLoader.enter(map, scene, pack, title):
- если сцена уже есть в проекте (редактор, десктоп, пакет уже скачан) — переход сразу;
- иначе игра встаёт на паузу (преследователь не должен убить игрока на экране загрузки), показывается «Загрузка: <имя> N / M МБ»;
- пакет качается
HTTPRequestсCache-Control: no-cache(браузер сверяет файл с сервером), записывается вuser://и подключается черезProjectSettings.load_resource_pack(path, false)(false— файлы основной игры в приоритете, устаревший пакет не затрёт код); - при ошибке на экране и в консоли пишется причина, портал можно повторить.
Грабли, на которые уже наступили:
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
# цепочка карт на реальных пакетах (порталы хаба в ряд на z=-15 с шагом 6 м: SANDBOX x=-12, BACKROOMS x=-6; в `--tp` ниже — SANDBOX):
/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:-12,1,-15
Для картинок: 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):
- Race 2:
MAP=race2 tools/test/race_game_test.sh(правила, см. «Карта Race 2»); боты проходимости —Blender models/race_02/tests/(run.sh,run_all.sh,compare.sh,frames.sh); пакет через портал хаба —tools/test/race2_check.txt. pad_menu_test.tscn— геймпад в меню (фокус, крестовина, A);pause_test.tscn— меню паузы, настройки, геймпад (см. «Меню паузы и настройки»);hub_tour.tscn— кадры хаба и меню паузы.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.wasm44 МБ.
Заметки для будущего меня
Договорённости с владельцем (важно не нарушать)
- Коммитить локально можно, пушить и выкладывать — только по прямой просьбе. Сервер выкладывать нужно лишь при правках
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 смотри только геометрию.
Ники и рейтинг — грабли
- Любой
Input-опрос не знает о LineEdit: блокировать надо самому (Net.input_blocked),input_frozenне трогать (гонка). Клавиши, набираемые в LineEdit, до_unhandled_inputне доходят (поле их съедает), но R/N в правилах всё равно проверяютNet.input_blocked— на всякий случай. Input.parse_input_eventв тестах доставляет события по-настоящему (в поле попадают буквы, действия не срабатывают) — так проверена панель ника без окна. Режим мыши в headless не хранится: захват проверять руками.- Тесты, которым нужен рейтинг, стартуют сервер с
XDG_DATA_HOME=<папка теста>(иначе пишут в~/.local/share/godot/app_userdata/world2-server/разработчика).pkill -fс путём теста убивает собственную оболочку — глуши по PID. - Время записи на сервере сравнивается с
minизTIMED_MAPS: тест Parkour «состаривает» старт на 45 с (run_start_tick -= 45000), иначе пришлось бы бежать минуту. - Хаб стал на 1 м выше (7 м): доски над порталами и «CHOOSE A MAP» над ними иначе перекрывались.
- SIGTERM не сбрасывает отложенную запись рейтинга (≤ 2 с) — см. раздел «Ники и рейтинг».
- Отправка профиля обязана идти раньше
submit_map(обаreliable, порядок сохраняется) — иначе вошедший в комнату покажется остальным без ника. - Правка набора RPC (
submit_profile,recv_nick,submit_record,request_leaderboard,recv_leaderboard,recv_record_result) — клиент и сервер выкладываются вместе.
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».
Race 2 — что выяснено
- Дальность прыжка (ровная плита, бот
race_02/tests/measure.gd): на 13 м/с — 14,7 / 24,9 / 29,1 м (одиночный / двойной / рывок+двойной), на 17 — 19,4 / 32,7 / 37,1, на 22 — 25,1 / 42,3 / 46,5. Рывок сбрасывает скорость доDASH_EXIT_SPEED= 13: разгон полосой до рывка пропадает, после полосы прыгать без рывка. - Бег по стенам съедает высоту: 4 стены каньона отнимают ~9 м — низ каньона (и всё после него) сделан на 9,2 м ниже крыш (
layout.ZP); при 3 стенах хватало 4,3 м. - Вентилятор шириной 6 м при горизонтальной скорости 13 м/с держит игрока ~0,46 с: чтобы подняться на 12,5 м, нужны сильный поток (
VENT_ACCEL = 60,VENT_MAX_RISE = 11) и двойной прыжок. Шахта-лифт работает потому, что у шахты есть стены: игрок упирается в них, гасит скорость и поднимается весь путь. - Подкат под плитой: центр капсулы стоит на 1,18 м над полом, полная высота ~2,4 м — под мостом нужен просвет ≥ 2,5 м (2,2 м не пролезть). Подкат с 22 м/с стартует со скорости ~27 и скользит ~55 м.
- Центр ворот лежит на плите: возрождение происходит ровно там. Широкие ворота на разлёте веток (cp6 — 100 м, cp10 — 56 м) допустимы: окно по высоте у пересечения — от −3 до +9 м от центра ворот.
- Лазерные пары (левая/правая половина моста, период 3 с, горят по 1 с с фазой 1,5 с) оставляют свободную полосу почти всё время (обе заняты ~10 % времени, при учёте поправки ±0,4 с).
- Боты (граблям не повторять): точка маршрута над пропастью разворачивает бота в воздухе (ориентиры только на плитах); прыжок держать до конца полёта (отпустил — «срезанный» прыжок вдвое короче); у ботов с
--clientзапуск рядом с локальным тестовым сервером добавляет лишнего игрока в комнату; неpkill -fс путём теста (убьёт свою оболочку) —ps … | grep '[g]odot'иkillпо PID. - Запекание:
BAKE_SAMPLES=64 ./rebuild.sh— проба за ~30 с; на дне между башнями (подножие финальной башни) свет получается тёмным — добавлены прожекторные панели на стенах ядра; чистовое запекание — фаза 2.
Дорожная карта
Телефон — не планируется
Владелец решил, что телефон не нужен (окт 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 на реальном устройстве.
Ближайшее: текстовый чат (ПЛАН, не реализовано — ждёт команды владельца)
Ники сделаны (раздел «Ники и рейтинг»): Net.nick_of(id), Net.my_nick, Net.input_blocked для панели ввода. Чат строится на них. Смена ника по команде /ник не нужна — есть клавиша N.
Чат
- Открытие: действие InputMap
chatна физические клавиши T и Enter (работает на любой раскладке); Enter — отправить и закрыть, Esc — закрыть. Чат — в комнате текущей карты (в хабе/песочнице/Backrooms/Parkour/Race разные). - UI: CanvasLayer, внизу слева, мрачный минимализм (тёмная полупрозрачная подложка, небольшой шрифт, без ярких цветов), последние ~8 строк, видны ~10 с и гаснут;
Ник: текст, ник цветом игрока (цвет от id, как у призрака), системные строки серым («зашёл»/«вышел»). Без BBCode: чужой текст не должен менять оформление (Label или RichTextLabel с выключенным BBCode). Желательно пузырь с последним сообщением над призраком на ~6 с. - Ввод при наборе: уже есть
Net.input_blockedи образец — панель ника вcore/overlay.gd(флаг отдельный отinput_frozen, мышь освобождается и потом захватывается, открывающая клавиша в поле не попадает, R/N/Tab/G в поле не действия). Повторить для чата. - Сеть: RPC
submit_chat(text)→ сервер валидирует и пересылает остальным в комнате (recv_chat(id, text)), отправитель видит своё сразу. Серверные ограничения: ≤ 200 символов, убрать управляющие, пустые отбросить, лимит ~5 сообщений за 5 с на игрока (молча отбрасывать). Команды/…обрабатываются на клиенте и не рассылаются.
Проверка (по существу): образец — tools/test/nicks_lb_test.py: чат только в своей комнате; лимиты длины/частоты; «[b]» не ломает оформление; при открытом чате бот не двигается, после закрытия — двигается; R/T в поле не срабатывают как действия. Регрессия: тест гонки, mantle_test, measure_movement. Кадры панели ника и чата через --write-movie: не перекрывают HUD гонки (сверху таймер, по центру отсчёт), кириллица рисуется.
Выкладка: меняется набор RPC → клиент и сервер вместе (порядок из «Деплой»), игрокам Ctrl+F5; старый клиент с новым сервером падает с «rpc node checksum failed».
Идеи (оценка независимого консультанта, окт 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.
Название и заставка
Игра называется Aman's World (config/name). Заставка загрузки — branding/splash.png (1280×720, генерируется скриптом на Pillow: дождь, силуэты домов, натриевое свечение; при смене надписи/стиля перерисуй и положи на то же место), иконка и фавиконка — branding/icon.svg. Всё задаётся в project.godot → [application] (boot_splash/image, boot_splash/bg_color, config/icon); веб-экспорт берёт из них index.png, index.icon.png и <title>. Папка пользовательских данных осталась прежней (config/use_custom_user_dir, custom_user_dir_name="world2"): профиль (profile.cfg) и рекорд (race_best.cfg) не должны потеряться при переименовании проекта. Серверный проект (deploy/server/project.godot) — отдельный, его имя не менялось.
Журнал изменений (окт 2026)
Последнее (31 окт 2026): геймпад Xbox, меню паузы с выходом в хаб и настройками, увеличенный и оформленный хаб с музыкой. Подробности — «Управление», «Меню паузы и настройки», «Карты и порталы». Выкладка сервера не нужна (net/* не менялся).
Сделано одной серией работ (хронологически):
-
Персонаж: модель киберниндзя со скелетом (752 треугольника, 14 анимаций, IK ног) вместо капсул; цвет свечения от id игрока; свой персонаж спрятан от своей камеры. Сальто камеры при двойном прыжке делалось и убрано по просьбе.
-
Подтягивание на уступы починено (не работало от ~2 м на всех картах), замеры мувсета и тестовые сцены в
tools/test/. -
Parkour: подъём по мегабашне (крыши, каньон, башня, лазерный зал, контейнеры, спираль, вершина), запечённый свет, панорама неба, мокрый блеск, дождь, туман, песня; мрачная антиутопия.
-
Race: гонка на ногах (трасса 633 м из Blender, три развилки, 8 ворот), правила и HUD, новые сетевые события, песня на «СТАРТ!»; сделано агентами, проверено двухклиентным тестом на локальном сервере.
-
Инфраструктура: пресеты экспорта карт (с
include_filterдля файлов, грузимых по строке), проверка пакетов «по существу», выкладка сайта и сервера по порядку из «Деплой». -
Замеры: вкладка в браузере ~300 МБ.
-
Ники, общий рейтинг, таблица Tab, доски в хабе, сдача в гонке (G), таймер Parkour, название «Aman's World» (заставка, иконка).
-
Карта Metro (гонка вниз 1495 м со скользящими рельсами и бустером) сделана и УДАЛЕНА по решению владельца («полный мусор», окт 2026): не понравилась. Из истории не восстанавливай без просьбы. Что узнали: (а) владельцу важнее всего скорость, маневренность и ощущение потока, как у первой гонки Race; (б) прежде чем отдавать большую карту агентам, показать владельцу концепцию и кадры черновика; (в) идея «скользящий рельс» (подхват в воздухе, езда 36 м/с, сход прыжком) работала технически, но потолок
MAX_SPEED = 22обрезает скорость на сходе; (г) слишком светлые «пятнистые» залы из-за запекания и белые засветки — следить за силой света при запекании. -
Race 2 «Верхние ярусы» (фаза 1): вторая гонка по крышам и мостам над городом, 13 ворот, ~1,2 км, три пути на каждом участке, вентиляторы-подбросы и ускоряющие полосы (хуки в
player.gd), общий с Race код правил (race_rules.gdберёт данные у карты), портал в хабе, рейтингrace2. Проходимость каждой ветки доказана ботами; чистовое запекание, небо и музыка — фаза 2.
Лицензии и авторство
- Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), CC-BY 4.0 (
maps/backrooms/model/CREDITS.md). При публикации нужно указывать автора. - Музыка хаба — «Local Forecast - Elevator», Kevin MacLeod (https://incompetech.com), CC-BY 4.0 (
maps/hub/audio/CREDITS.md). При публикации нужно указывать автора. - Остальные модели и звуки — материалы владельца проекта.