Build An Among Us Inspired Live-Multiplayer Game with Phaser 3 and Socket.io: Part 3

This is part 3 of a 3 part series. To view part 2, click here. If you get lost at any time during this tutorial, you can always refer to the solution code here.

In part 2 we completed our waiting room and enabled the creation of private rooms with socket.io. In part 3 we will create our players and game functionality.

Add Players:

Now that we’ve created the logic for joining our game room, we can implement what needs to happen upon joining. Inside of our ‘joinRoom’ socket listener callback function, let’s add our player to our game room object in the server state:

Now we can update our numPlayers variable on the gameRoom object. Additionally, we will emit the state to the client by sending the gameRoom object to the player that just joined via their socket:

Add a state property to the MainScene class:

In our create method inside MainScene, we can listen for ‘setState’. Inside of the callback function, we deconstruct off of the state object we receive, and assign it to our client site state object on the MainScene class.

Lets head back to socket/index.js to add an emition for our current player, and tell everyone in the game room about our new player that joined. Inside our ‘joinRoom’ callback, we can add these emitions:

Back in MainScene we add a listener for ‘currentPlayers’:

Here we use a method that we will create on the MainScene class for adding our player:

If the socket id that we receive is not ourself as the player, we call a different method on the class for adding other players:

You’ll notice that we depend on a scene.otherPlayers group to add the other players at the bottom of our method, add this towards the top of the create function:

Back to line 54 of our code, below our ‘currentPlayers’ socket listener, add another socket listener for ‘newPlayer’:

If you npm run start-dev and open your localhost:8080, you should see a player on the screen. Now, open an incognito window, go to localhost:8080 and enter the same room code you just used. There should be another astronaut on the screen now! But they cant move! Let’s fix that.

Add player Movement

Inside of our update function, let’s handle player movement. We place this inside of the update function because update can recognize these actions at any time, as opposed to create which runs once when the scene is launched.

First, we check if the astronaut exists to avoid reference errors. Inside of this if statement, we set our speed to a variable, and we use setVelocity to set the initial movement to 0. We check if the left cursor is being pressed, and if it is, we set the x axis velocity to our speed as a negative number, and if our right cursor is being pressed, we set the x axis velocity as a positive integer. If you take a look at vertical movement, we follow the same logic. Open your browser again and give it a try, your astronaut should be moving all over! But if you open an incognito, the movement isn’t emitted. Add the following code below the movement that we just created:

In our socket/index we add our listener, update the players movement on the server state, and broadcast it to the other players in our game room:

Back to MainScene to add the listener for ‘playerMoved’, this can go inside our create method, since its a listener, it can be pinged at any time if its initialized in create:

Give it a whirl in the browser and you should see that player movement is reflected on both browsers.

Handle Disconnect

In case someone leaves the came, we need to update our players object on the gameRoom, and update the client state. We tackle this in the following code:

On our client side in MainScene, at the bottom of the create method, we add the listener and update our client side state to reflect the servers players and numPlayers:

Try adding two players to your game and then closing one of your browsers, you should see the other player disappear and the console log in your terminal.

Add a Task Control Panel

Let’s add a control panel for tasks to pop up when it is clicked, just like in Among Us !

Create a control panel entity in our entity folder, and import it into MainScene. Also preload the image for our control panel, here we call it vending machine:

Create a control panel group and an instance of vending machine, and place is above players inside create:

The vending machine should now appear in your game. Now we will add functionality to it. We want our vending machine to become highlighted and clickable when we are overlapped with it.

First let’s create the overlap. At the bottom of the update method, we add an overlap to our player and the vending machine, and we add a callback function to our third argument of the overlap:

Now we will create the callback function as a method on our MainScene class.

Add the method to our class:

Callback functions used in the third argument of overlaps always take two arguments that are the first two game entities passed to the overlap when it’s created. If a group is one of the entities, the sprite always comes first in the arguments.

Now if you run your player over the vending machine in your browser it will highlight green. Notice that we aren’t emitting the highlight so it only shows on the client side. But oh no! It doesn’t un-tint when we walk away. Let’s fix that!

Inside of the if(this.astronaut) statement for our overlap, add another function that checks if it’s overlapped. Notice that this is not a method built into phaser, we are going to build it ourselves!

Here we are checking to see if the bounds of the player and vending machine overlap, and if they aren’t overlapping, we call an additional method called deactivateControlPanel, that clears tint and disables interactivity.

Add A Pop Up Scene for Tasks

Now that we have a clickable vending machine, we can setup logic to launch another scene that will handle the task when the vending machine is clicked.

In the scenes folder, add a scene called TaskScene.js.

We load a simple image to show the scene when we eventually launch it. Also notice the init() method on the class, this will be important in our next step as we pass our state to our new TaskScene when it’s launched from mainscene.

We pause physics here in MainScene so our player can’t move while we are in TaskScene

Notice that when we start TaskScene, we pass a second argument with an object holding the information that we want to pass to TaskScene. We then add these to the TaskScene class inside its init method.

Before we launch, we also need to add our TaskScene to our game instance:

You should now be able to walk up to a vending machine, click on it, and launch the TaskScene.

This concludes part 3 of this tutorial. You now have the tools to create an Among Us inspired, live-multiplayer game with private game rooms and task functionality using Phaser 3 and Socket.io.

Try:

  • Creating a return button to exit the TaskScene
  • Creating logic and graphics for a task in the TaskScene
  • Adding a start button the uses sockets to trigger the game starting for all players and events that need to happen in the beginning of the game
  • Adding a progress tracker with a scoring system
  • Adding a database for tasks and global scores

And I’ll leave the rest up to you! I hope you enjoyed this tutorial and I would love to see how you choose to expand on this game.

Build An Among Us Inspired Live-Multiplayer Game with Phaser 3 and Socket.io: Part 2

This is part 2 of a 3 part series. To view part 1, click here. If you get lost at any time during this tutorial, you can always refer to the solution code here.

In part 1, we completed our boilerplate including an express server, a Phaser 3 front end, and hooked up our socket.io. In part 2, we will create scenes, add entities, and introduce logic for sockets and multiple private game rooms.

Create the MainScene:

Inside of your src folder, create another folder called scenes. Inside of scenes, create a file called Mainscene.js. Inside of Mainscene setup your class:

Hop over to src/index.js and lets import our Mainscene in, add it, and start it. We add all of our scenes into this index.js, but will only be starting Mainscene from it as it’s the first scene we want to load when our game begins.

Back in Mainscene, let’s start filling it out with some code. We will be dancing between Mainscene and our socket/index.js so I’d recommend having them both open.

Inside of the preload method, load the assets that you wish to use for your players and the background image. Inside of create, we safe the context of ‘this’ to the variable scene, and we add the image for the background. Lastly, we create our socket on the Mainscene class. Be sure make a folder called assets where you will add the images for your background and players.

If you npm run start-dev you should now see the background we added, and in your terminal you should see “A socket connection to the server has been made: (socket number)”.

Create Private Game Rooms:

Moving to our socket/index.js lets start writing what we want to happen when we connect. Because this is a live-multiplayer game, we need to have state that comes from the server and unifies game data between clients. At the top of this index.js add the following gameRooms object:

The commented out code is for your own reference to what will eventually be added to this object from another part of our code.

Since this is an Among us inspired game, we want players to be able to join private game rooms with their friends using a game code. Let’s create a waiting room where they can retrieve and enter a game code. Inside of src/scenes create a new scene file called WaitingRoom.js. Inside of Mainscene, we will launch this WaitingRoom scene from the create method, so that it appears right when we begin our game. The reason we do this instead of starting it on the game instance is because we can pass it our socket so that we don’t create a new one on WaitingRoom (creating a new socket io would tell the server we have another player instead of connect the same one), and also so that we can show the Mainscene image running in the background.

Since we passed our socket to waiting room, inside src/scenes/WaitingRoom.js we must initialize it with phasers init() method.

Add the following graphics to WaitingRoom:

The codeform that we preload is available in the solution code’s assets/text here. It is a simple html document that includes an input field which we can extract input from, from inside our input element. Add the input field:

Here we are putting a click event listener on our entire codeform, and inside of the click function, we are checking to see if what was clicked was our submit button, named “enterRoom” in our codeform html. Then, we save the input that the user has entered to a variable called input. We “emit” the input.value(the text the user entered) from our socket, with the identifier ‘isKeyValid’. Currently, we don’t have a listener for this emit method, so let’s add one to our server/socket/index.js:

When we say Socket.on, and pass it the “isKeyValid” string, we are listening for an event when that specific key is emitted, when our socket.on hears it, it has a call back function that takes the input that we passed this emit inside out waiting room. then, we check our state object keys to see if this game exists. If it exists, we create a new socket.emit called keyIsValid, if it doesn’t exist, we create a new socket.emit called keyNotValid. Back in our WaitingRoom, we need to listen for this emits now. Noticing a pattern?

This code is added to the bottom of the create method on WaitingRoom scene.

Notice on the fourth line we have a new socket.emit called ‘joinRoom’. You can ignore this for now, we will return to it in a few steps. Let’s add functionality to our request button, and create the notValidText that we just referenced, you can put these above the socket listeners we just created.

Notice the socket.emit ‘getRoomCode’, lets create a listener on our server/socket/index.js

Inside our listener, we create a variable that stores a random code generated by the function we add below our sockets called codeGenerator. After creating this variable, we check to see if this code already exists as a room on our state object, if it does we generate a new code and save it to the variable, if not we keep the one we have. We then tell our gameRooms state object to create a new key that is our game room code and make the value an object that contains all the data we will need for out game. Lastly, we emit a new socket saying the room is created, with the roomKey attached to it. Now we must add a listener for this back in our WaitingRoom.

When we hear ‘roomCreated’, we save the roomKey to our local state (on the WaitingRoom scene) and set the roomKeyText on our interface to the room key we get from the callback function.

The user flow here is that a user can generate a game room key, enter it into the input field, and submit it to enter the game. Now we must add the logic for joining the game.

Following our ‘joinRoom’ socket emit, that includes the room key in the emit, let’s create a listener back in our server/socket/index.js. At the top of our io.on function, but below our socket connection console.log, include our ‘joinRoom’ listener. Inside of the listener, we use the socket.join method and pass it the room key, which will add us to the room!

Now when you npm run start-dev you should be able to generate a room key, submit it, and enter into Mainscene.

This concludes part 2 of this tutorial. Continue to part three here, where we will create our players and add our game functionality.

Build An Among Us Inspired Live-Multiplayer Game with Phaser 3 and Socket.io: Part 1

This is part 1 of a 3 part series. If you get lost at any time during this tutorial, you can always refer to the solution code here.

I recently set out on the (long, confusing, and questionably documented) journey of creating a live-multiplayer game using Phaser 3 and Socket.io. Initially, I built a single-player game (check it out here https://cyberpunk-game.herokuapp.com/) and naively thought to myself “I’ll simply get the single player set up and apply multiplayer afterwords, no problem.” Wrong, Hannah. This was a problem. 

The reason that building a single player framework and then applying a multiplayer aspect to it later on is a problem – is because it requires lots of refactoring to your code, file structure, and unnecessary duplicate work. When initially set up as a live-multiplayer, you will be faced with engineering problems that are most easily solved before you have massive amounts of entities, scenes and logic to go with it.

Why is multiplayer complicated? Checkout this video for a conceptual overview of how it works.

The problems that I’ve encountered in my research have made it a difficult stride to setup my game as a live-multiplayer. One reason being, that there are virtually no tutorials specific to Phaser 3 (and I mean PHASER 3, not Phaser 2) and Socket.io, that run when you smash that npm run start-dev command, or that don’t require a lot of other add on technologies that also don’t work or are confusing to learn – when you’re just trying to get the basics down.

If you are here, you’ve likely encountered these same problems in your research. Perhaps you’ve stumbled upon this tutorial, which provides a simple overview on how to setup a basic live-multiplayer game with Phaser 3 and Socket.io without additional technologies. Although, if you’ve gone through this tutorial, you’re likely banging your head on your keyboard and yelling while comparing your code to the solution code, as its riddled with bugs. I too have a keyboard imprinted on my forehead, as it took my team of four programmers to unriddle this. If you still feel compelled to complete this tutorial, worry not, I have provided the debugged solution code here.

But how about a much cooler tutorial that doesn’t have a bug infestation? If you are seeking a comprehensive, step by step, live multiplayer tutorial using Phaser 3 and Socket.io from scratch, this is the tutorial for you my friend.

Initialize the Repo:

Go to your terminal and follow your directories to wherever you want your project to belong. In your command line run: “mkdir amongus-tutorial” to create your folder. “cd  amongus-tutorial” to enter the folder. Inside your folder, run the commands “git init” to initialize git and “npm init” to initialize your package.json. Go through the prompts after running npm init, you don’t have to put anything in the prompts, just press enter to go all the way through, or you can name your project and add descriptions, your choice.

Setup the package.json:

Inside of your terminal, create the following folders inside your project:

Open your project directory in the code editor of your choice, I prefer VSCode. Navigate to your package.json in your code editor. For the sake of saving time, copy the following into your package.json, it can be found in the solution code here. In your terminal (inside your project directory) run npm install. What the heck are we installing here and which ones actually matter? Heres a run through:

  • Scripts: For this tutorial, we will be using the npm run start-dev script to build and start our server.
  • Dependencies: We are installing express as our server, Morgan logs middleware (so you can see bugs in your terminal), and Socket.io is how we keep track of who is connected to our server and what they do. The dependencies that we wont worry about in this tutorial are axios, sequelize (they speak to your database), passport (its used for login functionality) and redux (manages state on the frontend). All of these softwares can help you down the road if you want to expand on this project. For the sake of simplicity, we will be hardcoding our database.
  • Dev dependencies: we are installing babel to help us build webpack, and nodemon for node.js dev.

Setup the Server:

In your server directory, create the file index.js We need to require in the following at the top of this index.js:

Now we are going to make a function that creates our app. When it runs, it tells our app to use Morgan to log our middleware, we include a body parsing middleware that sits conveniently on express, and compression middle wear. We tell the server where to get our static files, and we provide an error handler at the end incase a client requested page associated with our app does not exist.

Below this function, we must tell our server where our home base is. But where is our home base? We haven’t created one yet, let’s make it quick. Navigate to your public folder and create a file called index.html. Include the following code:

Back in our server/index.js lets add some code to tell our server to get our index.html. We also include our second error handler, which catches errors on the server side.

Our last step for setting up our server is to listen to our Port, and boot the app. Add the following code to the bottom of your index.js file

Lastly, we need to add an index.js file to our src folder, we don’t need to add anything to it yet. And we need to create our webpack.config.js file to tell our app how to bundle.

Now smash that npm run start-dev command and take a look at http://localhost:8080/ !!!

Empty black screen? No console errors? Excellent! Our server is running.

 Let’s add some Phaser magic.

Setup Phaser

Inside of our src folder (if you haven’t figured it out yet, our src folder is our client side !!) create a folder called config and add a file to it called config.js. Inside of config.js we will be initializing our Phaser game settings with the following code:

Navigate to src/index.js this is where our game instance will live. At the top of your file, import in typings:

Follow this link to the solution code’s typings file, and include it in your typings folder so that the import we just wrote can access it.

In src/index.js let’s create our game class and access the config through our super.

Initialize Socket.io

Our server is up, we have our game initialized, now we need to incorporate socket. Inside of your server folder, create a new folder called socket, and create a file inside of it called index.js. Now that we’ve created where our sockets will live, we can require it into our server. Go to your server/index.js and find the startListening function that we wrote. Include our socket io variable and require our socket folder in at the bottom of startListening func.

We’ve taken care of sockets on our server side, but how do they communicate with the client side? Navigate to our index.html and include the socket script above our bundle.js script:

If you run npm run start-dev, why don’t we see the console.log from our socket/index.js file that says we are connected? It’s because, although we’ve plugged socket into the right places, we haven’t initialized a socket on our game yet. Lets create some scenes and find a place for it!

This concludes part 1 of our tutorial. Click here to move onto part 2, where we will create graphics, fill out our scenes, and add logic to our sockets.