javascript

Создание вашей первой игры на Phaser. Часть 0 — Подготовка к работе

  • вторник, 28 марта 2017 г. в 03:14:47
https://habrahabr.ru/post/324894/
  • Разработка игр
  • WebGL
  • JavaScript
  • HTML
  • Canvas


Phaser


Оглавление


0. Подготовка к работе [Вы тут]
1. Введение
2. (wip) Загрузка ресурсов
3. (wip) Создание игрового мира
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


К содержанию