Dev Story ‘Tanks Multiplayer’: Switching from UNET to PUN
This guest post was written by Florian Bukmaier, co-founder and developer at Rebound Games.
Rebound Games is an indie studio founded by two brothers in 2013. They are frequently releasing new Unity assets on the Unity Asset Store, ranging from editor extensions and scripting tools to complete projects.
The Asset Store is just their financial backup though – a perfect complement for funding games development and covering additional costs.
Florian shares what made them create their latest multiplayer project ‘Tanks Multiplayer’, some differences between Unity Networking (UNET) functionality and Photon Unity Networking (PUN), but also why they are doing the switch to Photon.
Get ‘Tanks Multiplayer’ on the Unity Asset Store
About the project
‘Tanks Multiplayer’ is a real time action shooter template, designed for Unity developers looking for a solid base or learning project for their own multiplayer game. Players are controlling a tank, shoot at other tanks and avoid hostile projectiles whenever possible. Each round consists of 1-12 players in teams of 3 people, trying to earn the most kills for their team.
There are several power-ups to collect during the game, such as shields, damage boosts or bouncing projectiles. The game is over, once the required kill score has been reached by one team. Also, the template makes use of additional Unity services, namely Unity Ads, Everyplay and Unity IAP – which are not the focus of this post.
Why did we create this? Simple – we thought it would be a fun thing to do. But fun alone does not pay any bills: even though Unity published a basic tank multiplayer project on the Asset Store after UNET (beta) was released, there is still a strong need for covering more advanced networking topics in great detail. Especially when parts of the network documentation are lacking, basically making UNET coding a quest of trial and error.
Months after UNET has been released, there are still questions showing up on the Unity forums, such as:
- how do I add multiple player prefabs?
- How do I implement networked object pooling?
- Why aren’t my SyncVars working?
- How to add teams?
- How to handle host disconnects?
- Where do I call RPCs in an authoritative manner?
We’ve wanted to solve these mysteries with a user-friendly project, well-documented code and comprehensive documentation.
Digging into UNET
Like every developer I guess, we weren’t born with a talent to magically understand a complex, new API on day 1 – especially when there is a low level (LLAPI) and high level API (HLAPI) to look at – we had to learn how to use it.
The open-source approach of UNET was of tremendous help in this process. We’ve heavily extended the default behaviors with custom callbacks and overrode many virtual methods, like those of the NetworkManager class, so it was inevitable taking a look at its inner workings. But first things first: synchronizing transform values, such as position and rotation, for a simple networked movement test is very easy thanks to the NetworkTransform component. Synchronizing player or game values across the network, like player health and remaining bullet count, is equally simple by using SyncVars – although there is one thing to be aware of: only the master client can change them, basically requiring an authoritative setup already.
After getting the basics right it was time to understand how UNET handles remote procedure calls, or Commands and ClientRpcs in this context. Why are there two different actions you might ask? Well, commands are instructions for and executed on the server, while ClientRpcs are actions delivered from the server to clients. Other than that, I don’t know – nevertheless we’ve immediately used this approach to not only execute critical methods (like calculating damage) authoritatively on the server and distribute the result via SyncVars to clients, but also for implementing an authorization concept where players send requests after successful user-initiated actions to the master. For example, this is done when a player died and has to watch a video ad for a few seconds. In this case, the server does not send a respawn call for this player to other clients, because it does not know the exact length of the video ad. Instead, the server waits until the player sends a respawn request after successfully watching the video ad and then executes the fulfillment procedure.
A glance at PUN
So far, everything worked well in the UNET version of ‘Tanks Multiplayer’. We’ve got the network concept down and several network modes (online/LAN/offline) running on desktop and mobile devices without issues. Now, why the sudden change in direction at the heart of it? To put it in a nutshell: while developing this asset, UNET really felt like a beta. Certainly it still has some rough edges with errors showing up in some situations but not others, or the need to work around something that is “deeply rooted” in the LLAPI, which could (or is already) fixed in a later patch release. Besides missing essential features for the foreseeable future, such as host migration on their relay servers, NAT punch-through and the ability to easily host your own server including load balancing (if you would ever need that), other developers reportedly experienced issues with the billing go-live process or the Unity cloud in general, where there have been outages for a whole weekend. If you put all your effort and motivation in your game, you don’t want it to fail on something like this. Don’t get me wrong – UNET is great for diving into networking topics right away or building a prototype with limited resources. The goal of this project has been accomplished to our full satisfaction. It’s just not something you want to use in production at this point; and that’s where Photon comes in. With PUN, there is a direct alternative including a reliable, battle-tested backend. Additionally, developers interested in our asset asked for supporting Photon through various channels. Also with Photon being the most popular networking solution for Unity (and providing a free version of PUN), meeting the demand felt like the right thing to do.
Doing the switch
The development phase for this project – this is from concept to release on the Unity Asset Store – took 3 months. The switch from UNET to PUN took roughly two weeks, with a networking prototype going after just one week. There are several reasons for the ease of transition: first of all, the component structure of UNET and PUN is very similar, although you won’t need as much NetworkIdentities / PhotonViews in the scene as in the UNET version. Secondly, because the asset was built with modularity and no cross-component interlacing in mind, nearly all methods could be used regardless of the networking implementation. This is a very critical design step at the beginning, even before writing the first line of code. Basically the structure stays the same, but obviously the network API has to be replaced. Let’s take a look at some conversion examples:
- OWNERSHIP CHECKS
UNET
if (!isLocalPlayer)
{
//keep turret rotation updated for all clients
OnTurretRotation(turretRotation);
return;
}
PUN
if (!photonView.isMine)
{
//keep turret rotation updated for all clients
OnTurretRotation(turretRotation);
return;
}
- SENDING RPCS
UNET
//on client
CmdShoot(pos, turretRotation);
//on server
[Command]
void CmdShoot(short[] position, short angle)
{
//instantiate shot
}
PUN
//on client
this.photonView.RPC("CmdShoot", PhotonTargets.AllViaServer, pos, turretRotation);
//on server
[PunRPC]
void CmdShoot(short[] position, short angle)
{
//instantiate shot
}
- SYNCING VARIABLES
UNET
[SyncVar]
public int health = 10;
//on server
[Server]
public void TakeDamage(Bullet bullet)
{
//substract health by damage
health -= bullet.damage;
}
PUN
//on server
public static void SetHealth(this PhotonPlayer player, int value)
{
player.SetCustomProperties(new Hashtable() { { "health", (byte)value } });
}
You might have noticed that the last section about syncing variables in PUN is quite different from the UNET version. This is because we are not actually syncing variables per se (where OnPhotonSerializeView could be a better fit), but replaced our SyncVars with player’s CustomProperties. Especially for player variables such as health or selected bullet, which do not need to be synchronized several times per second but more in an event-like fashion, custom properties save even more bandwidth than a constant update approach. Lastly, in order to support both UNET and PUN in the same Asset Store package, we are actively maintaining a separate Unity project for each version and are pulling distinct scripts into a unitypackage in a ‘main’ project. If the user downloads and imports ‘Tanks Multiplayer’, he can then choose which networking solution to use simply by importing the desired unitypackage. This also opens the possibility of supporting Photon Thunder later on – but that’s all for now!
For additional feedback or further questions, please reach out to us at info [at] rebound-games [dot] com.