Картинка для привлечения внимания
Доброго времени суток!
Где-то месяц назад (на момент написания этого поста) я задался целью создать свой клон игры 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.
<!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 = "TOUCHnTOnFLY", //Инструкция в главном меню
DEVELOPER_TEXT = "DevelopernEugene Obrezkovnghaiklor@gmail.com", //Куда ж без копирайтов :)
GRAPHIC_TEXT = "GraphicnDmitry Lezhenkondima.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:
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():
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 (Таблица рекордов)
Автор: ghaiklor