Przeglądarkowa gra wieloosobowa Tic Tac Toe w React

PubNub Developer Relations - Mar 10 - - Dev Community

Ten post, pochodzący z naszych archiwów, przeprowadzi Cię przez kroki tworzenia gry Tic Tac Toe w React, ale pamiętaj, że wersje używanych bibliotek nie są już najnowsze. W szczególności, artykuł ten wykorzystuje wersję 1 naszego React SDK, ale wszystkie kroki i zasady wymienione poniżej są nadal aktualne.

Gra w kółko i krzyżyk to kwintesencja dzieciństwa. Wszystko, czego wymaga, to coś do pisania i coś, czym można pisać. Ale co, jeśli chcesz zagrać z kimś, kto jest w innej lokalizacji? W takim przypadku musisz użyć aplikacji, która połączy ciebie i innego gracza z grą.

Aplikacja musi zapewniać wrażenia w czasie rzeczywistym, aby każdy twój ruch był natychmiast widoczny dla drugiego gracza i odwrotnie. Jeśli aplikacja nie zapewnia takich wrażeń, to ty i wiele innych osób prawdopodobnie nie będzie już z niej korzystać.

Jak więc deweloper może zapewnić połączone doświadczenie, w którym gracze mogą grać w kółko i krzyżyk lub w dowolną inną grę, bez względu na to, gdzie się znajdują?

Koncepcje gier wieloosobowych w czasie rzeczywistym

Istnieje kilka sposobów na zapewnienie infrastruktury czasu rzeczywistego dla gier wieloosobowych. Można pójść drogą budowania własnej infrastruktury od podstaw, korzystając z technologii i protokołów open-source, takich jak Socket.IO, SignalR lub WebSockets.

Chociaż może się to wydawać atrakcyjną drogą, napotkasz kilka problemów; jednym z nich jest skalowalność. Obsługa 100 użytkowników nie jest trudna, ale jak poradzić sobie z ponad 100 000 użytkowników? Poza kwestiami związanymi z infrastrukturą, wciąż musisz martwić się o utrzymanie swojej gry.

Koniec końców, jedyną rzeczą, która się liczy, jest zapewnienie graczom wspaniałych wrażeń z gry. Ale jak rozwiązać problem infrastruktury? Tutaj z pomocą przychodzi PubNub.

PubNub zapewnia infrastrukturę czasu rzeczywistego do zasilania dowolnej aplikacji za pośrednictwem globalnej sieci strumieni danych. Dzięki ponad 70 zestawom SDK, w tym najpopularniejszym językom programowania, PubNub upraszcza wysyłanie i odbieranie wiadomości do dowolnego urządzenia w czasie poniżej 100 ms. Jest bezpieczny, skalowalny i niezawodny, dzięki czemu nie musisz martwić się o tworzenie i utrzymywanie własnej infrastruktury.

Aby pokazać, jak łatwo jest stworzyć grę wieloosobową przy użyciu PubNub, zbudujemy prostą grę React tic tac toe przy użyciu PubNub React SDK. W tej grze dwóch graczy połączy się z unikalnym kanałem gry, gdzie będą grać przeciwko sobie. Każdy ruch wykonany przez gracza zostanie opublikowany na kanale, aby zaktualizować planszę drugiego gracza w czasie rzeczywistym.

Przegląd aplikacji

Oto jak będzie wyglądać nasza aplikacja, gdy ją ukończymy.

Screen shot of the React Tic Tac Toe GameGracze najpierw dołączają do lobby, gdzie mogą utworzyć kanał lub dołączyć do kanału. Jeśli gracz utworzy kanał, otrzyma identyfikator pokoju do współdzielenia z innym graczem. Gracz, który utworzył kanał, staje się graczem X i wykona pierwszy ruch po rozpoczęciu gry.

Create a room channel

Gracz, który dołączy do kanału z identyfikatorem pokoju, który otrzymał, staje się graczem O. Gracze mogą dołączać do kanałów tylko wtedy, gdy w kanale znajduje się jedna inna osoba. Jeśli na kanale znajduje się więcej niż jedna osoba, gra na tym kanale jest w toku i gracz nie będzie mógł do niej dołączyć. Gra rozpoczyna się, gdy na kanale znajduje się dwóch graczy.

Join the room channel

Na koniec gry wynik zwycięzcy jest zwiększany o jeden punkt. Jeśli gra zakończy się remisem, żaden z graczy nie otrzyma punktu. Graczowi X wyświetlany jest komunikat modalny z prośbą o rozpoczęcie nowej rundy lub zakończenie gry. Jeśli gracz X kontynuuje grę, plansza resetuje się do nowej rundy. W przeciwnym razie gra kończy się i obaj gracze wracają do lobby.

Exit to lobby

Konfiguracja lobby

Zanim skonfigurujemy lobby, zarejestruj darmowe konto PubNub, aby uzyskać bezpłatne klucze Pub/Sub API z panelu administracyjnego PubNub.

Po otrzymaniu kluczy wstaw je do konstruktora 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;
Enter fullscreen mode Exit fullscreen mode

Również w konstruktorze inicjalizowane są obiekty stanu i zmienne. Omówimy te obiekty i zmienne, gdy pojawią się w całym pliku. Wreszcie, zainicjowaliśmy PubNub na końcu konstruktora.

Wewnątrz metody renderowania i w instrukcji return dodajemy znaczniki dla komponentu 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>
);
Enter fullscreen mode Exit fullscreen mode

Komponent Lobby składa się z tytułu, pustej planszy do gry w kółko i krzyżyk (nic się nie dzieje, gdy gracz naciska kwadraty) oraz przycisków "Utwórz_"_ i "Dołącz_". Ten komponent jest wyświetlany tylko wtedy, gdy wartość stanu _isPlaying jest fałszywa. Jeśli jest ustawiona na true, gra się rozpoczęła, a komponent zostanie zmieniony na komponent Game, który omówimy w drugiej części samouczka.

Komponent Board jest również częścią komponentu Lobby. Wewnątrz komponentu Board znajduje się komponent Square. Nie będziemy szczegółowo omawiać tych dwóch komponentów, aby skupić się na komponentach Lobby i Game.

Gdy gracz naciśnie przycisk "Utwórz", przycisk ten jest wyłączony, więc gracz nie może utworzyć wielu kanałów. Przycisk "Dołącz" nie jest wyłączony, na wypadek gdyby gracz zdecydował się dołączyć do kanału. Po naciśnięciu przycisku "Create" wywoływana jest metoda onPressCreate().

Tworzenie kanału

Pierwszą rzeczą, którą robimy w onPressCreate () jest wygenerowanie losowego identyfikatora łańcucha, który jest obcięty do 5 znaków. Robimy to za pomocą funkcji shortid(). Dołączamy ciąg do "_tictactoelobby-",_który będzie unikalnym kanałem lobby subskrybowanym przez graczy.

// 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
  });
}
Enter fullscreen mode Exit fullscreen mode

Aby zapobiec dołączeniu więcej niż dwóch graczy do danego kanału, używamy funkcji Presence. Później przyjrzymy się logice sprawdzania zajętości kanału.

Gdy gracz zasubskrybuje kanał lobby, wyświetlany jest modal z identyfikatorem pokoju, aby inny gracz mógł dołączyć do tego kanału.

Share the room id

Ten modal i wszystkie modale używane w tej aplikacji są tworzone przez SweetAlert2 w celu zastąpienia domyślnych wyskakujących okienek JavaScript alert().

// 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'
  }
})
Enter fullscreen mode Exit fullscreen mode

Pod koniec onPressCreate() zmieniamy wartości stanu, aby odzwierciedlić nowy stan aplikacji.

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

Gdy gracz utworzy pokój, musi poczekać, aż inny gracz dołączy do tego pokoju. Przyjrzyjmy się logice dołączania do pokoju.

Dołączanie do kanału

Gdy gracz naciśnie przycisk "Dołącz", wywoływana jest funkcja onPressJoin(). Graczowi wyświetlany jest modal z prośbą o wprowadzenie identyfikatora pokoju w polu wprowadzania.

Enter the room id

Jeśli gracz wpisze identyfikator pokoju i naciśnie przycisk "OK", wywoływana jest metoda joinRoom(value ), gdzie value to identyfikator pokoju. Metoda ta nie jest wywoływana, jeśli pole wejściowe jest puste lub jeśli gracz naciśnie przycisk "Anuluj".

// 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);
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Pierwszą rzeczą, którą robimy w joinRoom () jest dołączenie wartości do '_tictactoelobby-',_podobnie jak to zrobiliśmy w onPressCreate().

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

Zanim gracz zasubskrybuje kanał lobby, musimy sprawdzić całkowitą zajętość kanału za pomocą funkcji hereNow(). Jeśli całkowita zajętość jest mniejsza niż 2, gracz może pomyślnie zasubskrybować kanał lobby.

// 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);
});
Enter fullscreen mode Exit fullscreen mode

Gdy gracz zasubskrybuje kanał lobby, wartość stanu elementu zostanie zmieniona na "O", a wiadomość zostanie opublikowana na tym kanale lobby. Wiadomość ta powiadamia gracza X, że do kanału dołączył inny gracz. Słuchacz wiadomości ustawiamy w funkcji componentDidUpdate(), do której przejdziemy wkrótce.

Jeśli całkowita zajętość jest większa niż 2, oznacza to, że gra jest w toku, a gracz próbujący dołączyć do kanału otrzyma odmowę dostępu. Poniższy kod znajduje się poniżej instrukcji if w funkcji 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'
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Przyjrzyjmy się teraz funkcji componentDidUpdate().

Rozpoczęcie gry

W componentDidUpdate() sprawdzamy, czy gracz jest podłączony do kanału, czyli sprawdzamy, czy this.lobbyChannel nie ma wartości null. Jeśli nie ma wartości null, konfigurujemy nasłuchiwacza, który nasłuchuje wszystkich wiadomości przychodzących na kanał.

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]
        });
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Sprawdzamy, czy otrzymana wiadomość to msg.message.notRoomCreator, która jest publikowana przez gracza dołączającego do kanału. Jeśli tak, tworzymy nowy kanał, "tictactoegame-",_z _identyfikatorem pokoju dołączonym do ciągu znaków. Kanał gry jest używany do publikowania wszystkich ruchów wykonanych przez graczy, które zaktualizują ich plansze.

Wreszcie, po zasubskrybowaniu kanału gry, wartość stanu isPlaying jest ustawiana na true. Spowoduje to zastąpienie komponentu lobby komponentem gry.

 this.setState({
   isPlaying: true
 });

 // Close the modals if they are opened
 Swal.close();
}
Enter fullscreen mode Exit fullscreen mode

Po wyświetleniu komponentu gry chcemy zamknąć wszystkie modale, jeśli zostały otwarte, z komponentu Lobby, wykonując Swal.close().

Teraz, gdy mamy dwóch graczy podłączonych do unikalnego kanału gry, mogą zacząć grać w kółko i krzyżyk! W następnej sekcji zaimplementujemy interfejs użytkownika i logikę dla komponentu gry.

Tworzenie funkcji gry

Pierwszą rzeczą, którą robimy w Game.js, jest skonfigurowanie konstruktora bazowego:

// 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;
Enter fullscreen mode Exit fullscreen mode

Dla obiektów stanu inicjalizujemy właściwość array squares, która służy do przechowywania pozycji gracza na planszy. Zostanie to wyjaśnione poniżej. Ustawiamy również wynik graczy na 0 i ustawiamy wartość whosTurn na myTurn, która jest inicjowana na true dla gracza X i false dla gracza O.

Wartości zmiennych turn i counter będą się zmieniać przez cały czas trwania gry. Na koniec gry wartość gameOver jest ustawiana na true.

Dodaj interfejs użytkownika

Następnie skonfigurujmy znaczniki dla komponentu Game wewnątrz metody renderowania.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pokazujemy wartość stanu w interfejsie użytkownika, aby poinformować graczy, czy to ich kolej na wykonanie ruchu, czy też kolej drugiego gracza. Wartość logiczna stanu whosTurn jest aktualizowana za każdym razem, gdy wykonywany jest ruch. Reszta interfejsu użytkownika składa się z komponentu planszy i wyniku gracza.

Dodaj logikę

Gdy gracz wykonuje ruch na planszy, wykonywane jest wywołanie onMakeMove(index), gdzie index jest pozycją, w której pion jest umieszczony na planszy. Plansza ma 3 rzędy i 3 kolumny, czyli łącznie 9 pól. Każde pole ma swoją unikalną wartość indeksu, zaczynając od wartości 0, a kończąc na wartości 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)
  }
}
Enter fullscreen mode Exit fullscreen mode

Po uzyskaniu stanu pól tablicy, instrukcja warunkowa jest używana do sprawdzenia, czy pole, którego dotknął gracz, jest puste i czy nadeszła jego kolej na wykonanie ruchu. Jeśli jeden lub oba warunki nie są spełnione, pion gracza nie jest umieszczany na polu. W przeciwnym razie pion gracza jest dodawany do pól tablicy w indeksie, na którym został umieszczony.

Na przykład, jeśli gracz X wykona ruch w wierszu 0, kolumnie 2, a instrukcja warunkowa jest prawdziwa, wówczas squares[2] będzie miało wartość "X".Example with the squares arrayNastępnie stan jest zmieniany, aby odzwierciedlić nowy stan gry, a tura jest aktualizowana, aby drugi gracz mógł wykonać swój ruch. Aby plansza drugiego gracza została zaktualizowana o bieżące dane, publikujemy je na kanale gry. Wszystko to dzieje się w czasie rzeczywistym, więc obaj gracze natychmiast zobaczą aktualizację swoich plansz, gdy tylko zostanie wykonany prawidłowy ruch. Ostatnią rzeczą do zrobienia w tej metodzie jest wywołanie checkForWinner(squares ), aby sprawdzić, czy jest zwycięzca.

Zanim to zrobimy, przyjrzyjmy się metodzie componentDidMount() , w której konfigurujemy odbiornik nowych wiadomości przychodzących do kanału gry.

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);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Ponieważ obaj gracze są podłączeni do tego samego kanału gry, obaj otrzymają tę wiadomość. Wywoływana jest metoda publishMove(index, piece ), gdzie index to pozycja, na której umieszczony został pion, a piece to pion gracza, który wykonał ruch. Ta metoda aktualizuje planszę o bieżący ruch i sprawdza, czy jest zwycięzca. Aby gracz, który wykonał bieżący ruch, nie musiał ponownie wykonywać tego procesu, instrukcja if sprawdza, czy pion gracza pasuje do wartości turn. Jeśli tak, jego plansza jest aktualizowana.

// 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)
}
Enter fullscreen mode Exit fullscreen mode

Logika aktualizacji planszy jest taka sama jak onMakeMove(). Przejdźmy teraz do 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Wszystkie zwycięskie kombinacje znajdują się w podwójnej tablicy possibleCombinations, gdzie każda tablica jest możliwą kombinacją do wygrania gry. Każda tablica w possibleCombinations jest porównywana z tablicą kwadratów. Jeśli istnieje zgodność, to jest zwycięzca. Prześledźmy to na przykładzie, aby uczynić to bardziej przejrzystym.

Powiedzmy, że gracz X wykonuje zwycięski ruch w wierszu 2 kolumnie 0. Indeks tej pozycji wynosi 6. Plansza wygląda teraz następująco:

Example of a winning moveZwycięska kombinacja dla gracza X to [2,4,6]. Tablica kwadratów została zaktualizowana do: ["O", "", "X", "O", "X", "", "X", "", ""].

W pętli for, gdy [a,b,c] mają wartości [2,4,6], instrukcja if w pętli for ma wartość true, ponieważ [2,4,6] mają tę samą wartość X. Wynik zwycięzcy musi zostać zaktualizowany, więc funkcja*announceWinner(* ) jest wywoływana w celu przyznania nagrody zwycięskiemu graczowi.

Jeśli gra zakończy się remisem, nie ma zwycięzcy w tej rundzie. Do sprawdzania remisów używamy licznika, który zwiększa się o jeden za każdym razem, gdy na planszy wykonywany jest ruch.

// 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);
}
Enter fullscreen mode Exit fullscreen mode

Jeśli licznik osiągnie wartość 9, gra kończy się remisem, ponieważ gracz nie wykonał zwycięskiego ruchu na ostatnim polu planszy. W takim przypadku metoda newRound( ) jest wywoływana z argumentem null, ponieważ nie ma zwycięzcy.

Zanim przejdziemy do tej metody, wróćmy do funkcji*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);
}
Enter fullscreen mode Exit fullscreen mode

Parametrem tej metody jest zwycięzca, czyli gracz, który wygrał grę. Sprawdzamy, czy zwycięzcą jest "X" lub "O" i zwiększamy wynik zwycięzcy o jeden punkt. Ponieważ gra jest zakończona, zmienna gameOver jest ustawiana na true i wywoływana jest metoda newRound ().

Rozpoczęcie nowej rundy

Gracz X ma możliwość rozegrania kolejnej rundy lub zakończenia gry i powrotu do lobby.

Endgame modal for Player X

Drugi gracz musi poczekać, aż gracz X zdecyduje, co zrobić.

Endgame modal for Player O

Gdy gracz X zdecyduje, co zrobić, wiadomość zostanie opublikowana na kanale gry, aby poinformować o tym drugiego gracza. Następnie aktualizowany jest interfejs użytkownika.

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
        });
      }
    })
  }
 }
Enter fullscreen mode Exit fullscreen mode

Jeśli wiadomość zostanie zresetowana, wszystkie wartości stanu i zmienne, z wyjątkiem wyniku dla graczy, zostaną zresetowane do wartości początkowych. Wszelkie otwarte modale są zamykane i rozpoczyna się nowa runda dla obu graczy.

W przypadku komunikatu endGame wszystkie modale są zamykane i wywoływana jest metoda endGame(). Metoda ta znajduje się w 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]
  });
}
Enter fullscreen mode Exit fullscreen mode

Wszystkie wartości stanu i zmienne są resetowane do wartości początkowych. Nazwy kanałów są resetowane do wartości null, ponieważ nowa nazwa jest generowana za każdym razem, gdy gracz tworzy pokój. Ponieważ nazwy kanałów nie będą już przydatne, gracze wypisują się zarówno z lobby, jak i kanału gry. Wartość isPlaying jest resetowana do false, więc komponent gry zostanie zastąpiony komponentem lobby.

Ostatnią metodą do włączenia w App.js jest componentWillUnmount(), która wypisuje graczy z obu kanałów.

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

To wszystko, co musimy zrobić, aby gra działała! Plik CSS dla gry można pobrać z repozytorium. Teraz uruchommy grę.

Uruchom grę

Istnieje kilka małych kroków, które musimy wykonać przed uruchomieniem gry. Po pierwsze, musimy włączyć funkcję Presence, ponieważ używamy jej do uzyskania liczby osób na kanale (użyliśmy withPresence podczas subskrybowania kanału lobby). Przejdź do panelu administratora PubNub i kliknij swoją aplikację. Kliknij na Keyset i przewiń w dół do Application add-ons. Przełącz przełącznik Presence na on. Zachowaj domyślne wartości bez zmian.

Enable presence in PubNub Admin Dashboard

Aby zainstalować trzy zależności używane w aplikacji i uruchomić aplikację, możesz uruchomić skrypt dependencies.sh, który znajduje się w katalogu głównym aplikacji.

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

npm start
Enter fullscreen mode Exit fullscreen mode

W terminalu przejdź do katalogu głównego aplikacji i wpisz następujące polecenie, aby skrypt był wykonywalny:

chmod +x dependencies.sh
Enter fullscreen mode Exit fullscreen mode

Uruchom skrypt za pomocą tego polecenia:

./dependencies.sh
Enter fullscreen mode Exit fullscreen mode

Aplikacja otworzy się w http://localhost:3000 z wyświetlonym komponentem lobby.Run the React app locallyOtwórz inną kartę, a najlepiej okno, i skopiuj i wklej http://localhost:3000. W jednym oknie utwórz kanał, klikając przycisk "Utwórz". Pojawi się okno modalne wyświetlające identyfikator pokoju. Skopiuj i wklej ten identyfikator. Przejdź do drugiego okna i kliknij przycisk "Dołącz". Gdy pojawi się okno modalne, wpisz identyfikator pokoju w polu wprowadzania i naciśnij przycisk "OK".

Create and join the channel

Gdy gracze zostaną połączeni, rozpocznie się gra. Okno użyte do utworzenia kanału wykona pierwszy ruch. Naciśnij dowolne pole na planszy i zobacz, jak pion X jest wyświetlany na planszy w czasie rzeczywistym dla obu okien. Jeśli spróbujesz nacisnąć inne pole na tej samej planszy, nic się nie stanie, ponieważ nie jest to już Twoja kolej na wykonanie ruchu. W drugim oknie naciśnij dowolne pole na planszy, a pion O zostanie umieszczony na tym polu.

Place the piece on the board

Kontynuuj grę do momentu wyłonienia zwycięzcy lub remisu. Następnie wyświetlony zostanie komunikat modalny ogłaszający zwycięzcę rundy lub informujący, że gra zakończyła się remisem. W tym samym oknie gracz X będzie musiał zdecydować, czy kontynuować grę, czy ją zakończyć. Modal dla gracza O powie mu, aby poczekał na nową rundę.

End of game modals

Wszystko, z wyjątkiem wyniku, zostanie zresetowane, jeśli gracz X będzie kontynuował grę. W przeciwnym razie obaj gracze są przenoszeni z powrotem do lobby, gdzie mogą tworzyć lub dołączać do nowych kanałów. Poniżej znajduje się demo gry:

Masz sugestie lub pytania dotyczące treści tego wpisu? Skontaktuj się z nami pod adresem [email protected].

Spis treści

Koncepcje gry wieloosobowej w czasierzeczywistymPrzegląd aplikacjiUstawlobbyUtwórz kanałDołącz dokanałuRozpocznijgręZbudujfunkcje gryDodajUIAdodajlogikęRozpocznijnową rundęUruchomgrę

Jak PubNub może ci pomóc?

Ten artykuł został pierwotnie opublikowany na PubNub.com

Nasza platforma pomaga programistom tworzyć, dostarczać i zarządzać interaktywnością w czasie rzeczywistym dla aplikacji internetowych, aplikacji mobilnych i urządzeń IoT.

Podstawą naszej platformy jest największa w branży i najbardziej skalowalna sieć komunikacyjna w czasie rzeczywistym. Dzięki ponad 15 punktom obecności na całym świecie obsługującym 800 milionów aktywnych użytkowników miesięcznie i niezawodności na poziomie 99,999%, nigdy nie będziesz musiał martwić się o przestoje, limity współbieżności lub jakiekolwiek opóźnienia spowodowane skokami ruchu.

Poznaj PubNub

Sprawdź Live Tour, aby zrozumieć podstawowe koncepcje każdej aplikacji opartej na PubNub w mniej niż 5 minut.

Rozpocznij konfigurację

Załóż konto PubNub, aby uzyskać natychmiastowy i bezpłatny dostęp do kluczy PubNub.

Rozpocznij

Dokumenty PubNub pozwolą Ci rozpocząć pracę, niezależnie od przypadku użycia lub zestawu SDK.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .