Project Helios | real-time multiplayer for the browser
Introduction & history
I started the project in 2015 out of my personal interest. Fascinated and inspired by the three.js library, I started developing my own browser game.
I had a lot of ideas from the start but also knew game projects are ambitious and challenging. Since this was going to be a project for myself, I decided not to make a backlog. Instead I made an infinite procedurally generated sandbox open for whatever features I felt like adding along the way.
The project was really fun and expanded faster than expected. Here is one of the last builds of this first version:
Features I added to the first version:
- procedurally generated biomes
- a voxel engine for the 3D models
- enemy AI
- a combat system
- inventory & equipment
- a mining & crafting system
However, after a while I decided to start a new version from scratch.
Reasons for this decision:
- While writing collision detection for the combat system, I came across performance issues. The game would be way more performant if I just made it in 2D. Also, I wasn’t happy about the look & feel of the 3D project. I never really achieved making the artwork look like my mood boards in three.js.
- I wasn’t satisfied with the code structure anymore. Since the start of the project I had learned a lot about design patterns and good practices from experimenting with angular and Typescript.
- I wanted to turn the game into a multiplayer. To do that, I had to make a lot of changes.
There are a lot of interesting features and aspects I could write about, but I am going to focus this blogpost on the multiplayer feature.
Real-time multiplayer in the browser
For client/server communication I decided to use Socket.io. It enables real-time bidirectional event-based communication, exactly what I needed! What makes it exceptionally interesting is its simplicity, asynchronous nature and the fact that it ties into Node.js seamlessly.
I had to rethink the concept of game-speed. In the first version of the game the speed was relative to fps. If your client was having performance issues and the game loop frequency changed, so did your in-game speed. As long as the game is single player this doesn’t really have consequences but for multiplayer it’s a different story. We want speed to be relative to actual time instead of fps. Here is what I did:
Imagine we want a ship to travel across the screen from left to right. This can be simply done with following line of code:
ship.position += 1
Right now, the ship will move 1 pixel/frame.
To make it relevant to time, I use a delta time factor. Delta time is the difference in time between the beginning of the current and the previous frame. When the game is running at a steady rate of 60 fps, delta time will be 1/60, for 22fps, 1/22.
ship.position.x += 1*deltatime
Now the ship will move at one pixel/second.
The first step of the transition to a multiplayer game was splitting up the game loop in two steps. Let me explain how the process works. (game state = the collection of all variables which can be influenced by players).
- First, I calculate a new game state based on user input and my programmed game-logic. For example:
if (ship.moving) ship.position.x += speed;
- Only after all logic is applied and all objects are updated, I redraw the full canvas based on this game state.
Now I basically want the first step to be done by the server and the resulting game state to be sent to the clients. This way clients will have a reference point to keep in sync.
In classic web development I would send http requests to the server and wait for a response. Can you imagine a server handling 60 http request per second for multiple clients? Also, imagine the gaming experience if your client had to wait on a http response.
Instead of handling http requests I made the server run his own game loop. Input will be sent directly to the server using socket.io. Each frame the server will broadcast its game state to the clients, again using socket.io.
You can imagine the game still won’t feel very smooth if I do nothing more. The client would still be waiting on the server to react.
Instead of waiting on the server, I can just run the same script on the client to predict the outcome and then update whenever the server is ready. This will provide a smoother gameplay experience.
Here is the final schematic & description of what I got so far:
Imagine I am player1 and I press my arrow key to go forward.
- The local inputInterfaceService is event based and outside the game loop. It takes notion of my keypress and updates the game state on the server.
- In the meanwhile, our local game loop is running asynchronous at 60fps.
- At the beginning of the client game loop, I check if I have a new update package to process. If that’s the case, my local game state will be overruled with the one in the package. (This is how the clients keep in sync)
- Right after that I allow the local inputInterfaceService to impose its changes. This means, whatever the update package did, we make sure our game state knows we are pressing forward. I do this to make sure the game feels as smooth as possible.
- The next step is applying the game-logic and calculate the new game state. In this case one of the tasks will be moving the ship forward. So, one of the lines will look something like this:
if (ship.moving) ship.position.x += speed*deltaTime;
- At the end of the loop we redraw the screen, based on the new game state
- In the meanwhile, our server game loop is running asynchronous at 22fps.
- Applying the game-logic and calculate the new server game state. This step is exactly the same as the 3th step of the client game loop.
- Next, I make an update package of the resulting game state on the server and I broadcast it to all clients.
To try the demo, open following link in two separate browser windows and use your arrow keys to move:
This project can also be found on GitHub: