Create a Multiplayer Pirate Shooter Game: In Your Browser

Create a Multiplayer Pirate Shooter Game: In Your Browser

Creating multiplayer games is challenging for several reasons: they can be expensive to host, tricky to design, and difficult to implement. With this tutorial, I hope to tackle that last barrier. 

This is aimed at developers who know how to make games and are familiar with JavaScript, but have never made an online multiplayer game. Once you're done, you should be comfortable implementing basic networking components into any game and be able to build on it from there! 

This is what we'll be building:

Screenshot of the Final Game - Two Ships Attacking Each Other

You can try out a live version of the game here! W or Up to move towards the mouse and click to shoot. (If no one else is online, try opening two browser windows on the same computer, or one on your phone, to watch how the multiplayer works). If you're interested in running this locally, the complete source code is also available on GitHub.

I put this game together using Kenney's Pirate Pack art assets and the Phaser game framework. You'll be taking on the role of a network programmer for this tutorial. Your starting point will be a fully functioning single-player version of this game, and it will be your job to write the server in Node.js, using Socket.io for the networking part. To keep this tutorial manageable, I'll be focusing on the multiplayer parts and skimming the Phaser and Node.js specific concepts.

There's no need to set up anything locally because we'll be making this game completely in the browser on Glitch.com! Glitch is an awesome tool for building web applications, including a back end, database and everything. It's great for prototyping, teaching and collaborating, and I'm excited to introduce you to it throughout this tutorial.

Let's dive in.

1. Setup

I've put up the starter kit on Glitch.com.

Some quick interface tips: at any time, you can see a live preview of your app by clicking on the Show button (top left).

The show button is at the top left on the Glitch interface

The vertical sidebar on the left contains all the files in your app. To edit this app, you'll need to "remix" it. This will create a copy of it on your account (or fork it in git lingo). Click on the Remix this button.

The remix button is at the top of the code editor

At this point, you'll be editing the app under an anonymous account. You can sign in (top-right) to save your work.

Now, before we go any further, it's important to become familiar with the code for the game you're trying to add multiplayer in. Take a look at index.html. There are three important functions to be aware of: preload (line 99), create (line 115), and GameLoop (line 142), in addition to the player object (line 35).

If you'd rather learn by doing, try these challenges to make sure you get the gist of how the game works:

  • Make the world bigger (line 29)note that there is a separate world size, for the in-game world, and a window size, for the actual canvas on the page.
  • Make SPACEBAR also thrust forward (line 53).
  • Change your player ship type (line 129).
  • Make the bullets move slower (line 155).

Installing Socket.io 

Socket.io is a library for managing real-time communication in the browser using WebSockets (as opposed to using a protocol like UDP if you're building a multiplayer desktop game). It also has fallbacks to make sure it still works even when WebSockets aren't supported. So it takes care of the messaging protocols and exposes a nice event-based message system for you to use.

The first thing we need to do is install the Socket.io module. On Glitch, you can do this by going to the package.json file and either typing in the module you want in the dependencies, or clicking Add package and typing in "socket.io".

The add package menu can be found at the top of the code editor when selecting the file packagejson

This would be a good time to point out the server logs. Click on the Logs button on the left to bring up the server log. You should see it installing Socket.io along with all of its dependencies. This is where you would go to see any errors or output from the server code.

The Logs button is on the left side of the screen

Now to go server.js. This is where your server code lives. Right now, it just has some basic boilerplate for serving up our HTML. Add this line at the top to include Socket.io:

Now we also need to include Socket.io on the client, so go back to index.html and add this at the top inside your <head> tag:

Note: Socket.io automatically handles serving up the client library on that path, so that's why this line works even though you don't see the directory /socket.io/ in your folders.

Now Socket.io is included and ready to go!

2. Detecting & Spawning Players

Our first real step will be to accept connections on the server and spawn new players on the client. 

Accepting Connections on the Server

At the bottom of server.js, add this code:

This tells Socket.io to listen for any connection event, which automatically gets triggered when a client connects. It will create a new socket object for each client, where socket.id is a unique identifier for that client.

Just to make sure this is working, go back to your client (index.html) and add this line somewhere in the create function:

If you launch the game and then look at your server log (click on the Logs button), you should see it log that connection event! 

Now, when a new player connects, we expect them to send us information about their state. In this case, we need to know at least the x, y and angle in order to properly spawn them in the right location. 

The event connection was a built-in event that Socket.io fires for us. We can listen for any custom-defined event we want. I'm going to call mine new-player, and I expect the client to send it as soon as they connect with information about their location. This would look like this:

You won't see anything in the server log yet if you run this. This is because we haven't told the client to emit this new-player event yet. But let's pretend that's taken care of for a moment, and keep going on the server. What should happen after we've received the location of the new player that joined? 

We could send a message to every other player connected to let them know that a new player has joined. Socket.io provides a handy function to do this:

Calling socket.emit would just send the message back to that one client. Calling socket.broadcast.emit sends it to every client connected to the server, except that one socket it was called on.

Using io.emit would send the message to every client connected to the server with no exceptions. We don't want to do that with our current setup because if you got a message back from the server asking you to create your own ship, there would be a duplicate sprite, since we already create the own player's ship when the game starts. Here's a handy cheatsheet for the different kinds of messaging functions we'll be using in this tutorial.

The server code should now look like this:

So each time a player connects, we expect them to send us a message with their location data, and we'll send that data right back to every other player so that they can spawn that sprite.

Spawning on the Client

Now, to complete this cycle, we know we need to do two things on the client:

  1. Emit a message with our location data once we connect.
  2. Listen for create-player events and spawn a player in that location.

For the first task, after we create the player in our create function (around line 135), we can emit a message containing the location data we want to send like this:

You don't have to worry about serializing the data you send. You can pass in any kind of object and Socket.io will handle it for you. 

Before moving forward, test that this works. You should see a message on the server logs saying something like:

We know that our server is receiving our announcement that a new player has connected, along with correctly getting their location data! 

Next, we want to listen in for a request to create a new player. We can place this code right after our emit, and it should look something like:

Now test it out. Open up two windows of your game and see if it works.

What you should see is that after opening two clients, the first client will have two spawned ships, whereas the second one will only see one.

Challenge: Can you figure out why this is happening? Or how you might fix it? Step through the client/server logic we've written and try to debug it.

I hope you've had a chance to think about it for yourself! What's happening is that when the first player connected, the server sent out a create-player event to every other player, but there was no other player around to receive it. Once the second player connects, the server again sends out its broadcast, and player 1 receives it and correctly spawns the sprite, whereas player 2 has missed player 1's initial connection broadcast. 

So the problem is happening because player 2 is joining late in the game and needs to know the state of the game. We need to tell any new player that's connecting what players already exist (or what has already happened in the world) so that they can catch up. Before we jump into fixing this, I have a brief warning.

A Warning About Syncing Game State

There are two approaches to keeping every player's game synced. The first one is to only send the minimal amount of information about what has been changed across the network. So each time a new player connects, you would send just the information for that new player to all other players (and send that new player a list of all other players in the world), and when they disconnect, you tell all other players that this individual client has disconnected.

The second approach is to send the entire game state. In that case, you would just send a full list of all the players to everyone each time a connect or disconnect occurs.

The first one is better in the sense that it minimizes the information sent across the network, but it can be very tricky and has the risk of players going out of sync. The second one guarantees players will always be in sync but involves sending more data with each message.

In our case, instead of trying to send messages when a new player has connected to create them, when they've disconnected to delete them, and when they've moved to update their position, we can consolidate all of that into one update event. This update event will always send the positions of every available player to all clients. That's all the server has to do. The client is then responsible for keeping its world up to date with that state it receives. 

 To implement this, I will:

  1. Keep a dictionary of players, with the key being their ID and the value being their location data.
  2. Add the player to this dictionary when they connect and send an update event.
  3. Remove the player from this dictionary when they disconnect and send an update event.

You can try to implement this on your own since these steps are fairly simple (the cheat sheet might come in handy). Here's what the full implementation might look like:

The client side is a little trickier. On one hand, we only have to worry about the update-players event now, but on the other hand, we have to account for creating more ships if the server sends us more ships than we know about, or destroying if we have too many.

Here's how I handled this event on the client:

I'm keeping track of the ships on the client in a dictionary called other_players that I simply have defined at the top of my script (not shown here). Since the server sends the player data to all players, I have to add a check so a client isn't creating an extra sprite for themselves. (If you're having trouble structuring this, here's the full code that should be in index.html at this point).

Now test this out. You should be able to create and close multiple clients and see the right number of ships spawning in the right positions!

3. Syncing Ship Positions

Here's where we get to the really fun part. We want to actually sync the positions of the ships across all the clients now. This is where the simplicity of the structure we've built up so far really shows. We already have an update event that can sync everyone's locations. All we need to do now is:

  1. Make the client emit every time they've moved with their new location.
  2. Make the server listen for that move message and update that player's entry in the players dictionary.
  3. Emit an update event to all clients.

And that should be it! Now it's your turn to try and implement this on your own.

If you get completely stuck and need a hint, you can look at the final completed project as reference.

Note on Minimizing Network Data

The most straightforward way to implement this is to update all players with the new locations every time you receive a move message from any player. This is great in that players will always receive the latest information as soon as it's available, but the number of messages sent across the network could easily grow to hundreds per frame. Imagine if you had 10 players, each sending a move message every frame, which the server then has to relay back to all the 10 players. That's already 100 messages per frame!

A better way to do it might be to wait until the server has received all messages from the players before sending out a big update containing all the information to all players. That way you squash the number of messages you're sending down to just the number of players you have in the game (as opposed to the square of that number). The problem with that, however, is that everyone will experience as much lag as the player with the slowest connection in the game. 

Another way to do it is to simply have the server send updates at a constant rate, regardless of how many messages it's received from players so far. Having the server update at around 30 times per second seems like a common standard.

However you decide to structure your server, be mindful of how many messages you're sending every frame early on as you develop your game.

4. Syncing Bullets

We're almost there! The last big piece will be to sync the bullets across the network. We could do it in the same way we synced the players:

  • Each client sends the positions of all its bullets every frame.
  • The server relays that to every player.

But there is a problem.

Securing Against Cheats

If you relay whatever the client sends you as the true position of the bullets, then a player could cheat by modifying their client to send you fake data, such as bullets that teleport to wherever the other ships are. You can easily try this out yourself by downloading the webpage, modifying the JavaScript, and running it again. This isn't just a problem for games made for the browser. In general, you can never really trust data coming from the client. 

To mitigate against this, we'll try a different scheme:

  • Client emits whenever they've fired a bullet with the location and direction.
  • Server simulates the motion of bullets.
  • Server updates each client with the location of all the bullets.
  • Clients render the bullets in the locations received by the server.

This way, the client is in charge of where the bullet spawns, but not how fast it moves or where it goes after that. The client may change the location of the bullets in their own view, but they can't alter what other clients see. 

Now, to implement this, I'll add an emit when you shoot. I'll no longer be creating the actual sprite either, since its existence and location is now determined completely by the server. Our new bullet shooting code in index.html should now look like this:

You can also now comment out this whole section that updates the bullets on the client:

Finally, we need to get the client to listen for bullet updates. I've opted to handle this the same way I do with players, where the server just sends an array of all the bullet locations in an event called bullets-update, and the client will create or destroy bullets to keep in sync. Here's what that looks like:

That should be everything on the client. I am assuming you know where to put these snippets and how to piece everything together at this point, but if you're running into any issues, remember you can always take a look at the final result for reference.

Now, on server.js, we need to keep track of and simulate the bullets. First, we create an array for keeping track of bullets, in the same way we have one for players:

Next, we listen for our shoot bullet event:

Now we simulate the bullets 60 times per second:

And the last step is to send the update event somewhere inside that function (but definitely outside the for loop):

Now you can actually test it! If all went well, you should see the bullets syncing across clients correctly. The fact that we did this on the server is more work, but also gives us a lot more control. For example, when we receive a shoot bullet event, we can check that the speed of the bullet is within a certain range, otherwise we know this player is cheating.

5. Bullet Collision

This is the last core mechanic we'll implement. Hopefully by now you've gotten used to the procedure of planning out our implementation, finishing the client implementation completely first before moving on to the server (or vice versa). This is a much less error-prone way than switching back and forth as you implement it.

Checking for collision is a crucial gameplay mechanic, so we'd like it to be cheat-proof. We'll implement it on the server in the same we did for the bullets. We'll need to:

  • Check if a bullet is close enough to any player on the server.
  • Emit an event to all clients whenever a certain player gets hit.
  • Have the client listen on the hit event and make the ship flash when hit.

You can try doing this one completely on your own. To make the player flash when hit, simply set their alpha to 0:

And it will ease back to full alpha again (this is done in the player update). For the other players, you would do a similar thing, but you'll have to take care of bringing their alpha back to one with something like this in the update function:

The only tricky part you might have to handle is making sure a player's own bullet can't hit them (otherwise you might always get hit with your own bullet every time you fire).

Notice that in this scheme, even if a client tries to cheat and refuses to acknowledge the hit message the server sends them, that will only change what they see on their own screen. All the other players will still see that that player got hit. 

6. Smoother Movement

If you followed all the steps up to this point, I'd like to congratulate you. You've just made a working multiplayer game! Go ahead, send it a friend and watch the magic of online multiplayer uniting players!

The game is fully functional, but our work doesn't stop there. There are a couple of issues that might impact the player's experience that we need to address:

  • Other players' movement will look really choppy unless everyone has a fast connection.
  • Bullets can feel unresponsive, since the bullet does not fire immediately. It waits for a message back from the server before it appears on the client's screen.

We can fix the first one by interpolating our position data for the ships on the client. So even if we're not receiving updates fast enough, we can smoothly move the ship towards where it should be as opposed to teleporting it there. 

The bullets will require a little more sophistication. We want the server to manage the bullets, because that way it's cheat-proof, but we also want to have the immediate feedback of firing a bullet and seeing it shoot. The best way is a hybrid approach. Both the server and the client can simulate the bullets, with the server still sending bullet position updates. If they're out of sync, assume the server is right and override the client's bullet position. 

Implementing the bullet system I described above is out of the scope for this tutorial, but it's good to know that this method exists.

Doing a simple interpolation for the positions of the ships is very easy. Instead of setting the position directly on the update event where we first receive the new position data, we simply save the target position:

Then, inside our update function (still in the client), we loop over all other players and push them towards this target:

This way you can have your server sending you updates 30 times per second, but still play the game at 60 fps and it will look smooth!

Conclusion

Phew! We just covered a lot of things. Just to recap, we've looked at how to send messages between a client and a server, and how to sync the state of the game by having the server relay it to all players. This is the simplest way to create an online multiplayer experience. 

We also saw how you can secure your game against cheating by simulating the important parts on the server and informing the clients of the results. The less you trust your client, the safer the game will be.

Finally, we saw how to overcome lag by interpolating on the client. Lag compensation is a broad topic and is of crucial importance (some games just become unplayable with high enough lag). Interpolating while waiting for the next update from the server is just one way to mitigate it. Another way is to try and predict the next few frames in advance, and correct once you receive the actual data from the server, but of course this can be very tricky.

A completely different way of mitigating the impact of lag is to just design around it. The benefit of having the ships turn slowly to move acts as both a unique movement mechanic and also a way to prevent sudden changes in movement. So even with a slow connection, it still wouldn't ruin the experience. Accounting for lag while designing the core elements of your game like this can make a huge difference. Sometimes the best solutions are not technical at all.

One final feature of Glitch you might find useful is that you can download or export your project by going into the advanced settings on the top left:

The advanced options menu allows you to import export or download your project

If you make something cool, please share it in the comments below! Or if you have any questions or clarifications about anything, I'd be more than happy to help. 

Source: Tuts Plus

About the Author