Dev Story: Building Action Henk’s Online Multiplayer In Just Two Weeks
This guest post was written by Roel Ezendam, Co-Founder & Game Coder for ‘Action Henk’ at indie studio RageSquid.
Action Henk is a toystalgic speedrunning platformer about managing momentum, acceleration and friction while running, jumping and sliding through tricky tracks & elaborate constructions. Take control of the heroic Henk as he races against players around the globe, breaks records against the best and earns medals in multipathed levels filled with loops, slides and giant jumps.
You can get Action Henk on Steam Early Access.
A while ago we from RageSquid decided that we wanted to add online multiplayer to our speed-running platformer game Action Henk. Because the game is currently on Steam Early Access and updates take place on a bi-weekly basis, this meant we would have to implement multiplayer in just two weeks. We felt like this was a good challenge and because we have some past multiplayer experience we were convinced that it was possible.
This article is about the steps and decisions we went through on a day-by-day basis and tries to give a better understanding at how the time is spent when building online multiplayer.
First off, let’s talk some specifics. We implemented the multiplayer with two coders, one doing all networking and game code and the other doing all the interface work needed. Simply building all the interface for the multiplayer (chat boxes, score boards, region selection) took about as much time as building the networking code itself.
Our game development tool of choice is Unity and because of our limited time we wanted to use a networking solution that already had most of the common network features in place. We decided to use Photon Unity Networking for this, as it is neatly integrated into Unity and has a lot of useful features out of the box, such as seamless scalability, connecting players with each other, syncing player information, cloud server regions and much more. Because of this, some of the parts of this article will talk about Unity- or Photon-specific features, but we will try to keep it as universally applicable as possible.
Day 1: rebuild the game’s core for multiple players
If you know you want to add multiplayer to your game from the very first day, this can save a lot of time in the long run, as you will set everything up with multiplayer in mind. We weren’t sure about multiplayer in Action Henk, so some parts of the game were built for this. Specifically, this meant rewriting the way players are spawned and kept track of. Instead of one player the game needed to have an arbitrary array of players, which can be controlled either locally, by ghost data, or networked.
Day 2: Set up the connection
After the game was ready to have multiple players, it was time to set up the network connection. This is where using a networking solution really comes in handy. Photon has very simple networking calls to start a game or join one. Instead of players having to host their own servers and messing around with port-forwarding and all that stuff, Photon does all game hosting in the cloud. Players can just start a ‘Room’ (in the Photon Cloud) and because of that, other players can easily find and join matches. It is important to note that this means that Photon’s task is basically to connect people and to send messages to each other. All messages are routed via a central game server that runs in one of Photon’s cloud regions (US, Europe, Singapore, Tokyo). This also means there is no authoritative server logic for the game. To make sure there is still a single entity that can make important decisions, one of the clients will become the ‘master client’ to take care of this.
Players can join a game from a list of all servers or start a game themselves. Another option is the ‘Instant Action’ button, which will join a random game. In photon this can be easily done by using the JoinRandomRoom function:
void OnJoinedLobby()
{
PhotonNetwork.JoinRandomRoom();
}
void OnPhotonRandomJoinFailed()
{
Debug.Log("JoinRandom Failed, creating room");
PhotonNetwork.CreateRoom(null);
}
void OnJoinedRoom()
{
Debug.Log("Joined or Created room, ready to start game");
StartGame();
}
Day 3-5: Player synchronization
So you got players connected and you can spawn a player on each other’s screen. But now it’s time to actually synchronize the motion of the players. The challenge of player syncing is the fact that there will always be a delay between computers and it’s about finding creative ways to hide this delay as good as possible from the players. This was definitely the most time consuming single feature of the multiplayer. The tricky thing about player syncing is that there is no single solution for it. Every game has different mechanics and motion and therefore syncing the information always needs a different approach. We will focus on Action Henk’s solution here, but if you’re looking for an overview of commonly used approaches, Joost van Dongen wrote a very informative article on core network structures.
The multiplayer gameplay of Action Henk is very similar to a game like Trackmania, meaning that the players don’t have any collision and just try to set a time as fast as possible on their own while also seeing the other players. This greatly simplified the complexity of the synchronization, as there was no issue with showing the other players with a slight delay. After all it’s about the time they set, the player only serves as a visualization of their actions.
The solution used in the end was to simply send input commands over the network and to simulate everyone’s physics locally. This was also saving a lot of bandwidth as Action Henk’s input is mostly binary and doesn’t require a constant stream of input values, just a message when something in the input has changed. In the ideal case every input message arrives at the other clients with exactly the same delay, the physics should be identical. Since this obviously isn’t the case and because floating-point imprecision will always create differences, the input messages are accompanied by position and velocity information. The networked player is then snapped towards this position, which will make sure that the simulation always uses the most up to date information. In order to not make the character snap around the screen, only the physics of the character snap. The model is kept at the original position and then smoothed back in to place.
Day 6: Scoring, finishing, actually playing a game.
So now we can run around the level, but there’s still no defined goal for the players. Luckily the game mode doesn’t require much code. The idea is that all players can just play the levels by themselves and try to set as quick of a time as possible on the levels. They are free to restart and retry the level as often as they want, until a timer runs out. The master client will rank all the players based on their best score, and hand out points based on their ranking. The server then goes to a new level and everything starts over again.
This meant that the only gamemode-specific data that needed to be synced was the best time of each player, one central countdown timer and a way to switch levels. In photon you can use CustomProperties to easily sync data like this. Setting a custom property on a player will make sure this value is automatically synced to all other clients.
public void OnFinish()
{
Hashtable properties = PhotonNetwork.player.customProperties;
properties["score"] = CheckpointManager.SP.GetFinishTime();
PhotonNetwork.SetPlayerCustomProperties(properties); //sync data to others
}
The same goes for room properties, they can be used to sync room information to all other clients.
public void StartNewLevel()
{
//check if we have to set up a level
if (PhotonNetwork.isMasterClient)
{
int levelID = GetRandomLevel();
Debug.Log("starting level " + levelID + " soon");
//PhotonNetwork.time is a time value that's synced to all clients
//We wait endgameDuration until we switch levels
Hashtable properties = PhotonNetwork.room.customProperties;
properties["level"] = levelID;
properties["levelstarttime"] = PhotonNetwork.time + endgameDuration;
PhotonNetwork.room.SetCustomProperties(properties);
}
}
However, switching levels can still be quite a tricky thing as it means people will all be in different scenes and every player has a different loading time. The solution for this was to basically disable all network communication and queue up all the messages that come in until the player is done loading the level. To ensure everyone has an equal advantage, there is a warm-up time at the start of each level so everyone has enough time to load the level and get ready.
Day 7 & the rest of week 2: Edge cases!
It was possible to play games against each other already. We managed to get the basic multiplayer working in just a week. So what was the whole other week spent on? Basically, fixing every little edge case that popped up during testing. An important thing to note about multiplayer coding is that there are a gazillion special cases that you never saw coming. The only thing we were sure about is that they would show up.
Most of these cases are something along the lines of “what if a player joins just before all the other players switch levels and the master client disconnects at the same time”. Most of them have to do with people connecting and disconnecting at various moments in your game and properly handling these events takes a good chunk of time. All important information that needs to be shared needs back-ups to take care of lag and disconnects. Our final week was all about testing and fixing new issues that would pop up continuously.
Conclusion
After two weeks we launched the multiplayer update to the public and it appeared to be stable apart from a couple of small issues that we were able to fix that same evening. Most of all it turned out to be a lot of fun! Even though it was quite a stressful challenge at times, in the end it was definitely a success. The things that made it possible in the end to achieve this were: Using an engine that allows for quick iteration (like Unity), using a network engine that saves you a lot of basic work (like Photon), having a game concept that allows for a bit of room for error in the network prediction and most of all taking a lot of time to fix special cases!
Any questions, comments or feedback? Just drop me an email at roel@ragesquid.com.