Building online multiplayers games with Meteor [codes + tutorials] - 3

This is the 3rd post of a blog series. Please refer to the introductory post for the series introduction.

Source Code

Source code at the end of the previous part:

Source code at the end of the this part:

Implementing game lobby (game matchings)

In the previous section, we have created an app where a single game instance will be spawn upon server startup. We will now extend this to support multiple game instances; We will create a simple game lobby with the following features:

  1. Creating games by users
  2. Displaying list of games
  3. Entering and playing in a particular game by users

App overview

The first thing we do is to update the <App> component to support two views. A "game list view" is displayed by default, and users can select a particular game from this list to enter the "game board view" of that particular game. So the render() function of the <App> component should look like this:

  render() {
    if (this.state.selectedGameId === null) {
      return (
        <GameList
          games={this.props.games}/>
      )
    } else {
      return (
        <GameBoard
          game={this.selectedGame()}/>
      )
    }
  }
Reactivity with component states

this.state is a special property in React components. React is smartly tracking the this.state and re-render the UI when it is updated. The above snippet is pretty straight forward - we render <GameList> component when there is no selectedGameId, or otherwise, render <GameBoard>. As you would expected, we will later add some UI controls to trigger some updates on this.state.selectedGameId.

To create this special this.state variable in React component is very easy, we simply need to override the default constructor:

  constructor(props) {
    super(props);
    this.state = {
      selectedGameId: null,
    }
  }

Obviously, we will also need two methods to update the selectedGameId states:

  handleEnterGame(gameId) {
    this.setState({selectedGameId: gameId});
  }

  handleBackToGameList() {
    this.setState({selectedGameId: null});
  }
Passing event handlers to sub-components

In this case, the UI controls that trigger handleEnterGame and handleBackToGameList existed inside the sub components <GameList> and <GameBoard>, we will have to pass along the handlers, i.e.

       <GameList
          games={this.props.games}
          enterGameHandler={this.handleEnterGame.bind(this)}/>
       <GameBoard
          game={this.selectedGame()}
          backToGameListHandler={this.handleBackToGameList.bind(this)}/>
Some other tweaks

You probably have also noticed that we need to pass in the selected game document to the <GameBoard> as well, let's also add a helper method to retrieve the game document given the selectedGameId:

  selectedGame() {
    let selectedGame = _.find(this.props.games, (game) => {
      return game._id === this.state.selectedGameId;
    });
    return selectedGame;
  }

Finally, we will also make a little tweak on createContainer method. Instead of passing in a single game document, we pass in all game documents in the collection. i.e.

export default createContainer(() => {  
  return {
    // game: Games.findOne()
    games: Games.find().fetch()
  };
}, App);

Putting everything together, the ./imports/ui/App.jsx file should look like this:

import React, { Component } from 'react';  
import { createContainer } from 'meteor/react-meteor-data';  
import Games from '../api/collections/games.js';  
import GameList from './GameList.jsx';  
import GameBoard from './GameBoard.jsx';

class App extends Component {  
  constructor(props) {
    super(props);
    this.state = {
      selectedGameId: null,
    }
  }

  handleEnterGame(gameId) {
    this.setState({selectedGameId: gameId});
  }

  handleBackToGameList() {
    this.setState({selectedGameId: null});
  }

  selectedGame() {
    let selectedGame = _.find(this.props.games, (game) => {
      return game._id === this.state.selectedGameId;
    });
    return selectedGame;
  }

  render() {
    if (this.state.selectedGameId === null) {
      return (
        <GameList
          games={this.props.games}
          enterGameHandler={this.handleEnterGame.bind(this)}/>
      )
    } else {
      return (
        <GameBoard
          game={this.selectedGame()}
          backToGameListHandler={this.handleBackToGameList.bind(this)}/>
      )
    }
  }
}

export default createContainer(() => {  
  return {
    games: Games.find().fetch()
  };
}, App);

Updating GameBoard

Now, that's update the <GameBoard> component. The only things we need to add in the file ./imports/ui/GameBoard.jsx are a handleBackToGameList() method and a button to trigger that method, i.e.

handleBackToGameList() {  
  this.props.backToGameListHandler();
}
<button onClick={this.handleBackToGameList.bind(this)}>Back</button>  

At the end, the file should look like this:

import React, { Component } from 'react';

export default class GameBoard extends Component {  
  currentPlayer() {
    // determine the current player by counting the filled cells
    // if even, then it's first player, otherwise it's second player
    let filledCount = 0;
    for (let r = 0; r < 3; r++) {
      for (let c = 0; c < 3; c++) {
        if (this.props.game.board[r][c] !== null) filledCount++;
      }
    }
    return (filledCount % 2 === 0? 0: 1);
  }

  handleCellClick(row, col) {
    let currentPlayer = this.currentPlayer();
    let game = this.props.game;
    game.board[row][col] = currentPlayer;
    Games.update(game._id, {
      $set: {board: game.board}
    });
  }

  handleBackToGameList() {
    this.props.backToGameListHandler();
  }

  renderCell(row, col) {
    let value = this.props.game.board[row][col];
    if (value === 0) return (<td>O</td>);
    if (value === 1) return (<td>X</td>);
    if (value === null) return (
      <td onClick={this.handleCellClick.bind(this, row, col)}></td>
    );
  }
  render() {
    return (
      <div>
        <button onClick={this.handleBackToGameList.bind(this)}>Back</button>
        <table className="game-board">
          <tbody>
            <tr>
              {this.renderCell(0, 0)}
              {this.renderCell(0, 1)}
              {this.renderCell(0, 2)}
            </tr>
            <tr>
              {this.renderCell(1, 0)}
              {this.renderCell(1, 1)}
              {this.renderCell(1, 2)}
            </tr>
            <tr>
              {this.renderCell(2, 0)}
              {this.renderCell(2, 1)}
              {this.renderCell(2, 2)}
            </tr>
          </tbody>
        </table>
      </div>
    )
  }
}

Creating GameList

Let's switch to the <GameList> component. The most important job of this component is to display the game list, so we will have a render() method which look likes:

render() {  
  return (
    <div>
      <div>
        <h1>List of games</h1>
        {this.props.games.map((game, index) => {
          return (
            <div key={game._id}>
              <span>Game {index+1}</span>
              <button onClick={this.handleEnterGame.bind(this, game._id)}>Enter</button>
            </div>
          )
        })}
      </div>
    </div>
  )
}

A quick tip on React: In general, everything within { and } is executed as javascript code. It's more or less the same as other template engine. An interesting construct in the above snippet is:

{this.props.games.map((game, index) => {
  return (
    XXXX
  )
}}

This is a very common pattern in React code. Consider it as a syntactic sugar to loop through the this.props.games list, each producing the content given in the return statement;

Let's digest the return statement a little bit more:

<div key={game._id}>  
  <span>Game {index+1}</span>
  <button onClick {this.handleEnterGame.bind(this, game._id)}>Enter</button>
</div>  

First of all, whenever we generate a list of children dynamically like this, you need to give a key attribute to the root DOM element. You can assign anything to this as long as they are unique across all the children. Using unique ids of the objects in the list is one way. In case such an id doesn't exist, we can also use the loop index, i.e. <div key={index}>. The reason React requires such keys is for smart re-rendering, which is not really our concern for the moment.

In each game entry, we display the game number <span>Game {index+1}</span>, and a button for "entering" that game. What the handleEnterGame handler does is to relay this to the handler we passed in previous from <App>:

handleEnterGame(gameId) {  
  this.props.enterGameHandler(gameId);
}

Another function required in the <GameList> component is the ability to create new game:

  handleNewGame() {
    let gameDoc = {
      board: [[null, null, null], [null, null, null], [null, null, null]]
    };
    Games.insert(gameDoc); // insert a new game document into the collection
  }

This is the exact piece of code copied from ./server/main.js. Likewise, we will add a button to trigger this:

<button onClick={this.handleNewGame.bind(this)}>New Game</button>  

Putting everything together, we will create a ./imports/ui/GameList.jsx file with the following content:

import React, { Component } from 'react';  
import Games from '../api/collections/games.js';

export default class GameList extends Component {  
  handleNewGame() {
    let gameDoc = {
      board: [[null, null, null], [null, null, null], [null, null, null]]
    };
    Games.insert(gameDoc); // insert a new game document into the collection
  }

handleEnterGame(gameId) {  
    this.props.enterGameHandler(gameId);
  }

render() {  
    return (
    <div>
      <div>
        <button onClick={this.handleNewGame.bind(this)}>New Game</button>
      </div>

<div>  
        <h1>List of games</h1>
        {this.props.games.map((game, index) => {
          return (
            <div key={game._id}>
              <span>Game {index+1}</span>
              <button onClick={this.handleEnterGame.bind(this, game._id)}>Enter</button>
            </div>
          )
        })}
      </div>
    </div>
    )
  }
}

Yes, we are inserting documents on client side! Even this, Meteor can still magically synchronize them between server and clients!

Cleanup

Since games are now created by users, we no longer need to do that upon server startup. We can remove that part of code, and the ./server/main.js file should look like this:

import {Meteor} from 'meteor/meteor';  
import Games from '../imports/api/collections/games.js'; // import Games collection

Meteor.startup(() => {  
});

Result

We can now check the result. Open your browser at http://localhost:3000, and the updated app should look like:

The "New Game" button allow you to generate a new game instance, which will get appened in the game list. The "Enter" button bring you into a GameBoard showing that particular game.

Don't forget to open two browser or two tabs to see the reactivity!

Quick tips on debugging database

For those who are new to Meteor or Mongo, I would like to end this post by giving some tips on debugging the database of a Meteor application. Meteor come with a very handy way to access the local mongodb through shell. If you open the terminal and execute :

$ meteor mongo

On the shell, you can check the list of collections by:

$ meteor:PRIMARY> show collections

and to see the content of a particular collection:

$ meteor:PRIMARY> db.games.find()

You can also filter the documents by putting in a selector, i.e:

$ meteor:PRIMARY> db.games.find({id: 'abcde'})

To remove all documents, you can do:

$ meteor:PRIMARY> db.games.remove({})

In this case, we pass in an empty selector, which get translated into everything. It's a good practice to explicitly pass in an empty selector to the remove call even when you want to remove all documents.

These few commands will come in very handy during development. For example, you can easily remove all the game documents and get a fresh start.

Conclusion

This concludes this section of the series. In the next section, we will add an user account system on the application, and allow a simple sign in process. Users will be required to sign in and join the game in order to make moves.

Next post:

http://blog.hiukim.com/building-online-multiplayers-games-with-meteor-codes-tutorials-4