Skip to content

Схема снапшота и форматы передачи ​

Данные, которые игра кладёт на провод: номера портов, бинарный snapshot-кадр и схема, которая его определяет, формат ввода и форматы мета-данных (авторизация, панель, статистика, чат, голосование). Номера портов и опкоды принадлежат движку; схема и ключи меты — игре.

Порты ​

Сервер → клиент ​

ПортИмяФорматОписание
0CONFIG_DATAJSONКлиентский конфиг (merge vimp-engine/src/config/clientDefaults.js + src/config/client.js игры-плагина (например, в @vimp-games/tanks) + prediction)
1AUTH_DATAJSONДанные формы авторизации
2AUTH_RESULTJSONОшибки авторизации (или null)
3MAP_DATAJSONДанные карты
4FIRST_SHOT_DATAJSONПервый кадр игры (одноразовый, минует буфер интерполяции): [gameSnapshot, 0, serverTime, 0]
5SHOT_DATAбинарныйSnapshot-кадр игры (см. ниже)
6SOUND_DATAJSONИмя системного звука (roundStart, victory, frag, …)
7GAME_INFORM_DATAJSONИгровые сообщения на экране ([код, параметры?]: 0 победа команды, 1 старт раунда, 2 game over — источник истины vimp-engine/src/config/gameCodes.js, общий для хоста и клиента)
8TECH_INFORM_DATAJSONТехнические сообщения «чёрного экрана» ([код, параметры?]: сервер полон, загрузка, кики); без данных — скрыть экран
11CLEARJSONПолная или частичная (по setId) очистка полотен
13PANEL_DATAJSONПанель HUD (per-user, только при изменении)
14STAT_DATAJSONСтатистика (broadcast, только при изменении)
15CHAT_DATAJSONСообщение чата (общее или персональное)
16VOTE_DATAJSONДанные голосования
17KEYSET_DATAJSONАктивный набор клавиш: 0 — наблюдатель, 1 — игрок; шлётся при смене статуса, а также при смене карты (набор наблюдателя непосредственно перед CLEAR)
18ACCOLADES_DATAJSONМеста участников комнаты в глобальном топе, формируются host/meta/modules/Accolades.js и применяются на клиенте lib/accolades.js

Клиент → сервер ​

ПортИмяОписание
1AUTH_RESPONSEДанные формы авторизации плюс лобби-JWT личности ({model, ..., token}); ник хост берёт из проверенного токена, а не из поля формы
5KEYS_DATAВвод: строка "seq:action:name" (см. ниже)
6CHAT_DATAТекст сообщения / чат-команда
7VOTE_DATAОтвет голосования [voteName, value] или запрос списка ('maps', 'teams')

Остальные порты — рукопожатие соединения, возобновление сессии, RTT ping/pong, обновление токена и отладочный лог хоста — внутренние для движка: плагин их не шлёт и не обрабатывает. Их номера остаются зарезервированными и не переиспользуются.

Разделение каналов: горячий снапшот и мета ​

Каждый тик снапшота (networkSendRate: 4 → 30 пакетов/сек) хост шлёт всем готовым пользователям бинарный кадр порта 5. Мета-данные идут своими JSON-каналами и только при изменении:

  • panel (13) — per-user; массив строк 'ключ:значение', ключи из схемы панели игры (t — время раунда — единственный ключ, заданный движком; напр. @vimp-games/tanks добавляет h — здоровье, w1/w2 — боезапас, wa — активное оружие). Полная панель шлётся при входе в игру, пустая (только ключи) — наблюдателю.
  • stat (14) — broadcast, дельта изменений (см. формат ниже).
  • chat (15) — общее сообщение либо персональное.
  • vote (16) — общее либо персональное голосование.
  • keyset (17) — точечно при смене статуса спектатор↔игрок.

Бинарный snapshot-кадр (порт 5) ​

Кодек целиком в Rust-ядре (vimp-engine-core): вы объявляете схему, движок упаковывает и декодирует кадр. Реестр ключей — данные игры: src/config/snapshot.js плагина (gameConfig.snapshot); версия формата остаётся у движка (SNAPSHOT_FORMAT_VERSION = 5), и при несовпадении версии клиент отбрасывает кадр.

Блоки сущностей (kind из снапшот-схемы игры) ​

Ключи сущностей, значения kind и форматы данных целиком заданы схемой снапшота игры-плагина — движок лишь навязывает раскладку блока (id + типизированные поля). Пример из референсного плагина (@vimp-games/tanks):

КлючidkindФормат данных
m11tanks{gameId: [x, y, angle, gunRotation, vx, vy, engineLoad, condition, size, teamId, angvel] | null}; null — удалить с полотна
w12tracersмассив [startX, startY, endX, endY, bodyX, bodyY, wasHit, shooterId]
w23bombs{shotId(base36): [x, y, angle, size, time, ownerId] | null}
w2e4explosionsмассив [x, y, radius]
c1/c25/6dynamics{'dN': [x, y, angle] | [x, y, angle, vx, vy, angvel]} — динамические элементы карты; хвост со скоростями опционален (см. ниже)

Все float исходно округлены хостом до 2 знаков; декодер восстанавливает значения повторным округлением Float32 (player-блок — без округления). События игровых сущностей могут нести id автора (@vimp-games/tanks использует shooterId/ownerId, добавлены в v3), чтобы клиент подавлял авторитетные дубли локально заспавненных сущностей (клиентское ядро, core/src/client/shot.rs игры-плагина, например в @vimp-games/tanks).

Опциональный хвост строки (optionalFrom, v4). Схема вправе объявить, что поля строки начиная с индекса optionalFrom пишутся, только когда несут смысл: такая строка начинается с флаг-байта (1 — дальше хвост, 0 — строка кончилась на обязательной части). @vimp-games/tanks использует его для динамических элементов карты: покоящийся ящик шлёт [x, y, angle] и не платит 12 байт за [vx, vy, angvel] каждый кадр, а только что получивший удар — шлёт скорости: клиент предсказывает динамику карты рядом со своим танком, а оценка скорости конечной разностью между кадрами 30 Гц сильнее всего врёт именно в момент удара. Распаковка всегда отдаёт строку полной ширины: отсутствующий хвост читается нулями («тело покоится»), поэтому интерполятор, hot-буфер и JS остаются фиксированной ширины.

Запись схемы — не только {id, kind}: class ('hot' — интерполируется клиентом между кадрами, 'event' — одноразовый, кадром как есть) и fields — схема полей строки (name, ty: f32/u8/u16/u32, interp: lerp/lerpAngle/discrete для class: 'hot'). fields обязана точно совпадать по количеству и порядку типов с Row-структурой ключа в vimp-engine-core/src/snapshot.rs (GameCore/ClientCore отклоняют конструктор при расхождении), а optionalFrom (если задан) обязан указывать на непустой хвост внутри fields.

Поле вправе объявить и role — свою роль в движковом контракте (в отличие от name, которое принадлежит игре). Ролей сегодня две, и они ходят парой в блоке динамики карты: role: 'z' на позиции 3 и role: 'level' на позиции 4 делают строку динамики слоёной (высота тела над своим уровнем и сам уровень). Раньше движок узнавал эту пару по именам полей, и переименование поля молча возвращало плоскую строку; теперь авторитет — только роли, а загрузка карты падает с ошибкой, если роль стоит не на своей позиции, если объявлена лишь одна из пары или если на этих двух местах стоят поля с именами z/level без ролей.

Третья роль, role: 'state', — байт состояния тела карты: u8, который движок пишет сразу за головой строки — на позиции 5 у слоёной строки, 3 у плоской — и до необязательного хвоста скоростей, поэтому optionalFrom обязан быть больше его позиции (покоящееся тело хвост не шлёт и потеряло бы байт). Строка становится [x, y, angle, z?, level?, state, vx?, vy?, angvel?]. Значение даёт игра (SimCtx::map_body_state), смысл значений — её. Схема без этой роли сохраняет прежнюю строку бит-в-бит; роль не на своей позиции или не того типа роняет загрузку карты.

Свойства схемы ​

СвойствоЗначенияПримечания
idu8, уникальныйключевой байт на проводе; валидация проверяет только уникальность
kindindexed8 · indexed32 · list16 · indexedNoNull8форма блока на проводе, см. ниже
classhot · eventhot — непрерывное состояние, event — одноразовое
fields[].tyf32 · u8 · u16 · u32big-endian
fields[].interplerp · lerpAngle · discrete (по умолчанию)интерполируются только f32-поля и только hot-блоки
fields[].rolez · level · stateполя строки динамики карты, которые пишет движок

Порядок полей позиционно привязан к построению Rust-Row игры, и соответствие не проверяется глубже количества и типов полей: перестановка двух полей одного типа только с одной стороны молча даёт мусор.

Четыре вида блоков ​

hasData — u8-маркер пустоты: 0 — сущности нет (JSON null), 1 — далее строка полей. Тело — конкатенация [u8 keyId][блок] для каждого ключа, у которого в этом тике есть содержимое.

KindПрефикс счётчикаСтрокаДля чего
indexed8u8 countu8 id, u8 hasData, полядо 255 адресуемых сущностей (игроки)
indexed32u16 countu32 id, u8 hasData, полямного короткоживущих сущностей (снаряды)
list16u16 countполяанонимные события (трассеры, взрывы)
indexedNoNull8u8 countu8 index, поляданные с фиксированными слотами, не исчезающие (динамика карты)

Интерполяция ​

Клиент рендерит мир в момент serverNow − delay (interpolation.delay 100 мс, interpolation.maxFrameAge 1000 мс в clientDefaults.js); при 30 кадрах/с (networkSendRate: 4) это около 3 кадров буфера. f32-поля с lerp интерполируются линейно, lerpAngle — по кратчайшей дуге окружности, всё остальное — скачком.

Округление ​

Каждый передаваемый f32 фактически несёт 2 знака после запятой, кроме пользовательского player-блока (предикту нужна полная точность). Округление выполняет игра, а не упаковщик: упаковщик пишет f32 как есть, а декодер пропускает каждое поле через round2. Значение, упакованное без округления, дойдёт до клиента другим числом, чем то, что хранит хост, — вызывайте vimp_engine_core::physics::round2 для float'ов, которые передаёте в build_snapshot_blocks. Для блока динамики карты, который ведёт сам движок, он делает это сам.

Проектирование схемы ​

  1. Один hot-ключ indexed8 на каждый постоянный тип актора (игроки, машины).
  2. indexedNoNull8 — для мирового состояния с фиксированными слотами (динамика карты).
  3. event list16 — для анонимных одноразовых эффектов.
  4. event indexed32 — для именованных короткоживущих сущностей, которым нужны обновления (летящие гранаты).
  5. Держите число hot-полей малым: каждое поле стоит байт 30 раз в секунду на сущность.
  6. Id автора ставьте последним в каждом блоке события оружия — на нём держится клиентское подавление дублей.

При добавлении нового оружия/сущности его snapshot-ключ обязан быть зарегистрирован в снапшот-схеме игры-плагина (src/config/snapshot.js, например в @vimp-games/tanks) — с полным fields для своего kind, иначе pack_body/конструктор ядра бросят ошибку. Если существующие kind не подходят — добавить новую раскладку блока в vimp-engine-core/src/snapshot.rs + vimp-engine-core/src/client/unpack.rs и поднять версию формата.

Формат ввода: "seq:action:name" ​

Клиент шлёт каждое событие клавиши строкой на порт 5 (client → server):

  • seq — инкрементный номер ввода (Uint32), пишется в локальную историю предиктора;
  • action — down | up;
  • name — команда (forward, fire, nextPlayer, …).

Сервер сохраняет lastInputSeq пользователя и возвращает его в player-блоке кадра — так клиент узнаёт, какие вводы уже учтены authoritative-состоянием, и переигрывает (reconciliation) только более поздние. Подробно — в client-api.md.

У наблюдателя те же строки обрабатываются сервером как переключение наблюдаемого игрока (nextPlayer/prevPlayer).

RTT и кики ​

Хост пингует каждого клиента (rttPingInterval, 3 с), клиент отвечает; сглаженная (EMA) задержка публикуется в статистику столбцом latency. Клиента кикает:

  • при сглаженной latency > maxLatency (1000 мс; порог рассчитан на P2P-хостинг с домашних каналов и спайки при смене карты) — код 4003;
  • при maxMissedPings (5) подряд пропущенных ответах — код 4004.

Все три значения — дефолты хоста движка, игра вправе их переопределить (см. game-config.md).

Форматы мета-данных ​

Авторизация (порт 1) ​

Payload AUTH_DATA (PS_AUTH_DATA): { elems, params, texts } (hostPlugin.authSchema — см. plugin-api.md):

  • elems — DOM-id, которые нейтральный каркас экране авторизации открывает игре-плагину (authId, errorId, enterId, fieldsId, и для текстов ниже — titleId/informsId); значения плагина должны совпадать с реальными id в экране авторизации.
  • params[] — { name, value, options }, схема полей игрока (тот же контракт дескрипторов, что и roomForm): options несёт control/label/unit/numeric/min/max/options/source/storage/regExp/required/maxlength/hidden плюс уже существующий validator (ключ в authSchema.validators, проверяется только на хосте — код валидатора по проводу не идёт). Клиент строит по одному контролу на param через formBuilder.js и вставляет в #+elems.fieldsId, подставляя значение из localStorage[storage], если оно есть.
  • texts — опционально { title, sections }, заполняет elems.titleId/elems.informsId нейтрального каркаса.

Панель (порт 13) ​

Массив строк 'ключ:значение', например ['t:97', 'h:100', 'w1:200', 'wa:w1']. Отправляются только изменившиеся ключи; t (время раунда, сек) — при каждом изменении секунды. Пустая панель (наблюдателю) — время + список ключей без значений (контейнеры скрываются).

Статистика (порт 14) ​

statArray = [tBodies, tHead, fullUpdate?] (формирует Stat):

  • statArray[0] — строки таблиц: [id строки, номер таблицы, массив ячеек | null, номер tbody]. null вместо ячеек — удалить строку; пустая строка в ячейке — очистить значение; undefined/пропуск — не менять.
  • statArray[1] — шапки: [номер таблицы, массив ячеек, номер строки tHead].
  • statArray[2] — флаг полного обновления (boolean, опционально).

Ячейки строки игрока: [name, status, score, deaths, latency] (порядок — key из game:stat).

Чат (порт 15) ​

  • Пользовательское сообщение: [текст, имя автора, teamId].
  • Системное сообщение: строка 'группа:номер:параметры,через,запятую' — клиент собирает текст из шаблонов messages игры, а для движковых групп (s, v, m, c) при отсутствии текста берёт английское умолчание движка (src/config/chatMessages.js). У игровой группы (например, b у танков) умолчания нет: нет текста — нет сообщения.

Голосование (порт 16) ​

Сервер шлёт payload:

  • name — имя/тип голосования (клиент ищет шаблон в client.js → modules.vote.params.templates);
  • params — опционально; строки для подстановки в плейсхолдеры {0}, {1} заголовка;
  • values — опционально; массив готовых вариантов или строка-команда ('maps', 'teams') — клиент запрашивает актуальный список у сервера (порт 7 client → server).

Ответ клиента: [voteName, selectedValue]. Запрос динамического списка: строка 'maps' | 'teams'.

Голосование «Change host» (лобби-режим) этим портом не пользуется: оно идёт между клиентами и платформой, поэтому хост, о котором оно, его не видит. Его окно — голосование движка с именем @changeHost; ответ на него не уходит как [voteName, value] по порту 7. Имена с @ зарезервированы за голосованиями движка.


← Предыдущая: Справочник конфигурации игры · Следующая: Rust-ядро движка (vimp-engine-core) →

vimp-engine 0.38.1 · vimp-engine-core 0.23.2 · create-vimp-game 0.5.4