Оглавление
0. Подготовка к работе
1. Введение
2. Загрузка ресурсов [Вы тут
]
3. Создание игрового мира
4. (wip) Группы
5. (wip) Мир физики
6. (wip) Управление
7. (wip) Добавление целей
8. (wip) Последние штрихи
Продолжим серию уроков, посвященных использованию Phaser в связке с TypeScript. В этом уроке, мы рассмотрим загрузку ресурсов в Phaser, а также немного "причешем" код из предыдущей части.
Как и в прошлых статьях, не пропускайте комментарии в коде.
Полный код, написанный в этой статье, вы найдете в Github репозитории с тегом part-2
.
Приступим!
Минутка рефакторинга
Первым делом давайте организуем написанный ранее код более правильно:
Во-первых, создадим директорию src/states
и положим в нее файл src/states/state.ts
:
'use strict';
/** Imports */
import App from '..';
abstract class State extends Phaser.State {
// Перезапишем свойство `game` у наших стейтов.
// Это нужно для того, чтобы если мы добавим в `App` какое-то поле, TypeScript
// смог его подхватить, и не ругался на него, если мы бы вызывали его через
// `this.game`.
game: App;
}
export default State;
Мы создали абстрактный класс State
и унаследовали его от Phaser.State
. Эта, на первый взгляд, странная манипуляция нужна для того, чтобы TypeScript правильно обрабатывал свойство this.game
в методах наших стейтов. По умолчанию свойство game
имеет тип — Phaser.Game
, и соответственно TypeScript ничего не узнал бы про поля, которые мы добавили в App
(В этой серии стайтей мы не будем добавлять custom'ные поля к App
, но иметь подобный абстрактный класс заранее не помешает).
Вероятней всего, у вас возникнет вопрос: "А какое свойство мы вообще можем захотеть добавить вApp
?". Т.к. каждый стейт имеет свойство-ссылку наApp
—this.game
, мы, к примеру, можем добавть вApp
глобальную зависимость для всех стейтов, или ссылку на storage-объект, WebSocket-подключение и т.д.
Во-вторых, перенесем наш MainState
в src/states/main.state.ts
и удалим из него лишние, на текущий момент, методы (preload
и update
):
'use strict';
/** Imports */
import State from './state';
// Основной стейт игры
export default class MainState extends State {
create(): void { }
}
А в src/index.ts
добавим на 10
ую строку импорт этого стейта:
// ...
import MainState from './states/main.state';
// ...
В-третих, создадим BootState
и PreloaderState
в src/states/boot.state.ts
и src/states/preloader.state.ts
, соответственно:
'use strict';
/** Imports */
import State from './state';
// Этот стейт нужен для загрузки критических ресурсов перед `preloader`;
// для вывода информации в консоль;
// для прочих стартовых манипуляций;
export default class BootState extends State {
create(): void {
// `this.game` - это ссылка на инстанс нашего `App`, в который мы добавили
// данный стейт.
this.game.state.start('preloader'); // Переход на `preloader` стейт
}
}
и
'use strict';
/** Imports */
import State from './state';
// В этом стейте мы будем загружать все основные (core) ресурсы для нашей игры:
// * Лого, сплешскрины
// * Спрайты меню
// * Звуки нажатия на экран / кнопки
// * И т.д.
export default class PreloaderState extends State {
preload(): void {
console.debug('Assets loading started');
}
create(): void {
console.debug('Assets loading completed');
this.game.state.start('main'); // Переход на `main` стейте после загрузки всех изображений
}
}
Как сказано выше, PreloaderState
нам понадобится для загрузки ресурсов для нашей игры (естественно, если игра имеет большое количество ресурсов, они должны загружаться перед соответствующими им стейтами, а в preloader
должны загружаться только критические. Но в нашем случае мы будем загружать все ресурсы перед началом игры).
Заметьте, что preload(): void
будет вызван первым, а create(): void
уже после того, как все ресурсы будут загружены.
Вообще, разбиение на стейты довольно субъективное дело. Вы можете добавлять любое их количество (Prepreloader
,Preprepreloader
и т.д.), но, по моему опыту, необходимо минимум 3 стейте:
BootState
— для инициализации и вывода информации о сборке.PreloaderState
— для загрузки ресурсов.MainState
— для основного loop'а игры.
В-четвертых, подключим эти стейты в src/index.ts
:
// ...
import BootState from './states/boot.state';
import PreloaderState from './states/preloader.state';
import MainState from './states/main.state';
// ...
export default class App extends Phaser.Game {
constructor(config: Phaser.IGameConfig) {
super(config);
// Регистрируем стейты игры
this.state.add('boot', BootState);
this.state.add('preloader', PreloaderState);
this.state.add('main', MainState);
this.state.start('boot'); // Инициализируем и запускаем `boot` стейт
}
}
// ...
Заметьте, что мы заменилиthis.state.start('main');
наthis.state.start('boot');
.
Загрузка ресурсов
Теперь, когда перестройка завершена, загрузим необходимые ресурсы для нашей игры. Мы будем это делать через вызов метода this.game.load
внутри метода preload(): void
(PreloaderState
).
Каждый стейт имеет ссылку наApp
, к которой его подключили:this.game
.
Phaser сам вызовет этот метод, когда наша игра запустится, и загрузит все, что мы определили в нем.
Phaser загружает ресурсы своим, магическим, образом черезPhaser.Loader
. Вам не нужно передавать вthis.game.load
callback'ов или Promis'ов, он отслеживает загрузку сам.
Добавим в src/states/preloader.state.ts
следующий код:
'use strict';
/** Imports */
import State from './state';
// Webpack заменит подобные require'ы на URL до этих изображений, а сами
// изображения положит в папку `dist/asserts/images`.
// А код ниже, он заменит примерно на это:
// `const skyImage = '/assets/images/sky.png';`
// (Подробнее про подобные "подмены" Webpack'а, смотрите в документации).
const skyImage = require('assets/images/sky.png');
const platformImage = require('assets/images/platform.png');
const starImage = require('assets/images/star.png');
const dudeImage = require('assets/images/dude.png');
export default class PreloaderState extends State {
preload(): void {
console.debug('Assets loading started');
this.game.load.image('sky', skyImage); // <=
this.game.load.image('platform', platformImage); // <=
this.game.load.image('star', starImage); // <=
this.game.load.spritesheet('dude', dudeImage, 32, 48); // <=
}
create(): void {
console.debug('Assets loading completed');
this.game.state.start('main'); // Переход на `main` стейте после загрузки всех изображений
}
}
Сами ресурсы лежат в Github репозитории в директорииassets/images
, вам также нужно добавить их в ваш локальный проект, в ту же директорию (assets/images
).
На строках 20-23
мы загружаем 4 ресурса: 3 изображения и один спрайт. Обратите внимание на первый агрумент (ключ) this.game.load.image()
и this.game.load.spritesheet()
, это идентификатор, который мы будем использовать в дальнейшем для доступа к данному ресурсу (ключ может быть любой строкой).
3
и 4
аргумент у this.game.load.spritesheet()
— это высота и ширина одного изображения в спрайт-листе.
Создание спрайта
Чтобы добавить спрайт в нашу игру, нам нужно добавить следующий код в create(): void
(MainState
):
'use strict';
/** Imports */
import State from './state';
// Основной стейт игры
export default class MainState extends State {
create(): void {
this.game.add.sprite(0, 0, 'star'); // <=
}
}
Теперь, в левом верхнем углу браузера, вы должны увидеть желтую звездочку:
Первый аргумент this.game.add.sprite()
— ось x
, второй — ось y
, а третий — это тот самый ключ, который мы указали при загрузке изображения.
Порядок отрисовки элементов на экране соответствует порядку их создания. Поэтому, если вы хотите добавить фон за звездой, вам необходимо создать этот спрайт первым, перед спрайтом звезды.
На самом деле, у вас есть возможность задать Z индекс вручную и отсортировать по нему элементы в мире игры, но изначально Phaser делает это за вас, устанавливая каждому последующему элементу Z индекс + 1.
На последок, давайте добавим чуть больше звезд:
'use strict';
/** Imports */
import State from './state';
// Основной стейт игры
export default class MainState extends State {
create(): void {
const map = [
'X X XX XXX XXX ',
'X X X X X X X X',
'XXXX XXXX XXX XXX ',
'X X X X X X XX ',
'X X X X XXX X X '
].map((line) => line.split('')); // Разбиваем линии на отдельные символы
// Обход всех линий и символов
map.forEach((line, y) => line.forEach((char, x) => {
if (char !== 'X') {
return;
}
// Если символ соответствует `X`, нарисуем вместо него звезду.
// 24 - ширина изображения.
// 22 - высота.
this.game.add.sprite(x * 24, y * 22, 'star');
}));
}
}
И получим это:
На этом урок подошел к концу.
Github Repo: https://github.com/SuperPaintman/phaser-typescript-tutorial