Работа с JSON-конфигами уровней

При разработке игр на базе библиотеки Phaser конфигурация уровней часто выносится во внешние JSON-файлы. Такой подход отделяет данные от логики, упрощает масштабирование проекта и позволяет редактировать контент без изменения исходного кода.

Типовой JSON-конфиг уровня может включать:

{
  "level": 1,
  "world": {
    "width": 2000,
    "height": 1200,
    "background": "forest"
  },
  "player": {
    "x": 100,
    "y": 300,
    "texture": "hero"
  },
  "platforms": [
    { "x": 400, "y": 500, "width": 200, "height": 32 },
    { "x": 800, "y": 400, "width": 300, "height": 32 }
  ],
  "enemies": [
    { "type": "slime", "x": 600, "y": 450 },
    { "type": "bat", "x": 900, "y": 300 }
  ],
  "collectibles": [
    { "type": "coin", "x": 450, "y": 450 },
    { "type": "coin", "x": 850, "y": 350 }
  ]
}

Ключевые принципы:

  • Чёткая иерархия данных
  • Минимум логики внутри JSON
  • Использование строковых идентификаторов текстур
  • Группировка объектов по типам

Загрузка JSON в Phaser

Загрузка выполняется через this.load.json внутри метода preload() сцены:

preload() {
  this.load.json('level1', 'assets/levels/level1.json');
}

После завершения загрузки доступ к данным осуществляется через кэш:

create() {
  const levelData = this.cache.json.get('level1');
}

Важно помнить, что JSON загружается асинхронно. Использование данных допустимо только после завершения preload().


Организация сцены на основе JSON

Настройка мира

Размер игрового мира берётся из конфигурации:

const levelData = this.cache.json.get('level1');

this.physics.world.setBounds(
  0,
  0,
  levelData.world.width,
  levelData.world.height
);

this.add.image(0, 0, levelData.world.background)
  .setOrigin(0);

В результате мир автоматически адаптируется под параметры уровня.


Создание игрока

this.player = this.physics.add.sprite(
  levelData.player.x,
  levelData.player.y,
  levelData.player.texture
);

this.player.setCollideWorldBounds(true);

Позиция и текстура полностью контролируются данными JSON.


Генерация платформ из массива

Для динамического создания объектов удобно использовать циклы:

this.platforms = this.physics.add.staticGroup();

levelData.platforms.forEach(platform => {
  const p = this.platforms.create(platform.x, platform.y, 'platform');
  p.displayWidth = platform.width;
  p.displayHeight = platform.height;
  p.refreshBody();
});

Ключевой момент — вызов refreshBody(), необходимый при изменении размеров статического тела.


Работа с врагами

Создание фабрики врагов

Логика создания врагов не должна быть захардкожена в цикле. Лучше выделить фабричный метод:

createEnemy(type, x, y) {
  switch (type) {
    case 'slime':
      return this.physics.add.sprite(x, y, 'slime');
    case 'bat':
      return this.physics.add.sprite(x, y, 'bat');
  }
}

Инициализация на основе JSON

this.enemies = this.physics.add.group();

levelData.enemies.forEach(enemyData => {
  const enemy = this.createEnemy(
    enemyData.type,
    enemyData.x,
    enemyData.y
  );

  this.enemies.add(enemy);
});

Такой подход облегчает расширение типов противников.


Коллекционные предметы

this.collectibles = this.physics.add.group();

levelData.collectibles.forEach(item => {
  const collectible = this.collectibles.create(
    item.x,
    item.y,
    item.type
  );

  collectible.setImmovable(true);
});

Здесь item.type совпадает с ключом текстуры, что позволяет не создавать дополнительных условий.


Использование глубокой структуры данных

JSON может содержать дополнительные параметры:

{
  "type": "slime",
  "x": 600,
  "y": 450,
  "hp": 3,
  "speed": 50,
  "patrol": {
    "left": 550,
    "right": 700
  }
}

Чтение параметров:

enemy.hp = enemyData.hp;
enemy.speed = enemyData.speed;
enemy.patrolBounds = enemyData.patrol;

Это позволяет:

  • Управлять сложностью уровня
  • Настраивать поведение
  • Добавлять вариативность без изменения кода сцены

Валидация структуры JSON

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

if (!levelData.player || !levelData.world) {
  throw new Error('Некорректная структура уровня');
}

Для крупных проектов целесообразно:

  • Создавать схему структуры
  • Использовать TypeScript-интерфейсы
  • Применять автоматическую проверку данных

Работа с несколькими уровнями

Динамическая загрузка

init(data) {
  this.currentLevel = data.level || 1;
}

preload() {
  this.load.json(
    'currentLevel',
    `assets/levels/level${this.currentLevel}.json`
  );
}

Перезапуск сцены

this.scene.restart({ level: this.currentLevel + 1 });

Такой механизм позволяет использовать одну и ту же сцену для всех уровней.


Разделение логики и данных

Правильная архитектура предполагает:

  • JSON — только данные
  • Сцена — логика
  • Отдельные классы — поведение объектов

Пример выделения класса врага:

class Slime extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y, config) {
    super(scene, x, y, 'slime');
    scene.add.existing(this);
    scene.physics.add.existing(this);

    this.hp = config.hp;
    this.speed = config.speed;
  }
}

Создание:

const slime = new Slime(
  this,
  enemyData.x,
  enemyData.y,
  enemyData
);

Теперь JSON управляет параметрами, а класс — поведением.


Расширенные сценарии использования

Триггеры событий

В JSON можно описывать зоны:

{
  "triggers": [
    {
      "x": 1200,
      "y": 300,
      "width": 200,
      "height": 300,
      "event": "bossFight"
    }
  ]
}

Создание триггеров:

levelData.triggers.forEach(trigger => {
  const zone = this.add.zone(
    trigger.x,
    trigger.y,
    trigger.width,
    trigger.height
  );

  this.physics.add.existing(zone);
  zone.body.setAllowGravity(false);
  zone.body.moves = false;

  zone.eventName = trigger.event;
});

Параметры сложности

{
  "difficulty": {
    "enemyMultiplier": 1.5,
    "spawnRate": 2000
  }
}

Применение:

this.spawnRate = levelData.difficulty.spawnRate;
this.enemyMultiplier = levelData.difficulty.enemyMultiplier;

Организация файловой структуры проекта

assets/
  levels/
    level1.json
    level2.json
src/
  scenes/
    GameScene.js
  entities/
    Slime.js
    Bat.js

Разделение повышает читаемость и масштабируемость.


Оптимизация работы с конфигами

  1. Минимизировать повторяющиеся данные
  2. Использовать массивы вместо отдельных ключей
  3. Хранить только конфигурацию, а не состояние
  4. Не сохранять временные значения

Пример полной интеграции

create() {
  const data = this.cache.json.get('currentLevel');

  this.setupWorld(data.world);
  this.createPlayer(data.player);
  this.createPlatforms(data.platforms);
  this.createEnemies(data.enemies);
  this.createCollectibles(data.collectibles);
}

Каждый метод отвечает за свою часть конфигурации. Такая архитектура делает проект:

  • Масштабируемым
  • Предсказуемым
  • Удобным для тестирования
  • Лёгким в сопровождении

Работа с JSON-конфигами в Phaser формирует фундамент гибкой игровой архитектуры, в которой контент управляется данными, а поведение — кодом.