Real-Time Gaming Blog

Multiplayer Tic-Tac-Toe Game in React Native Mobile

7 min read Michael Carroll on Dec 28, 2022
Try PubNub Today

Free up to 1MM monthly messages. No credit card required.

Subscribe to our newsletter

By submitting this form, you are agreeing to our Terms and Conditions and Privacy Policy.

Build your own online multiplayer tic-tac-toe game with React Native for iOS and Android using PubNub. Part one of this tutorial focuses on the environment setup, creating, and joining lobbies.

Note: This is part one of building an online multiplayer tic-tac-toe game in React Native using PubNub. If you have finished part one, please view part two of the tutorial where you will implement more backend functionality and play the game. You can view the completed application on GitHub.

Tic-tac-toe is a classic paper-and-pencil game that consists of rudimentary rules: two players, X and O, take turns placing their pieces in a square on a 3×3 table. A winner is declared when one of the two players places three of their pieces in a horizontal, vertical, or diagonal row.

In this tutorial series, you will develop a React tic-tac-toe game, an open-source JavaScript framework, where two players play against one another in real time. In part one, you'll implement the frontend by setting up the game environment and the game lobby where players will be able to enter their usernames, then create or join a room to play the mobile app. In Part Two, you’ll implement and test the game.

Multiplayer Gaming and PubNub

This game is meant to provide a connected shared experience for players, where they can play with their friends anywhere around the world. To do so, you’ll use PubNub to power the game’s real-time infrastructure. All you have to focus on is developing a great experience for the players.

PubNub provides a secure, scalable, and reliable infrastructure to power any application through its real-time data APIs. With multiple SDKs available for your application needs, PubNub makes it easy to send and receive messages on any device in under 100 milliseconds.

You will use the PubNub React SDK to connect two players to a game channel where they will play against each other. Each move the player makes will be published to the channel (the lobby room), as a JSON payload, so the other player’s game board updates with the current move. By updating the table in real time for each move, players will feel as if they are playing next to each other.

Game Functionality

The completed application, available on GitHub, will function and publish on both Android and iOS devices.

android ios game

You'll add a lobby where players can join or create a room. If a player creates a room, they become the room creator and wait for another player to join their room.

create room channel

If another player wants to join that same room, they enter the lobby room name (channel name) in the alert prompt and become the first player's opponent. If a player tries to join a room that already has two people, they will be unable to join.

join room channel

Once the game starts, the tic-tac-toe game board is displayed, along with the initialized score for the players.

start of game

If the game ends in a draw, then neither player gets a point. But if there is a winner, the winner’s score is updated. The room creator gets an alert asking them if they want to play another round or exit the game. If the room creator continues the game, the board will reset for the new round. If the room creator decides to exit the game, both players will return to the lobby.

exit to lobby

Before you can begin implementing the game, you'll need to set up your environment.

React Tic-Tac-Toe Environment Setup

If you don’t already have React Native set up on your machine, then be sure to follow React's getting started guide - a fantastic beginner walkthrough in getting started with React Native. Follow the rest of the instructions in the documentation to install any essential dependencies.

In your terminal, go to the directory you want to save your project in and create a new application.

react-native init ReactNativeTicTacToe

You need to install five dependencies and link them to the app you just created. To make this easy, add the following script file dependencies.sh to your app’s root directory.

# dependencies.sh
npm install --save pubnub pubnub-react@1
npm install --save react-native-prompt-android
npm install --save react-native-spinkit
npm install --save shortid
npm install --save lodash
react-native link

You'll need to make the script executable with the chmod command.

chmod +x dependencies.sh

Run the script by calling dependencies.sh from the terminal.

./dependencies.sh

Finally, now that your machine is set up, you'll need to create a free PubNub account, as you'll need your publish/subscribe keys to send information across the PubNub Network.

Initializing the Project

Create a file named index.js in your app’s root directory and add the following code.

import {AppRegistry} from 'react-native';
import App from './App.js';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

You'll need to create a new file named App.js. This is the main file for the game and it contains the components for the lobby and the table. Begin by importing the components and dependencies that will be used throughout the project.

import React, { Component } from 'react';
import PubNubReact from 'pubnub-react';
import {
  Platform,
  StyleSheet,
  View,
  Alert,
  Text,
} from 'react-native';

import Game from './src/components/Game';
import Lobby from './src/components/Lobby';
import shortid  from 'shortid';
import Spinner from 'react-native-spinkit';
import prompt from 'react-native-prompt-android';

Add the base constructor where you will insert your Pub/Sub keys to connect to PubNub, initialize the local state objects, and initialize the variables.

export default class App extends Component {
  constructor(props) {
    super(props);
    this.pubnub = new PubNubReact({
      publishKey: "ENTER_YOUR_PUBLISH_KEY_HERE",
      subscribeKey: "ENTER_YOUR_SUBSCRIBE_KEY_HERE"
    });

    this.state = {
      username: '',
      piece: '', // Creator of the room is 'X' and the opponent is 'O'
      x_username: '', // Username for the room creator
      o_username: '', // Username for the opponent
      is_playing: false, // True when the opponent joins a room channel
      is_waiting: false, // True when the room creator waits for an opponent
      is_room_creator: false, 
      isDisabled: false // True when the 'Create' button is pressed
    };

    this.channel = null;
    this.pubnub.init(this); // Initialize PubNub
  }
 }

Each state object and variable will be discussed later on. Ensure you initialize PubNub after initializing the state.

Next, subscribe to the channel gameLobby when the component mounts.

componentDidMount() {
   this.pubnub.subscribe({
     channels: ['gameLobby']
   });
 }

The channel gameLobby is the main channel that players publish (send messages to) and subscribe to (receive messages from) when they are in the lobby. More logic will be added to this functionality later on, but for now, focus on the render method.

render() {
  return (
    <View style={styles.container}>
      <View style={styles.title_container}>
        <Text style={styles.title}>RN Tic-Tac-Toe</Text>
      </View>

      <Spinner 
        style={styles.spinner} 
        isVisible={this.state.is_waiting} 
        size={75} 
        type={"Circle"} 
        color={'rgb(208,33,41)'}
      />

      {
        !this.state.is_playing &&
        <Lobby 
          username={this.state.name} 
          onChangeUsername={this.onChangeUsername}
          onPressCreateRoom={this.onPressCreateRoom} 
          onPressJoinRoom={this.onPressJoinRoom}
          isDisabled={this.state.isDisabled}
        />
      }
    
      {
          this.state.is_playing &&
          <Game 
            pubnub={this.pubnub}
            channel={this.channel} 
            username={this.state.username} 
            piece={this.state.piece}
            x_username={this.state.x_username}
            o_username={this.state.o_username}
            is_room_creator={this.state.is_room_creator}
            endGame={this.endGame}
          />
        }
    </View>
  );
}

The Lobby component is shown first because this.state.is_playing is initialized to false. Once an opponent has joined a room channel that is waiting for another player, then this.state.is_playing is set to true and the Lobby component will be replaced by the Game component. The Spinner component is displayed to the room creator as long as the room creator is waiting for another player to join the game.

spinner compnent

Add the CSS styles at the end of the file.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: 'white',
  },
  spinner: {
    flex: 1,
    alignSelf: 'center',
    marginTop: 20,
    marginBottom: 50
  },
  title_container: {
    flex: 1,
    marginTop: 18
  },
  title: {
    alignSelf: 'center',
    fontWeight: 'bold',
    fontSize: 30,
    color: 'rgb(208,33,41)'
  },
});

Before you finish the rest of App.js, take a look at the Lobby component.

Implementing the Lobby Component

In the lobby, players can enter their usernames and create or join a room. The logic that saves the username and calls the right method when a button is pressed is implemented in App.js. All the methods in the lobby component are used to style the buttons, but there are only three methods of focus in this tutorial: onChangeUsernameonPressCreateRoom(), and onPressJoinRoom, which are passed in as props from App.js.

In the app’s root directory, create a new folder named src and within that folder, create another folder named components. Inside of components, create a new file named Lobby.js. Add the following to the new file.

import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
  TextInput,
  TouchableHighlight
} from 'react-native';

export default class Lobby extends Component {
  constructor() {
    super();
    this.state = {
      pressCreateConfirm: false, // Set to true when the Create button is pressed
      pressJoinConfirm: false // Set to true when the Join button is pressed
     };
  }

  onHideUnderlayCreateButton = () => {
    this.setState({ pressCreateConfirm: false });
  }

  onShowUnderlayCreateButton = () => {
    this.setState({ pressCreateConfirm: true });
  }

  onHideUnderlayJoinButton = () => {
    this.setState({ pressJoinConfirm: false });
  }

  onShowUnderlayJoinButton = () => {
    this.setState({ pressJoinConfirm: true });
  }

  render() {
    return (        
      <View style={styles.content_container}>
        <View style={styles.input_container}>
          <TextInput
            style={styles.text_input}
            onChangeText={this.props.onChangeUsername}
            placeholder={" Enter your username"}
            maxLength={15}
            value={this.props.username}
          />
        </View>

        <View style={styles.container}>
          <View style={styles.buttonContainer}>
            <TouchableHighlight
              activeOpacity={1}
              underlayColor={'white'}
              style={
                this.state.pressCreateConfirm
                    ? styles.buttonPressed
                    : styles.buttonNotPressed
              }
                onHideUnderlay={this.onHideUnderlayCreateButton}
                onShowUnderlay={this.onShowUnderlayCreateButton}
                disabled={this.props.isDisabled}
                onPress={this.props.onPressCreateRoom}
              >
                <Text
                  style={
                  this.state.pressCreateConfirm
                      ? styles.cancelPressed
                      : styles.cancelNotPressed
                      }
                  >
                  Create
                </Text>
            </TouchableHighlight>
          </View>

          <View style={styles.buttonBorder}/>
            <View style={styles.buttonContainer}>
                <TouchableHighlight
                activeOpacity={1}
                underlayColor={'white'}
                style={
                  this.state.pressJoinConfirm
                      ? styles.buttonPressed
                      : styles.buttonNotPressed
                }
                  onHideUnderlay={this.onHideUnderlayJoinButton}
                  onShowUnderlay={this.onShowUnderlayJoinButton}
                  onPress={this.props.onPressJoinRoom}
                >
                  <Text
                    style={
                    this.state.pressJoinConfirm
                        ? styles.cancelPressed
                        : styles.cancelNotPressed
                        }
                    >
                    Join
                  </Text>
            </TouchableHighlight>
          </View>
        </View>
      </View>
    );
  }
}

In summary, the logic in Lobby.js is setting up the username field and the two buttons. As previously mentioned, the only logic you do is to style the buttons. In this case, the buttons' background color, border color, and text color change when the button is pressed.

button styling

Make sure to add the CSS styles to the end of the file:

const styles = StyleSheet.create({
  content_container: {
    flex: 1,
  },
  input_container: {
    marginBottom: 20,
  },
  container: {
    flexDirection: 'row',
    paddingLeft: 11,
    paddingRight: 11
  },
  buttonContainer: {
    flex: 1,
    textAlign: 'center',
  },
  buttonBorder: {
    borderLeftWidth: 4,
    borderLeftColor: 'white'
  },
  text_input: {
    backgroundColor: '#FFF',
    height: 40,
    borderColor: '#CCC', 
    borderWidth: 1
  },
  buttonPressed:{
    borderColor: 'rgb(208,33,41)',
    borderWidth: 1,
    padding: 10,
    borderRadius: 5
  },
  buttonNotPressed: {
    backgroundColor: 'rgb(208,33,41)',
    borderColor: 'rgb(208,33,41)',
    borderWidth: 1,
    padding: 10,
    borderRadius: 5
  },
  cancelPressed:{
    color: 'rgb(208,33,41)',
    fontSize: 16,
    textAlign: 'center',
    alignItems: 'center',
  },
  cancelNotPressed: {
    color: 'white',
    fontSize: 16,
    textAlign: 'center',
    alignItems: 'center',
  },
});

Saving the Username

Whenever the player types in the username field, onChangeUsername is called. This method, along with the rest of the methods in this post, is found in App.js.

onChangeUsername = (username) => {
    this.setState({username});
}

You need to save the username in the username state and limit the number of characters to 15 characters so the username won’t be too long. You can increase or decrease this number if you wish.

Creating the Room Channel

Next, implement the method for onPressCreateRoom, which is called when the user presses the Create button.

onPressCreateRoom = () => {
  if(this.state.username === ''){
    Alert.alert('Error','Please enter a username');
  }
}

The first check is to see if the username field is not empty. If the field is empty, the player is alerted to enter a username. A random room ID is generated and truncated to five characters. The ID is then appended to tictactoe–, which will be used as the game channel that players will subscribe and publish to in the PubNub Network. Below the if statement, add the following code. In order to obtain the number of people in the channel, PubNub's Presence function is used.

else{
      let roomId = shortid.generate(); // Random channel name generated
      let shorterRoomId = roomId.substring(0,5); // Truncate to a shorter string value
      roomId = shorterRoomId;
      this.channel = 'tictactoe--' + roomId;
      this.pubnub.subscribe({
        channels: [this.channel],
        withPresence: true
      });
   ...

Once the room creator subscribes to the new channel, you alert the room creator to share the room ID with other potential players.

share roomid

Add the following code to the else statement used above to alert the room creator to share.

// alert the room creator to share the room ID with their friend
Alert.alert(
  'Share this room ID with your friend',
  roomId,
  [
    {text: 'Done'},
  ],
  { cancelable: false }
);

Since you want to change the state for certain objects, setState is used to perform this change. Below the alert, in the else statement, add the following to set the state and close the else statement.

    this.setState({
      piece: 'X',
      is_room_creator: true,
      is_waiting: true,
      isDisabled: true
    });

    this.pubnub.publish({
      message: {
        is_room_creator: true,
        username: this.state.username
      },
      channel: 'gameLobby'
    });  
  } // Close the else statement

After changing the state of four objects, the boolean is_room_creator and the room creator’s username will be published to gameLobby. The Spinner component will be displayed to the room creator while they wait for someone to join the game.

Back in the componentDidMount function, you need to set up a listener to listen for certain messages that arrive in gameLobby .

componentDidMount(){
  ...
  this.pubnub.getMessage('gameLobby', (msg) => {
    // Set username for Player X
    if(msg.message.is_room_creator){
      this.setState({
        x_username: msg.message.username
      })
    }
   ...
  });
}

You need to get the room creator’s username, so an if statement is used to check if the message arrived is msg.message.is_room_creator. If true, change the state x_username to the room creator's username.

Joining the Room Channel

The last method onPressJoinRoom is needed to finish setting up the lobby.

onPressJoinRoom = () => {
  if(this.state.username === ''){
    Alert.alert('Error','Please enter a username');
  }
  else{
    // Check for platform
    if (Platform.OS === "android") {
      prompt(
        'Enter the room name',
        '',
        [
         {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
         {text: 'OK', onPress: (value) =>  
         (value === '') ? '' : this.joinRoom(value)},
        ],
        {
            type: 'default',
            cancelable: false,
            defaultValue: '',
            placeholder: ''
          }
      );      
    }
    else{
      Alert.prompt(
        'Enter the room name',
        '',
        [
         {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
         {text: 'OK', onPress: (value) =>  
         (value === '') ? '' : this.joinRoom(value)},
        ],
        'plain-text',
      );
    }  
  }
}

Ensure that the username field is not empty. If it’s not empty, then a prompt is shown to the opponent to enter the room name.

join room alert

Since you need to consider both platforms (iOS and Android), check which platform the app is running on and use the appropriate prompt. For Android, use the prompt dependency react-native-prompt-android, since Alert.prompt is only supported for iOS devices.

Essentially, both prompts accomplish the same goal: call joinRoom(value), where value is the room name and cannot be an empty value when OK is pressed.

joinRoom = (room_id) => {
  this.channel = 'tictactoe--' + room_id;

  // Check that the lobby is not full
  this.pubnub.hereNow({
    channels: [this.channel], 
  }).then((response) => { 
    // If totalOccupancy is less than or equal to 1, then the player can't join a room since it has not been created
    if(response.totalOccupancy <= 1){
      Alert.alert('Lobby is empty','Please create a room or wait for someone to create a room to join.');
    }
    // Room is available to join
    else if(response.totalOccupancy === 2){
      this.pubnub.subscribe({
        channels: [this.channel],
        withPresence: true
      });
      
      this.setState({
        piece: 'O',
      });  
      
      this.pubnub.publish({
        message: {
          readyToPlay: true, // Game can now start
          not_room_creator: true,
          username: this.state.username
        },
        channel: 'gameLobby'
      });
    } 
    // Room already has two players
    else{
      Alert.alert('Room full','Please enter another room name');
    }
  }).catch((error) => { 
      console.log(error)
  });
}

Since there can't be more than two people in the same lobby, hereNow is used to check the total occupancy for the channel. If the total occupancy is less than one, the player is trying to join a room that has not been created or there is a typo in the room name.

If the total occupancy is two, then there is a player in the channel, the room creator, who is waiting for another player to start the game.

If the total occupancy is greater than two, then the player is trying to join a room with a game in progress, so an alert tells the player that the room is full and to join another room.

Once the opponent successfully subscribes to the game channel, a message is published with the opponent’s username and readyToPlay set to true. Since the player is not the room creator, not_room_creator is set to true.

Finishing the Lobby Component

To finish the Lobby component, add the last logic for the listener in componentDidMount.

componentDidMount() {
  ...
  this.pubnub.getMessage('gameLobby', (msg) => {
    ...

    else if(msg.message.not_room_creator){
      this.pubnub.unsubscribe({
        channels : ['gameLobby']
      }); 
      // Start the game
      this.setState({
        o_username: msg.message.username,
        is_waiting: false,
        is_playing: true,
      });  
    }
  });
}

Both players will unsubscribe from gameLobby since they are subscribed to the game room channel. In setState, three actions are performed:

  1. Set the opponent's username to o_username

  2. Set is_waiting to false so the Spinner component will disappear from the room creator’s view

  3. Set is_playing to true so the game between the two players can start.

The last method to implement before finishing the lobby is componentWillUnmount.

componentWillUnmount() {
  this.pubnub.unsubscribe({
    channels : ['gameLobby', this.channel]
  });
}

This method is called when the component is unmounted and destroyed. The player is unsubscribed from gameLobby and this.channel, the channel the player is subscribed to for the game.

You've successfully set up your React Tic-Tac-Toe game and initialized the lobby room. Now that you have finished the logic for the lobby, you'll continue in Part Two of this tutorial by setting up the Tic-Tac-Toe Table and adding the game logic, and implementing real-time interactivity between the players.

Be sure to continue in Part Two to complete building your own Tic-Tac-Toe real-time multiplayer game in React Native.

If you have any other questions or concerns, please feel free to reach out to devrel@pubnub.com.

More from PubNub

NPP and HIPAA: Notice of Privacy Practices Definition
Healthcare CategoryJan 6, 20235 min read

NPP and HIPAA: Notice of Privacy Practices Definition

A Notice of Privacy Practices (NPP) is one of the requirements of HIPAA and helps patients understand their personal data rights.

Michael Carroll

Michael Carroll

HIPAA Violation Examples
Healthcare CategoryJan 5, 20236 min read

HIPAA Violation Examples

HIPAA violations can be financially expensive and devastating to a brand. Examine some examples of HIPAA violations, and learn...

Michael Carroll

Michael Carroll

HIPAA Technical Safeguards: How To Protect Sensitive Data
Healthcare CategoryJan 5, 20236 min read

HIPAA Technical Safeguards: How To Protect Sensitive Data

HIPAA covered entities must follow the five technical safeguards to achieve HIPAA compliance and prevent data corruption.

Michael Carroll

Michael Carroll