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

от автора


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

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

Где-то месяц назад (на момент написания этого поста) я задался целью создать свой клон игры 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 (Таблица рекордов)

ссылка на оригинал статьи http://habrahabr.ru/post/214013/


Комментарии

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

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