habrahabr

Разрабатываем Flappy Bird на Phaser (Часть I)

  • понедельник, 17 марта 2014 г. в 06:00:24

http://habrahabr.ru/post/214013/


Картинка для привлечения внимания

Доброго времени суток, Хабр!

Где-то месяц назад (на момент написания этого поста) я задался целью создать свой клон игры Flappy Bird. Но все никак не доходили до этого руки. Катализатором сего действия стал небольшой хакатон. «А почему бы и нет» — подумал я, и взялся за реализацию этой игры.

Учитывая, что разработать нужно было за 2 дня, я не изобретал «велосипедов» и взял готовый игровой движок — Phaser.

В этой части мы рассмотрим инициализацию игровой сцены, напишем «прелоадер» ресурсов и подготовим фундамент для игрового меню.

Что такое Phaser?

Phaser is a fast, free and fun open source game framework for making desktop and mobile browser HTML5 games. It uses Pixi.js internally for fast 2D Canvas and WebGL rendering.


Phaser — это фреймворк, который позволяет нам очень быстро создавать игры. Я не утрирую, с его помощью создать игру действительно легко и быстро. Не отвлекаемся на Actor'ов, рендеринг, физику — фокусируемся на игровой логике.
Его однозначными плюсами есть Pixi.js. Это один из быстрейших движков, который рендерит с помощью WebGL. А в случае, если WebGL не поддерживается — на Canvas.
Также Phaser радует огромным набором готовых классов: SpriteAnimation, TileMap, Timer, GameState и много другое. В том числе, и компоненты физического движка: RigidBody, Physics и т.п.
Наличие данных компонентов значительно упрощает разработку.

Подключаем Phaser и другие зависимости


Я не нагружал игру множеством зависимостей, поэтому список небольшой: Phaser, WebFont и Clay. Первый нужен для разработки игры, WebFont для загрузки шрифтов с Google Fonts и Clay для таблицы рекордов.

Приведенный ниже код содержится в файле index.html.

index.html
<!DOCTYPE html> <head> <meta charset="utf-8"> <title>Flappy Bird</title> <link rel="shortcut icon" href="/favicon.ico" /> <style type="text/css"> * { margin: 0; padding: 0; } </style> </head> <body> <script type="text/javascript"> var Clay = Clay || {}; Clay.gameKey = "gflappybird"; Clay.readyFunctions = []; Clay.ready = function(fn) { Clay.readyFunctions.push(fn); }; (function() { var clay = document.createElement("script"); clay.async = true; clay.src = ("https:" == document.location.protocol ? "https://" : "http://") + "clay.io/api/api-leaderboard-achievement.js"; var tag = document.getElementsByTagName("script")[0]; tag.parentNode.insertBefore(clay, tag); })(); </script> <script src="//ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/phaser/1.1.4/phaser.min.js"></script> <script src="js/Game.js"></script> </body> </html>


В index.html мы просто подключаем зависимости, ничего лишнего. В том числе и наш скрипт Game.js, который мы рассмотрим позже. Не добавляем ни строчки HTML, т.к. Phaser рендерит сцену непосредственно в body.
Phaser может рендерит и в созданный вами контейнер, если это необходимо.

Подключаем шрифты


В Game.js находится только одна функция — GameInitialize(). В замыкании этой функции и происходят все вычисления. Перед тем как ее вызвать, нужно дождаться загрузки шрифтов. Иначе, есть большая вероятность того, что шрифты не успеют загрузиться и они не будут доступны Phaser. Для этого используем WebFont:

WebFont.load({ google: { families: ['Press+Start+2P'] }, active: function() { GameInitialize(); } }); 


Мы «попросили» WebFont загрузить нам шрифт «Press Start 2P» с Google Fonts и при окончании загрузки вызываем функцию GameInitialize(), которая продолжит инициализацию всех необходимых игровых объектов.

В дальнейшем содержание поста будет рассказываться исключительно в рамках функции GameInitialize().

Объявляем константы, создаем экземпляр Phaser.Game, добавляем GameState'ы


Для начала добавим переменные, которые будут иметь значения де-факто при использовании. Так как использование const не слишком «валидно», то используем переменные:

Игровые константы
var DEBUG_MODE = true, //рендерим отладочную информацию SPEED = 180, //скорость полета птички GRAVITY = 1800, //коэффициент гравитации в игровом мире BIRD_FLAP = 550, //с каким ускорением птичка "взлетает" PIPE_SPAWN_MIN_INTERVAL = 1200, //минимальная задержка перед следующей трубой PIPE_SPAWN_MAX_INTERVAL = 3000, //максимальная задержка AVAILABLE_SPACE_BETWEEN_PIPES = 130, //минимальное свободное пространство между трубами (по вертикали) CLOUDS_SHOW_MIN_TIME = 3000, //минимальная задержка перед следующим облаком CLOUDS_SHOW_MAX_TIME = 5000, //максимальная задержка перед следующим облаком MAX_DIFFICULT = 100, //на основе этого коэффициента также вычисляется расстояние между трубами SCENE = '', //идентификатор сцены, где нужно рендерить. В данном случае пусто (по умолчанию рендерит в body) TITLE_TEXT = "FLAPPY BIRD", //Название игры в главном меню HIGHSCORE_TITLE = "HIGHSCORES", //Название игрового меню HIGHSCORE_SUBMIT = "POST SCORE", //Название кнопки в рекордах для сохранения своего рекорда INSTRUCTIONS_TEXT = "TOUCH\nTO\nFLY", //Инструкция в главном меню DEVELOPER_TEXT = "Developer\nEugene Obrezkov\nghaiklor@gmail.com", //Куда ж без копирайтов :) GRAPHIC_TEXT = "Graphic\nDmitry Lezhenko\ndima.lezhenko@gmail.com", LOADING_TEXT = "LOADING...", //Сообщение о загрузке игры WINDOW_WIDTH = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth, WINDOW_HEIGHT = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight; 


Также нам понадобятся вспомогательные переменные для хранения всех созданных объектов Phaser:

Переменные для Phaser-объектов
var Background, //Игровой фон Clouds, CloudsTimer, //Облака и таймер для спауна облаков Pipes, PipesTimer, FreeSpacesInPipes, //Наши трубы, таймер и "прозрачный" объект, который будет "триггером" пролета Bird, //Птичка Town, //TileSprite города на фоне FlapSound, ScoreSound, HurtSound, //Звуки взлета, пролета трубы и проигрыша SoundEnabledIcon, SoundDisabledIcon, //Иконки включения\отключения звука TitleText, DeveloperText, GraphicText, ScoreText, InstructionsText, HighScoreTitleText, HighScoreText, PostScoreText, LoadingText, //все текстовые объекты PostScoreClickArea, //Зона клика для сохранения рекорда isScorePosted = false, //Флаг для проверки, был ли рекорд "запостен" isSoundEnabled = true, //Флаг для проверки, нужно ли воспроизводить звук Leaderboard; //И собственно Leaderboard объект от Clay.io 


Вкратце опишем, что за переменная и зачем она нужна.

  • Background — здесь храним Rectangle с цветом #53BECE.
  • Clouds — группа объектов. Каждый из них является обычным спрайтом.
  • CloudsTimer — таймер, который спаунит новые облака.
  • Pipes — группа объектов. Аналогично облакам, каждый объект является спрайтом.
  • PipesTimer — таймер, который спаунит новые трубы.
  • FreeSpacesInPipes — для того, чтобы определить, что птичка пролетела, нам нужно как-то это событие словить. В этой переменной как раз хранятся объекты без спрайта, который являются триггерами.
  • Bird — храним птичку, у которой есть RigidBody и SpriteMap для анимации.
  • Town — TileMap города, который двигается на фоне.
  • FlapSound — звук, который воспроизводим при щелчке мышкой (взмах крыльями).
  • ScoreSound — звук пролета через трубу.
  • HurtSound — звук окончания игры, коллизия с трубой либо выход за рамки игрового мира.
  • SoundEnabledIcon, SoundDisabledIcon — два спрайта с отображением иконки включенного звука, и выключенного аналогично.
  • TitleText, InstuctionsText, DeveloperText, GraphicText — элементы текста, который мы отображаем в игровом меню.
  • ScoreText — текст, который отображаем во время игры.
  • HighScoreTitleText, HighScoreText, PostScoreText — текст в таблице рекордов.
  • LoadingText — текст загрузки игры.
  • PostScoreClickArea — Rectangle, который будет помогать определить, нажал ли пользователя на кнопку Post Score.
  • isScorePosted — флаг, в целях защиты от повторного постинга этого же рекорда (если пользователь два раза нажмет Post Score в рекордах).
  • isSoundEnabled — флаг, по которому определяем, включенный\выключенный звук в игре.
  • Leaderboard — объект, который хранит респонс от Clay.io.


После объявления всех переменных, можем начать инициализацию Phaser.Game и добавление в игру необходимых GameState'ов.

Phaser.Game() принимает следующие параметры:

new Game(width, height, renderer, parent, state, transparent, antialias)

Нас интересует width, height, renderer, parent. Достаточно указать размеры холста, метод рендеринга и пустой контейнер, чтобы Phaser начал рендерить игровую сцену в body.

Инициализируем Phaser.Game используя наши константы, объявленные раньше:

var Game = new Phaser.Game(WINDOW_WIDTH, WINDOW_HEIGHT, Phaser.CANVAS, SCENE); 


Мы инициализировали игровую сцену, но у нас еще нету игровых State'ов. Нужно исправить эту оплошность.
В Game.state хранится указатель на Phaser.StateManager. В нем есть нужная нам функция add() для добавления собственных State'ов. Ее сигнатура:

add(key, state, autoStart)

key — это строка для идентификации State'а (его ID), state — это объект Phaser.State, autoStart — запускать ли State сразу после его инициализации. В данном случае, autoStart нам не нужен, чтобы могли сами определять вызов State'ов в нужные моменты игры.
Добавим все игровые State'ы в игровую сцену:

Game.state.add('Boot', BootGameState, false); Game.state.add('Preloader', PreloaderGameState, false); Game.state.add('MainMenu', MainMenuState, false); Game.state.add('Game', GameState, false); Game.state.add('GameOver', GameOverState, false); 


Каждый из этих игровых State'ов будет рассмотрен дальше.

Последним шагом, который запустит loop игрового процесса, является старт BootGameState'а.

Game.state.start('Boot'); 


Привожу полный код инициализации игры:

Инициализация игры
//Создаем instance игры на весь экран с использованием Canvas var Game = new Phaser.Game(WINDOW_WIDTH, WINDOW_HEIGHT, Phaser.CANVAS, SCENE); //Включаем поддержку RequestAnimationFrame Game.raf = new Phaser.RequestAnimationFrame(Game); Game.antialias = false; Game.raf.start(); //Добавляем все игровые State в объект Game //В следующих частях каждый из State'ов будет подробно описан Game.state.add('Boot', BootGameState, false); Game.state.add('Preloader', PreloaderGameState, false); Game.state.add('MainMenu', MainMenuState, false); Game.state.add('Game', GameState, false); Game.state.add('GameOver', GameOverState, false); //Главным шагом является старт загрузки Boot State'а Game.state.start('Boot'); //Получаю Clay Leaderboard и сохраняю в вспомогательную переменную Clay.ready(function() { Leaderboard = new Clay.Leaderboard({ id: 'your-leaderboard-id' }); }); 

Как создавать игровые State'ы?


В Phaser есть конструктор Phaser.State(). Все что нужно для создания игрового State'а — это вызвать этот конструктор:

var BootGameState = new Phaser.State(); 


После этого мы можем переопределить выполнение функций Phaser своими. В State можно выделить 4 основных loop'а: create, preload, render, update.

  • Phaser.State.create вызывается после успешной смены State'ов. Сюда можно писать инициализацию логики игры, заполнение переменных и т.п.
  • Phaser.State.preload вызывается и работает во время загрузки ресурсов. Если вам нужно загрузить какой-то спрайт или звук — делайте это здесь.
  • Phaser.State.render вызывается каждый раз, как рендерится кадр (frame). Здесь делаем операции по рендерингу.
  • Phaser.State.update вызывается после рендеринга. Здесь производим расчеты и, собственно, бизнес-логика игры.


Теперь рассмотрим наш стартовый State, который инициализирует игровой loop.

В дальнейших пунктах я буду указывать в скобках имя переменной, в которой хранится Phaser.State()

Уведомим игрока, что загрузка началась (BootGameState)


Создаем instance Phaser.State. После его успешной загрузки добавляем текст с надписью «Loading...» и располагаем по центру. Не забываем начать загрузку PreloaderState'а.

var BootGameState = new Phaser.State(); BootGameState.create = function() { LoadingText = Game.add.text(Game.world.width / 2, Game.world.height / 2, LOADING_TEXT, { font: '32px "Press Start 2P"', fill: '#FFFFFF', stroke: '#000000', strokeThickness: 3, align: 'center' }); LoadingText.anchor.setTo(0.5, 0.5); Game.state.start('Preloader', false, false); }; 



Пишем «прелоадер» ресурсов (PreloaderGameState)


Чтобы загрузить спрайт, звук, анимацию и т.п., в Phaser, можно использовать Phaser.Loader. Указатель на него лежит в Game.load после того, как мы инициализировали сцену. Для нашей игры будет достаточно три метода:

Phaser.Loader.spritesheet(key, url, frameWidth, frameHeight, frameMax, margin, spacing) Phaser.Loader.image(key, url, overwrite) Phaser.Loader.audio(key, urls, autoDecode) 


Используя эти методы, напишем функцию, которая будет загружать в игру ресурсы:

var loadAssets = function loadAssets() { Game.load.spritesheet('bird', 'img/bird.png', 48, 35); Game.load.spritesheet('clouds', 'img/clouds.png', 64, 34); Game.load.image('town', 'img/town.png'); Game.load.image('pipe', 'img/pipe.png'); Game.load.image('soundOn', 'img/soundOn.png'); Game.load.image('soundOff', 'img/soundOff.png'); Game.load.audio('flap', 'wav/flap.wav'); Game.load.audio('hurt', 'wav/hurt.wav'); Game.load.audio('score', 'wav/score.wav'); }; 


Теперь перейдем к PreloaderGameState. Создаем новый Phaser.State().

var PreloaderGameState = new Phaser.State(); 


Переопределяем метод preload, в котором вызываем функцию loadAssets():

PreloaderGameState.preload = function() { loadAssets(); }; 



После успешной загрузки ресурсов, вызывается функция create, в которой мы можем добавить анимацию исчезания Loading текста и загрузку MainMenuState.

PreloaderGameState.create = function() { var tween = Game.add.tween(LoadingText).to({ alpha: 0 }, 1000, Phaser.Easing.Linear.None, true); tween.onComplete.add(function() { Game.state.start('MainMenu', false, false); }, this); }; 


Полный исходный код PreloaderGameState():

PreloaderGameState
var PreloaderGameState = new Phaser.State(); PreloaderGameState.preload = function() { loadAssets(); }; PreloaderGameState.create = function() { var tween = Game.add.tween(LoadingText).to({ alpha: 0 }, 1000, Phaser.Easing.Linear.None, true); tween.onComplete.add(function() { Game.state.start('MainMenu', false, false); }, this); }; 

В итоге


Результатом данной работы является наличие игровой сцены, рабочий preloader. После успешной загрузки всех ресурсов, вызывается MainMenuState, который мы рассмотрим в следующей части.

Полезные ссылки


Phaser
Phaser (GitHub)
Phaser (документация)
Phaser.Game()
Phaser.Loader()
Phaser.State()
Phaser.StateManager()
Pixi.js (GitHub)

FlappyBird
FlappyBird (GitHub)
UPD: В недавних фиксах я убрал полноэкранный режим, так как многие жалуются на производительность.

Хочу услышать мнение сообщества Хабрахабр. Интересно ли вам продолжение? Во второй части рассмотрим следующее:

  • Делаем игровое меню
  • Инициализируем все игровые объекты
  • Добавляем приятных мелочей
  • Подготавливаем базу для бесшовного перехода в сам игровой процесс



Оценочный план на будущие части.
Часть 2 (Меню)
Часть 3 (Игровой процесс)
Часть 4 (Таблица рекордов)