Карты и ассеты
Всё, что игра поставляет как данные: JSON карт, картинки тайлов и спрайтов, звуки и процедурно сгенерированные («запечённые») текстуры. Куда эти файлы попадают в пакете — в packaging.md; как на них указывает манифест — в plugin-api.md.
Формат карты
Карты пишутся JS-модулями в src/data/maps/ и экспортируются в dist/maps/<name>.json скриптом сборки. Имя файла — имя карты, оно может содержать пробелы (pool mini.json).
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, он никогда не движется. Второй уровень добавляется поверх: карта без этих полей загружается ровно как раньше:
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Углы не масштабируются. Клиенты получают уже масштабированную карту — не масштабируйте повторно в части.
Значения по умолчанию динамических тел
| Свойство | По умолчанию |
|---|---|
linearDamping | 0.0 |
angularDamping | 0.01 |
| friction | 0.2 (зафиксировано движком) |
| restitution | 0.0 (зафиксировано движком) |
density обязателен; img и layer нужны клиентскому рендереру.
Респауны определяют вместимость команды
Число точек появления команды — жёсткая вместимость этой команды на этой карте. Движок выдаёт точки последовательно с индекса 0; когда они кончаются, новый участник в эту команду войти не может. Карта с 10 точками на команду ограничивает матч двух команд 20 игроками независимо от
maxPlayers.
Давайте каждой команде одинаковое число точек, если асимметрия не задумана, и следите, чтобы сумма покрывала roomDefaults.maxPlayers (правило B10).
Картинки — вы поставляете их сами
Тайлсеты и картинки динамических объектов — ваши. Движок вообще не отдаёт игровых картинок; каждый файл, названный картой, обязан лежать в вашем собственном пакете.
spriteSheet.img и physicsDynamic[].img — голые имена файлов. Ваша часть Map превращает их в URL, а база берётся у движка как сервис assetsBase — та же база, относительно которой разрешаются звуки:
// 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, то есть пустой холст без единой ошибки.
Конвейер картинок
- Храните исходники в
assets/img/(под git — в отличие от звуков, им не нужна обработка). - Скрипт копирования (
copy-game-images.js, по образцуcopy-game-sounds.js) копирует их вbuild/img/— корень dev standalone-запуска — и вdist/img/, упакованный ассет. - Запускайте его и из
build:assets, и изpredev, чтобыnpm run devпоказывал карту, даже если ffmpeg никогда не был установлен. - Сделайте так, чтобы сборка манифеста падала, если карта называет картинку, которой нет в
dist/img/: движок этого не диагностирует — часть просто не получает текстуру, и карта рисуется пустой (статически это проверяет правилоE2). dist/обычно в.gitignore, а npm применяет правила игнорирования внутри каталогов изfiles: убедитесь черезnpm pack --dry-run, что картинки есть в тарболе.
Конвейер звуков
- Кладите сырые исходники в
assets/audio-raw/. npm run audio:processзапускает ffmpeg: нормализация громкости по EBU R128 и кодирование в оба формата.webmи.mp3вbuild/sounds/(промежуточный каталог в.gitignore).copy-game-sounds.jsочищаетdist/sounds/и копирует тудаbuild/sounds/.manifest.assetsBaseнаправляет клиента в/games/<id>/sounds/.
Оба кодека обязательны: codecList клиента — ['webm', 'mp3'], и он берёт первый поддерживаемый браузером. Отсутствующий .mp3 ломает Safari (правило E1).
Объявите каждый звук в клиентском конфиге:
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)
Процедурные текстуры генерируются один раз на холст при старте вместо поставки картинками:
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.
← Предыдущая: Клиентская среда для авторов игр · Следующая: Упаковка и сборка игрового плагина →