Использование ReactJS с Redux — довольно распространенный сценарий, но даже при этом люди часто сталкиваются с вопросами типа «как…», «когда…» и «почему…».

Говоря о Phaser, это может происходить практически постоянно. Иногда даже спрашивают, зачем использовать ReactJS поверх Phaser… На это есть несколько причин.

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

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

Но как тогда мы можем подружиться со всеми этими людьми в одном приложении?

Это очень хороший вопрос, первая часть ответа обсуждается здесь, в моей предыдущей статье https://medium.com/@costarassco/how-to-create-applications-architecture-in-frontend-and-why-it. -важно-764273fce03f». Я писал об архитектуре такого приложения там.

Со второй частью я собирался поделиться с вами сегодня…

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

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

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

Точка входа:

import React, { Component } from 'react';
import { Provider } from 'react-redux';
import ReactDOM from "react-dom";
import store from './redux/store';
import Game from './components/game';

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Game />
      </Provider>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('app'));
export default App;

Здесь мы просто предоставляем наше глобальное взаимное состояние провайдеру, который передает его дочерним элементам в компоненте Game.

Компонент, отвечающий за загрузку и настройку исходных данных:

import React, { Component } from 'react';
import Phaser from 'phaser';
import PhaserReact from "phaser3-react";
import { connect } from 'react-redux';
import BootScene from '../scenes/boot/boot';
import PlayScene from '../scenes/play/play';
import { getProfileData } from '../services/http-service';
import { setProfileData } from '../redux/actions';

class Game extends Component {
  async _onLoadProfileData() {
    try {
      const { profile } = await getProfileData();
      this.props.onSetProfileData(profile);
      // other stuff
      } catch (e) {
      console.log('could not load profile data... = ', e);
    }
  }

  _onLoadGame() {
    window.game = new Phaser.Game({
      type: Phaser.AUTO,
      parent: 'game-container',
      width: window.innerWidth,
      height: window.innerHeight,
      dom: {
        createContainer: true
      },
      plugins: {
        global: [
          {
            key: 'phaser-react',
            plugin: PhaserReact,
            start: true
          }
        ]
      },
      scene: [BootScene, PlayScene]
    });
  }

  async componentDidMount() {
    await this._onLoadProfileData();
    this._onLoadGame();
  }

  render() {
    return <div id="game-container"/>;
  }
};

const mapDispatchToProps = dispatch => ({
  onSetProfileData: (profileData) => dispatch(setProfileData(profileData)),
  // other callbacks
});

export default connect(undefined, mapDispatchToProps)(Game);

Главное здесь:

  1. Загрузка исходных данных, таких как данные профиля, планеты, другие пользователи и мобы в системе и т. Д., После того, как компонент смонтирован;
  2. Другой — установка глобального плагина в игровом объекте Phaser;

Родительская сцена:

import PlanetScene from './planet/planet';
import SpaceSystemScene from './space-system/space-system';
import store from '../../redux/store';
import { toggleSatOnPlanetBoolean, toggleLeavePlanetBoolean } from '../../redux/actions';

class PlayScene extends Phaser.Scene {
  constructor() {
    super({ key: 'PlayScene' });
    this.hasPlanetSceneBeenLoaded = false;
    this.hasSpaceSceneBeenLoaded = false;
  }

  update() {
    if (
      !this.hasPlanetSceneBeenLoaded
      && store.getState().profileData
      && store.getState().profileData.hasJustSatOnPlanet
    ) {
      this.hasPlanetBeenLoaded = true;
      this.hasSpaceSceneBeenLoaded = false;
      this.scene.remove('SpaceSystemScene');
      this.planetScene = game.scene.add('PlanetScene', new PlanetScene(), true);
      store.dispatch(toggleSatOnPlanetBoolean(false));
    } else if (
      !this.hasSpaceSceneBeenLoaded
      && store.getState().profileData
      && store.getState().profileData.hasJustLeftPlanet
    ) {
      this.hasPlanetBeenLoaded = false;
      this.hasSpaceSceneBeenLoaded = true;
      this.scene.remove('PlanetScene');
      this.spaceSystemScene = game.scene.add('SpaceSystemScene', new SpaceSystemScene(), true);
      store.dispatch(toggleLeavePlanetBoolean(false));
    }
  }
    
  create() {
    const { isOnPlanet } = store.getState().profileData;
    if (isOnPlanet) {
      this.planetScene = game.scene.add('PlanetScene', new PlanetScene(), true);
      this.hasPlanetBeenLoaded = true;
    } else {
      this.spaceSystemScene = game.scene.add('SpaceSystemScene', new SpaceSystemScene(), true);
      this.hasSpaceSceneBeenLoaded = true;
    }
  }
}

export default PlayScene;

В этой сцене уже происходит волшебство…

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

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

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

store.dispatch(toggleLeavePlanetBoolean(false));

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

Отдохнем немного

Но как насчет ReactJS и прочего, что вы спросите, верно?

Я забыл об этом, позвольте мне поделиться всеми подробностями ниже.

Поскольку я говорил об одной из главных сцен, вот она:

import store from '../../../redux/store';
import GalaxyMapScene from '../modals/galaxy-map/galaxy-map';
// lots of other imports

class SpaceSystemScene extends Phaser.Scene {
  constructor () {
    super({ key: 'SpaceSystemScene' });
    // lots of stuff
  }

  onGalaxyMapClose() {
    this.scene.remove('GalaxyMapScene');
  }

  onGalaxyMapShow() {
    this.closeAllOpenedScenes();
    this.galaxyMapScene = game.scene.add('GalaxyMapScene', new GalaxyMapScene({ closeCallback: this.onGalaxyMapClose.bind(this) }), true);
  }
 // other methods
}

Здесь мы создали модальное окно карты галактики scene, которое открывается

Вот код сцены карты галактики:

import GalaxyMapComponent from './components/galaxy-map';

class GalaxyMapScene extends Phaser.Scene {
  constructor ({ closeCallback }) {
    super({ key: 'GalaxyMapScene' });
    this.onClose = closeCallback;
  }

  onDestroy() {
    this.galaxyMap.destroy();
  }

  addEvents() {
    this.events.on('destroy', () => this.onDestroy());
  }

  create() {
    const background = this.add.image(game.config.width / 2, game.config.height / 2, 'galaxy-map-background').setDisplaySize(1070, 570);
    this.galaxyMap = this.add.reactDom(GalaxyMapComponent, { scope: this, onClose: this.onClose });
  
    this.addEvents();
  }
}

export default GalaxyMapScene;

В компонентах у нас будет окно ReactJS, которое отвечает за пользовательский интерфейс в игре.

Модальное окно карты галактики:

import React, { useCallback, useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import store from '../../../../../redux/store';
import { getOpenedCellsForGalaxyMap } from '../../../../../services/data-parser';

import './galaxy-map.less';

const GalaxyMap = ({ scope, onClose }) => {
  const openedCells = getOpenedCellsForGalaxyMap();
  const [chosenSystem, setChosenSystem] = useState();
  const [systems, setSystems] = useState([]);
  const [radiusVisibility] = useState(100);

  useEffect(() => {
    setSystems(store.getState().profileData.galaxyMap);
  }, [store]);

  const getGalaxies = useMemo(() => {
    const radarSize = 2 * radiusVisibility;
    return (
      <div className='system-points'>
        {
          systems.map((system, index) => {
            // my cool system element
          })
        }
      </div>
    );
  }, [radiusVisibility, systems, checkApproachability, chosenSystem, onSystemPointClick, myCurrentPlanet, getPathLine])
  
 const onGoToGyper = useCallback(() => { }, [chosenSystem])
  return (
    <div className='galaxy-map-container' style={{ left: game.config.width / 2 - 542, top: game.config.height / 2 - 286 }}>
      {getCellsTemplate}
      {getGalaxies}
    </div>
  )
}

GalaxyMap.propTypes = {
  scope: PropTypes.object,
  onClose: PropTypes.func,
}

export default GalaxyMap;

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

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

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

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

Спасибо за внимание.

Ваше здоровье.