При разработке игр на базе библиотеки 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 }
]
}
Ключевые принципы:
Загрузка выполняется через this.load.json внутри метода
preload() сцены:
preload() {
this.load.json('level1', 'assets/levels/level1.json');
}
После завершения загрузки доступ к данным осуществляется через кэш:
create() {
const levelData = this.cache.json.get('level1');
}
Важно помнить, что JSON загружается асинхронно. Использование данных
допустимо только после завершения preload().
Размер игрового мира берётся из конфигурации:
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');
}
}
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;
Это позволяет:
Перед использованием данных рекомендуется проверка:
if (!levelData.player || !levelData.world) {
throw new Error('Некорректная структура уровня');
}
Для крупных проектов целесообразно:
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 });
Такой механизм позволяет использовать одну и ту же сцену для всех уровней.
Правильная архитектура предполагает:
Пример выделения класса врага:
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
Разделение повышает читаемость и масштабируемость.
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 формирует фундамент гибкой игровой архитектуры, в которой контент управляется данными, а поведение — кодом.