Skip to content

Карты и ассеты ​

Всё, что игра поставляет как данные: JSON карт, картинки тайлов и спрайтов, звуки и процедурно сгенерированные («запечённые») текстуры. Куда эти файлы попадают в пакете — в packaging.md; как на них указывает манифест — в plugin-api.md.

Формат карты ​

Карты пишутся JS-модулями в src/data/maps/ и экспортируются в dist/maps/<name>.json скриптом сборки. Имя файла — имя карты, оно может содержать пробелы (pool mini.json).

js
export default {
  setId: 'c1',              // какая запись gameSets строит части этой карты
  scale: 0.6,               // масштаб карты; иначе gameConfig.mapScale

  spriteSheet: {            // ТОЛЬКО КЛИЕНТ (ядро это игнорирует)
    img: 'tiles.png',
    frames: [[480, 64, 32, 32], [128, 224, 32, 32], …],   // [x, y, w, h]
  },
  layers: {                 // ТОЛЬКО КЛИЕНТ: renderLayer → индексы тайлов
    1: [0, 2, 4],
    2: [1],
    4: [3],
  },

  physicsStatic: [1],       // индексы тайлов, ставшие статическими коллайдерами
  physicsDynamic: [
    { position: [640, 480], angle: 0, width: 64, height: 64,
      density: 1.0, img: 'b1.png', layer: 2,
      linearDamping: 0.2, angularDamping: 0.05 },
  ],

  step: 32,                 // размер тайла в мировых единицах
  map: [                    // сетка индексов тайлов; 0 = пусто
    [1, 1, 1, 1],
    [1, 0, 0, 1],
    [1, 1, 1, 1],
  ],

  // НЕПРОЗРАЧНЫЕ игровые данные: движок их не читает и не масштабирует
  // (необязательный объект, допустим и на каждой записи physicsDynamic)
  game: { surfaces: { sand: [[3, 4], [3, 5]] } },

  respawns: {
    // [x, y, angleDeg] или [x, y, angleDeg, level] на многоуровневой карте
    team1: [[130, 520, 0], [130, 620, 0], …],
    team2: [[1600, 520, 180], …],
  },
};
ПолеПотребительПримечания
setIdклиентвыбирает parts.gameSets[setId] для построения карты; иначе gameConfig.mapSetId
scaleхост + ядропереопределение gameConfig.mapScale для карты
spriteSheet, layersтолько клиентядро их не видит
physicsStaticядроиндексы тайлов, порождающие статические тела
physicsDynamicядро + клиентподвижные предметы
stepобадлина ребра тайла до масштабирования
mapобапострочная сетка индексов тайлов
respawnsхостточки появления по командам
levelsядро + клиентуровни над землёй (2.5D); нет поля — плоская карта
rampsядро + клиентпереходы между уровнями
game, physicsDynamic[i].gameваше ядро + клиентнепрозрачный объект (или отсутствует), немасштабируемый; доходит до GameMap::game_data() / dynamic_game_data(i), ClientCore.set_map, каждой статической части (game) и частей d{i} (через ...item). Capability map.gameData, правило E7

Уровни и рампы (2.5D) ​

Уровень 0 — это map / physicsStatic / layers, он никогда не движется. Второй уровень добавляется поверх: карта без этих полей загружается ровно как раньше:

js
  levels: {
    '1': {                    // ключ = номер уровня строкой, начиная с 1
      map: [[0, 9, 9, 0]],    // те же размеры, что у `map`; 0 = здесь уровня нет
      floor: [9],             // тайлы, по которым можно ехать (плита моста)
      walls: [7],             // перила: блокируют движение и луч на L1
      layers: { 5: [9] },     // ТОЛЬКО КЛИЕНТ, смысл как у layers верхнего уровня
    },
  },
  ramps: [
    { tile: 3, dir: 'east', from: 0, to: 1 },   // dir = направление ПОДЪЁМА
  ],
ПолеПотребительПримечания
levels.<n>.mapядро + клиентсетка уровня n; размеры обязаны совпадать с map
levels.<n>.floorядро + клиентпроезжаемые тайлы уровня
levels.<n>.wallsядро + клиентперила; обязаны быть подмножеством floor
levels.<n>.layersтолько клиентrenderLayer → индексы тайлов этой сетки
ramps[].tileядро + клиентиндекс тайла в сетке уровня from
ramps[].dirядро + клиентnorth = -y, south = +y, west = -x, east = +x
ramps[].from / .toядро + клиентпо умолчанию 0 / 1
physicsDynamic[].levelядроуровень, на котором стоит предмет; по умолчанию 0
respawns[team][i][3]хост + ядронеобязательный уровень точки появления

MapConfig::validate отвергает карту с несовпадающими размерами сеток, перилами вне floor, неизвестным тайлом рампы или номером уровня вне диапазона — load_map возвращает ошибку до создания первого тела. Правило контракта E4 (debugging.md) выполняет те же проверки статически, до сборки и без ядра.

Что доходит до клиента: MAP_DATA несёт levels и ramps нетронутыми, applyMapData передаёт оба в set_map клиентского ядра (оно строит ту же многоуровневую геометрию, что и хост, — иначе предсказание по уровням молча расходится) и собирает данные статической отрисовки по уровням. Ключи s0..sN идут сквозь все уровни, и каждый экземпляр части получает свои level, solid (блокирующие тайлы: physicsStatic на уровне 0, levels.<n>.walls выше) и floor (levels.<n>.floor, пуст на уровне 0). Игра, которая не может работать без всего этого, объявляет requires: ['map.layers'] в манифесте (client-api.md описывает сторону отрисовки).

Каскад масштабирования ​

Хост масштабирует карту, прежде чем отдать её ядру и клиентам:

effectiveScale = map.scale ?? gameConfig.mapScale
step        *= effectiveScale
dynamic position/width/height *= effectiveScale
respawns x, y                 *= effectiveScale

Углы не масштабируются. Клиенты получают уже масштабированную карту — не масштабируйте повторно в части.

Значения по умолчанию динамических тел ​

СвойствоПо умолчанию
linearDamping0.0
angularDamping0.01
friction0.2 (зафиксировано движком)
restitution0.0 (зафиксировано движком)

density обязателен; img и layer нужны клиентскому рендереру.

Респауны определяют вместимость команды ​

Число точек появления команды — жёсткая вместимость этой команды на этой карте. Движок выдаёт точки последовательно с индекса 0; когда они кончаются, новый участник в эту команду войти не может. Карта с 10 точками на команду ограничивает матч двух команд 20 игроками независимо от maxPlayers.

Давайте каждой команде одинаковое число точек, если асимметрия не задумана, и следите, чтобы сумма покрывала roomDefaults.maxPlayers (правило B10).

Картинки — вы поставляете их сами ​

Тайлсеты и картинки динамических объектов — ваши. Движок вообще не отдаёт игровых картинок; каждый файл, названный картой, обязан лежать в вашем собственном пакете.

spriteSheet.img и physicsDynamic[].img — голые имена файлов. Ваша часть Map превращает их в URL, а база берётся у движка как сервис assetsBase — та же база, относительно которой разрешаются звуки:

js
// src/config/client.js — объявите сервис для части, которой он нужен
componentDependencies: {
  renderer:   ['Map'],
  assetsBase: ['Map'],
}

// src/client/parts/Map.js — постройте URL
constructor(data, _assets, dependencies) {
  // '/games/<id>/img/' в лобби и на dedicated-сервере,
  // то, что передано в `startStandaloneGame({ assetsBase })`, — в dev
  this._imageBase = `${dependencies.assetsBase}img/`;

  Assets.load(`${this._imageBase}${data.spriteSheet.img}`);
}

Часть, запросившая сервис, который она не объявила, просто получает undefined (пул провайдеров описан в client-api.md). Защищайтесь от этого явно — отсутствующая база даёт запрос undefinedimg/tiles.png, то есть пустой холст без единой ошибки.

Конвейер картинок ​

  1. Храните исходники в assets/img/ (под git — в отличие от звуков, им не нужна обработка).
  2. Скрипт копирования (copy-game-images.js, по образцу copy-game-sounds.js) копирует их в build/img/ — корень dev standalone-запуска — и в dist/img/, упакованный ассет.
  3. Запускайте его и из build:assets, и из predev, чтобы npm run dev показывал карту, даже если ffmpeg никогда не был установлен.
  4. Сделайте так, чтобы сборка манифеста падала, если карта называет картинку, которой нет в dist/img/: движок этого не диагностирует — часть просто не получает текстуру, и карта рисуется пустой (статически это проверяет правило E2).
  5. dist/ обычно в .gitignore, а npm применяет правила игнорирования внутри каталогов из files: убедитесь через npm pack --dry-run, что картинки есть в тарболе.

Конвейер звуков ​

  1. Кладите сырые исходники в assets/audio-raw/.
  2. npm run audio:process запускает ffmpeg: нормализация громкости по EBU R128 и кодирование в оба формата .webm и .mp3 в build/sounds/ (промежуточный каталог в .gitignore).
  3. copy-game-sounds.js очищает dist/sounds/ и копирует туда build/sounds/.
  4. manifest.assetsBase направляет клиента в /games/<id>/sounds/.

Оба кодека обязательны: codecList клиента — ['webm', 'mp3'], и он берёт первый поддерживаемый браузером. Отсутствующий .mp3 ломает Safari (правило E1).

Объявите каждый звук в клиентском конфиге:

js
sounds: {
  codecList: ['webm', 'mp3'],
  // необязательно: пространственная геометрия; опустите — действуют дефолты
  // движка. Это и ЕСТЬ дефолты — они в МИРОВЫХ единицах, рассчитаны на
  // масштаб 1:1, поэтому пересчитайте их под свой масштаб, прежде чем
  // объявлять
  spatial: { mode: 'topDown', virtualElevation: 180, innerRadius: 40 },
  sounds: {
    shot:   { file: 'shot',   priority: 60, volume: 0.6 },
    engine: { file: 'engine', loop: true,   volume: 0.3 },
  },
}

file — базовое имя без расширения. Не задавайте path — движок перезаписывает его значением ${assetsBase}sounds/. Полный набор ключей spatial, их значения по умолчанию (vimp-engine/src/config/spatialDefaults.js) и закрытые списки значений mode/panningModel/distanceModel проверяет правило E6: неизвестный ключ или значение — не ошибка и не слышны: движок молча откатывается к умолчанию. Одновременно звучит не больше 30 мировых голосов; ранжирование — priority² / max(distance², 1).

Запечённые ассеты (baked assets) ​

Процедурные текстуры генерируются один раз на холст при старте вместо поставки картинками:

js
bakedAssets: {
  vimp: [
    { name: 'explosionTexture', component: 'ExplosionEffect',
      params: { radius: 50, blur: 2, color: 0xffffff } },
  ],
}
  • name должно существовать в ClientPlugin.bakers; иначе запись молча игнорируется (правило C8).
  • component — класс части, получающий результат в аргументе assets.
  • Запекайте белые/серые фигуры и красьте их через tint в рантайме — одна запечённая текстура обслуживает все цветовые варианты и сохраняет батчинг вызовов отрисовки.

Чек-лист ассетов новой игры ​

  • [ ] У каждой карты есть respawns для каждой играющей команды, с достаточным числом точек.
  • [ ] Каждый индекс тайла из map существует в spriteSheet.frames.
  • [ ] Каждый индекс из physicsStatic — тот, который вы действительно хотите сделать твёрдым.
  • [ ] У setId каждой карты есть запись gameSets со списком частей карты.
  • [ ] Каждое имя звука из кода есть в конфиге звуков и как пара webm + mp3 в dist/sounds/.
  • [ ] Имена soundCues разрешаются в объявленные звуки.
  • [ ] Каждое имя бейкера из bakedAssets есть в bakers.

«Тихие» ловушки вокруг этих файлов собраны в pitfalls.md.


← Предыдущая: Клиентская среда для авторов игр · Следующая: Упаковка и сборка игрового плагина →

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