...

воскресенье, 16 марта 2014 г.

Разрабатываем 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://") + "http://ift.tt/1iQWSCN";
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 (Таблица рекордов)


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий