React의 브라우저 기반 멀티플레이어 틱택토 게임

PubNub Developer Relations - Mar 10 - - Dev Community

아카이브에 있는 이 게시물은 React에서 틱택토 게임을 만드는 단계를 안내하지만, 사용된 라이브러리 버전이 더 이상 최신 버전이 아니라는 점에 유의하세요. 특히 이 글에서는 React SDK 버전 1을 사용하지만 아래에 나열된 모든 단계와 원칙은 오늘날에도 여전히 유효합니다.

틱택토는 전형적인 어린 시절 게임입니다. 이 게임에는 쓸 수 있는 무언가와 함께 쓸 수 있는 무언가만 있으면 됩니다. 하지만 다른 장소에 있는 사람과 함께 놀고 싶다면 어떻게 해야 할까요? 이 경우에는 사용자와 다른 플레이어를 게임에 연결해주는 애플리케이션을 사용해야 합니다.

이 애플리케이션은 실시간 경험을 제공해야 하므로 내 모든 움직임이 다른 플레이어에게 즉시 표시되고 그 반대의 경우도 마찬가지입니다. 애플리케이션이 이러한 경험을 제공하지 않는다면 여러분을 비롯한 많은 사람들이 더 이상 사용하지 않을 것입니다.

그렇다면 개발자는 어떻게 플레이어가 전 세계 어디에 있든 틱택토를 비롯한 모든 게임을 플레이할 수 있는 연결된 경험을 제공할 수 있을까요?

실시간 멀티플레이어 게임 콘셉트

멀티플레이어 게임에 실시간 인프라를 제공하는 방법에는 여러 가지가 있습니다. Socket.IO, SignalR, WebSockets 등의 기술 및 오픈 소스 프로토콜을 사용하여 처음부터 자체 인프라를 구축하는 방법을 선택할 수 있습니다.

이 방법은 매력적인 방법처럼 보일 수 있지만 몇 가지 문제에 직면하게 되는데, 그중 하나가 확장성 문제입니다. 100명의 사용자를 처리하는 것은 어렵지 않지만 100,000명 이상의 사용자를 어떻게 처리할 수 있을까요? 인프라 문제 외에도 게임 유지 관리에 대해서도 걱정해야 합니다.

결국 중요한 것은 게임 플레이어에게 훌륭한 경험을 제공하는 것입니다. 그렇다면 인프라 문제를 어떻게 해결할 수 있을까요? 바로 이때 PubNub이 등장합니다.

PubNub은 글로벌 데이터 스트림 네트워크를 통해 모든 애플리케이션을 구동할 수 있는 실시간 인프라를 제공합니다. 가장 인기 있는 프로그래밍 언어를 포함한 70개 이상의 SDK를 통해 PubNub은 모든 디바이스로 메시지를 100ms 이내에 간편하게 송수신할 수 있습니다. 안전하고 확장 가능하며 안정적이므로 자체 인프라를 만들고 유지 관리하는 것에 대해 걱정할 필요가 없습니다.

PubNub을 사용하여 멀티플레이어 게임을 개발하는 것이 얼마나 쉬운지 보여드리기 위해 PubNub React SDK를 사용하여 간단한 React 틱택토 게임을 만들어 보겠습니다. 이 게임에서는 두 명의 플레이어가 고유한 게임 채널에 연결하여 서로 대결을 펼칩니다. 플레이어의 모든 움직임은 채널에 게시되어 실시간으로 다른 플레이어의 보드가 업데이트됩니다.

앱 개요

개발이 완료되면 앱의 모습은 다음과 같습니다.

Screen shot of the React Tic Tac Toe Game플레이어는 먼저 로비에서 채널을 만들거나 채널에 참여할 수 있습니다. 플레이어가 채널을 생성하면 다른 플레이어와 공유할 수 있는 방 ID를 받게 됩니다. 채널을 생성한 플레이어는 플레이어 X가 되어 게임이 시작되면 첫 번째로 움직입니다.

Create a room channel

받은 방 ID로 채널에 참여한 플레이어는 플레이어 O가 됩니다. 플레이어는 채널에 다른 사람이 한 명 있을 때만 채널에 참여할 수 있습니다. 한 명 이상이면 해당 채널에서 게임이 진행 중이며 플레이어는 참여할 수 없습니다. 채널에 두 명의 플레이어가 있으면 게임이 시작됩니다.

Join the room channel

게임이 끝나면 승자의 점수가 1점씩 올라갑니다. 게임이 동점으로 끝나면 두 플레이어 모두 점수를 얻지 못합니다. 새 라운드를 시작하거나 게임을 종료할지 묻는 모달이 플레이어 X에게 표시됩니다. 플레이어 X가 게임을 계속하면 새 라운드를 위해 보드가 초기화됩니다. 그렇지 않으면 게임이 종료되고 두 플레이어는 로비로 돌아갑니다.

Exit to lobby

로비 설정

로비를 설정하기 전에 무료 PubNub 계정에 가입하여 PubNub 관리자 대시보드에서 무료 Pub/Sub API 키를 받습니다.

키를 받으면 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

또한 생성자에서 상태 객체와 변수가 초기화됩니다. 파일 전체에 걸쳐 객체와 변수가 등장할 때 살펴볼 것입니다. 마지막으로 생성자 마지막에 PubNub를 초기화했습니다.

렌더 메소드 내부와 반환 문 안에 로비 컴포넌트에 대한 마크업을 추가합니다.

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

로비 컴포넌트는 제목, 빈 틱택 토 보드(플레이어가 사각형을 누르면 아무 일도 일어나지 않음), '만들기' 및 '참여' 버튼으로 구성됩니다. 이 구성 요소는 상태 값이 Playing인 경우에만 표시됩니다. 참으로 설정되어 있으면 게임이 시작된 것이고 구성 요소는 튜토리얼의 두 번째 부분에서 살펴볼 게임 구성 요소로 변경됩니다.

보드 컴포넌트는 로비 컴포넌트의 일부이기도 합니다. 보드 컴포넌트 안에는 스퀘어 컴포넌트가 있습니다. 로비 및 게임 구성 요소에 집중하기 위해 이 두 구성 요소에 대해서는 자세히 설명하지 않겠습니다.

플레이어가 '만들기' 버튼을 누르면 버튼이 비활성화되어 플레이어가 여러 채널을 만들 수 없습니다. '가입' 버튼은 플레이어가 채널에 가입하기로 결정할 경우를 대비하여 비활성화되지 않습니다. '만들기' 버튼을 누르면 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
  });
}
Enter fullscreen mode Exit fullscreen mode

특정 채널에 두 명 이상의 플레이어가 참여하는 것을 방지하기 위해 프레즌스를 사용합니다. 나중에 채널의 점유를 확인하는 로직을 살펴보겠습니다.

플레이어가 로비 채널에 가입하면 다른 플레이어가 해당 채널에 참여할 수 있도록 방 ID가 포함된 모달이 표시됩니다.

Share the room id

이 모달과 이 앱에서 사용되는 모든 모달은 JavaScript의 기본 alert() 팝업 상자를 대체하기 위해 SweetAlert2에 의해 생성됩니다.

// 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

onPressCreate()가 끝나면 앱의 새 상태를 반영하도록 상태 값을 변경합니다.

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

플레이어가 방을 생성하면 다른 플레이어가 해당 방에 참여할 때까지 기다려야 합니다. 방에 참가하는 로직을 살펴봅시다.

채널에 참여하기

플레이어가 '참가' 버튼을 누르면 onPressJoin( ) 호출이 호출됩니다. 플레이어에게 입력 필드에 방 ID를 입력하라는 모달이 표시됩니다.

Enter the room id

플레이어가 ID를 입력하고 '확인' 버튼을 누르면 joinRoom(value )가 호출되며, 여기서 값은 방 ID입니다. 입력 필드가 비어 있거나 플레이어가 '취소' 버튼을 누르면 이 메서드는 호출되지 않습니다.

// 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

joinRoom( )에서 가장 먼저 하는 작업은 onPressCreate()에서 한 것과 유사하게 'tictactoelobby-'에 값을 추가하는 것입니다.

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

플레이어가 로비 채널을 구독하기 전에 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);
});
Enter fullscreen mode Exit fullscreen mode

플레이어가 로비 채널을 구독하면 조각의 상태 값이 'O'로 변경되고 해당 로비 채널에 메시지가 게시됩니다. 이 메시지는 다른 플레이어가 채널에 가입했음을 플레이어 X에게 알립니다. 곧 설명할 componentDidUpdate()에서 메시지 리스너를 설정합니다.

총 점유 인원이 2보다 크면 게임이 진행 중이며 채널에 참여하려는 플레이어의 액세스가 거부됩니다. 다음 코드는 hereNow()의 if 문 아래에 있습니다.

// 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

이제 componentDidUpdate()를 살펴봅시다.

게임 시작

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

도착한 메시지가 채널에 참여한 플레이어가 게시한 msg.message.notRoomCreator인지 확인합니다. 그렇다면 문자열에 방 아이디를 추가하여 'tictactoegame-'이라는 새 채널을 만듭니다. 게임 채널은 보드를 업데이트할 플레이어의 모든 움직임을 게시하는 데 사용됩니다.

마지막으로 게임 채널에 가입하면 isPlaying의 상태 값이 true로 설정됩니다. 이렇게 하면 로비 컴포넌트가 게임 컴포넌트로 대체됩니다.

 this.setState({
   isPlaying: true
 });

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

게임 컴포넌트가 표시되면 Swal.close()를 수행하여 로비 컴포넌트에서 열려있는 모든 모달을 닫으려고 합니다.

이제 두 명의 플레이어가 고유한 게임 채널에 연결되었으므로 틱택토 게임을 시작할 수 있습니다! 다음 섹션에서는 게임 컴포넌트의 UI와 로직을 구현해 보겠습니다.

게임 기능 빌드하기

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

상태 객체의 경우, 보드에서 플레이어의 위치를 저장하는 데 사용되는 배열 사각형 프로퍼티를 초기화합니다. 이에 대해서는 아래에서 더 자세히 설명하겠습니다. 또한 플레이어 점수를 0으로 설정하고 누구의 턴 값을 myTurn으로 설정하여 플레이어 X의 경우 참으로, 플레이어 O의 경우 거짓으로 초기화합니다.

턴과 카운터 변수의 값은 게임이 진행되는 동안 변경됩니다. 게임이 끝나면 게임오버는 참으로 설정됩니다.

UI 추가

다음으로 렌더 메서드 내부의 게임 컴포넌트에 대한 마크업을 설정해 보겠습니다.

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

플레이어가 움직일 차례인지 아니면 다른 플레이어의 차례인지 알 수 있도록 UI에 상태 값을 표시합니다. state의 부울 값은 이동이 이루어질 때마다 업데이트됩니다. 나머지 UI는 보드 구성 요소와 플레이어의 점수로 구성됩니다.

로직 추가하기

플레이어가 보드에서 이동을 하면 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)
  }
}
Enter fullscreen mode Exit fullscreen mode

배열 사각형의 상태를 얻은 후 조건문을 사용하여 플레이어가 터치한 사각형이 비어 있는지, 그리고 플레이어가 움직일 차례인지 확인합니다. 조건 중 하나 또는 두 가지 조건이 모두 충족되지 않으면 플레이어의 말이 사각형에 놓이지 않습니다. 그렇지 않으면 플레이어의 말이 말이 놓인 인덱스의 배열 사각형에 추가됩니다.

예를 들어, 플레이어 X가 행 0, 열 2에서 이동을 하고 조건문이 참이면 squares[2] 의 값은 "X"가 됩니다.Example with the squares array다음으로, 게임의 새로운 상태를 반영하도록 상태가 변경되고 상대 플레이어가 움직일 수 있도록 턴이 업데이트됩니다. 다른 플레이어의 보드가 현재 데이터로 업데이트될 수 있도록 데이터를 게임 채널에 게시합니다. 이 모든 과정은 실시간으로 이루어지므로 두 플레이어는 유효한 이동이 이루어지는 즉시 자신의 보드가 업데이트되는 것을 볼 수 있습니다. 이 메서드에서 마지막으로 해야 할 일은 체크포위너(squares) 를 호출하여 승자가 있는지 확인하는 것입니다.

그 전에 게임 채널에 도착하는 새 메시지에 대한 리스너를 설정하는 componentDidMount( )를 살펴봅시다.

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

두 플레이어가 동일한 게임 채널에 연결되어 있으므로 두 플레이어 모두 이 메시지를 수신하게 됩니다. 여기서 인덱스는 말이 놓인 위치, 피스는 이동을 한 플레이어의 말입니다. 이 메서드는 현재 이동으로 보드를 업데이트하고 승자가 있는지 확인합니다. 현재 이동을 한 플레이어가 이 과정을 다시 반복하지 않도록 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)
}
Enter fullscreen mode Exit fullscreen mode

보드를 업데이트하는 로직은 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

모든 승리 조합은 가능한 모든 배열이 게임에서 승리할 수 있는 조합인 이중 배열 possibleCombinations에 있습니다. possibleCombinations의 모든 배열은 배열 사각형과 비교하여 확인됩니다. 일치하는 조합이 있으면 승자가 있는 것입니다. 이를 더 명확하게 이해하기 위해 예제를 살펴보겠습니다.

플레이어 X가 행 2 열 0에서 승리한 수를 두었다고 가정해 봅시다. 해당 위치의 인덱스는 6입니다. 이제 바둑판은 다음과 같이 보입니다:

Example of a winning move플레이어 X의 승리 조합은 [2,4,6]입니다. 배열 사각형은 다음과 같이 업데이트됩니다: ["O", "", "X", "O", "X", "", "X", "", ""].

for 루프에서 [a,b,c] 의 값이 [2,4,6]인 경우, [2,4,6]이 모두 같은 값인 X를 가지므로 for 루프의 if 문은 true가 됩니다. 우승자의 점수를 업데이트해야 하므로 우승자를 시상하기 위해announceWinner( )가 호출됩니다.

게임이 동점으로 끝나면 해당 라운드의 승자는 없습니다. 동점 게임을 확인하기 위해 보드에서 이동이 이루어질 때마다 1씩 증가하는 카운터를 사용합니다.

// 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

카운터가 9에 도달하면 플레이어가 보드의 마지막 칸에서 승리한 수를 두지 않았기 때문에 게임이 무승부로 끝납니다. 이 경우 승자가 없으므로 null 인수를 사용하여 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);
}
Enter fullscreen mode Exit fullscreen mode

이 메서드의 매개변수는 게임에서 승리한 플레이어인 winner입니다. 승자가 'X' 또는 'O'인지 확인하고 승자의 점수를 1점씩 증가시킵니다. 게임이 종료되었으므로 게임 오버 변수가 true로 설정되고 newRound() 메서드가 호출됩니다.

새 라운드 시작

플레이어 X는 다른 라운드를 플레이하거나 게임을 종료하고 로비로 돌아갈 수 있습니다.

Endgame modal for Player X

다른 플레이어는 플레이어 X가 어떻게 할지 결정할 때까지 기다리라고 말합니다.

Endgame modal for Player O

플레이어 X가 무엇을 해야 할지 결정하면 게임 채널에 메시지가 게시되어 다른 플레이어에게 알립니다. 그러면 UI가 업데이트됩니다.

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

메시지가 리셋되면 플레이어의 점수를 제외한 모든 상태 값과 변수가 초기값으로 재설정됩니다. 아직 열려 있는 모달은 모두 닫히고 두 플레이어의 새 라운드가 시작됩니다.

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

모든 상태 값과 변수가 초기값으로 재설정됩니다. 채널 이름은 플레이어가 방을 생성할 때마다 새 이름이 생성되므로 null로 재설정됩니다. 채널 이름이 더 이상 유용하지 않으므로 플레이어는 로비와 게임 채널 모두에서 구독을 취소합니다. isPlaying의 값은 거짓으로 재설정되므로 게임 컴포넌트가 로비 컴포넌트로 대체됩니다.

App.js에 포함할 마지막 메서드는 두 채널에서 플레이어의 구독을 취소하는 componentWillUnmount()입니다.

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

이제 게임이 작동하기 위해 필요한 모든 작업이 완료되었습니다! 게임의 CSS 파일은 리포지토리에서 얻을 수 있습니다. 이제 게임을 실행해 보겠습니다.

게임 실행

게임을 실행하기 전에 몇 가지 작은 단계를 수행해야 합니다. 먼저, 채널에 있는 사람의 수를 얻기 위해 프레즌스 기능을 활성화해야 합니다(로비 채널을 구독할 때 위드프레즌스를 사용했습니다). PubNub 관리자 대시보드로 이동하여 애플리케이션을 클릭합니다. 키셋을 클릭하고 애플리케이션 추가 기능까지 아래로 스크롤합니다. 프레즌스 스위치를 켜기로 전환합니다. 기본값은 그대로 유지합니다.

Enable presence in PubNub Admin Dashboard

앱에 사용되는 세 가지 종속성을 설치하고 앱을 실행하려면 앱의 루트 디렉터리에 있는 의존성.sh 스크립트를 실행하면 됩니다.

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

npm start
Enter fullscreen mode Exit fullscreen mode

터미널에서 앱의 루트 디렉토리로 이동하여 다음 명령을 입력하여 스크립트를 실행할 수 있도록 합니다:

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

이 명령으로 스크립트를 실행합니다:

./dependencies.sh
Enter fullscreen mode Exit fullscreen mode

앱이 http://localhost:3000 에서 열리고 로비 구성 요소가 표시됩니다.Run the React app locally다른 탭 또는 가급적 창을 열고 http://localhost:3000 을 복사하여 붙여넣습니다. 한 창에서 '만들기' 버튼을 클릭하여 채널을 만듭니다. 방 아이디를 표시하는 모달이 나타납니다. 해당 아이디를 복사하여 붙여넣습니다. 다른 창으로 이동하여 '가입' 버튼을 클릭합니다. 모달이 나타나면 입력 필드에 방 아이디를 입력하고 '확인' 버튼을 누릅니다.

Create and join the channel

플레이어가 연결되면 게임이 시작됩니다. 채널을 만들 때 사용한 창에서 첫 번째 이동이 이루어집니다. 보드의 아무 칸이나 누르면 양쪽 창에 실시간으로 X 조각이 보드에 표시되는 것을 확인할 수 있습니다. 같은 보드의 다른 사각형을 누르려고 하면 더 이상 움직일 차례가 아니므로 아무 일도 일어나지 않습니다. 다른 창에서 보드의 아무 칸이나 누르면 조각 O가 그 칸에 놓입니다.

Place the piece on the board

승자가 나오거나 동점이 될 때까지 게임을 계속합니다. 그러면 라운드의 승자를 알리는 모달이 표시되거나 게임이 동점으로 끝났음을 알리는 모달이 표시됩니다. 같은 모달에서 플레이어 X는 게임을 계속할지 아니면 게임을 종료할지 결정해야 합니다. 플레이어 O를 위한 모달은 새 라운드를 기다리라고 알려줍니다.

End of game modals

플레이어 X가 게임을 계속하면 점수를 제외한 모든 것이 초기화됩니다. 그렇지 않으면 두 플레이어는 로비로 돌아가 새로운 채널을 만들거나 참여할 수 있습니다. 아래에서 게임 데모를 확인하세요:

이 게시물 내용에 대한 제안이나 질문이 있으신가요? devrel@pubnub.com 으로 문의해 주세요.

콘텐츠

실시간 멀티플레이어 게임 개념앱개요로비설정하기채널만들기채널에가입하기게임시작하기게임 기능구축하기UI추가하기로직추가하기새 라운드시작하기게임실행하기

펍넙이 어떤 도움을 줄 수 있나요?

이 문서는 원래 PubNub.com에 게시되었습니다.

유니티 플랫폼은 개발자가 웹 앱, 모바일 앱, IoT 디바이스를 위한 실시간 인터랙티브를 제작, 제공, 관리할 수 있도록 지원합니다.

저희 플랫폼의 기반은 업계에서 가장 크고 확장성이 뛰어난 실시간 에지 메시징 네트워크입니다. 전 세계 15개 이상의 PoP가 월간 8억 명의 활성 사용자를 지원하고 99.999%의 안정성을 제공하므로 중단, 동시 접속자 수 제한 또는 트래픽 폭증으로 인한 지연 문제를 걱정할 필요가 없습니다.

PubNub 체험하기

라이브 투어를 통해 5분 이내에 모든 PubNub 기반 앱의 필수 개념을 이해하세요.

설정하기

PubNub 계정에 가입하여 PubNub 키에 무료로 즉시 액세스하세요.

시작하기

사용 사례나 SDK에 관계없이 PubNub 문서를 통해 바로 시작하고 실행할 수 있습니다.

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