DevGang
Авторизоваться

Браузерная многопользовательская игра «Крестики-нолики» на React

В этом посте из наших архивов вы узнаете, как создать игру «Крестики-нолики» в React, но имейте в виду, что используемые версии библиотек уже не являются последними. В частности, в этой статье используется версия 1 нашего React SDK, но все шаги и принципы, перечисленные ниже, действительны и сегодня.

Крестики-нолики — это типичная игра детства. Все, что для этого нужно — это что-то, на чем писать и чем писать. Но что, если вы хотите поиграть с кем-то, кто находится в другом месте? В этом случае вам нужно будет использовать приложение, которое подключает вас и другого игрока к игре.

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

Так как же разработчику обеспечить объединенный опыт, позволяющий игрокам играть в крестики-нолики или в любую другую игру, где бы они ни находились?

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

Существует несколько способов обеспечить инфраструктуру реального времени для многопользовательских игр. Вы можете пойти по пути создания собственной инфраструктуры с нуля, используя технологии и протоколы с открытым исходным кодом, такие как Socket.IOSignalR или WebSockets.

Хотя этот путь может показаться привлекательным, вы столкнетесь с несколькими проблемами; одной из таких проблем является масштабируемость. Обслужить 100 пользователей несложно, но как справиться с более чем 100 000 пользователей? Помимо проблем с инфраструктурой, вам все равно придется беспокоиться о поддержании своей игры.

В конце концов, единственное, что имеет значение, — это предоставить игрокам отличные впечатления от вашей игры. Но как решить инфраструктурную проблему? Здесь на помощь приходит PubNub.

PubNub предоставляет инфраструктуру реального времени для работы любого приложения через свою глобальную сеть потоков данных. Благодаря более чем 70 SDK, включая самые популярные языки программирования, PubNub упрощает отправку и получение сообщений на любое устройство менее чем за 100 мс. Он безопасен, масштабируем и надежен, поэтому вам не придется беспокоиться о создании и обслуживании собственной инфраструктуры.

Чтобы показать, насколько легко разработать многопользовательскую игру с использованием PubNub, мы создадим простую игру React «крестики-нолики», используя PubNub React SDK. В этой игре два игрока подключатся к уникальному игровому каналу, где будут играть друг против друга. Каждый ход, который делает игрок, будет публиковаться на канале, чтобы обновлять доску другого игрока в режиме реального времени.

Обзор приложения

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

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

Игрок, который присоединяется к каналу с предоставленным ему идентификатором комнаты, становится игроком O. Игроки могут присоединяться к каналам только в том случае, если на канале есть еще один человек. Если на этом канале несколько человек, то на этом канале идет игра, и игрок не сможет присоединиться. Игра начинается, когда на канале появляются два игрока.

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

Настройте лобби

Прежде чем мы настроим лобби, зарегистрируйте бесплатную учетную запись PubNub, чтобы получить бесплатные ключи API Pub/Sub с панели администратора PubNub.

Получив ключи, вставьте их в конструктор App.js.

App.js
import React, { Component } from 'react';
import Game from './Game';
import Board from './Board';
import PubNubReact from 'pubnub-react';
import Swal from "sweetalert2";
import shortid  from 'shortid';
import './Game.css';

class App extends Component {
  constructor(props) {
    super(props);
    // REPLACE with your keys
    this.pubnub = new PubNubReact({
      publishKey: "YOUR_PUBLISH_KEY_HERE",
      subscribeKey: "YOUR_SUBSCRIBE_KEY_HERE"
    });

    this.state = {
      piece: '', // X or O
      isPlaying: false, // Set to true when 2 players are in a channel
      isRoomCreator: false,
      isDisabled: false,
      myTurn: false,
    };

    this.lobbyChannel = null; // Lobby channel
    this.gameChannel = null; // Game channel
    this.roomId = null; // Unique id when player creates a room
    this.pubnub.init(this); // Initialize PubNub
  }

  render() {
    return ();
    }
  }

  export default App;

Также в конструкторе инициализируются объекты состояния и переменные. Мы будем просматривать объекты и переменные, когда они встречаются в файле. Наконец, мы инициализировали PubNub в конце конструктора.

Внутри метода render и внутри оператора return мы добавляем разметку для компонента Lobby.

return (
    <div>
      <div className="title">
        <p> React Tic Tac Toe </p>
      </div>

      {
        !this.state.isPlaying &&
        <div className="game">
          <div className="board">
            <Board
                squares={0}
                onClick={index => null}
              />

            <div className="button-container">
              <button
                className="create-button "
                disabled={this.state.isDisabled}
                onClick={(e) => this.onPressCreate()}
                > Create
              </button>
              <button
                className="join-button"
                onClick={(e) => this.onPressJoin()}
                > Join
              </button>
            </div>

          </div>
        </div>
      }

      {
        this.state.isPlaying &&
        <Game
          pubnub={this.pubnub}
          gameChannel={this.gameChannel}
          piece={this.state.piece}
          isRoomCreator={this.state.isRoomCreator}
          myTurn={this.state.myTurn}
          xUsername={this.state.xUsername}
          oUsername={this.state.oUsername}
          endGame={this.endGame}
        />
      }
    </div>
);

Компонент «Lobby» состоит из заголовка, пустой доски для крестиков-ноликов (если игрок нажимает на квадраты, ничего не происходит) и кнопок «Create» и «Join». Этот компонент отображается только в том случае, если значение состояния _isPlaying имеет значение false. Если для него установлено значение true, то игра началась, и компонент меняется на компонент Game, о котором мы поговорим во второй части урока.

Компонент «Board» также является частью компонента «Lobby». В компоненте Board есть компонент Square. Мы не будем вдаваться в подробности этих двух компонентов, а сосредоточимся на компонентах «Lobby» и «Game».

Когда игрок нажимает кнопку «Create», кнопка отключается, поэтому игрок не может создавать несколько каналов. Кнопка «Join» не отключена на тот случай, если игрок вместо этого решит присоединиться к каналу. После нажатия кнопки «Create» вызывается метод onPressCreate().

Создать канал

Первое, что мы делаем в onPressCreate() — это генерируем случайный идентификатор строки, усеченный до 5 символов. Мы делаем это с помощью shortid(). Мы добавляем строку к «tictactoelobby», который будет уникальным каналом лобби, на который подписываются игроки.

// Create a room channel
onPressCreate = (e) => {
  // Create a random name for the channel
  this.roomId = shortid.generate().substring(0,5);
  this.lobbyChannel = 'tictactoelobby--' + this.roomId; // Lobby channel name

  this.pubnub.subscribe({
    channels: [this.lobbyChannel],
    withPresence: true // Checks the number of people in the channel
  });
}

Чтобы предотвратить присоединение более двух игроков к данному каналу, мы используем Presence. Позже мы рассмотрим логику проверки занятости канала.

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

Это модальное окно и все модальные окна, используемые в этом приложении, созданы SweetAlert2 для замены всплывающих окон alert() по умолчанию в JavaScript.

// Inside of onPressCreate()
// Modal
Swal.fire({
  position: 'top',
  allowOutsideClick: false,
  title: 'Share this room ID with your friend',
  text: this.roomId,
  width: 275,
  padding: '0.7em',
  // Custom CSS to change the size of the modal
  customClass: {
      heightAuto: false,
      title: 'title-class',
      popup: 'popup-class',
      confirmButton: 'button-class'
  }
})

В конце onPressCreate() мы изменяем значения состояния, чтобы они отражали новое состояние приложения.

this.setState({
  piece: 'X',
  isRoomCreator: true,
  isDisabled: true, // Disable the 'Create' button
  myTurn: true, // Player X makes the 1st move
});

Как только игрок создает комнату, ему приходится ждать, пока к этой комнате присоединится другой игрок. Давайте посмотрим на логику присоединения к комнате.

Присоединяйтесь к каналу

Когда игрок нажимает кнопку «Join», вызывается вызов onPressJoin(). Игроку отображается модальное окно с просьбой ввести идентификатор комнаты в поле ввода.

Если игрок вводит идентификатор комнаты и нажимает кнопку «OK», вызывается joinRoom(value), где value — идентификатор комнаты. Этот метод не вызывается, если поле ввода пусто или игрок нажимает кнопку «Cancel».

// The 'Join' button was pressed
onPressJoin = (e) => {
  Swal.fire({
    position: 'top',
    input: 'text',
    allowOutsideClick: false,
    inputPlaceholder: 'Enter the room id',
    showCancelButton: true,
    confirmButtonColor: 'rgb(208,33,41)',
    confirmButtonText: 'OK',
    width: 275,
    padding: '0.7em',
    customClass: {
      heightAuto: false,
      popup: 'popup-class',
      confirmButton: 'join-button-class',
      cancelButton: 'join-button-class'
    }
  }).then((result) => {
    // Check if the user typed a value in the input field
    if(result.value){
      this.joinRoom(result.value);
    }
  })
}

Первое, что мы делаем в joinRoom() — добавляем значение к ' tictactoelobby', аналогично тому, что мы делали в onPressCreate().

// Join a room channel
joinRoom = (value) => {
  this.roomId = value;
  this.lobbyChannel = 'tictactoelobby--' + this.roomId;
}

Прежде чем игрок подпишется на канал лобби, мы должны проверить общую занятость канала с помощью hereNow(). Если общая заполняемость меньше 2, игрок может успешно подписаться на канал лобби.

// Check the number of people in the channel
this.pubnub.hereNow({
  channels: [this.lobbyChannel],
}).then((response) => {
    if(response.totalOccupancy < 2){
      this.pubnub.subscribe({
        channels: [this.lobbyChannel],
        withPresence: true
      });

      this.setState({
        piece: 'O', // Player O
      });

      this.pubnub.publish({
        message: {
          notRoomCreator: true,
        },
        channel: this.lobbyChannel
      });
    }
}).catch((error) => {
  console.log(error);
});

После того, как игрок подписывается на канал лобби, значение состояния фигуры меняется на «O», и на этом канале лобби публикуется сообщение. Это сообщение уведомляет игрока «X» о том, что к каналу присоединился другой игрок. Мы настраиваем прослушиватель сообщений в компоненте ComponentDidUpdate(), к которому мы вскоре доберемся.

Если общая занятость превышает 2, то игра продолжается, и игроку, пытающемуся присоединиться к каналу, будет отказано в доступе. Следующий код находится под оператором if в hereNow().

// Below the if statement in hereNow()
else{
  // Game in progress
  Swal.fire({
    position: 'top',
    allowOutsideClick: false,
    title: 'Error',
    text: 'Game in progress. Try another room.',
    width: 275,
    padding: '0.7em',
    customClass: {
        heightAuto: false,
        title: 'title-class',
        popup: 'popup-class',
        confirmButton: 'button-class'
    }
  })
}

Давайте теперь посмотрим на компонент DidUpdate().

Начало игры

В компоненте ComponentDidUpdate() мы проверяем, подключен ли плеер к каналу, то есть проверяем, что this.lobbyChannel не имеет значения null. Если оно не равно null, мы настраиваем прослушиватель, который прослушивает все сообщения, поступающие на канал.

componentDidUpdate() {
  // Check that the player is connected to a channel
  if(this.lobbyChannel != null){
    this.pubnub.getMessage(this.lobbyChannel, (msg) => {
      // Start the game once an opponent joins the channel
      if(msg.message.notRoomCreator){
        // Create a different channel for the game
        this.gameChannel = 'tictactoegame--' + this.roomId;

        this.pubnub.subscribe({
          channels: [this.gameChannel]
        });
      }
    });
  }
}

Проверяем, пришло ли сообщение msg.message.notRoomCreator, которое публикует игрок, присоединившийся к каналу. Если это так, мы создаем новый канал ' tictactoegame', к строке которого добавляется идентификатор комнаты. Игровой канал используется для публикации всех ходов игроков, которые будут обновлять их доски.

Наконец, после подписки на игровой канал значение состояния isPlaying устанавливается равным true. При этом компонент лобби будет заменен игровым компонентом.

 this.setState({
   isPlaying: true
 });

 // Close the modals if they are opened
 Swal.close();
}

После отображения игрового компонента мы хотим закрыть все модальные окна, если они открыты, из компонента «Lobby», выполнив Swal.close().

Теперь, когда у нас есть два игрока, подключенные к уникальному игровому каналу, они могут начать играть в крестики-нолики! В следующем разделе мы реализуем пользовательский интерфейс и логику игрового компонента.

Создайте игровые функции

Первое, что мы делаем в Game.js — настраиваем базовый конструктор:

Game.js
import React from 'react';
import Board from './Board';
import Swal from "sweetalert2";

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(''), // 3x3 board
      xScore: 0,
      oScore: 0,
      whosTurn: this.props.myTurn // Player X goes first
    };

    this.turn = 'X';
    this.gameOver = false;
    this.counter = 0; // Game ends in a tie when counter is 9
  }

  render() {
    return ();
  }
 }
export default Game;

Для объектов состояния мы инициализируем свойство Squares массива, которое используется для хранения позиций игрока на доске. Это будет дополнительно объяснено ниже. Мы также устанавливаем счет игроков равным 0 и устанавливаем значение whosTurn в myTurn, которое инициализируется как true для игрока «X» и false для игрока «O».

Значение переменных Turn и Counter будет меняться по ходу игры. В конце игры для GameOver устанавливается значение true.

Добавьте пользовательский интерфейс

Далее давайте настроим разметку для компонента Game внутри метода render.

render() {
  let status;
  // Change to current player's turn
  status = `${this.state.whosTurn ? "Your turn" : "Opponent's turn"}`;

  return (
    <div className="game">
      <div className="board">
        <Board
            squares={this.state.squares}
            onClick={index => this.onMakeMove(index)}
          />
          <p className="status-info">{status}</p>
      </div>

      <div className="scores-container">
        <div>
          <p>Player X: {this.state.xScore} </p>
        </div>

        <div>
          <p>Player O: {this.state.oScore} </p>
        </div>
      </div>
    </div>
  );
}

Мы показываем значение статуса в пользовательском интерфейсе, чтобы игроки знали, сейчас их очередь делать ход или очередь другого игрока. Логическое значение состояния whosTurn обновляется каждый раз при совершении перемещения. Остальная часть пользовательского интерфейса состоит из компонента «Board» и очков игрока.

Добавьте логику

Когда игрок делает ход на доске, выполняется вызов onMakeMove(index), где index — это позиция, в которой фигура размещена на доске. На доске 3 ряда и 3 столбца, всего 9 клеток. Каждый квадрат имеет свое уникальное значение индекса, начиная со значения 0 и заканчивая значением 8.

onMakeMove = (index) =>{
  const squares = this.state.squares;

  // Check if the square is empty and if it's the player's turn to make a move
  if(!squares[index] && (this.turn === this.props.piece)){
    squares[index] = this.props.piece;

    this.setState({
      squares: squares,
      whosTurn: !this.state.whosTurn
    });

    // Other player's turn to make a move
    this.turn = (this.turn === 'X') ? 'O' : 'X';

    // Publish move to the channel
    this.props.pubnub.publish({
      message: {
        index: index,
        piece: this.props.piece,
        turn: this.turn
      },
      channel: this.props.gameChannel
    });

    // Check if there is a winner
    this.checkForWinner(squares)
  }
}

После получения состояния массива квадратов используется условный оператор, чтобы проверить, пуст ли квадрат, которого коснулся игрок, и настала ли его очередь сделать ход. Если одно или оба условия не выполнены, то фишка игрока не ставится на поле. В противном случае фишка игрока добавляется к квадратам массива по индексу, в котором фишка была помещена.

Например, если игрок «X» делает ход в строке 0, столбце 2 и условное утверждение истинно, то squares[2] будут иметь значение «X».

Затем состояние изменяется, чтобы отразить новое состояние игры, и ход обновляется, чтобы другой игрок мог сделать свой ход. Чтобы доска другого игрока обновилась текущими данными, мы публикуем данные в игровой канал. Все это происходит в реальном времени, поэтому оба игрока сразу же увидят обновление своих досок, как только будет сделан правильный ход. Последнее, что нужно сделать в этом методе, — это вызвать checkForWinner(squares), чтобы проверить, есть ли победитель.

Прежде чем мы это сделаем, давайте взглянем на компонент DidMount (), где мы настраиваем прослушиватель новых сообщений, поступающих в игровой канал.

componentDidMount(){
  this.props.pubnub.getMessage(this.props.gameChannel, (msg) => {
    // Update other player's board
    if(msg.message.turn === this.props.piece){
      this.publishMove(msg.message.index, msg.message.piece);
    }
  });
}

Поскольку оба игрока подключены к одному игровому каналу, они оба получат это сообщение. Вызывается метод PublishMove (index, Piece), где index — это позиция, в которой была размещена фигура, а Piece — это фигура игрока, сделавшего ход. Этот метод обновляет доску текущим ходом и проверяет, есть ли победитель. Чтобы игроку, сделавшему текущий ход, не пришлось повторять этот процесс снова, оператор if проверяет, соответствует ли фигура игрока значению хода. Если да, то их доска обновляется.

// Opponent's move is published to the board
publishMove = (index, piece) => {
  const squares = this.state.squares;

  squares[index] = piece;
  this.turn = (squares[index] === 'X')? 'O' : 'X';

  this.setState({
    squares: squares,
    whosTurn: !this.state.whosTurn
  });

  this.checkForWinner(squares)
}

Логика обновления доски такая же, как и в onMakeMove(). Давайте теперь рассмотрим checkForWinner().

checkForWinner = (squares) => {
  // Possible winning combinations
  const possibleCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  // Iterate every combination to see if there is a match
  for (let i = 0; i < possibleCombinations.length; i += 1) {
    const [a, b, c] = possibleCombinations[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      this.announceWinner(squares[a]);
      return;
    }
  }
}

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

Предположим, что игрок «X» делает выигрышный ход в строке 2, столбце 0. Индекс этой позиции равен 6. Теперь доска выглядит следующим образом:

Выигрышная комбинация для игрока «X» — [2,4,6]. Квадраты массива обновляются до: [“O”, “”, “X”, “O”, “X”, “”, “X”, “”, “”].

В цикле for, когда [a,b,c] имеет значения [2,4,6], оператор if в цикле for будет истинным, поскольку все [2,4,6] имеют одинаковое значение из «X».​ Необходимо обновить счет победителя, поэтому вызывается announceWinner() для награждения победившего игрока.

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

// Below the for loop in checkForWinner()
// Check if the game ends in a draw
this.counter++;
// The board is filled up and there is no winner
if(this.counter === 9){
  this.gameOver = true;
  this.newRound(null);
}

Если счетчик достигает 9, то игра заканчивается вничью, поскольку игрок не сделал выигрышного хода на последней клетке доски. Когда это происходит, метод newRound() вызывается с нулевым аргументом, поскольку победителя нет.

Прежде чем мы перейдем к этому методу, давайте вернемся к announceWinner().

// Update score for the winner
announceWinner = (winner) => {
  let pieces = {
    'X': this.state.xScore,
    'O': this.state.oScore
  }

  if(winner === 'X'){
    pieces['X'] += 1;
    this.setState({
      xScore: pieces['X']
    });
  }
  else{
    pieces['O'] += 1;
    this.setState({
      oScore: pieces['O']
    });
  }
  // End the game once there is a winner
  this.gameOver = true;
  this.newRound(winner);
}

Параметр этого метода — winner, то есть игрок, выигравший игру. Мы проверяем, является ли победителем «X» или «O», и увеличиваем счет победителя на один балл. Поскольку игра окончена, переменной gameOver присваивается значение true и вызывается метод newRound().

Начать новый раунд

Игрок «X» имеет возможность сыграть еще один раунд или завершить игру и вернуться в лобби.

Другой игрок сказал подождать, пока игрок «X» не решит, что делать.

Как только игрок «X» решает, что делать, на игровом канале публикуется сообщение, информирующее об этом другого игрока. Затем пользовательский интерфейс обновляется.

newRound = (winner) => {
  // Announce the winner or announce a tie game
  let title = (winner === null) ? 'Tie game!' : `Player ${winner} won!`;
  // Show this to Player O
  if((this.props.isRoomCreator === false) && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Waiting for a new round...',
      confirmButtonColor: 'rgb(208,33,41)',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
      } ,
    });
    this.turn = 'X'; // Set turn to X so Player O can't make a move
  }

  // Show this to Player X
  else if(this.props.isRoomCreator && this.gameOver){
    Swal.fire({
      position: 'top',
      allowOutsideClick: false,
      title: title,
      text: 'Continue Playing?',
      showCancelButton: true,
      confirmButtonColor: 'rgb(208,33,41)',
      cancelButtonColor: '#aaa',
      cancelButtonText: 'Nope',
      confirmButtonText: 'Yea!',
      width: 275,
      customClass: {
          heightAuto: false,
          title: 'title-class',
          popup: 'popup-class',
          confirmButton: 'button-class',
          cancelButton: 'button-class'
      } ,
    }).then((result) => {
      // Start a new round
      if (result.value) {
        this.props.pubnub.publish({
          message: {
            reset: true
          },
          channel: this.props.gameChannel
        });
      }

      else{
        // End the game
        this.props.pubnub.publish({
          message: {
            endGame: true
          },
          channel: this.props.gameChannel
        });
      }
    })
  }
 }

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

Для сообщения endGame все модальные окна закрываются и вызывается метод endGame(). Этот метод находится в App.js.

// Reset everything
endGame = () => {
  this.setState({
    piece: '',
    isPlaying: false,
    isRoomCreator: false,
    isDisabled: false,
    myTurn: false,
  });

  this.lobbyChannel = null;
  this.gameChannel = null;
  this.roomId = null;

  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}

Все значения состояния и переменные сбрасываются до исходных значений. Имена каналов сбрасываются до нуля, поскольку новое имя генерируется каждый раз, когда игрок создает комнату. Поскольку названия каналов больше не будут полезны, игроки отписываются как от лобби, так и от игрового канала. Значение isPlaying сбрасывается на false, поэтому игровой компонент будет заменен компонентом лобби.

Последний метод, который нужно включить в App.js, — это компонент WillUnmount(), который отписывает игроков от обоих каналов.

componentWillUnmount() {
  this.pubnub.unsubscribe({
    channels : [this.lobbyChannel, this.gameChannel]
  });
}

Это все, что нам нужно сделать, чтобы игра заработала! Вы можете получить CSS-файл для игры в репозитории. Теперь давайте запустим игру.

Запустить игру

Прежде чем запустить игру, нам нужно сделать пару небольших шагов. Во-первых, нам нужно включить функцию присутствия, поскольку мы используем ее для получения количества людей на канале (мы использовали withPresence при подписке на канал лобби). Перейдите на панель администратора PubNub и нажмите на свое приложение. Нажмите «Keyset» и прокрутите вниз до «Application add-ons». Включите переключатель «Presence». Оставьте значения по умолчанию прежними.

Чтобы установить три зависимости, используемые в приложении, и запустить приложение, вы можете запустить сценарий dependency.sh, который находится в корневом каталоге приложения.

# dependencies.sh
npm install --save pubnub pubnub-react
npm install --save shortid
npm install --save sweetalert2

npm start

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

chmod +x dependencies.sh

Запустите скрипт с помощью этой команды:

./dependencies.sh

Приложение откроется по адресу http://localhost:3000 с отображением компонента лобби.

Откройте другую вкладку или, желательно, окно, скопируйте и вставьте http://localhost:3000. В одном окне создайте канал, нажав кнопку «Create». Появится модальное окно с идентификатором комнаты. Скопируйте и вставьте этот идентификатор. Перейдите в другое окно и нажмите кнопку «Join». Когда появится модальное окно, введите идентификатор комнаты в поле ввода и нажмите кнопку «OK».

Как только игроки соединятся, игра начнется. Окно, которое вы использовали для создания канала, делает первый шаг. Нажмите на любой квадрат на доске и увидите, как фигура «X» отображается на доске в реальном времени для обоих окон. Если вы попытаетесь нажать еще одну клетку на той же доске, ничего не произойдет, потому что теперь не ваша очередь делать ход. В другом окне нажмите на любую клетку доски, и фигура «O» окажется в клетке.

Продолжайте играть до тех пор, пока не будет выявлен победитель или ничья. Затем отображается модальное окно, объявляющее победителя раунда или объявляющее, что игра закончилась вничью. В том же модальном режиме игроку «X» придется решить, продолжать ли игру или выйти из игры. Модальное окно для игрока «O» предложит ему дождаться нового раунда.

Все, кроме счета, сбрасывается, если игрок «X» продолжает игру. В противном случае оба игрока вернутся в лобби, где смогут создавать новые каналы или присоединяться к ним.

Источник:

#React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу