Real-Time Chat in a Phaser Game with MongoDB and Socket.io
Rate this tutorial
When building a multiplayer game, you're probably going to want to implement a way to interact with other players beyond the general gameplay experience. This could be in the form of video, audio, or written chat within a game.
So how would you manage that real-time interaction and how would you store it in your database?
To get a better idea of what we hope to accomplish, take a look at the following animated image:
The actual game aspect in the above animation is a bit lackluster, but what's important is the chat functionality. In the above example, the chat messages and chat input are baked into the Phaser game. When we enter a message, it is sent to the server through sockets and the server saves the messages to MongoDB. In addition to saving, the server also broadcasts the messages back to every client and retrieves all messages for new clients.
There are a few moving pieces when it comes to this example, however, there aren't too many requirements. We'll need the following to be successful:
We're going to start by creating the backend for our game. It will do all of the heavy lifting for us that isn't related to visuals.
On your computer, create a new directory and run the following commands from within the directory:
The above commands will install the MongoDB Node.js driver, Express, Socket.io, and a library for handling cross-origin resource sharing between the game and the server.
A lot of the above code is boilerplate when it comes to configuring Express and MongoDB. We'll do a quick breakdown on the pieces that matter as of right now.
First you'll notice the following line:
ATLAS_URIis an environment variable on my computer. You can obtain the value to this variable within the MongoDB Atlas dashboard. The value will look something like this:
You can choose to hard-code your value or use an environment variable like me. It doesn't matter as long as you know what you're choosing. Using an environment variable is beneficial as it makes the project easier to share without the risk of exposing potentially sensitive information that might be hard-coded.
The next thing you'll notice is the following:
After we connect to our MongoDB Atlas cluster, we obtain the collection that we plan to use. In this case, the database we plan to use is
gamedevand the collection is
chats, neither of which need to exist prior to starting your application.
With the basics added to the application, let's focus on the more important things, starting with the REST API endpoint:
Even though we're using Socket.io for most of our communication, it makes sense to have an endpoint for initially obtaining any chat data. It is not frequently accessed and it will prevent too much stress on the socket layer.
What we're saying in the endpoint is that we want to find a single document based on the
roomvalue that was passed in with the request. This value will represent our game room or our chat room, however you want to interpret it. This single document will have all of our previous chat conversations for the particular room. This data will be used to get the clients up to speed when they join.
Now we can get into the real interesting things!
Let's revisit the Socket.io logic within the main.js file:
You'll notice that this time we have some logic for the
joinpayload is received from the client, the
gameIdthat the client provides with the payload is used to try to find an existing MongoDB document. If a document exists, it means the chat room exists. If it doesn't, we should create one. After retrieving or creating a document in MongoDB, we can join the socket room with Socket.io, emit an event back to the client that we've joined, and specify that the active room is that of the
gameIdthat we just passed.
A socket can be a part of multiple rooms, but we only care about one, the active one.
Now let's have a look at the
messageevent for our sockets:
When the client sends a message, we want to append it to the document defined by the active room. Remember, the room, game id, and document id are all the same thing and each socket maintains that information as well.
After the message is saved, its then broadcast to all sockets that are part of the same room.
The backend is now fully capable of handling the chat system within our game.
With the backend under control, now we can focus on the game that we can distribute to clients, otherwise known as other players.
Create a new directory on your computer and within that directory create an index.html file with the following HTML markup:
It may seem like there's a lot happening in the above HTML, but it is really just Phaser configuration.
First you'll notice the following two lines:
In the above lines we are including the Phaser game development framework and the Socket.io client library. These libraries can be used directly from a CDN or downloaded to your project directory.
Most of our boilerplate focuses around the
Because this is a game, we're going to enable game physics. In particular, we're going to use arcade physics and the environment gravity will be specific on the y-axis. While we won't be doing much in terms of the physics in this game, you can learn more about arcade physics in a previous tutorial I wrote titled,
This brings us to our scene lifecycle functions.
initScenefunction is where we initialize our variables and our connection to the backend server:
autoConnectas false because we don't want to connect until our scene is finished being created. We'll manually connect in a different lifecycle function.
The next lifecycle function is the
preloadScenefunction, but before we get there, we should probably create our input form.
Within your game project directory, create a new form.html file with the following HTML:
The styling information isn't too important, but the actual
<input>tag is. Pay attention to the
nameattribute on our tag as this will be important when we want to work with the data supplied by the user in the form.
preloadScenefunction of the index.html file, add the following:
Now we're able to use our user input form within the game!
To display text and interact with the user input form, we need to alter the
createScenefunction within the index.html file:
There is a lot happening in the above
createScenefunction and it can be a little tricky to understand. We're going to break it down to make it easy.
Take a look at the following lines:
The first line says that we are loading and positioning the input form that we had added to our project in the
preloadScenefunction. Next we are creating a chat box to hold our text. We are positioning and styling the chat box to our liking. Finally, we are giving Phaser control of the enter key, something we'll use to submit the form in our game.
Before we look at what happens with the enter key, take a look at the following lines:
In the above lines we are manually connecting to our socket server. When the client socket says we have connected, we attempt to join a game. Remember the
joinwe're waiting for on the server? This is where we are sending it and in this case we are saying we want to join the
After the server says we've joined, it sends us back a message which we can pick up here:
When we know we've joined, we can execute an HTTP request against the API endpoint and provide the room, which again is our game id. The response will be all of our chat messages which we'll add to an array. To eliminate the risk of messages displaying off the screen, we are popping items from the top of the array if the length is greater than our threshold.
When we call
setTextusing an array value, Phaser will automatically separate each array item with a new line character.
As of right now we've joined a game and we received all the prior messages. Now we want to send messages:
When the enter key is pressed, we get the
<input>tag from the form.html file by the
nameattribute. If the value is not empty, we can send a message to the server and clear the field.
In our configuration, our own messages are not displayed immediately. We let the server decide what should be displayed. This means we need to listen for messages coming in:
If a message comes in from the server we push it into our array. If that array is larger than our threshold, we pop an item from the top before rendering it to the screen. We're doing this because we don't want scrolling within our game and we don't want messages going off the screen. It isn't fool proof, but it will work good enough for us.
If you ran the game right now, you'd have something like this:
As you can see, we only have a chat area towards the right of the screen. Not particularly exciting for a game if you ask me.
What we're going to do is add a little flare to the game. Nothing extravagant, but just a little to remind us that we're actually doing game development with MongoDB.
Before we do that, go ahead and download the following image:
It really doesn't matter what image you use, but I'm saving you the trouble of having to find one for this game.
With the image inside of your project, we need to change the
preloadScenefunction of the index.html file:
Notice that we're now loading the image that we just downloaded. The goal here will be to have many of these images bouncing around the screen.
createScenefunction, let's make the following modification:
In the above code we are creating an object pool with 15 possible sprites. Next we are looping and taking each of our 15 sprites from the object pool and placing them randomly on the screen. We're adding some physics information to each sprite and saying that they collide with the edges of our game.
This should cause 15 leaf sprites bounding around the screen.