Пишем HTML5-игру (введение в Phaser framework)
Эта статья посвящена разработке стильных, модных и молодежных HTML5 приложений с помощью нового фреймворка Phaser. В ней описан процесс установки библиотеки и создание классической игры Pong.
Введение
Phaser — это движок для разработки мобильных и десктопных HTML5 игр, базирующийся на библиотеке PIXI.js. Поддерживает рендеринг в Canvas и WebGL, анимированные спрайты, частицы, аудио, разные способы ввода и физику объектов. Исходники доступны как для просмотра, так и для свободной модификации. Он создан Ричардом Дейви (Richard Davey), известному благодаря активному участию в сообществе программистов, использующих Flixel framework. Ричард не скрывает, что вдохновлялся Фликселем, поэтому некоторые вещи в Фазере будут знакомы опытным флешерам. Первая версия нового движка вышла 13 сентября этого года, сейчас ведется не только активное развитие библиотеки, но и написание документации, поэтому в данный момент уроков по ней, мягко говоря, немного. Что, по моему скромному мнению, следует исправлять, и прямо сейчас.
Установка библиотеки и локального веб-сервера
Итак, начнем. Для запуска и тестирования приложений нам необходимо установить локальный веб-сервер. Все примеры из комплекта библиотеки используют PHP, поэтому и сервер нужен соответствующий. Я использовал MAMP для MacOS, для Windows подойдет отечественный Denwer или любой другой аналог.
После установки веб-сервера необходимо скачать последнюю версию Фазера c GitHub:https://github.com/photonstorm/phaser. В данный момент (13 октября 2013 года) рекомендую качать dev ветку, так как эта версия содержит в себе ряд очень полезных изменений по сравнению с основной, в том числе и больший объем документации. Для тех, кто не использует GitHub, доступна прямая ссылка на архив:https://github.com/photonstorm/phaser/archive/dev.zip.
Чтобы убедиться, что все настроено правильно, можно запустить небольшое приложение-пример Hello Phaser. Создайте папку hellophaser в директории вашего веб-сервера, предназначенной для сайтов, и скопируйте туда три файла из папкиDocs/Hello Phaser:
Запустите свой любимый браузер и откройте URL со скопированными файлами (в моем случаеhttp://localhost:8888/hellophaser/). Если все хорошо, вы увидите вращающийся симпатичный логотип, такой как на скриншоте ниже:
Разработка игры
Подготовка необходимых файлов
Теперь можно приступать к разработке нашей первой игры. Создайте для нее папку phaser-pong на вашем веб-сервере и скопируйте туда файл phaser.js из папки build с исходниками фреймворка. Также создайте в ней папку assets, где мы будем хранить все ресурсы, относящиеся к игре, и файл index.html (собственно, здесь и будет наша игра).
Скопируйте в папку assets изображения шарика, ракетки и фона. Можно взять следующие файлы (в качестве фона я взял звездное небо из примеров Фазера), а можно нарисовать что-то свое. Главное — это убедиться, что вы загружаете в игру нужные картинки с корректными именами и подходящими размерами. Также не стоит выбирать слишком большие изображения, с их отрисовкой могут возникнуть проблемы. Поэтому перед использованием фотографии своего кота уменьшите ее до, скажем, 480х640 (разрешение нашей игры), и все будет хорошо.
В результате содержимое папки phaser-pong будет таким:
А в папке assets будет три картинки:
Создание главного объекта игры, загрузка ресурсов
Наконец-то все подготовительные этапы выполнены, и начинается собственно разработка. Откройте index.html и вставьте туда следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<span style="color: #009900;"><<span style="color: #000000; font-weight: bold;">script</span> <span style="color: #000066;">src</span><span style="color: #66cc66;">=</span><span style="color: #ff0000;">"phaser.js"</span>><<span style="color: #66cc66;">/</span><span style="color: #000000; font-weight: bold;">script</span>></span> <span style="color: #009900;"><<span style="color: #000000; font-weight: bold;">script</span> <span style="color: #000066;">type</span><span style="color: #66cc66;">=</span><span style="color: #ff0000;">"text/javascript"</span>></span> var game = new Phaser.Game(480, 640, Phaser.AUTO, '', { preload: preload, create: create, update: update }); function preload() { game.load.image('bet', 'assets/bet.png'); game.load.image('ball', 'assets/ball.png'); game.load.image('background', 'assets/starfield.jpg'); } function create() { game.add.tileSprite(0, 0, 480, 640, 'background'); } function update () { } <span style="color: #009900;"><<span style="color: #66cc66;">/</span><span style="color: #000000; font-weight: bold;">script</span>></span> |
Откройте в браузере адрес новой игры (у меня это http://localhost:8888/phaser-pong/) и вы увидите ее окно с нарисованным фоном
Что же происходит в написанном коде? Вначале мы импортируем библиотеку. Потом создаем объект типа Game
, задавая разрешение нашего приложения, в данном случае ширину 480 и высоту 640 пикселей. Phaser.AUTO
означает, что тип рендера будет выбираться автоматически. При желании можно задать Canvas или WebGL. Четвертый параметр задает родительский DOM-объект для игры, его мы не указываем.
В пятом параметре перечислены основные функции игры. preload()
содержит код для загрузки ресурсов, create()
— инициализацию игры, а update()
— команды, выполняемые при обновлении приложения. На настольном компьютереupdate()
вызывается примерно 60 раз в секунду. Именно эта функция будет содержать в себе основную игровую логику.
В функции create()
мы добавляем статический спрайт с фоном нашей игры. Спрайт заполняет пространство, указанное в первых четырех параметрах tileSprite
.
Игровые объекты
Сейчас перейдем к самому интересному — наполним нашу игру логикой. После объявления переменной game
и перед функцией preload()
объявим объекты с ракетками игрока и компьютера, мячиком, а также укажем скорости их движения:
1 2 3 4 5 6 |
var playerBet; var computerBet; var ball; var computerBetSpeed = 190; var ballSpeed = 300; |
Для создания ракеток напишем функцию createBet(x, y)
:
1 2 3 4 5 6 7 8 9 |
function createBet(x, y) { var bet = game.add.sprite(x, y, 'bet'); bet.anchor.setTo(0.5, 0.5); bet.body.collideWorldBounds = true; bet.body.bounce.setTo(1, 1); bet.body.immovable = true; return bet; } |
Метод создает спрайт с указанными координатами и добавляет его в игру. Поле anchor
отвечает за точку отсчета координат спрайта, устанавливаем его по центру изображения ракетки. body
содержит в себе элементы для работы с физикой. Здесь мы ограничиваем движение ракетки пределами игрового пространства, задаем силу «отскока» и указываем, что при столкновении с объектами ракетка не будет отлетать в сторону.
Добавим два вызова этой функции в create(), сразу после создания фона. Ракетки будут добавлены в игру после фонового изображения, поэтому мы будем их видеть на экране:
1 2 |
playerBet = createBet(game.world.centerX, 600); computerBet = createBet(game.world.centerX, 20); |
Аналогичным образом создадим шарик, дописав следующий код сразу после вызовов функции createBet()
1 2 3 4 |
ball = game.add.sprite(game.world.centerX, game.world.centerY, 'ball'); ball.anchor.setTo(0.5, 0.5); ball.body.collideWorldBounds = true; ball.body.bounce.setTo(1, 1); |
В результате увидим, что в нашей игре появились две ракетки и мячик, пока неподвижные:
Логика
Картинка получилась симпатичной, но думаю, стоит ее слегка оживить.
Добавляем переменную, отвечающую за состояние шарика и функцию, которая будет его запускать:
1 2 3 4 5 6 7 8 9 |
var ballReleased = false; function releaseBall() { if (!ballReleased) { ball.body.velocity.x = ballSpeed; ball.body.velocity.y = -ballSpeed; ballReleased = true; } } |
Функция проверяет, что шарик еще не запущен, и в таком случае задает ему скорость с помощью поля velocity.
Вызов функции повесим на нажатие кнопки мышки, написав следующую строку в create():
1 |
game.input.onDown.add(releaseBall, this); |
Теперь клик мышкой запускает шарик, и он отскакивает от границ игры. Добавим движения и ракеткам, отредактировав функцию update()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function update () { //Управляем ракеткой игрока playerBet.x = game.input.x; var playerBetHalfWidth = playerBet.width / 2; if (playerBet.x <span style="color: #009900;">< playerBetHalfWidth<span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span></span> <span style="color: #009900;"> playerBet.x <span style="color: #66cc66;">=</span> playerBetHalfWidth;</span> <span style="color: #009900;"> <span style="color: #66cc66;">}</span></span> <span style="color: #009900;"> else if <span style="color: #66cc66;">(</span>playerBet.x ></span> game.width - playerBetHalfWidth) { playerBet.x = game.width - playerBetHalfWidth; } //Управляем ракеткой компьютерного соперника if(computerBet.x - ball.x <span style="color: #009900;">< -<span style="color: #cc66cc;">15</span><span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span></span> <span style="color: #009900;"> computerBet.body.velocity.x <span style="color: #66cc66;">=</span> computerBetSpeed;</span> <span style="color: #009900;"> <span style="color: #66cc66;">}</span></span> <span style="color: #009900;"> else if<span style="color: #66cc66;">(</span>computerBet.x - ball.x ></span> 15) { computerBet.body.velocity.x = -computerBetSpeed; } else { computerBet.body.velocity.x = 0; } } |
Ракетка игрока следует за указателем мыши, ракетка компьютера — за шариком. Для ракетки игрока мы также добавили ограничение на максимальное и минимальное значение координаты x
, чтобы она не пыталась выскочить за пределы игрового поля.
Вся суть игры заключается в отбивании шарика ракетками, поэтому нужно организовать проверку столкновений шарика с ракетками. К счастью, в Фазере уже есть соответствующий функционал, поэтому нам достаточно его использовать.
Допишем следующие три строки в конец update()
:
1 2 3 |
//Проверяем и обрабатываем столкновения мячика и ракеток game.physics.collide(ball, playerBet, ballHitsBet, null, this); game.physics.collide(ball, computerBet, ballHitsBet, null, this); |
Метод collide
проверяет столкновение двух объектов (первые два параметра) и вызывает указанную в третьем функцию для выполнения каких-либо действий над столкнувшимися спрайтами. Эта функция выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function ballHitsBet (_ball, _bet) { var diff = 0; if (_ball.x <span style="color: #009900;">< _bet.x<span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span></span> <span style="color: #009900;"> <span style="color: #66cc66;">//</span> Шарик находится с левой стороны ракетки</span> <span style="color: #009900;"> diff <span style="color: #66cc66;">=</span> _bet.x - _ball.x;</span> <span style="color: #009900;"> _ball.body.velocity.x <span style="color: #66cc66;">=</span> <span style="color: #66cc66;">(</span>-<span style="color: #cc66cc;">10</span> * diff<span style="color: #66cc66;">)</span>;</span> <span style="color: #009900;"> <span style="color: #66cc66;">}</span></span> <span style="color: #009900;"> else if <span style="color: #66cc66;">(</span>_ball.x ></span> _bet.x) { // Шарик находится с правой стороны ракетки diff = _ball.x -_bet.x; _ball.body.velocity.x = (10 * diff); } else { // Шарик попал в центр ракетки, добавляем немножко трагической случайности его движению _ball.body.velocity.x = 2 + Math.random() * 8; } } |
При столкновении шарик меняет направление своего движения в зависимости от того, на какую часть ракетки попадает.
Осталось только добавить проверку на пропущенный гол. Если кто-то его пропустил, ставим шарик в изначальную позицию по центру поля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function checkGoal() { if (ball.y <span style="color: #009900;">< <span style="color: #cc66cc;">15</span><span style="color: #66cc66;">)</span> <span style="color: #66cc66;">{</span></span> <span style="color: #009900;"> setBall<span style="color: #66cc66;">(</span><span style="color: #66cc66;">)</span>;</span> <span style="color: #009900;"> <span style="color: #66cc66;">}</span> else if <span style="color: #66cc66;">(</span>ball.y ></span> 625) { setBall(); } } function setBall() { if (ballReleased) { ball.x = game.world.centerX; ball.y = game.world.centerY; ball.body.velocity.x = 0; ball.body.velocity.y = 0; ballReleased = false; } } |
checkGoal()
вызывается постоянно, поэтому копируем ее в конец update()
:
1 2 |
//Проверяем, не забил ли кто-то гол checkGoal(); |
Все! Открываем браузер, наслаждаемся фантастическим и современным геймплеем нашей игры, радуемся жизни и свежеприобретенным навыками программирования.
Заключение
Естественно, игре не хватает еще многого, как минимум подсчета очков и определения победителей. Но мне кажется, что для введения в разработку с Phaser достаточно показанных вещей. Движок поддерживает много других классных функций, которые я собираюсь показать на примере новой игры, чуть более сложной и непосредственно относящейся к Хабру, чтобы было интереснее.
Стей тьюнед.
В ходе разработки я активно использовал код из примера breakout.php. Кроме этого примера, в папке с Фазером есть и другие игры, поэтому тем, кому не терпится использовать новый фреймворк, рекомендую в первую очередь посмотреть на содержимое папки examples.
Полезные ссылки:
1. Англоязычное введение в Фазер
2. Форум, посвященный движку
3. Твиттер Ричарда Дейви
Update: по совету товарища hell0w0rd закинул пример на гитхаб и создал страничку, чтобы можно было опробовать игру:
https://github.com/nononononono/phaser-pong
http://nononononono.github.io/phaser-pong/