Looterkings: Re-connecting with Photon Bolt
This guest post was written by Frederik Reher, developer at Looterkings.
Looterkings is an indie studio founded by the Youtubers Manuel Schmitt (aka SgtRumpel), Erik Range (aka Gronkh) and Valentin Rahmel (aka Sarazar) in 2015.
Their first game, the roguelike dungeon-crawler Looterkings, has been released as Early Access on Steam in August 2016 and is currently in development.
It’s an action-multiplayer game for up to 4 players each of whom take control over a little monster-slaying goblin.
Frederik explains the difficulties the developers ran into when trying to let players reconnect to an already open game session and how Photon Bolt’s token system helped to resolve this issue.
Obstacles
„How about we let players reconnect into a game?“, said the Game Designer and my job began. Up to then, we had stabilised Looterkings‘ multiplayer mode to the point where it did not crash frequently any more and where interaction between players did not cause major data desync between host and clients. We had finally implemented the ‚stacking‘ feature which enables 2 players to combine their goblins for more damage and other benefits, complete with animation synchronisation of 2 separate player-controlled units. How hard could it be?
Well, as it turns out, quite hard. At least if we had not used bolt. When we designed the
interaction system, we did not think about reconnecting. Getting it to work and getting it to
work fast were top priorities. So we ended up with a system relying heavily on global
events in order to prevent cluttering our scenes with BoltEntities. Every mushroom a goblin can pick up is synchronized using events. The same is true for crates, barrels, doors and shops.
A lifesaver – Bolt’s token system
So how to tell a clueless client everything that has happened in the level so far? In other networking solutions, we might have sent an RPC per interactive object, or – even worse -just made the objects into networked entities. But thankfully, bolt has tokens.
Tokens (named IProtocolToken in Bolt) are Bolt‘s way of serializing arbitrary data. They consist of a Read and a Write method, both accepting a UdpPacket. UdpPacket provides methods for serializing and deserializing standard data types. How you layout your data using those methods is up to you. You can add tokens to almost everything in bolt.
Whether it is a connection attempt and you want to send a passphrase to the host, whether it is spawning a unit and passing some parameters, not unlike using a constructor (the attach token is so useful, I wonder why Unity has not implemented a similar system for their Instantiate method). Tokens can also be added to entity states, player commands and, most importantly, to events.
public class MyToken : IProtocolToken
{
public void Read( UdpPacket packet )
{
// deserialize data here
}
public void Write( UdpPacket packet )
{
// serialize data here
}
}
Data of a running game
So what data does a client need when it connects into a running game? The first thing a client needs to know is how to build the level. Levels in Looterkings are procedurally assembled from pre-created Rooms, Intersections and Deadends. So the first token a
client receives after the level has loaded is the token with the level seed. Depending on the game mode, the next token a client receives contains data about a special mission players can try to accomplish for permanent buffs. Both tokens only require a single type of event which helps keeping the amount of different events manageable.
The next event sent contains information about the level‘s current condition. Our levels have multiple parts that change as players interact and progress. The most important part of those are interactive objects. To make sure crates are destroyed, mushrooms eaten and chests are open, every interactive object‘s state needs to be serialized. Our interactive objects all have an ID, a flag whether they are interactive at the moment and a byte denoting their current state in case of the object has more than two states, e.g. a puzzle using cardinal directions.
Additionally, information about rooms and doors has to be sent. Doors leading to rooms the players have not yet visited display a foggy white frame. Opening and closing doors is also done per event. This means we have to send data on whether a door is open and on whether a room has been visited before.
Lastly, there is information about the other players that needs to be sent. As alluded to in the introduction, two goblins can stack on top of each other, creating a single unit in the process. The new player needs to know who stacks with whom and who‘s on top.
Furthermore, players are surrounded by particle effects every time they level up. We send a player‘s current experience points in the attach token but since the new player is connecting into a level that may be running for minutes already, we have to provide other players‘ current experience points to prevent unwanted particle action from occurring. So the final Read and Write methods are looking like this:
public void Read( UdpPacket packet )
{
// entitystatuses (ID, isUsable, state)
entityStatuses = new NetworkEntityStatus[ packet.ReadInt() ];
for ( int i = 0; i < entityStatuses.Length; i++ )
{
entityStatuses[ i ] = new NetworkEntityStatus(
packet.ReadInt( NETWORK_ENTITY_ID_BITS ),
packet.ReadBool(),
packet.ReadByte() );
}
// doorStatuses (ID, isOpen, hasBeenOpen)
doorStatuses = new DoorOpenStatus[ packet.ReadByte() ];
for ( int i = 0; i < doorStatuses.Length; i++ )
doorStatuses[ i ] = new DoorOpenStatus( packet.ReadByte(), packet.ReadBool(), packet.ReadBool() );
// roomStatuses (ID, isCleared)
roomStatuses = new RoomStatus[ packet.ReadByte() ];
for ( int i = 0; i < roomStatuses.Length; i++ )
sectorStatuses[ i ] = new RoomStatus( packet.ReadByte(), packet.ReadBool() );
// playerStackPartnerIds (positive = on top)
playerStackPartnerIds = new int[ 4 ];
for ( int i = 0; i < 4; i++ )
playerStackPartnerIds[ i ] = packet.ReadByte() - 4; // shift by -4 to revert +4 on write
// playerCrawlStatuses (-1 = dc, 0 = dead, 1 = alive)
playerCrawlStatuses = new int[ 4 ];
for ( int i = 0; i < 4; i++ )
playerCrawlStatuses[ i ] = packet.ReadByte() - 1; // shift by -1 to revert +1 on write
// playerExps
playerExps = new int[ 4 ];
for ( int i = 0; i < 4; i++ )
playerExps[ i ] = packet.ReadInt();
}
public void Write( UdpPacket packet )
{
// entityStatuses (ID, isUsable, state)
packet.WriteInt( entityStatuses.Length );
for ( int i = 0; i < entityStatuses.Length; i++ )
{
packet.WriteInt( entityStatuses[ i ].id, NETWORK_ENTITY_ID_BITS );
packet.WriteBool( entityStatuses[ i ].isUsable );
packet.WriteByte( entityStatuses[ i ].state );
}
// doorStatuses (ID, isOpen, hasBeenOpen)
packet.WriteByte( (byte)doorStatuses.Length );
for ( int i = 0; i < doorStatuses.Length; i++ )
{
packet.WriteByte( (byte)doorStatuses[ i ].id );
packet.WriteBool( doorStatuses[ i ].isOpen );
packet.WriteBool( doorStatuses[ i ].hasBeenOpen );
}
// roomStatuses (ID, isCleared)
packet.WriteByte( (byte)roomStatuses.Length );
for ( int i = 0; i < roomStatuses.Length; i++ )
{
packet.WriteByte( (byte)roomStatuses[ i ].id );
packet.WriteBool( roomStatuses[ i ].isCleared );
}
// playerStackPartnerIds (positive = on top)
for ( int i = 0; i < 4; i++ )
packet.WriteByte( (byte)( playerStackPartnerIds[ i ] + 4 ) ); // shift +4 so stack lower does not get lost
// playerCrawlStatuses (-1 = dc, 0 = dead, 1 = alive)
for ( int i = 0; i < 4; i++ )
packet.WriteByte( (byte)( playerCrawlStatuses[ i ] + 1 ) ); // shift by +1 so -1 does not get lost
// playerExps
for ( int i = 0; i < 4; i++ )
packet.WriteInt( playerExps[ i ] );
}
So now the client knows what the world around him looks like, there‘s only one thing missing: information about the client itself. Each player can buy multiple weapons, outfits and hats during a run. Only the server and the local player know what items a player possesses and this information changes how items in the shop are displayed. So we have to synchronize equipment manually as well. We can do that easily by serializing an array of ItemIDs which are basically bytes.
Round-up
So this is how we solved the problem of synchronizing reconnecting players usingBolt‘s token system. Could we have achieved similar functionality with other networking solutions? Most likely. Would it have required us to change every interactive object into some form of BoltEntity? Most definitely. Even so be aware that tokens have their drawbacks and limitations. We learned that the hard way when we bloated a token and the event refused to send it, effectively halting the game. But in a nutshell, tokens are a pretty handy tool and they made creating Looterkings an easier process.