Xevious Multiplayer with MLAPI

Paul Marsh
4 min readMay 24, 2021

Previously I implemented a quick multi-player test of Xevious using Photon V2. Whilst Photon seems really good for creating Battle Royale style cloud based multiplayer games, I want something that is more local-network-only. With this in mind I turned to Unity’s new (in-preview), MLAPI multiplayer networking.

MLAPI shares some common features with Photon, e.g. there are objects to help sync transforms and animations. There is also a NavMeshAgent sync too. Where it seems to really differ is it is more IP client/server based, rather than cloud Lobby/Room based.

Networking modes

MLAPI makes heavy use of modes. The basic networking modes are Server, Client, and Host.

When the code is running in server mode it really means that it is a dedicated game server. (Pictures taken from Unity’s documentation)

Dedicated Server Mode

This means that although the objects, such as a player, are created and maintained on the server, it is essentially, ‘headless’. I.e. you do not see the visuals on the server. The Xevious two player game would require three applications, 1 server and 2 clients. This mode seems like overkill for me as I will only ever expect to have two players.

Client mode is architecturally simple, but it is still…interesting to implement. When the application is running as a Client then it is given created objects but it can still own objects. I.e. it is responsible for them but ultimately the server is in charge of replicating them to other clients. This mode-like way of working can be tricky. You often write code like this:

private void Update()
{
if (IsClient)
{
transform.Translate(_speed * Time.deltaTime * movement);

However, I found MLAPI to be a little more confusing than Photon with the way it handles the modes. To explain this further I need to introduce the final mode, Host.

Listen Server or Hosted

Hosted mode makes the application both a server and a client. For an Xevious two player game it means we only need two applications or rather two clients. However, one of the clients is additionally acting as the server. This has a couple of ramifications for implementation and run-time. For run-time it means the process is having to do the work of both a dedicated server and a dedicated client. However, because there is no network latency it means that user running this server might be at an advantage. From an implementation point of view it exposes the additional complexity of the MLAPI modes.

Consider the following code inside the main player’s Ship prefab, which derives from NetworkBehaviour.

void Start()
{
print($"IsHost {IsHost} IsServer {IsServer} IsClient {IsClient}");

What should happen when the Ship is created on the Host vs. when it is created on the stand-alone client player? Assuming the same mode-approach as Photon I would expect the following.

IsHost true IsServer true IsClient true

When the app is the host then it automatically becomes both a server and client. If the object is owned by the Host’s player’s instance then I would expect the above output. For player1, who is created on the Host, that is what I will see.

However, I get exactly the same result for objects created on behalf of the client, i.e. player2. That is not what I expect. Whilst I understand that technically it is accurate, since the Host/Server is creating that object I think it is pretty confusing. To resolve this problem you have to turn to yet another mode helper, IsLocalPlayer.

if (IsLocalPlayer)
{
print("Ship is local player");
Player = PlayerManager.PlayerType.One;
}
else
{
print("Ship is NOT the local player");
Player = PlayerManager.PlayerType.Two;
}

Whilst I admit I have written a number of mode-like applications before I have also found that they can become very confusing very quickly. I will see how my time with MLAPI continues but so far it is not too bad, you just have to treat the network modes very carefully for server choices.

Spawning

One of the issues I hit with Photon was the spawning and pooling was really awkward to use. To get the results I needed (such as sync’ of the parent/child relationship) I ended up having to write a lot of custom code.

The story for MLAPI seems a little easier, but even so I have already hit some surprises. For example, by default you can allow the MLAPI Network Manager to automatically create the player prefabs for you. But at some point you will probably need a reference to the instance. So how do you do that?

var localId = networkManager.LocalClientId;
var ship = networkManager.ConnectedClients[localId].PlayerObject;

Okay, that seems really easy. Except…that only works on the server. If your app is running as a client you do not get a LocalClientId, well you do but it is 0. Plus the ConnectedClients is not populated on the client. This is where mode-like apps can become painful. Maybe it really is a good thing because where the hybrid host is both client & server then it is very tempting to believe you can write one component that covers both requirements. It is a much better design choice to keep their responsibilities nice and separate.

The Test

The final test shared many of the same type of changes as I needed for the Photon variant. But since the MLAPI services were all LAN based (I suspect that even running on the same machine the traffic is still going up and down the network stack) the lag was noticeably reduced. However, now the acid test, spawning the enemy…

--

--