{{notification.text}}

MirGames

img1.jpg

С развитием возможностей браузеров и HTML, идея создания кроссплатформенных игр и приложений с каждым годом становится всё более и более популярной. Уже сейчас существует множество решений, упрощающих создание таких приложений. На вскидку можно назвать WinJS, Intel XDK, Sencha, Ionic, Enyo, Adobe AIR. Ну и, конечно, популярные игровые движки вроде Cocos2D, EasyJS, Three.js и Phaser.

Я - большой фанат языка TypeScript, и, соответственно, фреймворков, которые позволяют его использовать. В частности, я хочу показать вам, как можно использовать фреймворк Phaser и язык TypeScript для создания простого платформера. Также мы будем использовать сборщик проектов GulpJS.

Что такое Phaser?

Phaser - это фреймворк для разработки десктопных и мобильных HTML5 игр. Вы пишете игру, используя возможности HTML5 и JS, затем эту игру можно будет запускать в любом современном браузере, в том числе и на мобильных устройствах. Впрочем, на мобильных устройствах подобные приложения обычно запускаются с помощью PhoneGap или CocoonJS, что позволяет добиться лучшей производительности и доступа к продвинутым возможностям устройств, таким как нотификации, работу с контактами устройства, считывание данных различных датчиков.

Phaser распространяется с открытым исходным кодом по лицензии MIT, что означает, что вы можете использовать этот код без ограничений, но с сохранением уведомлений об авторском праве в копиях ПО, т.е. в тексте вашей лицензии необходимо будет добавить указание авторских прав на этот фреймворк.

Официальный сайт Phaser: http://phaser.io/

TypeScript

Несколько слов о TypeScript для тех, кто никогда с ним не сталкивался. TypeScript - это статически-типизированное надмножество ECMAScript 6 или, проще говоря, это типизированный JavaScript с классами.

Типизированность языка сильно увеличивает возможности авто-дополнения кода, уменьшает количество ошибок и делает код более строгим. Код на языке TypeScript транслируется в язык JavaScript (ECMAScript 3 или ECMAScript 5).

Подробнее о нём можно прочитать в моём обзоре этого языка, а также на сайте TypeScript. Наш пример будет настолько прост, что не потребует каких-либо глубоких знаний языка.

Установка

Итак, что нам потребуется, чтобы начать работать с Phaser?

Нам потребуется:

  • Node.js для работы компилятора TypeScript и пакетного менеджера NPM.
  • Веб-сервер.
  • Task-manager, типа gulp или grunt для сборки нашего проекта.
  • Ваш любимый браузер.
  • Ваша любимая среда разработки. Желательно, чтобы она поддерживала TypeScript, это может быть Visual Studio, Sublime Text, Eclipse, Vim, Webstorm и т.п. Я использую Visual Studio.
  • Ну и, разумеется, базовое умение работать в командной строке, базовые знания HTML и JavaScript.

Web Server

Нынешние браузеры, к сожалению, а может быть и к счастью, параноидальны относительно файлов, загружаемых из локальной файловой системы, поэтому для полноценной работы движка Phaser нам потребуется установить локальный веб-сервер. Веб-серверов существует великое множество, и выбор не принципиален. Если вы до этого работали с Apache, используйте его. Если вы фанат IIS, IIS Express, nginx, Mongoose, или голого веб-сервера, написанного вами на nodejs, - то выбирайте то, что вам ближе.

Проще всего, конечно, использовать простой веб-сервер для Node.JS. Например, можно использовать NPM, чтобы установить пакет http-server.

Phaser

Нам потребуются пакеты phaser, который содержит движок, и пакет http-server - простейший веб сервер на Node. Для этого в командой строке перейдем в нужную нам папку и установим всё, что необходимо:

mkdir C:\Projects\PhaserGame
cd C:\Projects\PhaserGame
npm install phaser@2.1.0
npm install http-server -g

Я явно указал здесь версию Phaser 2.1.0, так как в разных версиях API может отличаться.

Теперь, если выполнить команду

http-server

то запустится веб-сервер в текущей папке, который будет слушать запросы на порту 8080. Зайдя по адресу http://localhost:8080/, вы должны увидеть содержимое текущей папки, которое состоит из одной подпапки "node_modules".

Task-Manager

Также нам потребуется таск-менеджер для сборки наших скриптов, и несколько плагинов для него. Я предпочитаю использовать Gulp, но вы можете использовать и Grunt, и любые другие, удобные вам. Так, или иначе, нужно установить Gulp с помощью пакетного менеджера:

npm install gulp -g
npm install gulp

Для сборки TypeScript файлов, нужно установить gulp-typescript.

npm install gulp-typescript

И также необходимо создать описание сценария нашей сборки в файле gulpfile.js. Содержимое его будет простым:

var gulp = require('gulp');
var typescript = require('gulp-typescript');

gulp.task('phaser', function() {
    return gulp
        .src('node_modules/phaser/build/phaser.js')
        .pipe(gulp.dest('release/'));
});

gulp.task('compile-scripts', function() {
    return gulp
        .src('lib/*.ts')
        .pipe(typescript())
        .js.pipe(gulp.dest('release/'));
});

gulp.task('default', ['phaser', 'compile-scripts']);

Здесь мы объявляем две задачи:

  1. phaser - копирует скрипт движка в папку release.
  2. compile-Scripts - компилирует все TypeScript файлы из папки lib и помещает их в папку release.

Теперь в командой строке можно набрать gulp, и проект будет собран.

Инкрементальная сборка

Так как каждый раз запускать gulp вручную для сборки не удобно, можно включить автоматическую сборку измененных файлов с помощью gulp watch. Для этого мы немного изменим наш gulpfiles.js:

var gulp = require('gulp');
var typescript = require('gulp-typescript');

var tsProject = typescript.createProject({});

gulp.task('phaser', function() {
    return gulp
        .src('node_modules/phaser/build/phaser.js')
        .pipe(gulp.dest('release/'));
});

gulp.task('compile-scripts', function() {
    return gulp
        .src('lib/*.ts')
        .pipe(typescript(tsProject))
        .js.pipe(gulp.dest('release/'));
});

gulp.task('watch', ['compile-scripts'], function () {
    gulp.watch('lib/*.ts', ['compile-scripts']);
});

gulp.task('default', ['phaser', 'compile-scripts']);

Теперь можно набрать в командной строке gulp watch, чтобы включить слежение за TypeScript файлами и автоматическую компиляцию измененных файлов. Имеет смысл открыть для этого отдельную командную строку: в одной будет запущен http-server, в другой - инкрементальная сборка.

Hello, world!

Отлично, движок скачан, веб-сервер запущен. Попробуем создать простейшее приложение на TypeScript и Phaser.

Для этого нам потребуется создать простейший index.html в корне нашего проекта, который будет загружать наши скрипты:

<!doctype html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Hello world!</title>
        <script src="release/phaser.js"></script>
        <script src="release/game.js"></script>
        <style>html, body { padding: 0; margin: 0; width: 100%; height: 100%; }</style>
    </head>
    <body>
    </body>
</html>

И простой typescript файл game.ts в папке lib:

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var game = new Phaser.Game(640, 480, Phaser.AUTO, '', {
    create: create
});

function create() {
    var text = "Hello world!";
    var style = { font: "65px Arial", fill: "#ff0044", align: "left" };

    var t = game.add.text(game.world.centerX, game.world.centerY, text, style);
    t.anchor.set(0.5, 0.5);
}

Теперь, если сервер запущен и всё правильно, то можно получить надпись "Hello world!" по адресу "http://localhost:8080/".

Самая интересная часть в этом коде - это конструктор класса Phaser.Game. В качестве параметров он принимает:

  1. Ширину в пикселях, или процентах (в случае процентов значение должно быть строкой).
  2. Высоту.
  3. Метод вывода: Phaser.AUTO (автоматический выбор), Phaser.CANVAS, Phaser.WEBGL, Phaser.HEADLESS (без рендеринга).
  4. Идентификатор DOM элемента, в который будет рендерится игра, или сам DOM элемент.
  5. Объект, содержащий обработчики событий: preload, create, update, render. Preload используется для загрузки данных игры. Create вызывается после загрузки данных. Update и Render вызываются для изменения состояния игровой модели и при её отображении.

Также несколько слов про phaser.d.ts. Этот файл представляет собой так называемый TypeScript Definition File, т.е. описание типов и контрактов для внешних JS библиотек. Сам по себе Phaser написан на JavaScript и интегрируется с TypeScript с помощью этих контрактов. К сожалению, контракты описаны не всегда корректно, поэтому иногда приходится сверяться с документацией и исправлять описание. Например, для конструктора Phaser.Game не определена перегрузка, в которой он принимает ширину и высоту в виде строк.

Загрузка картинок

Загружать изображения в Phaser очень просто. Все необходимые ресурсы загружаются внутри функции preload. Загрузить картинку можно, вызвав

game.load.image(<name>, <path>);

Затем мы можем добавить загруженный спрайт в сцену в функции create:

game.add.sprite(<x>, <y>, <name>);

Изображения для платформера можно найти в интернете. Я нашел подходящие на сайте http://hamaluik.com/posts/shrunken-adventures-art/.

tiles_8.png pix.png

Я добавил все изображения в папку assets. Этот набор графики содержит в себе все нужные тайлы и анимации персонажа, но начнем мы с того, что просто выведем одну картинку:

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var game = new Phaser.Game(640, 480, Phaser.CANVAS, '', {
    preload: () => {
        game.load.image('gerry', 'assets/gerry.png');
    },
    create: () => {
        game.add.sprite(50, 50, 'gerry');
    }
});

img2.png

Анимация

Spritesheet с анимированным персонажем уже был подготовлен за нас, нам осталось лишь его загрузить. Помимо Spritesheet, Phaser поддерживает анимацию из текстурного атласа (Texture Atlas), который можно создать, используя Texture Packer.

img3.jpg

Настройки для Texture Packer, необходимые для генерации атласа, совместимого с Phaser:

  • Data Format: JSON (Array)
  • Texture Format: PNG
  • Max size: 2048
  • Allow rotation: false
  • Border padding: 0
  • Shape padding: 0

Для загрузки атласа нужно использовать функцию game.load.atlas, которая принимает на вход имя ресурса и пути до json и png файлов. Так как мы имеем дело со Spritesheet, будем использовать game.load.spritesheet, которая принимает имя ресурса, путь до png файла и размеры кадра. Анимация к спрайту добавляется функцией spriteObject.animations.add - указывается название анимации и кадры, которые в неё входят. В случае с атласом указываются имена файлов, в случае со Spritesheet - номера кадров.

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var game = new Phaser.Game(640, 480, Phaser.CANVAS, '', {
    preload: () => {
        game.load.image('gerry', 'assets/gerry.png');
        game.load.spritesheet('pix', 'assets/pix.png', 16, 16);
    },
    create: () => {
        game.add.sprite(50, 50, 'gerry');
        var pix = game.add.sprite(20, 20, 'pix');
        pix.animations.add('idle', [0]);
        pix.animations.add('run', [1, 2]);
        pix.animations.add('lookAt', [3]);
        pix.animations.play('run', 10, true);
    }
});

Spritesheet персонажа содержит в себе 4 кадра. Первый - это спокойное состояние, второй и третий - анимация передвижения и последний - персонаж смотрит вверх.

Тайловая карта

Персонаж у нас есть, теперь нужно создать карту уровня. Для этого нам потребуется spritesheet с тайлами земли, он у нас уже есть. Карту уровня из тайлов можно собрать, используя приложение Tiled.

Создадим новую карту размером, скажем 50 на 50. Размер тайла - 16x16px.

img4.png

Далее добавим наш Tileset

img5.png

И нарисуем наш уровень.

img6.jpg

Сохраняем результат в виде JSON файла level.json в той же папке assets. Важно, чтобы файл тайлов, использованный в Tiled назывался так же, как тот файл, который вы будете загружать.

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var map, layer, layer2, pix;

var game = new Phaser.Game(640, 480, Phaser.CANVAS, '', {
    preload: () => {
        game.load.image('gerry', 'assets/gerry.png');
        game.load.spritesheet('pix', 'assets/pix.png', 16, 16);
        game.load.spritesheet('tiles', 'assets/tiles.png', 16, 16);
        game.load.tilemap('level', 'assets/level.json', null, Phaser.Tilemap.TILED_JSON);
    },
    create: () => {
        game.add.sprite(50, 50, 'gerry');

        pix = game.add.sprite(20, 20, 'pix');
        pix.animations.add('idle', [0]);
        pix.animations.add('run', [1, 2]);
        pix.animations.add('lookAt', [3]);
        pix.animations.play('run', 10, true);

        map = game.add.tilemap('level'); // добавляем в игру карту тайлов
        map.addTilesetImage('tiles'); // связываем графику тайлов с описанием уровня

        layer = map.createLayer('Tile Layer 1'); // указывается название слоя, как в Tiled
        layer.resizeWorld(); // изменяем размер "мира", чтобы он вмещал в себя слой целиком

        layer2 = map.createLayer('Tile Layer 2');
    }
});

img7.png

Физика

В Phaser есть несколько доступных физических движков: Arcade, Ninja и P2. В теории доступны ещё Box2D и Chipmunk, но они пока в разработке, поэтому использовать их можно на свой страх и риск. Можно включать одновременно несколько движков, но каждый игровой объект привязан только к одному из них. Движки отличаются детальностью симуляции и, соответственно, производительностью, поэтому часть физики с целью оптимизации имеет смысл просчитывать более простым движком.

Про Box2D и Chipmunk вы могли уже слышать, так как они популярны. Arcade, Ninja и P2 - это всё изобретения разработчиков Phaser.

Arcade Physics работает на очень простой и быстрой реализации AABB столкновений, т.е. проверка столкновений осуществляется на основе прямоугольников без учета поворота этих прямоугольников.

Ninja Physics учитывает вращение прямоугольников, поэтому может использоваться для более сложной геометрии, но работает медленнее Arcade.

P2 - это более полноценный физический движок, по возможностям близкий уже Box2D.

Мы будем использовать самую простую и быструю физическую модель Arcade.

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var map, layer, layer2, pix;

var game = new Phaser.Game(640, 480, Phaser.CANVAS, '', {
    preload: () => {
        game.load.image('gerry', 'assets/gerry.png');
        game.load.spritesheet('pix', 'assets/pix.png', 16, 16);
        game.load.spritesheet('tiles', 'assets/tiles.png', 16, 16);
        game.load.tilemap('level', 'assets/level.json', null, Phaser.Tilemap.TILED_JSON);
    },
    create: () => {
        game.physics.startSystem(Phaser.Physics.ARCADE); // включаем аркадную физику

        pix = game.add.sprite(20, 20, 'pix');
        pix.animations.add('idle', [0]);
        pix.animations.add('run', [1, 2]);
        pix.animations.add('lookAt', [3]);
        pix.animations.play('run', 10, true);
        game.physics.enable(pix);

        pix.body.gravity.y = 250; // устанавливаем уровень гравитации для тела спрайта
        pix.body.bounce.y = 0.1; // уровень отскока

        map = game.add.tilemap('level');
        map.addTilesetImage('tiles');
        map.setCollisionBetween(1, 25); // все тайлы от 1 до 25 устанавливаем как непроходимые

        layer = map.createLayer('Tile Layer 1');
        layer.resizeWorld();

        layer2 = map.createLayer('Tile Layer 2');
    },
    update: () => {
        game.physics.arcade.collide(pix, layer); // проверяем столкновения спрайта персонажа с тайлами карты
    }
});

Контроль

Клавиши управления нужно зарегистрировать в объекте game.input.keyboard:

cursors = game.input.keyboard.createCursorKeys();
jumpKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);

Затем можно будет осуществлять проверку нажатия той или иной клавиши

if (cursors.left.isDown) {
    …
}

При нажатии на пробел наш персонаж будет прыгать. Мы будем единовременно устанавливать ему отрицательную вертикальную скорость, если персонаж стоит на земле.

При нажатии на клавиши влево и вправо - будем менять горизонтальную скорость и инвертировать спрайт персонажа.

/// <reference path="../node_modules/phaser/build/phaser.d.ts"/>

var map, layer, layer2, pix, cursors, jumpKey, jumpTimer = 0;

var game = new Phaser.Game(640, 480, Phaser.CANVAS, '', {
    preload: () => {
        game.load.image('gerry', 'assets/gerry.png');
        game.load.spritesheet('pix', 'assets/pix.png', 16, 16);
        game.load.spritesheet('tiles', 'assets/tiles.png', 16, 16);
        game.load.tilemap('level', 'assets/level.json', null, Phaser.Tilemap.TILED_JSON);
    },
    create: () => {
        game.physics.startSystem(Phaser.Physics.ARCADE);

        pix = game.add.sprite(20, 20, 'pix');
        pix.animations.add('idle', [0]);
        pix.animations.add('run', [1, 2]);
        pix.animations.add('lookAt', [3]);
        pix.animations.play('idle', 1, false); // персонаж должен спокойно падать
        game.physics.enable(pix);

        pix.body.gravity.y = 250;
        pix.body.bounce.y = 0.1;
        pix.anchor.setTo(0.5, 0.5);

        map = game.add.tilemap('level');
        map.addTilesetImage('tiles');
        map.setCollisionBetween(1, 25);

        layer = map.createLayer('Tile Layer 1');
        layer.resizeWorld();

        layer2 = map.createLayer('Tile Layer 2');
        layer2.resizeWorld();

        cursors = game.input.keyboard.createCursorKeys();
        jumpKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
    },
    update: () => {
        game.physics.arcade.collide(pix, layer);

        // обнуляем скорость
        pix.body.velocity.x = 0;

        // при нажатии клавиши влево меняем скорость и инвертируем спрайт
        if (cursors.left.isDown) {
            pix.body.velocity.x = -100;
            pix.scale.x = -1;
        }
        else if (cursors.right.isDown) { // иначе при нажатии вправо, меняем скорость и восстанавливаем спрайт
            pix.body.velocity.x = 100;
            pix.scale.x = 1;
        }

        // при нажатии пробела, если персонаж на земле, и с предыдущего прыжка прошло 650мс
        if (jumpKey.isDown && pix.body.onFloor() && game.time.now > jumpTimer) {
            pix.body.velocity.y = -200; // меняем вертикальную составляющую скорости
            jumpTimer = game.time.now + 650;
        }

        // если персонаж движется только по горизонтали, запускаем анимацию
        if (Math.abs(pix.body.velocity.x) >= 1 && Math.abs(pix.body.velocity.y) < 1) {
            pix.animations.play('run', 10, true);
        }

        // если персонаж не движется, то переводим его в спокойное состояние
        if (Math.abs(pix.body.velocity.x) < 1 && Math.abs(pix.body.velocity.y) < 1) {
            pix.animations.play('idle', 1, false);
        }
    }
});

Отлично, теперь персонаж может бегать и прыгать по тайлам карты, но, к сожалению, может выходить за границы камеры. Для исправления этого недостатка достаточно включить слежение камеры за спрайтом

    game.camera.follow(pix, Phaser.Camera.FOLLOW_PLATFORMER);

Заключение

img8.png

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

    game.physics.arcade.collide(pix, layer, (obj1, obj2) => {
        var tile = <Phaser.Tile>obj2;

        if (tile.index == 17) {
            ...
        }
    });

Нужно добавить врагов и обработчики столкновений с ними, меню, звуки, разбить код на модули, но это уже выходит за рамки этого туториала.

В целом, Phaser производит приятное впечатление и содержит всё необходимое для создания простых 2д игр:

  • Спрайты
  • Физика
  • Карты тайлов
  • Анимация, в том числе на основе костей
  • Системы частиц
  • Камера
  • Звук

Однако, хочется заметить, что Phaser страдает от нехватки документации, и изучать его проще всего по примерам на http://examples.phaser.io/index.html. Также нужно учитывать, что он не стабилен и новая версия запросто может сломать что-то в вашей игре.

09.11.14 22:59

Комментарии

09.11.14 23:38

Мефыч, это офигенно!

10.11.14 00:37

Неплохо бы еще возможность посмотреть демку - без установок и прочего, прямо по ссылке из статьи.

10.11.14 00:42

Неплохо бы еще возможность посмотреть демку - без установок и прочего, прямо по ссылке из статьи.

Постараюсь добавить.

15.11.14 15:42
Отредактировано: 26.01.15 01:02

Добавил демку.

({{comment.CreationDate | date:'dd.MM.yy HH:mm'}})
Отредактировано: {{comment.UpdatedDate | date:'dd.MM.yy HH:mm'}}