Разработка браузерных игр с использованием Phaser3, React, Typescript

от автора

Думаю, ни для кого не секрет, что каждый уважающий себя разработчик программного обеспечения должен иметь в своем портфолио хотя бы один пет-проект, а лучше полноценный продукт, дающий дополнительный постоянный заработок. Предметных областей и тематик приложений великое множество, но среди них есть одна, которая заслуживает отдельного внимания — разработка своей собственной игры.

Преимущества разработки игр

Прежде всего, следует понимать, что разработка игр достаточно трудоемкий процесс. Как правило, он включает в себя как знание особенностей фронтенда, так и бекенда и всех сопутствующих серверных технологий, особенно, если речь идет о мультиплеерах. Кроме того, часто необходимо задумываться над производительностью, используемыми ресурсами, архитектурой и алгоритмами. Потребуется подкачать знание математики, геометрии, физики. Навыки художника, пусть и в минимальном объеме, тоже будут очень кстати. А если игра имеет коммерческую цель — то и маркетинг, анализ рынка пригодятся. В итоге, разработка игр позволяет прокачать свои навыки настолько, что любое собеседование или работа в коммерческой компании над различными веб — сервисами покажется легкой прогулкой. Тем более, что с игрой в портфолио вы будете выделяться среди других кандидатов на вакансии. Быть разработчиком игр — весело и круто.

С чего начать?

Браузерная игра — достойная идея, но нужно идти в ногу со временем и использовать последние технологии. В этой статье использую и постараюсь раскрыть связку:

  • Typescript

  • React

  • Webpack

  • HTML/CSS

  • Phaser3

Разумеется, помимо технических навыков следует вспомнить базовые понятия:

  • Математики

  • Физики

  • Компьютерной графики

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

Кадр из игры warcraft 2

Кадр из игры warcraft 2

Почему Phaser3?

Потому что на данный момент это самый часто используемый и активно развивающийся open-source фреймворк для разработки браузерных игр и интерактивных приложений на JavaScript/TypeScript

Какие будут ваши доказательства?

https://phaser.io

https://labs.phaser.io

На официальных ресурсах Phaser можно найти бесчисленное количество примеров кода, игр и best-практик. Также среди достоинств: регулярные обновления и новые фичи, огромное комьюнити разработчиков, открытая и полная документация, доступны книги от создателя фреймворка Richard Davey @photonstorm

Практика

https://github.com/tfkfan/phaser3-game-demo

Выше представлена ссылка на демо проекта.

В основе демо лежит стартер https://github.com/tfkfan/phaser3-react-template

Теперь по порядку.

Требования: NodeJS >= v20, NPM >= v10

Для начала, выгружаем проект. Устанавливаем зависимости и запускаем:

npm install npm start

Демо содержит 2 связанные, но изначально не особо ладящие друг с другом, технологии — React и Phaser. Для того, чтобы они работали вместе без проблем, в Index.html объявлено два разных контейнера, каждый из них привязывает свой фреймворк соответственно:

<div id="root" class="app-container"> ....  <div id="game-root">

Заметьте, что контейнер React с id =»root» находится первым, на нем будет строиться все UI проекта, блок с z-index отличным от нуля(для отрисовки UI поверх игровых сцен), нестатический и позиционированный, что добавляет удобства в верстке. В блоке id=»game-root» используется только canvas, поэтому можно пожертвовать его позиционированием, прилепляем его к вернему левому краю абсолютным позиционированием.

Любая Phaser игра начинается с конфигурации фреймворка.

phaser-game.ts :

const config = {   type: Phaser.WEBGL, // Тип приложения - WEBGL/CANVAS   parent: 'game-root',   canvas: document.getElementById('game-canvas') as HTMLCanvasElement,   width: window.innerWidth ,   height: window.innerHeight,   pixelArt: true,   scene: [BootstrapScene, GameScene],   physics: {  // подключение физического движка     default: 'arcade',     arcade: {       debug: false     }   } }

Все параметры, впринципе, должны быть интуитивно понятны, но самый главный из них это набор сцен:

scene: [BootstrapScene, GameScene]

Сцены — основной объект для отрисовки игрового содержимого, через нее проходят все ресурсы, события и процессы в игре. Первая из них используется в качестве предзагрузчика. Все загруженные в первой сцене ресурсы будут доступны в других. Ресурсы могут быть разные, это и спрайт-листы, и атласы анимаций, и звуковые файлы, Tilemap-файлы, шейдеры и пр.

Любая сцена имеет 4 важных функции, изменяя которые, можно управлять игровой логикой:

  • preload — загружает ресурсы, и это все.

  • init — запускается следом. Позволяет получить данные при переходе из предыдущей сцены, инициализирует игровую логику.

  • create — позволяет создать объекты и привязать их к сцене.
    Большинство игровых объектов достаточно просто объявить в этом методе.
    Под капотом они сами обновляются в игровом цикле.

  • update — игровой цикл. Здесь можно добавить дополнительную логику, когда базового функционала метода create уже не хватает.

В конструкторе передается строковый ключ этой сцены.

export default class BootstrapScene extends Phaser.Scene {     constructor() {         super('bootstrap')     }      init() {         store.dispatch(setLoading(true))     }      preload() {         this.load.on(Phaser.Loader.Events.PROGRESS, (value: number) => {             CONTROLS.setProgress(100 * value);         });         this.load.tilemapTiledJSON('worldmap', './assets/maps/new/map01merged.json');         this.load.image('tiles', './assets/maps/new/tiles.png');         this.load.atlas('mage', './assets/playersheets/mage.png', './assets/playersheets/mage.json');         this.load.image('fireball', './assets/skillsheets/fire_002.png');         this.load.spritesheet('buff', './assets/skillsheets/cast_001.png', {frameWidth: 192, frameHeight: 192});         this.load.image('face', './assets/images/face.png');         this.load.spritesheet('fireballBlast', './assets/skillsheets/s001.png', {frameWidth: 192, frameHeight: 192});         this.load.audio('intro', ['./assets/music/phaser-quest-intro.ogg']);         this.load.glsl('fireball_shader', './assets/shaders/fireball_shader.frag');     }      create() {         CONTROLS.setProgress(100);         store.dispatch(setLoading(false))          this.sound.add('intro').play({             seek: 2.550         });          this.add.shader('fireball_shader', window.innerWidth/2, window.innerHeight/2, window.innerWidth ,window.innerHeight);     } }

Сцена данного предзагрузчика также имеет функционал, позволяющий показать прогресс загрузки всех прописанных ресурсов, выводя данные с помощью глобального объекта контроля React компонентов CONTROLS, но об этом позднее. Также прописываем инструкцию проигрывания музыки на старте:

this.sound.add('intro').play({             seek: 2.550 });
Входная предзагрузочная сцена. На заднем плане - шейдер

Входная предзагрузочная сцена. На заднем плане — шейдер

Главная сцена, на которой будет строиться весь геймплей — GameScene.

Рассмотрим метод create:

create() {         CONTROLS.setVersion(`Phaser v${Phaser.VERSION}`)         this.input.keyboard.on(Phaser.Input.Keyboard.Events.ANY_KEY_DOWN, (evt: { key: string; }) => {             this.player.setSkillIndex(this.skillIndexMap[evt.key])             const direction = this.keymap[evt.key]             if (direction)                 this.player.walk(direction, true)         });         this.input.keyboard.on(Phaser.Input.Keyboard.Events.ANY_KEY_UP, (evt: { key: string; }) => {             const direction = this.keymap[evt.key]             if (direction)                 this.player.walk(direction, false)         });          this.input.on(Phaser.Input.Events.POINTER_DOWN, (evt: {             worldX: number;             worldY: number;         }) => this.player.attack(new Vector2(evt.worldX, evt.worldY)));          this.createAnimations()         this.displayMap()         this.createPlayer()         this.cameras.main.startFollow(this.player)          // examples          // Animation/Sprite         this.anims.create({             key: 'explosion',             frames: this.anims.generateFrameNumbers('fireballBlast', {start: 0, end: 19, first: 0}),             frameRate: 20,             repeat: -1         })          this.add.sprite(2500, 1100, "").play('explosion')          // Arcade Physics / collision          const items = this.add.group([this.createItem()])         this.physics.add.collider(this.player, items, (object1: Phaser.Tilemaps.Tile | Phaser.GameObjects.GameObject, object2: Phaser.Tilemaps.Tile | Phaser.GameObjects.GameObject) => {             object2.destroy(true)             setTimeout(() => {                 items.add(this.createItem(), true)             }, 3000)         })    }

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

Спрайт — это миниатюрный игровой «контейнер» текстур и анимаций с различными параметрами: координаты позиции на игровом поле, скорости, ускорения движения и др. Например:

export default class Face extends Phaser.Physics.Arcade.Sprite {     constructor(scene: Scene, x: number, y: number) {         // Сцена, координаты, ключ текстуры         super(scene, x, y, 'face');         // Привязка к физике         this.scene.physics.add.existing(this)         // Привязка к сцене         this.scene.add.existing(this)     } }

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

Спрайт-лист

Спрайт-лист

Создадим анимацию взрыва из 20 нарезанных сверху-вниз, слева-направо кадров текстуры fireballBlast:

this.anims.create({             key: 'explosion',             frames: this.anims.generateFrameNumbers('fireballBlast', {start: 0, end: 19, first: 0}),             frameRate: 20,             repeat: -1 })

Ширина и высота кадра, а также ключ текстуры берется из загрузки на предыдущей сцене:

this.load.spritesheet('fireballBlast', './assets/skillsheets/s001.png', {frameWidth: 192, frameHeight: 192});

Далее создадим спрайт в точке (2500, 1100) и запустим анимацию «explosion» при помощи функции play

this.add.sprite(2500, 1100, "").play('explosion')
Взрыв

Взрыв

Для создания персонажа используем функцию this.createPlayer()

createPlayer(): Mage {         return this.player = new Mage(this, 2100, 1000, store.getState().application.nickname) }

Где персонаж является объектом класса Mage

export default class Mage extends Player {     private skillFactory: SkillFactory = new SkillFactory(); // Factory объект создания умений     private skills = ["Fireball", "Buff"] // Всего 2 умения     private currentSkillIndex = 0 // Индекс текущего умения      constructor(scene: Scene, x: number, y: number, name:string) {         super(scene, x, y, "mage", name);         //Сцена, позиция игрока, ключ текстуры, имя     }     // Измений текущее умение     public setSkillIndex(index: number) {         if (index === undefined || index < 0 || index > 1)             return         CONTROLS.setSkill(index)         this.currentSkillIndex = index     }     // Кастовать умение по цели     override attack(target: Vector2) {         this.skillFactory.create(this.scene, this.x, this.y, target, this.skills[this.currentSkillIndex])         super.attack(target)     } }

В свою очередь он наследуется от класса Player с логикой анимирования
движущегося и атакующего персонажа в 8 направлениях(взависимости от
нажатой клавиши)

//Phaser.Physics.Arcade.Sprite - класс спрайта, используемый в физическом движке и имеющий расширенный функционал export default abstract class Player extends Phaser.Physics.Arcade.Sprite {      private animationKey: string;     private attackAnimationKey: string;     public isMoving: boolean;     public isAttack: boolean;     public name: string;     public target: Vector2;     private nameHolder: Phaser.GameObjects.Text;     private directionState: Map<Direction, boolean> = new Map([         [Direction.RIGHT, false],         [Direction.UP, false],         [Direction.DOWN, false],         [Direction.LEFT, false]     ]);     private directionVerticalVelocity: Map<Direction, number> = new Map([         [Direction.UP, -GameConfig.playerAbsVelocity],         [Direction.DOWN, GameConfig.playerAbsVelocity]     ])     private directionHorizontalVelocity: Map<Direction, number> = new Map([         [Direction.RIGHT, GameConfig.playerAbsVelocity],         [Direction.LEFT, -GameConfig.playerAbsVelocity]     ])      protected constructor(scene: Scene, x: number, y: number, textureKey: string, name: string) {         super(scene, x, y, textureKey);         this.name = name;         this.init();     }      private init() {         this.isMoving = false;         this.isAttack = false;         this.animationKey = Direction.UP;         this.scene.physics.add.existing(this)         this.scene.add.existing(this);          this.nameHolder = this.scene.add.text(0, 0, this.name, {             font: '14px pixel',             stroke: "#ffffff",             strokeThickness: 2         }).setOrigin(0.5);     }      attack(target: Vector2) {         this.isAttack = true         this.target = target         this.attackAnimationKey = `${this.animationKey}attack`          this.play(this.attackAnimationKey);         this.on(Phaser.Animations.Events.ANIMATION_COMPLETE, () => {             this.isAttack = false;             this.handleMovingAnimation()         }, this);     }      walk(direction: Direction, state: boolean) {         if (this.directionState.get(direction) === state)             return;          this.directionState.set(direction, state)         const vec = [0, 0]         const activeState = Array.from(this.directionState.entries())             .filter(value => value[1])             .map(value => {                 if (this.directionVerticalVelocity.has(value[0])) {                     vec[1] = this.directionVerticalVelocity.get(value[0])                 } else if (this.directionHorizontalVelocity.has(value[0]))                     vec[0] = this.directionHorizontalVelocity.get(value[0])                 return value[0]             })         this.isMoving = activeState.length > 0          if (activeState.length === 1)             this.animationKey = activeState[0]         else if (activeState.length === 2)             this.animationKey = activeState[1] + activeState[0]          this.setVelocity(vec[0], vec[1])          this.handleMovingAnimation()     }      private handleMovingAnimation() {         if (this.isAttack)             return;         if (this.isMoving)             this.play(this.animationKey);         else {             this.play(this.animationKey);             this.stop()         }     }      override preUpdate(time, delta): void {         super.preUpdate(time, delta);         this.nameHolder.setPosition(this.x, this.y - 30);     } }
Спрайт-лист мага

Спрайт-лист мага

Для создания анимаций движения персонажа во всех направлениях и умений по спрайтам:

createAnimations() {         GameConfig.playerAnims.map((key) => ({             key,             frames: this.anims.generateFrameNames("mage", {                 prefix: key,                 start: 0,                 end: 4             }),             frameRate: 8,             repeat: !key.includes("attack") && !key.includes("death") ? -1 : 0         })).concat([             {                 key: 'fireballBlast',                 frames: this.anims.generateFrameNumbers('fireballBlast', {start: 0, end: 19, first: 0}),                 frameRate: 20,                 repeat: 0             },             {                 key: 'buff',                 frames: this.anims.generateFrameNumbers('buff', {start: 0, end: 19, first: 0}),                 frameRate: 20,                 repeat: 0             }         ]).forEach((config) => this.anims.create(config));     }

В том числе, используя атлас анимаций мага mage.json, указывающий координаты и размеры конкретного кадра:

"frames": {     "up0": {       "frame": {         "x": 0,         "y": 0,         "w": 75,         "h": 61       }     },     "up1": {       "frame": {         "x": 0,         "y": 61,         "w": 75,         "h": 61       }     },     "up2": {       "frame": {         "x": 0,         "y": 122,         "w": 75,         "h": 61       }     },     "up3": {       "frame": {         "x": 0,         "y": 183,         "w": 75,         "h": 61       }     },     "up4": {       "frame": {         "x": 0,         "y": 244,         "w": 75,         "h": 61       }     }, ....

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

this.input.keyboard.on(Phaser.Input.Keyboard.Events.ANY_KEY_DOWN, (evt: { key: string; }) => {             this.player.setSkillIndex(this.skillIndexMap[evt.key])             const direction = this.keymap[evt.key]             if (direction)                 this.player.walk(direction, true) });

Аналогично с мышью, добавляем обработку клика:

this.input.on(Phaser.Input.Events.POINTER_DOWN, (evt: {             worldX: number;             worldY: number;  }) => this.player.attack(new Vector2(evt.worldX, evt.worldY)));

Умение, или выстрел — важная составляющая игры. Это абстрактный класс, который сам по себе также является спрайтом и содержит функции отрисовки анимации. Количество текстур не имеет значения. Метод play так или иначе запустит нужную анимацию.

export abstract class Skill extends Phaser.Physics.Arcade.Sprite {     protected target: Vector2;     protected initialPosition: Vector2;      private finallyAnimated = false;      protected constructor(scene: Phaser.Scene, x: number, y: number, image: string, target: Vector2) {         super(scene, x, y, image, 0);         this.scene.add.existing(this);         this.scene.physics.add.existing(this)         this.target = target;         this.initialPosition = new Vector2(x, y)         this.init()     }      protected preUpdate(time: number, delta: number) {         super.preUpdate(time, delta);         if (!this.finallyAnimated && new Vector2(this.x, this.y).distance(this.target) < GameConfig.skillCollisionDistance) {             this.finallyAnimated = true             this.setVelocity(0, 0)             this.animateFinally().then(sprite => this.destroy(true))                 .catch(e => this.destroy(true))         }     }      protected abstract playFinalAnimation(): void      animateFinally(): Promise<Skill> {         return new Promise((resolve, reject) => {             try {                 this.on(Phaser.Animations.Events.ANIMATION_COMPLETE, (animation: Phaser.Animations.Animation) => {                     try {                         resolve(this)                     } catch (e) {                         reject(e)                     }                 }, this);                 this.playFinalAnimation()             } catch (e) {                 reject(e)             }         })     }      init(): void {         const vel = new Vector2(this.target.x - this.initialPosition.x, this.target.y - this.initialPosition.y).normalize()         this.setPosition(this.initialPosition.x, this.initialPosition.y)         this.setVelocity(vel.x * GameConfig.skillAbsVelocity, vel.y * GameConfig.skillAbsVelocity)     } }

Огненный шар

export class Fireball extends Skill {     constructor(scene: Phaser.Scene, x: number, y: number, target: Vector2) {         super(scene, x, y, "fireball", target);     }      override init() {         super.init();         this.setScale(0.02, 0.02);     }      override playFinalAnimation() {         this.play("fireballBlast");         this.setScale(1, 1)     } }

Баф

export class Buff extends Skill {     constructor(scene: Phaser.Scene, x: number, y: number, target: Vector2) {         super(scene, x, y, "buff", target);     }      override playFinalAnimation() {         this.play("buff");     }      override init(): void {         this.setPosition(this.initialPosition.x, this.initialPosition.y)     } }
Спрайт-лист бафа

Спрайт-лист бафа

Также стоит упомянуть механику обработки столкновений в игре. Для этого используется функционал аркадного физического движка.

Создадим предмет — лицо, как группу предметов, для его последующего
респауна по истечению 3х секунд после столкновения персонажа с ним.

createItem(): Face {         return new Face(this, 2500, 1100) }
// Arcade Physics / collision  const items = this.add.group([this.createItem()]) this.physics.add.collider(this.player, items, (object1: Phaser.Tilemaps.Tile | Phaser.GameObjects.GameObject, object2: Phaser.Tilemaps.Tile | Phaser.GameObjects.GameObject) => {             object2.destroy(true) // Уничтожение объекта со сцены при столкновении             setTimeout(() => {                 items.add(this.createItem(), true) // пересоздание             }, 3000) })
Предмет

Предмет

Отрисовка игрового поля

Игровая карта представляет собой набор файлов: map01merged.json, tiles.png, tiles.tsx ( не путать с typescript tsx файлом).

В качестве редактора уровней использовался — Tiled, предназначеный для построения любых, в том числе изометрических уровней, карт, на основе тайлов и тайлсетов.

https://www.mapeditor.org

Богатая поддержка Tiled в Phaser позволяет гибко оперировать с самими тайлами карты — клетками. Их можно заменять, удалять, применять эффекты и обработку коллизий игровых объектов с ними.

Тайлсет

Тайлсет

Рендеринг карты очень простой

displayMap() {         this.map = this.add.tilemap('worldmap');         const tileset = this.map.addTilesetImage('tiles', 'tiles');         for (let i = 0; i < this.map.layers.length; i++)             this.map.createLayer(0, tileset, 0, 0).setVisible(true);     }

Пользовательский интерфейс

Как я уже сказал, взаимодействие игрока происходит через устройства ввода и пользовательский интерфейс, который представляет из себя обычные React компоненты.

Отладочная информация

Отладочная информация

Чтобы отобразить отладочную информацию в левом верхнем углу экрана необходимо:

Объявить компонент с отладочной информацией

const DebugPanel = () => {     const [fps, setFps] = useState(0);     const [version, setVersion] = useState('');     const [skill, setSkill] = useState(0);     CONTROLS.registerGameDebugControls({         setVersion,         setFps,         setSkill     })      return (         <>             <div>                 <span >                     Fps: {fps}                 </span>                 <br></br>                 <span >                     Version: {version}                 </span>                 <br></br>                 <span >                     Current skill: {skill+1}                 </span>             </div>         </>     ); };  export default DebugPanel;

Связать хуки компонента с глобальным объектом CONTROLS, зарегистрировав их

CONTROLS.registerGameDebugControls({         setVersion,         setFps,         setSkill     })

Объявить необходимый регистратор в файле controls.ts

export type ValueSetter<T> = (T) => void;  // Create your own react controls interface interface GameDebugControls {     setVersion: ValueSetter<string>     setFps: ValueSetter<number>     setSkill: ValueSetter<number> }  interface GameLoaderControls {     setProgress: ValueSetter<number> }  // Add your own react controls interface GameControlsMap {     debug?: GameDebugControls     loader?: GameLoaderControls }  class GameControls {     private controls: GameControlsMap = {}      // Create your own register controls method     public registerGameDebugControls(controls: GameDebugControls) {         this.controls.debug = controls     }      public registerGameLoaderControls(controls: GameLoaderControls) {         this.controls.loader = controls     }      // Create your own valueSetter method     public setFps(fps: number) {         if (checkExists(this.controls.debug))             this.controls.debug.setFps(fps)     }      public setSkill(skill: number) {         if (checkExists(this.controls.debug))             this.controls.debug.setSkill(skill)     }      public setVersion(version: string) {         if (checkExists(this.controls.debug))             this.controls.debug.setVersion(version)     }      public setProgress(progress: number) {         if (checkExists(this.controls.loader))             this.controls.loader.setProgress(progress)     } }  export const CONTROLS: GameControls = new GameControls()

И спокойно вызывать из игровой сцены

CONTROLS.setVersion(`Phaser v${Phaser.VERSION}`) CONTROLS.setFps(Math.trunc(this.sys.game.loop.actualFps));

Точно таким же компонентом является и форма входа в игру:

export const Login = () => {     const dispatch = useAppDispatch()      const onStart = (evt) => {         evt.preventDefault()         const data = new FormData(evt.target)         if(!data.get("name")) {             alert("Name is required")             return;         }         CONTROLS.setProgress(50)         dispatch(setLoading(true))         dispatch(setNickname(data.get("name").toString()))         setTimeout(() => {             dispatch(setLoading(false))             dispatch(setCurrentPage(Page.GAME))             launchGame()         }, 3000)     };     return (         <div className="center-extended">             <div className="fade-in">                 <Card className="game-form">                     <Form onSubmit={onStart} initialValues={{name: "name"}}>                         <Input type="text" placeholder="Input your name" name='name'/>                          <Button type="submit" color="success">Start game!</Button>                     </Form>                 </Card>             </div>         </div>     ); };  export default Login;
Форма входа в игру

Форма входа в игру

Для отключения событий клика по блоку React компонентов достаточно поправить свойство «pointer-events»:

document.getElementById("root").style.pointerEvents="none"

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

Вебсокеты

В данном демо также имеется поддержка работы с вебсокетами. Для работы с ними есть файл network.ts

class Network {     private socket: any;     private events: Map<number, [any, OnMessageHandler]> = new Map<number, [any, OnMessageHandler]>()      constructor() {         if (!window.WebSocket) {             // @ts-ignore             window.WebSocket = window.MozWebSocket;         }         if (window.WebSocket) {             this.socket = new WebSocket("ws://localhost:8085/websocket");         } else {             alert("Your browser does not support Web Socket.");         }          this.socket.addEventListener('open', (event) => {             console.log("Connection established");         });          this.socket.addEventListener('error', (event) => {             console.log(event.message);         });          this.socket.addEventListener('close', (event) => {             console.log("Web Socket closed");         });          this.socket.addEventListener('message', (evt) => {             const eventData = JSON.parse(evt.data);             if (this.events.has(eventData.type)) {                 const arr = this.events.get(eventData.type)                 arr[1].call(arr[0], eventData.data);             }         });     }      public on(type: number, handler: OnMessageHandler, thisArg:any) {         this.events.set(type, [thisArg, handler]);     }      public send(type: number, data: any = null) {         if (this.socket.readyState !== WebSocket.OPEN) {             console.log("Socket is not ready");             return;         }          this.socket.send(this.createEvent(type, data));     }      private createEvent = (eventType: number, payload: any = null) => {         const obj: any = {             type: eventType,             data: null         };         if (payload) {             obj.data = payload         }         return JSON.stringify(obj);     } }  export const network = new Network();

Для отправки сообщения на сервер достаточно вызвать метод send из любого места приложения:

network.send(TYPE, JSON_OBJECT)

Для обработки входящего сообщения достаточно объявить где-нибудь обработчик вида:

network.on(TYPE, (data)=> {}, this)

Итог

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

В скором времени выйдет статья, раскрывающая backend мултьтиплееров

Делитесь материалом с коллегами, пишите комментарии на какую тему хотели бы увидеть материал

Ссылки

https://github.com/tfkfan/phaser3-game-demo

https://github.com/tfkfan/phaser3-react-template


ссылка на оригинал статьи https://habr.com/ru/articles/771630/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *