Hopzone: Exploring Multiplayer 2D Games with JavaScript and P5.js

cover image

Table Of Contents

  1. Project Description
  2. How to make a multiplayer 2D game in JavaScript using P5.js?
  3. Back-end
  4. Front-end
  5. Source code

📝 Project Description

I've been working on a neat project – creating a multiplayer 2D game in JavaScript using Processing (P5.js). The big question was, "What can we pull off with this combo?"

I delved into various game development aspects, checking out different technical options and how to handle game data. Also, I explored real-time application transport protocols. All the while, I was getting cozy with the JavaScript library P5.js.

Then, I put theory into practice and made a real browser game that you can play with friends. But it's not just about the game – I also thought about game development approaches and gave some advice on what works best for different game types.

The result is Hopzone, a 2D multiplayer platformer game set in space. Players navigate platforms, dodge monsters, and strive to survive the longest.

Project

This adventure is part of a student research project within the Multimedia & Creative Technologies bachelor's program. As an integral module of the curriculum, I took on the challenge of researching a novel topic not covered in the standard learning program. The culmination of this exploration is a full-stack web application that offer a fresh perspective on multiplayer browser games. 🎮✨

❓ How to make a multiplayer 2D game in JavaScript using P5.js?

P5.js is a JavaScript library that makes programming accessible and fun for beginners, designers, educators, and more. It is an interpretation of the Processing language, but made for the modern web.

But can you use this visual way of programming to create a browser game? And what does it take to add multiplayer functionality? These are the questions that I explored in my bachelor thesis, where I tried to answer the following research question: What are the possibilities for a multiplayer 2D game in JavaScript based on Processing (P5.js)?

In this section, I will summarize the main findings of my research and show you how I built a working multiplayer game prototype using P5.js and other technologies.

Multiplayer strategies

The first thing to consider when making a multiplayer game is how to communicate between different players and keep the game state synchronized. There are two main strategies for this: peer-to-peer (P2P) or client-server.

Peer-to-peer (P2P)

In a P2P network, all clients are directly connected to each other. Communication happens without a central server. This means that each player acts as both a server and a client.

Representation of a P2P network

A visual representation of a P2P network

Client-server

In a client-server network, all clients communicate with a central server. The server acts as the authoritative source of truth for the game state and updates all connected clients.

Representation of a P2P network

A visual representation of a client-server network

The P2P strategy has some drawbacks, such as the possibility of cheating, the lack of a single source of truth, and the increased complexity and scalability issues as more players join the game. Therefore, the client-server strategy with an authoritative server is a better choice for most multiplayer games.

Network protocols

The next thing to consider is the network protocol that enables the communication between the client and the server. There are several network protocols that support web-based games, but I only looked at the ones that are suitable for real-time applications. These are:

  • HTTP: The standard protocol for data communication over the web. It is connectionless and stateless, meaning that the client and the server do not maintain a connection or remember each other after each request. This can cause problems for a multiplayer game that requires a continuous data stream between the client and the server.
  • Long polling: A technique that builds on top of HTTP to send information as quickly as possible to the client. When the server receives a request from the client, it does not close the connection until it has something to send back. When the client receives a response from the server, it immediately makes another request to receive new data. This allows for a more real-time communication, but still suffers from the high frequency of request-response cycles.
  • Web sockets: A protocol that enables a bidirectional and stateful communication between the client and the server. The client and the server maintain a connection and can exchange data at any time. This is ideal for a multiplayer game that requires a constant data flow between the client and the server.
  • MQTT: A protocol that uses the publish-subscribe messaging pattern. This means that a sender of messages (in this case, the server), also called a publisher, does not send messages directly to receivers. The publisher sends messages to a certain category. The receivers can subscribe to one or more categories to receive messages. The MQTT messages can also be sent over web sockets. This protocol is mainly designed for low-power devices with limited computing power, such as IoT devices.

Based on these protocols, web sockets are the best choice for this use case, as they are stateful and bidirectional. MQTT is also a good candidate, but it is more suited for IoT devices.

Back-end framework

The client-server strategy requires a back-end framework that can handle the web socket communication and the game logic. There are many frameworks that can do this, but I only compared three of them:

  • ASP.NET SignalR: A package for ASP.NET that simplifies the process of adding real-time web functionality to applications. It supports web sockets and other fallback protocols. ASP.NET is synchronous, multithreaded, and mainly written in C#. These features make it highly performant for heavy workloads. Combined with the SignalR package, it can be used for real-time multiplayer games.
  • Google Firebase: A platform that provides various services for web applications, including its Realtime Database. This is a NoSQL database hosted in the cloud on Google's servers. Clients connected to this database receive real-time updates when the data changes. Firebase also provides other services such as authentication.
  • Node.js Express: A runtime environment that allows writing server applications in JavaScript¹²[12]. It is single-threaded and asynchronous by nature, which makes it good at handling many I/O operations and real-time processes. By using the Express library, a fast and performant web server can be set up. To add real-time functionality, the Socket.IO library can be used, which supports web sockets and other fallback protocols.

🔢 Back-end

Architecture

The server is made in Express, a Node js web application framework, with the main communication being handled by Socket.io. The Node.js framework was deliberately chosen for easy syncing between client/server code, as they are both written in TypeScript. A more perfomant web framework like ASP .NET Core and its multithreading capabilities with SignalR as websocket handler would have also been possible, but this would really have slowed down the developing process of the game.

Let's talk about the individual components of the system. I will focus on the server part here.

  • Database: The central storage unit. The database holds and persists the state of all active game rooms. For data storing, MongoDB is used. This choice was made because MongoDB is a document-based database, and not bound to a database structure. This allows for easily altering how a game should look structure wise.
  • Server: The server is the central component of the system. It handles new client connections, security, logging and other central tasks. It is responsible for creating and destroying games, and also for persisting data to the database.
  • Game Controller: This object exists for each active game lobby/session. It holds the game state and is responsible for altering this state when receiving updates from the worker thread. It is also responsible to sending out the state of the game to each of its connected clients.
  • Worker Thread: Because there are many calculations to be made when a game starts, this computing load is decoupled from the game controller in a worker thread. The thread receives information about player inputs while also receiving the previous state of the game. It performs calculations on this state and sends this updated state back to the game controller. This seems the most ideal solution to this problem to decouple this load from the main process.[3]
  • Player Controller: For each connected client there is a player controller on the server side. It receives messages from the client, and based on these messages performs an action in the game on behalf of the player.

Hopzone architecture diagram

This diagram shows the architecture of both the server and client side part of the game. It is inspired by Michał Męciński's model who also tackled a similar problem. [2]

The Game State

The game state is stored and modified using an object oriented approach. A single game's state looks like this

interface Game {
    players: PlayerObject[]
    platforms: Platform[]
    movingPlatforms: MovingPlatform[]
    boostedPlatforms: BoostedPlatform[]
    enemies: Enemy[]
    alivePlayers: number
    deadPlayers: number
}

Every object inside the game state inherits from the same base class GameObject. This class holds basic properties like their x and y coordinates, width and height, aswell as methods for detecting intersections with other game objects (hit detection).

When a game is started, the worker thread is responsible for updating this game state and sending it over to the front-end.

Security

For authorization, Google's Firebase is used. This choice was made because it is easy to use and integrate. Middleware is added to the socket server to perform authorization on each socket connection. To authorize users, they have to provide a valid token with their connection.

💻 Front-end

The front-end is built in the Next.js framework.

Styling

For quick development, the web application is styled using the CSS framework TailwindCSS.

Auth

The application uses Google's Firebase for authentication.

Communcation Back-end

The communication with the back-end happens over the WebSocket protocol. This allows for a bidirectional communication between server and client. the Socket.io library is used for handling this connection. The client listens to different events from the back-end such as game state updates, while also sending out messages to the back-end such as when a player wants to move.

TypeScript was used to its advantage by having a global enum containing all possible socket messages. Each message is prefixed by either b2f_ (back-end to front-end) of f2b_ (front-end to back-end), to easily distinguish from where the message originates.

enum SocketMessages {
  connectionFailed = 'connect_error',
  connectionSuccess = 'connect',
  activeRooms = 'b2f_gamerooms',
  lobbyInfo = 'b2f_lobby',
  joinLobby = 'f2b_joinLobby',
  leaveLobby = 'f2b_leaveLobby',
  gameState = 'b2f_gameState',
  moveLeft = 'f2b_moveLeft',
  moveRight = 'f2b_moveRight',
  stopMoving = 'f2b_stopMoving',
  gameLoading = 'b2f_gameLoading',
  getScoreboard = 'f2b_scoreboard',
  scoreboard = 'b2f_scoreboard',
  newLobby = 'f2b_newLobby',
  startGame = 'f2b_startGame',
  restartGame = 'f2b_restartGame'
}
show(p5: p5Types, image: p5Types.Image): void {
    p5.imageMode(p5.CENTER)

    if (this.xSpeed >= 0) {
        p5.image(image, this.x, this.y, this.width, this.height);
        return;
    } 
    
    p5.push()
    p5.scale(-1, 1)
    p5.image(image, -this.x, this.y, this.width, this.height)
    p5.pop()
}

The P5 Instance

The game itself is rendered using the javascript library from processing (P5.js) [4]. The main purpose of processing is for non experienced programmers to learn to code in a visual context. It allows for quickly and easily creating visuals in a programming context. To use the P5 context within our Next.js application, the react-p5 npm package is used [5]. This package gives us access to the P5 instance of our application.

For this project, P5.js is used for rendering the game. When a game is started, it creates a canvas element where the game's state is rendered. An object oriented approach was taken to define every single game object. The P5 instance receives the game state from the server, and renders every game object inside this state. A single game's state looks like this. The models are mapped one to one with the models used on the server side.

Secrets

The front-end requires some secret files to work correctly such as configuration for Firebase or communication with the back-end.

.env

NEXT_PUBLIC_FB_APIKEY=****
NEXT_PUBLIC_FB_AUTHDOMAIN=****
NEXT_PUBLIC_FB_PROJECTID=****
NEXT_PUBLIC_FB_STORAGEBUCKET=****
NEXT_PUBLIC_FB_MESSAGINGSENDERID=****
NEXT_PUBLIC_FB_APPID=****
NEXT_PUBLIC_BACKEND=URL TO THE BACK-END SOCKET SERVER

🧑‍💻 Source code

If you want to have a look at the source code of this project, you can visit Github organization of the project.