HTML5 Phaser ECMAScript 6

Часть 9: Игровые сцены в Phaser и зачем они нужны

Для того что бы начать создавать игры с помощью Phaser, надо понимать как они устроены. Тема довольно большая, по этому я расскажу только самое необходимое для старта. Разбираться в деталях мы будем по мере продвижения.

Лучше всего игру представить как спектакль из нескольких действий. По мере развития сюжета, одна сцена заменяется другой. Это очень практично, так как декорации на каждой сцене заранее расположены в нужных местах. Достаточно повернуть сцену вокруг оси, что бы заменить сцену на новую (круглая сцена разделенная перегородками на два или три сектора).

театральная сцена с сменой декораций

Обратите внимание, нас ни кто не заставляет использовать много сцен. Но если использовать одну, то придеться быстро подготавливать новую сцену во время антракта… сотрудникам театра придеться быстро разбираться: какую декорацию надо оставить, какую добавить, что куда переместить.

Так и в играх, логично иметь сцену загрузки, меню, настроек, самой игры или даже конца (game over). Подготовив их заранее, можно сделать анимированный переход с одной к другой. В Phaser сцены называются stage-ами, перевод: состояние (тоже не плохая ассоциация).

Разбивка всей игры на сцены имеет еще одно ключевое преимущество. Каждая сцена содержит логику (код) которая необходима для работы данной сцены. Например, сцене меню нет необходимости знать, что при нажатии стрелок на клавиатуре, главный герой должен перемещаться на экране. За счет этого очень легко разбирать код и вносить изменения. В играх принято используют минимум: Boot, Preload и Game сцены.

  1. Boot — сцена запуска. Подходит для установки исходных значения игры. При этой сцене на экране игрок видит белый/черный экран. По этому, данная сцена должна выполнить быстро только самые важные операции и подготовить графику для сцену загрузки.
  2. Preload — в играх очень много контента и на его загрузку в память нужно время. Эта сцена отображает на экране фон с логотипом и полоску загрузки. Полоска помогает сообщить пользователю, что игра не зависла и надо немного подождать.
  3. Game — непосредственно сама игра

Применяя на практике новые знания, привожу новое содержимое app.js файла:

var game;

import Boot from './states/Boot.js';
import Preload from './states/Preload.js';
import Game from './states/Game.js';

window.onload = function () {
  game = new Phaser.Game(480, 800, Phaser.AUTO, 'game');

  game.state.add('boot', Boot);
  game.state.add('preload', Preload);
  game.state.add('game', Game);

  game.state.start('boot');
};

Первое что надо сделать при разработке игры, это создать сам объект игры Phaser.Game(). Два числовых параметра это расширения нашей игры. Выбор значения третьего параметра, лучше доверить системе (движок который будет отвечать за отрисовку). Наконец четвертый параметр это id селектора HTML страницы. Именно этот селектор Phaser будет использовать как «экран» вывода графики.

Дальше добавляем все три сцены методом game.state.add(). Два параметра которые мы передаем: имя сцены и класс сцены. Так как я вынес класс каждой сцены в отдельный файл для удобства, то его надо импортировать командой import в начале app.js файла. Как только все три сцены добавлены в игру, можно запускать первую методом game.state.start(). Как видите все очень просто!

Сцена должна содержать одно из следующих методов: preload, create, update. При запуске сцены Phaser в первую очередь выполняет код из create метода, а после preload. А вот метод update выполняется циклично. В create лучше всего задавать исходные значения сцены. Так как к сцене игрок может еще вернуться много раз, надо убедится, что все необходимые действия по очистке старых данных были произведены. В preload можно подгрузить дополнительную графику. В игре может быть очень много графики и нет необходимости подгружать ВСЮ заранее в Preload сцене (экономия памяти). Update метод является главным, в нем происходит вся логика игры: расчет движения, удаления или добавления в цену дополнительных объектов, а также считывание состояний клавиш клавиатуры, мышки и т.д.

Давайте посмотрим принцип работы сцены на примере Boot.js файла:

export default class Boot {
  preload() {
    this.load.image('preload_bg', 'assets/images/preload_bg.png');
    this.load.image('preload_progress', 'assets/images/preload_progress.png');
  }

  create() {
    this.game.state.start('preload');
  }
}

В Boot сцене мы подгрузим только два изображения, которые нам нужны для реализации сцены загрузки (Preload) данных. Для этого, надо использовать метод this.load.image() с двумя параметрами: любое уникальное имя, для идентификации данного графического изображения и путь к файлу.

Следующая сцена немного сложней, вот содержимое Preload.js файла:

export default class Preload {
  constructor() {
    this.load_complete = false;
    this.logo_shown = false;
  }

  create() {
    this.logo_shown_timer = this.game.time.create(false);
    this.logo_shown_timer.add(3000, this.on_logo_shown, this);
    this.logo_shown_timer.start();

    this.add.sprite(0, 0, 'preload_bg');
    var preload_progress = this.add.sprite(55, 557, 'preload_progress');

    this.load.setPreloadSprite(preload_progress);
    this.load.onLoadComplete.addOnce(this.on_load_complete, this);

    this.load.image('menu_bg', 'assets/images/menu_bg.png');
    this.load.image('menu_logo', 'assets/images/menu_logo.png');
    this.load.image('menu_skill_circle', 'assets/images/menu_skill_circle.png');
    this.load.image('menu_avatar', 'assets/images/menu_avatar.png');
    this.load.image('menu_button_divider', 'assets/images/menu_button_divider.png');

    this.load.image('button_learn', 'assets/images/button_learn.png');
    this.load.image('button_listen', 'assets/images/button_listen.png');
    this.load.image('button_write', 'assets/images/button_write.png');
    this.load.image('button_game', 'assets/images/button_game.png');
    this.load.image('button_achievement', 'assets/images/button_achievement.png');
    this.load.image('button_top', 'assets/images/button_top.png');
    this.load.image('button_settings', 'assets/images/button_settings.png');

    this.load.image('skill_learn_bg', 'assets/images/skill_learn_bg.png');
    this.load.image('skill_listen_bg', 'assets/images/skill_listen_bg.png');
    this.load.image('skill_write_bg', 'assets/images/skill_write_bg.png');
    this.load.image('skill_progress', 'assets/images/skill_progress.png');
    this.load.image('skill_level', 'assets/images/skill_level.png');
    this.load.image('skill_icon_learn', 'assets/images/skill_icon_learn.png');
    this.load.image('skill_icon_listen', 'assets/images/skill_icon_listen.png');
    this.load.image('skill_icon_write', 'assets/images/skill_icon_write.png');

    this.load.start();
  }

  update() {
    if (this.load_complete && this.logo_shown)
      this.game.state.start('game');
  }

  on_load_complete() {
    this.load_complete = true;
  }

  on_logo_shown() {
    this.logo_shown = true;
  }
}

Изображение в сцену добавляется в виде sprite-а. Для этого используется метод this.add.sprite() где первые два параметра это: x, y координаты и уникальное имя графического изображения, которое мы придумали в Boot сцене. После того как мы добавили фоновое изображение, добавляем изображение полоски прогресса и методом this.load.setPreloadSprite() передаем встроенному в Phaser механизму загрузки. Метод this.load.onLoadComplete.addOnce() дает возможность добавить функцию, которая будет выполнена по завершению загрузки контента. Чуть ниже знакомый нам код загрузки изображений… сам процесс загрузки запускается методом this.load.start().

При каждом обновлении сцены, мы проверяем состояние загрузки. Как только загрузка завершена, мы переходим на главную сцену игры Game. Если загрузка графики пройдет быстро, пользователь может не увидеть окошко загрузки. Что бы избежать неприятного мигания экрана, я добавил таймер, который гарантированно не даст перейти на новую сцену, пока со времени старта не прошло 3 секунды.

Готовую игру можно посмотреть тут Learn English (запуск будет с паузой из за того, что phaser.js файл весит 700кб). В следующем посту я расскажу как я создал составной компонент, анимировал прогресс и с какими сложностями мне придеться разбираться. Но сцену меню мне все же удалось сделать и частично оживить. В целом я не рассчитал, что на написание постов будет уходить так много времени.

8 thoughts on “Часть 9: Игровые сцены в Phaser и зачем они нужны

  1. Тут обязательно следует добавить, что концепция «сцен» — это, похоже, универсальный паттерн для многих игровых фреймворков

    Я впервые познакомился с этой концепцией еще в Stencyl дот com

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

    PS: посты не забрасывай. Хоть они и отнимают много времени, это дополнительная прошивка в мозг — потом сам будешь лучше помнить и врубаться )
    Я обычно веду небольшой дневник в текстовом файле, где кратко набрасываю концепции и рецепты, которые разъяснил для себя, и которые для мне кажутся более быстрыми и актуальными, чем офф. доки

  2. Спасибо, кстати, за наводку на Phaser Editor — похоже, я пока на нем буду баловаться с HTML5

    я обнаружил, что метку о триальности он сохраняет в месте распаковки (я под Windows взял дистрибутив в zip) после первого запуска. Поэтому через две недели можно будет просто удалить все файлы в рабочей папке программы и скопировать их снова из распаковки. Хотя если дело хорошо зайдет — не пожалею и денег.

    Все-таки концепция «генерируем код» — она крутая. И верстка сильно ускоряется, плюс встроенные плюшки — вроде упаковки атласов и работа с анимациями. В Java + LibGDX все более фрагментарно и раздробленно.

  3. PS: для смены сцен с красивыми переходами в Phaser можно подрубить плагинчик
    github.com/cristianbote/phaser-state-transition

    демо переходов — codepen.io/cristianbote/full/GjgVxg

    я как раз в комменте про сцены хотел рассказать, что поскольку сцены — это расширенный класс группы, ими можно баловаться, как спрайтами, то есть делать анимации переходов между экранами. На LibGDX вручную такое делал, а на Phaser как сделать — на момент написания первого комментария еще не знал ^_^

    А тут готовый плагин и пара строчек ))

  4. Спасибо за поддержку! Разбирать новый материал, кодить и писать посты одновременно очень сложно. Но ты прав, я заметил что они помогают закрепить знания.

    На счет лицензии Phaser Editor — ее автор сказал, что она обнуляется с каждой новой версией и как раз сейчас ожидается очередное обновление. Из туториалов он посоветовал начать тут. Я лично не буду его пока смотреть, что бы не заразиться =) Это может приостановить практическую разработку игры.

    А вот генерацию спраитов интерфейса и аудио я буду делать одной из утилит grunt (чуть позже напишу пост). Она делает все автоматом и создает xml файл с данными о карте которую понимает Phaser.

    Ты уверен, что сцену можно анимировать tween-ом? Я посмотрел плагин, очень понравился. Надо будет разобрать его исходник, может его подход пригодится и в другой части программы. Тем более мне сейчас как раз панель с кнопками надо анимизировать.

  5. //сцену можно анимировать tween-ом

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

    В LibGDX точно отвечаю — что это просто группа элементов и там я просто добавляю местный аналог tween (там аналог называется Actions)

    Параллельно просто ковыряю простые туториалы по Phaser, хочу небольшой кликер сделать для одного из своих сайтов для тренировки. Основной проект пока должен доделать под Android на LibGDX

  6. Я бегло просмотрел исходник плагина. Ты прав, он делает скриншот сцены и анимирует его. Не совсем пока понятно, как он получает сркиншот второй сцены. Ведь она еще визуально не видна, а по примерам видно, что анимация текущей и новой, происходит одновременно.

    На счет кликера — если с английским у тебя гуд, то могу поделиться ссылкой на статью, в которой создается простой кликер на Phaser-е. Недостатком всех этих туториалов то, что они заведомо делаю все грубо, не раскрывая подробностей, а именно эти подробности делают кликер привлекательными. Баян но в тему:

    пошаговая инструкция: как рисовать лошадь

    Первое исключение это прошлогодний марафон Элспера, по этому я решил делать вместе с ним игру.

  7. Учитывая, что полоску загрузки можно генерить на лету из graphics — я решил вовсе отказаться от Boot

    Настройки и все загрузки теперь в Preloader

    Логотип сделан как обычный HTML-объект поверх картинки, который убирается в Preloader в create

    http://zaxx.ru/wp-content/uploads/2017/11/2017-11-30_141226.png

  8. Я тоже об этом думал Zaxx. Но позже сообразил, что есть частный случай когда эта Boot сцена полезна. Вот смотри, когда пользователь выбрал язык интерфейса, я прописал его в профиле и перекинул игрока на Preload сцену. Таким простым движением я как бы перезаупстил игру, но за счет обновленного профиля, игрок уже видит игру в другом языке =)

    А теперь представь, если бы я в Preload сцене делал всю инициализацию, а это: AchievementManager, LessonManager, TweenManager, BackButtonManager и самый главный TranslateManager. А так они инициализируются только один раз при старте игры. Но возможно есть и другое решение моей задачи, без Boot. Я ее оставил из доверия к более опытным разработчикам, которые советуют ее создавать.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *