Создание HTML5-игр для самых маленьких(Phaser) Часть 4

Konstantin
Konstantin Ostrovsky
2019-03-07 14:32:01
64

В прошлой статье мы ознакомились с основными терминами разработки игр, получили представление о структуре проекта и написали небольшое ТЗ к нашей игре. Давайте же приступим к ее написанию!

Весь код игры я поместил в репозиторий GitHub. Демо результата Вы можете посмотреть здесь.

Откройте папку с проектом при помощи bash или другого доступного терминала. В случае если у Вас Windows - запустите командную строку и перейдите в папку с проектом, который мы приготовили в первой статье

Запустите сервер в режиме разработки, для этого Вам необходимо выполнить команду:

npm run start или yarn start если Вы используете yarn

Чем отличается сборка под developer(разработку) от сборки для production(релиз-версия)?

В dev-версии все скрипты не минифицированны и Вам будет удобно производить отладку, так же после сборки автоматически запускается отслеживание изменений в файлах. Если Вы измените файл скрипта, то проект будет автоматически собран заново и страница в браузере обновится. Это очень удобно во время разработки. Production-версия же минифицирована и не включает в себя скрипт отслеживания изменений файлов.

После запуска Вам следует открыть в браузере адрес http://localhost:3000, где Вы увидите главную страницу с игрой. Если этого не произошло, то проверьте ошибки в консоли, возможно 3000 порт уже занят. Напишите в комментариях если у Вас возникнут сложности, мы постараемся решить их вместе.

В базовом шаблоне размер canvas равен 700x400 px. Что бы изменить размер canvas измените параметры width и height в файле /src/const/config.ts.

Начнем мы пожалуй с основной сцены меню. По умолчанию приложение запускает сцену Welcome. Что бы ее отредактировать откройте файл /src/scenes/Welcome.ts.

Как мы видим, в файле находится класс, который наследуется от базового класса сцены Phaser.Scene. Соответственно класс наследует все свойства и методы объекта Phaser.Scene.

В Phaser 3 нам доступны следующие методы жизненного цикла сцены:

  • preload - предварительная загрузка ресурсов сцены(аудио, спрайты и тд.)
  • create - вызывается сразу после того как сцена была инициализирована
  • update - вызывается при каждом рендеринге сцены

В конструктор базового класса необходимо передать конфигурацию типа Phaser.Scenes.Settings.Config. В качестве агрумента передается json, описывающий физику, название, активность и прочие параметры сцены.

Если ресурсы используются на множестве сцен, более логично будет загружать их при старте игры в сцене Boot по пути /src/scenes/Boot.ts. Где загружать ресурсы - решать только Вам, главное помнить что пользователь не должен ощущать "тормозов" из за загрузки ресурсов. Я считаю, что лучше заранее подгрузить ресурсы в Boot и заставить человека подождать один раз, чем заставлять его ожидать при смене сцен многократно. 

Графику, шрифты и аудио Вы можете взять из репозитория на GitHub и разместить в своем проекте.

Первым делом, подготовим все ресурсы для отрисовки. Для этого давайте загрузим все изображения, шрифты и аудио-файлы. 

Поместите и отредактируйте код в метод preload:

this.load.audio('<имя аудио-спрайта в игре>', ['<путь к файлу>']); - загрузка аудио. Вы можете передать больше разнообразных форматов аудио для корректной работы в различных браузерах. Необходимый минимум: ogg, mp3;

this.load.image('<имя изображения в игре>', '<путь к файлу>') - загрузка изображения. Поддерживаются все наиболее распространенные форматы изображений, кроме SVG, но над этим ведется работа и вскоре ситуация изменится;

this.load.bitmapFont('<имя>', '<спрайт png>', '<шрифт xml-fnt>') -  загрузка шрифта. Может показаться странным, что шрифты загружаются не из файлов .ttf или подобных, но это наиболее правильное решение для игровых шрифтов. png - спрайт хранит все буквы шрифта, а файл с расширением .fnt очень похож на .xml и служит описанием карты для изображения .png. Если Вам необходимо конвертировать .ttf шрифт в .fnt, вы можете воспользоваться сервисом littera.

Что бы узнать какие еще файлы Вы можете загружать при помощи this.load, следует обратиться к документации класса Phaser.Loader.LoaderPlugin.

Вот что у Вас должно получиться:

...
preload() {
    this.load.audio('bump', ['assets/audio/bump.mp3']);
    this.load.audio('bump-1', ['assets/audio/bump-1.mp3']);
    this.load.audio('cash', ['assets/audio/cash.mp3']);
    this.load.audio('select', ['assets/audio/select.mp3']);
    this.load.audio('auch', ['assets/audio/auch.mp3']);
    this.load.audio('lose', ['assets/audio/lose.mp3']);
    this.load.audio('win', ['assets/audio/win.mp3']);

    this.load.image('coin', 'assets/images/coin.png');
    this.load.image('sofa', 'assets/images/sofa.png')
    this.load.image('sofa_1', 'assets/images/sofa_1.png')
    this.load.image('sofa_2', 'assets/images/sofa_2.png')
    this.load.image('sofa_3', 'assets/images/sofa_3.png')
    this.load.image('sofa_4', 'assets/images/sofa_4.png')
    this.load.image('ball', 'assets/images/ball.png')
    this.load.image('arrow', 'assets/images/arrow.png')
    this.load.bitmapFont('joystix', 'assets/fonts/joystix.png', 'assets/fonts/joystix.fnt')
}
....

Выглядит немного нагружено, правда? Предлагаю слегка оптимизировать этот код следующим образом:

  
  preload() {
    let audios = [
      {name: "bump", link: "assets/audio/bump.mp3"},
      {name: "bump-1", link: "assets/audio/bump-1.mp3"},
      {name: "cash", link: "assets/audio/cash.mp3"},
      {name: "select", link: "assets/audio/select.mp3"},
      {name: "auch", link: "assets/audio/auch.mp3"},
      {name: "lose", link: "assets/audio/lose.mp3"},
      {name: "win", link: "assets/audio/win.mp3"}
    ]
    for(let audio of audios) this.load.audio(audio.name, audio.link);
    
    let images = [
      {name: 'coin', link: 'assets/images/coin.png'},
      {name: 'sofa', link: 'assets/images/sofa.png'},
      {name: 'sofa_1', link: 'assets/images/sofa_1.png'},
      {name: 'sofa_2', link: 'assets/images/sofa_2.png'},
      {name: 'sofa_3', link: 'assets/images/sofa_3.png'},
      {name: 'sofa_4', link: 'assets/images/sofa_4.png'},
      {name: 'arrow', link: 'assets/images/arrow.png'},
      {name: 'sofa_4', link: 'assets/images/sofa_4.png'}
    ]
    for(let image of images) this.load.image(image.name, image.link);
    
    this.load.bitmapFont('joystix', 'assets/fonts/joystix.png', 'assets/fonts/joystix.fnt')
  }

Мы объединили все изображения и звуки в отдельные массивы и реализовали их загрузку в цикле. На мой взгляд код стал выглядеть более лаконично, но это лишь один из вариантов как можно правильно устроить работу с подобными данными.

 

После того как все изображения были загружены, Вы можете отобразить их на сцене.  Очистите содержимое метода create у сцены Welcome.Вывод текста bitmap осуществляется при помощи метода dynamicBitmapText.

this.add.dynamicBitmapText(<x - int>, <y - int>, '<имя шрифта - string>', 'Название игры', <font-size - int>);
this.add.dynamicBitmapText(0, 0, 'joystix', 'Название игры', 30);

В результате текст должен отобразиться в верхнем левом углу. Так произошло потому что начало координат в Phaser находится в верхнем левом углу. Вот наглядный пример расположения осей координат XY:

Давайте переместим надпись в центр холста. Для этого нам нужно сместить ее на половину всей высоты и половину всей ширины холста. Размеры холста хранятся в классе systems и он доступен нам в текущем контексте по ссылке this.sys.

Ширина холстаthis.sys.canvas.width

Высота холстаthis.sys.canvas.height

this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2, 'joystix', 'Название игры', 30);

Текст сместился, но не совсем туда куда мы планировали, это происходит потому что центр смещения объекта изначально находится в левом верхнем углу. Изменить его позицию можно при помощи метода setOrigin(float), который в качестве агрумента принимает два числа для каждой оси координат(XY) или одно число для установки сразу на обе оси. Число обычно указывают в диапазоне от 0 до 1. Соответственно если указать значение 0.5, то смещение будет производиться относительно центра объекта.

this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2, 'joystix', 'Название игры', 30).setOrigin(0.5);

Теперь когда Вы знаете как правильно позиционировать объекты на сцене, предлагаю Вам самостоятельно расположить на сцене кнопки меню: Игра, Магазин, Настройки. Размер текста сделать чуть меньше заголовка, приблизительно 20px. В результате у Вас должно получиться следующее:

this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2 - 100, 'joystix', 'Название игры', 30).setOrigin(0.5);

this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2, 'joystix', 'Игра', 20).setOrigin(0.5);
this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2 + 50, 'joystix', 'Магазин', 20).setOrigin(0.5);
this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2 + 100, 'joystix', 'Настройки', 20).setOrigin(0.5);

Давайте отобразим HUD - элементы в верхней части сцены. Так как эти элементы будут присутствовать на всех сценах, предлагаю вынести их в отдельный класс что бы не дублировать код. Воспользуемся классом Helper в папке /src/classes/Helper.ts.

Добавим туда два метода - один для отображения количества монет, а другой для отображения лучшего счета пользователя. Данные о счете и количестве монет мы будем хранить в LocalStorage. Метод должен получать значение из LocalStorage и выводить его на сцену. Методы следует сделать статическими, так как они никак не будут привязаны к контексту класса. В качестве аргумента мы будем принимать объект сцены типа Phaser.Scene, для того что бы иметь возможность с ней взаимодействовать (добавлять изображения и текст). Назовем эти методы drawCoins и drawBestScore:


const STORAGE_COINS = 'coins',
STORAGE_BEST_SCORE = 'best-score';

export default class Helper {

	static drawCoins(context: Phaser.Scene) : void {
		let coinsCount = window.localStorage.getItem(STORAGE_COINS);
		if(!coinsCount) coinsCount = "0";

		context.add.image(25, 25, 'coin').setDisplaySize(30,30);
		context.add.dynamicBitmapText(50, 11, 'joystix', coinsCount, 20);
	}

	static drawBestScore(context: Phaser.Scene) : void {
		let bestScore = window.localStorage.getItem(STORAGE_BEST_SCORE);
		if(!bestScore) bestScore = "0";

		context.add.dynamicBitmapText(context.sys.canvas.width - 25, 11, 'joystix', bestScore, 20).setOrigin(1, 0);
	}
}

Теперь эти методы можно использовать в сцене меню, для этого импортируйте класс Helper из папки ./scr/classes/ и вызовите методы в методе create, передав параметром объект this.

import Helper from "../classes/Helper"
....
Helper.drawCoins(this);
Helper.drawBestScore(this);

В результате Вы должны увидеть следующее:

Все что нам осталось сделать на этой сцене - добавить обработку событий при нажатии на элементы меню. В примере игры из моего репозитория для навигации необходимо использовать клавиатуру, но в данном уроке мы будем делать навигацию при помощи курсора.

По умолчанию все элементы сцены - статические и не реагируют на нажатия. Что бы это исправить необходимо вызвать метод setInteractive, поле чего можно добавлять слушателей на события нажатия на элемент. Давайте попробуем это сделать. Для этого добавим кнопке "игра" обработчик, который будет выводить alert "Игра начинается...". События нажатия на элемент называется pointerdown. С списком всех событий Phaser Вы можете ознакомиться на  этой странице.


let startButton = this.add.dynamicBitmapText(this.sys.canvas.width / 2, this.sys.canvas.height / 2, 'joystix', 'Игра', 20).setOrigin(0.5);
startButton.setInteractive(); // Разрешаем кнопке реагировать на нажатия и другие события
startButton.on(Phaser.Input.Events.POINTER_DOWN, (event) => {
  alert("Игра начинается...")
})

За работу с сценами отвечает класс SceneManager. Благодаря этому классу Вы можете запускать/сменять сцены, смещать и делать многое другое, но в настоящее время нас интересует механизм смены сцены. По нажатию на кнопку "игра", сцена должна меняться на основную сцену игры. Это можно реализовать при помощи метода start:

this.scene.start("<название сцены>")

Самое время создать три новые сцены пунктов меню. Назовем их: Game - игра, Shop - магазин, Settings - настройки.

Что бы добавить сцену, необходимо добавить файл в папку /src/scenes  с именем соответствующим названию сцены и расширением .ts. В этом файле необходимо создать класс, описывающий сцену, который унаследован от Phaser.Scene. Название класса , файла и сцены должны совпадать.

После того как Вы создали сцену, необходимо добавить ее в нашу игру. Для этого откройте файл /src/game.ts и добавьте сцену по аналогии с другими сценами игры.

Базовый шаблон сцены выглядит следующим образом:


DemoScene.ts


export class DemoScene extends Phaser.Scene {

  constructor() {
    super({
      key: "DemoScene",
    });
  }
  
  preload() {
    // Preload all files here
  }

  create() : void {
    // Main method
  }

  update() : void {
    // Callback of update every frame
  }

}

 

Подведем итоги

Мы узнали о жизненном цикле сцены в Phaser, научились создавать новые сцены и производить смену сцен. В ходе статьи мы узнали как подгружать изображения, шрифты и аудио-файлы в игру. Была создана сцена меню, на которой отображается HUD и навигация по меню, которая сменяет сцены.

Задание

  • Создать сцены для всех пунктов меню;
  • Вывести название сцены в центре холста для каждой из трех сцен;
  • Реализовать переход на сцену при клике на пункт меню;
  • Добавить декор-элементы в виде движущихся платформ слева и справа в меню(более подробно в 3 части).

Скачать архив с примерами из этой статьи

 

Другие части статьи: