How to Build a Multiplayer Tabletop Game Simulator with Vue, Phaser, Node, Express, and Socket.IO

freeCodeCampfreeCodeCamp
13 min read

By M. S. Farzan

Putting together all of the pieces of a full stack JavaScript application can be a complex endeavor.

In this tutorial, we're going to build a multiplayer tabletop game simulator using Vue, Phaser, Node/Express, and Socket.IO to learn several concepts that will be useful in any full stack app.

You can follow along with this video tutorial as well (1 hour 16 minute watch):

All of the project files for this tutorial are available on GitHub.

Project Overview

Our project will feature a Phaser game instance that will allow us to create tokens and cards on screen, and move them around on a digital game board.

The Phaser instance will be wrapped in a Vue component that will handle things like multiplayer chat and commands. Together, Phaser and Vue will comprise our front end (referred to from here on as the "client"), and we'll use Socket.IO to communicate with other players and tie together the front and back ends of our app.

The back end (referred to from here on as the "server") will be a simple Express server that receives Socket.IO events from the client and acts accordingly. The whole application will run on Node as its runtime.

You don't need to be an expert in any of the above frameworks to complete this project, but it would be a good idea to have a solid foundation in basic JavaScript and HTML/CSS before trying to tackle the specifics. You can also follow along with my series on Learning JavaScript by Making Digital Tabletop Games and Web Apps.

You'll also want to make sure that you have Node and Git installed, along with your favorite code editor and a command line interface (you can follow my tutorial on setting up an IDE here if you need help).

Let's get started!

Part 1: Client Basics

We'll begin building our client by installing the Vue CLI, which will help us with some tooling and allow us to make changes to our files without having to reload our web browser.

In a command line, type in the following to install the Vue CLI globally:

npm install -g @vue/cli

Navigate to a desired directory and create a new folder for our project:

mkdir tabletop-project
cd tabletop-project

Now we can use the Vue CLI to template a front end project for us:

vue create client

You can just hit "enter" at the ensuing prompts unless you have specific preferences.

The Vue CLI has helpfully templated a front end project for us, which we can view in our code editor:

Image

Let's navigate to our new client folder in our CLI and run the template app:

cd client
npm run serve

After a little work, the Vue CLI should begin displaying our app in a web browser at the default http://localhost:8080:

Image

Cool! We have the basic structure of our client. Let's break it by creating two new components in the /components folder, called Game.vue and Chat.vue (you can go ahead and delete HelloWorld.vue and anything in the assets folder if you're obsessed with tidiness like I am).

Replace the code in App.vue with the following:

<template>
    <div id="app">
        <div id="game">
            <Game />
        </div>
        <div id="border" />
        <div id="input">
            <Chat />
        </div>
    </div>
</template>

<script>
    import Chat from './components/Chat.vue';
    import Game from './components/Game.vue';

    export default {
        name: 'App',
        components: {
            Chat,
            Game
        }
    }
</script>

<style>
    #app {
        font-family: 'Trebuchet MS';
        text-align: left;
        background-color: black;
        color: cyan;
        display: flex;
    }
    #game {
        width: 50vw;
        height: 100vh;
    }
    #input {
        width: 50vw;
        height: 100vh;
    }
    #border {
        border-right: 2px solid cyan;
    }
    @media (max-width: 1000px) {
        #app {
            flex-direction: column;
        }
        #game {
            width: 100vw;
            height: 50vh;
        }
        #input {
            width: 100vw;
            height: 50vh;
        }
    }
</style>

As you can see, a Vue component ordinarily has three sections: Template, Script, and Style, which contain any HTML, JavaScript, and CSS for that component, respectively. We've just imported our Game and Chat components here and added a little styling to give it a cyberpunk feel when it's all up and running.

That's actually all that we need to do to set up our App.vue component, which will house everything else in our client. Before we can actually do anything with it, we'll need to get our server working!

Part 2: Server Basics

At our root directory (tabletop-project, above /client), initialize a new project in a new command line interface by typing:

npm init

Like with our client, you can go ahead and press "enter" at the prompts unless there are specifics that you'd like to designate at this time.

We'll need to install Express and Socket.IO, along with Nodemon to watch our server files for us and reboot as necessary:

npm install --save express socket.io nodemon

Let's open up the new package.json file in that root directory and add a "start" command in the "scripts" section:

  "scripts": {
    "start": "nodemon server.js"
  },

Create a new file called server.js in this directory, and enter the following code:

const server = require('express')();
const http = require('http').createServer(server);
const io = require('socket.io')(http);

io.on('connection', function (socket) {
    console.log('A user connected: ' + socket.id);

    socket.on('send', function (text) {
        let newText = "<" + socket.id + "> " + text;
        io.emit('receive', newText);
    });

    socket.on('disconnect', function () {
        console.log('A user disconnected: ' + socket.id);
    });
});

http.listen(3000, function () {
    console.log('Server started!');
});

Excellent! Our simple server will now listen at http://localhost:3000, and use Socket.IO to log to the console when a user connects and disconnects, with their socket ID.

When the server receives a "send" event from a client, it will create a new text string that includes the socket ID of the client that emitted the event, and emit its own "receive" event to all clients with the text that it received, interpolated with the socket ID.

We can test the server by returning to our command line and starting it up :

npm run start

The command console should now display:

Image

Cool! Let's return to the Chat component of our client to start building out our front end functionality.

Part 3: Chat

Let's open a separate command line interface and navigate to the /client directory. Within that directory, install the client version of Socket.IO:

npm install --save socket.io-client

In /client/src/components/Chat.vue, add the following code:

<template>
    <div id="container">
        <div id="output">
            <h1>STRUCT</h1>
            <p v-for="(text, index) in textOutput" :key="index">{{text}}</p>
        </div>
        <div id="input">
            <form>
                <input type="text" v-model="textInput" :placeholder="textInput" />
                <input type="submit" value="Send" v-on:click="submitText" />
            </form>
        </div>
    </div>
</template>

<script>
    import io from 'socket.io-client';
    let socket = io('http://localhost:3000');

    export default {
        name: 'Chat',
        data: function () {
            return {
                textInput: null,
                textOutput: []
            }
        },
        methods: {
            submitText: function (event) {
                event.preventDefault();
                socket.emit('send', this.textInput);
            }
        },
        created: function () {
            socket.on('connect', () => {
                console.log('Connected!');
            });
            socket.on('receive', (text) => {
                this.textOutput.push(text);
                this.textInput = null;
            });
        }
    }
</script>

<style scoped>
    #container {
        text-align: left;
        display: flex;
        flex-direction: column;
        margin-left: 1vw;
        min-height: 100vh;
    }
    h1 {
        text-align: center;
    }
    .hotpink {
        color: hotpink;
    }
    #input {
        position: fixed;
        margin-top: 95vh;
    }
    input[type=text] {
        height: 20px;
        width:  40vw;
        border: 2px solid cyan;
        background-color: black;
        color: hotpink;
        padding-left: 1em;
    }
    input[type=submit]{
        height: 25px;
        width: 5vw;
        background-color: black;
        color: cyan;
        border: 2px solid cyan;
        margin-right: 2vw;
    }
    input[type=submit]:focus{
        outline: none;
    }
    input[type=submit]:hover{
        color: hotpink;
    }
    @media (max-width: 1000px) {
        #container {
            border-left: none;
            border-top: 2px solid cyan;
            min-height: 50vh;
        }
        #input {
            margin-top: 43vh;
        }
        #output {
            margin-right: 10vw;
        }
        input[type=text] {
            width: 60vw;
        }
        input[type=submit] {
            min-width: 10vw;
        }
    }
</style>

Let's examine the above from bottom to top before moving forward. Between the

0
Subscribe to my newsletter

Read articles from freeCodeCamp directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

freeCodeCamp
freeCodeCamp

Learn to code. Build projects. Earn certifications—All for free.