Phaser


Оглавление


0. Подготовка к работе [Вы тут]
1. Введение
2. Загрузка ресурсов
3. Создание игрового мира
4. (wip) Группы
5. (wip) Мир физики
6. (wip) Управление
7. (wip) Добавление целей
8. (wip) Последние штрихи


Эта серия статей научит вас основам и "хорошему тону" игрового фремворка Phaser. За данный курс, я постараюсь объяснить вам основные идеи и возможности фреймворка, а также покажу как его грамотно использовать в связке с TypeScript и Webpack.


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

Думаю стоит оговориться, что на момент написания данной статьи, я использую Phaser v2.6.2 и TypeScript v2.2.1.


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


  • part-0 — состояние проекта на момент текущей статьи
  • part-1 — на момент статьи #1 Введение

и так далее.


В двух словах о Phaser


Phaser — опенсорсный (MIT), кросс-браузерный HTML5 фреймворк для создания браузерных игр с использованием WebGL и Canvas. В отличии от других фреймворков, Phaser в первую очередь целится на мобильные платформы и оптимизирован под них.


Инструменты


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


git clone https://github.com/SuperPaintman/phaser-typescript-tutorial.git

И установить Node.js для запуска сборщика и других NPM скриптов.


Структура проекта


Прежде чем перейти непосредственно к самому фремворку, рассмотрим структуру будущего приложения.


В качестве основы для нашего проекта, я взял данный Phaser TypeScript шаблон, который использует Webpack в качестве сборщика.


Давайте рассмотрим основные его файлы (внимание на комментарии):


webpack.config.js


Конфигурация для сборщика Webpack. В зависимости от переменной окружения NODE_ENV соберет билд для разработки, или оптимизированный билд для продакшена.


webpack.config.js
'use strict';
/** Requires */
const path                  = require('path');

const webpack               = require('webpack');
const CleanWebpackPlugin    = require('clean-webpack-plugin');
const HtmlWebpackPlugin     = require('html-webpack-plugin');
const ExtractTextPlugin     = require('extract-text-webpack-plugin');
const CheckerPlugin         = require('awesome-typescript-loader').CheckerPlugin;
const ImageminPlugin        = require('imagemin-webpack-plugin').default;

const p                     = require('./package.json');

/** Constants */
const IS_PRODUCTION     = process.env.NODE_ENV === 'production';

const assetsPath        = path.join(__dirname, 'assets/'); // папка с ресурсами игры
const stylesPath        = path.join(__dirname, 'styles/'); // папка с css стилями

// Путь до папки с собранными билдами phaser. Из-за того, что phaser собирается
// по-старинке, его (а также его зависимости) придётся подключать как
// глобальный объект.
const phaserRoot        = path.join(__dirname, 'node_modules/phaser/build/custom/');

// Пути до библиотек phaser'а
const phaserPath        = path.join(phaserRoot, 'phaser-split.js');
const pixiPath          = path.join(phaserRoot, 'pixi.js');
const p2Path            = path.join(phaserRoot, 'p2.js');

// Папка, в которую у будет собран наш билд
const outputPath        = path.join(__dirname, 'dist');

// Путь до шаблона `index.html` файле
const templatePath      = path.join(__dirname, 'templates/index.ejs');

/** Helpers */
/**
 * Проверяет, содержит ли массив данный элемент
 * @param  {T[]}  array
 * @param  {T}    searchElement
 * 
 * @return {boolean}
 */
function includes(array, searchElement) {
  return !!~array.indexOf(searchElement);
}

/**
 * Создает правила для `expose-loader`, который добавляет модуль к глобальному
 * объекту window по переданному имени
 * @param  {string} modulePath
 * @param  {string} name]
 * 
 * @return {Object}
 */
function exposeRules(modulePath, name) {
  return {
    test: (path) => modulePath === path,
    loader: 'expose-loader',
    options: name
  };
}

/**
 * Удаляет из массива все элементы равные `null`
 * @param  {T[]} array
 * 
 * @return {T[]}
 */
function filterNull(array) {
  return array.filter((item) => item !== null);
}

/**
 * Вызывает функцию `fn`, если `isIt` равет `true`, в противном случае будет
 * вызвана функция `fail`.
 * 
 * @param  {boolean}  isIt
 * @param  {function} fn
 * @param  {function} fail
 *
 * @return {any}
 */
function only(isIt, fn, fail) {
  if (!isIt) {
    return fail !== undefined ? fail() : null;
  }

  return fn();
}

/**
 * Хелпер на основе `only`. Вызывает первую функцию, если
 * `NODE_ENV` === 'production', т.е. если сборка производится для продакшена.
 * @param  {function} fn
 * @param  {function} fail
 *
 * @return {any}
 */
const onlyProd = (fn, fail) => only(IS_PRODUCTION, fn, fail);
/**
 * Хелпер на основе `only`. Вызывает первую функцию, если
 * `NODE_ENV` !== 'production', т.е. если сборка производится для разработки.
 * @param  {function} fn
 * @param  {function} fail
 *
 * @return {any}
 */
const onlyDev = (fn, fail) => only(!IS_PRODUCTION, fn, fail);

module.exports = {
  entry: {
    main: path.join(__dirname, 'src/index.ts')
  },
  output: {
    path: outputPath,
    // На продакшене также добавим к именам файлов их хещ, чтобы обойти
    // проблему с кешированием версий
    filename: `js/[name]${onlyProd(() => '.[chunkhash]', () => '')}.js`,
    chunkFilename: `js/[name]${onlyProd(() => '.[chunkhash]', () => '')}.chunk.js`,
    sourceMapFilename: '[file].map',
    publicPath: '/'
  },
  devtool: onlyDev(() => 'source-map', () => ''), // Отключим sourcemap'ы на проде.
  resolve: {
    extensions: ['.ts', '.js'],
    alias: {
      pixi:   pixiPath,     // сделаем возможным подключить 'pixi' библиотеку как обычный NPM пакет
      phaser: phaserPath,   // сделаем возможным подключить 'phaser' библиотеку как обычный NPM пакет
      p2:     p2Path,       // сделаем возможным подключить 'p2' библиотеку как обычный NPM пакет
      assets: assetsPath,   // алиас до папки `assets/`
      styles: stylesPath    // алиас до папки `styles/`
    }
  },
  plugins: filterNull([
    /** DefinePlugin */
    // Глобальные переменные, будт полезны для отключения каких-либо функций на
    // проде, или напротив включения оптимизаторов и пр.
    new webpack.DefinePlugin({
      IS_PRODUCTION:  JSON.stringify(IS_PRODUCTION),
      VERSION:        JSON.stringify(p.version)
    }),

    /** JavaScript */
    // Минимизирует JS для продовой сборки
    onlyProd(() => new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      comments: false
    })),

    /** Clean */
    // Удалит `dist` папку перед каждой сборкой
    new CleanWebpackPlugin([outputPath]),

    /** TypeScript */
    new CheckerPlugin(),

    /** Images */
    // Оптимизирует изображения и svg'хи
    onlyProd(() => new ImageminPlugin({
      test: /\.(jpe?g|png|gif|svg)$/
    })),

    /** Template */
    // Данный плагин автоматически сгенерирует для нас `index.html` файл
    // на основе `templatePath`, а также сам вставит в этот шаблон все
    // сгенерированные скрипты и стили
    new HtmlWebpackPlugin({
      title:    'Phaser TypeScript boilerplate project',
      template: templatePath
    }),

    /** CSS */
    // Экспортирует CSS import'ы в отдельный `.css` файл (по-умолчанию Webpack
    // вставляет CSS прямо в JS файлы).
    new ExtractTextPlugin({
      filename: `css/[name]${onlyProd(() => '.[chunkhash]', () => '')}.css`
    }),

    /** Chunks */
    // Разобьем нашу сборку на несколько файлов (т.к. вендорные файлы и файлы
    // самого phaser'а вряд ли будут меняться в процессе разработки, нет нужды
    // заставлять наших клиентов тянуть каждый раз эти данные заново. Чанки как
    // раз помогут в этом, браузер сможет доставать из кеша файлы, которые не
    // поменялись):
    //   * Чанк для прочих модулей
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: (module) => /node_modules/.test(module.resource)
    }),
    //   * Чанк для phaser модулей (p2, PIXI, phaser)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'phaser',
      minChunks: (module) => includes([p2Path, pixiPath, phaserPath], module.resource)
    }),
    //   * Чанк для инициализационных функций webpack'а
    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons'
    })
  ]),
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 8080,
    inline: true,
    watchOptions: {
      aggregateTimeout: 300,
      poll: true,
      ignored: /node_modules/
    }
  },
  module: {
    rules: [
      /** Assets */
      // Скопирует файлы из asset'ов
      {
        test: (path) => path.indexOf(assetsPath) === 0,
        loader: 'file-loader',
        options: {
          name: `[path][name]${onlyProd(() => '.[sha256:hash]', () => '')}.[ext]`
        }
      },

      /** CSS */
      {
        test: /\.styl$/,
        exclude: /node_modules/,
        loader: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            'css-loader',
            'stylus-loader'
          ]
        })
      },

      /** JavaScript */
      exposeRules(pixiPath, 'PIXI'),     // добавит `PIXI` модуль в глобальный объект `window`
      exposeRules(p2Path, 'p2'),         // добавит `p2` модуль в глобальный объект `window`
      exposeRules(phaserPath, 'Phaser'), // добавит `Phaser` модуль в глобальный объект `window`
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'awesome-typescript-loader'
      }
    ]
  }
};

assets/


В эту директорию мы будем складывать все ресурсы нашего приложения: изображения, музыку, JSON и прочее.


styles/style.styl


В нем будут содержаться CSS стили (с использованием препроцессора Stylus) для нашей страницы. В рамках данной серии, будет достаточно этого:


body
  margin: 0px

templates/index.ejs


EJS шаблон для страницы игры (Webpack сам добавит в него загрузку всех скриптов и стилей):


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>

tsconfig.json


Конфиг для TypeScript:


{
  "compilerOptions": {
    "target": "es5", // Для большей поддержки браузерами
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "removeComments": false,
    "noImplicitAny": false,
    "pretty": true
  },
  "files": [
    // Нужно указать откуда тайпинги Phaser'а явно, т.к. в данной папке
    // содержатся несколько разных их версий, которые будут конфликтовать между
    // собой.
    "./node_modules/phaser/typescript/box2d.d.ts",
    "./node_modules/phaser/typescript/p2.d.ts",
    "./node_modules/phaser/typescript/phaser.comments.d.ts",
    "./node_modules/phaser/typescript/pixi.comments.d.ts"
  ],
  "include": [
    // А так-же укажем откуда брать тайпинги по glob'у
    "./src/**/*.ts",
    "./node_modules/@types/**/*.ts"
  ]
}

src/typings.d.ts


В данном файле мы должны объявить все глобальные переменные, которые создали в webpack.DefinePlugin:


declare const IS_PRODUCTION: boolean;
declare const VERSION: string;

src/index.ts


Это основной файл нашего приложение, он будет входной точной в него:


'use strict';

На этой основе мы будем создавать платформер.


Github Repo: https://github.com/SuperPaintman/phaser-typescript-tutorial


К содержанию

Поделиться с друзьями
-->

Комментарии (7)


  1. naryl
    27.03.2017 02:30
    +1

    О, я как раз буквально два дня назад искал движок с требованиями:
    1. Поддержка браузеров и мобилок.
    2. Скрипты на стандартном Javascript. (что отметает Unity)
    Остановился на Phaser и пока доволен. Правда, «хороший тон» у меня свой, т.к. пишу на маргинальном Parenscript и, соответственно, тон хороший в понятиях лиспа, а не js. А теперь позвольте немного побуду редактором. :)

    > бесплатный (MIT)
    Бесплатный — это $0. А MIT — это свободный, или «опенсорсный». Продавать лицензия MIT формально не запрещает.


    1. SuperPaintman
      27.03.2017 09:22

      Бесплатный — это $0. А MIT — это свободный, или «опенсорсный». Продавать лицензия MIT формально не запрещает.

      Спасибо, исправил. Думаю, так действительно будет более правильно.


  1. Maklaud
    27.03.2017 17:41
    +1

    Вот у него есть уроки по Phaser. Насколько хороши именно они — не знаю, но в целом у него уроки классные.


  1. oWart
    27.03.2017 21:26

    Прицел на мобильные платформы и оптимизация под них это хорошо… А будет статья про подготовку игры именно для публикации в PlayMarket/AppStore? Там же как-то ее собирать надо, чтоб вся графика находилась в приложении (извините за терминологию, я абсолютно в этом не разбираюсь, никогда игры не делал). Плюс интересует организация управления под телефоны. Вот например в Sligher'е добавили область для пальца, типа джойстика и т.д (там несколько вариантов), посредствам чего это делается?


    1. SuperPaintman
      27.03.2017 22:04
      +1

      А будет статья про подготовку игры именно для публикации в PlayMarket/AppStore

      В рамках этой серии статей — нет, ее цель познакомить читателя с азами Phaser в связке с TypeScript


      Там же как-то ее собирать надо, чтоб вся графика находилась в приложении (извините за терминологию, я абсолютно в этом не разбираюсь, никогда игры не делал)

      Это легко делается с помощью PhoneGap или Cordova.
      Собственно это и один из способов собрать JavaScript приложение для мобильных устройств, оно же и упакует все ресурсы в APK (или аналог на Iphone) файл.


      Для Desktop'а, думаю нужно смотреть в сторону Electron.


      Плюс интересует организация управления под телефоны. Вот например в Sligher'е добавили область для пальца, типа джойстика и т.д (там несколько вариантов), посредствам чего это делается?

      Это делается обычным спрайтом из 5-9 состояний (спокойствие, джойстик вверх, вниз, вправо, влево и т.д.):


      dpad

      Взят отсюда
      image


  1. Opaspap
    28.03.2017 10:54

    Phaser очень плохо ложится на современные системы сборок, выгнать его из global было не просто.


    1. SuperPaintman
      28.03.2017 11:00

      Не совсем понимаю как его "глобальность" мешает современным сборщикам. Cuncking работает, можно разбивать билд на логические части; TypeScript вообще не парится, что переменная Phaser не импортирована, а глобально.


      Единственно, что появляется 3 глобальных переменных: Phaser, PIXI и p2. Но, думаю, это не такая большая беда.


      И суда по этому участку кода, автор делает это намеренно.