Valhalla Odyssey Multiplayer

English
2024/11/24

Lobbies

This is the publishing of some notes I took while learning how to make a working lobby. This isn't a detailed tutorial (I recommend not only the Unity Docs but also Code Monkey's excellent video if you're completely new to the topic). In this article I just want to show some features that might be useful to think about when making your own lobbies. Really the main reason for this article is styling practice to find a good format I like for this. To test some CSS and HTML out for making nice code blocks. My next two post will feature more code and written examples for why things work the way they do. Features I implemented

One key aspect of a good multiplayer game is letting player's host a lobby in which their friends can directly join them in. There are many ways to achieve this, but thankfully Unity makes this a bit easier (albeit confusing at first). Unity's cloud services and their C# libraries for Lobbies, Authentication, and Lobby Service do a lot of the heavy lifting for you. But as with any good set of tools, you still have to know how to wield them to create something useful. If you decide to create your own system with these tools like I have, there are a few things to keep in mind. - The lobby state is static - Updates for one player (someone joining / leaving / changing user names / etc.) Are not sent to other players. You have to create your own system to update them. - Unity only allows so many messages through their system or you will get rate limited. So you need to pace how often you let players send updates - How you decide to authenticate players is also important, if your game doesn't need to really track player's data or anything like that (Valhalla Odyssey doesn't) then unity's base authentication service is perfect for that. You can let player's sign in anonymously when they load up the lobby scene

    AuthenticationService.Instance.SignedIn += () => { Debug.Log("Signed in " + AuthenticationService.Instance.PlayerId); };
    await AuthenticationService.Instance.SignInAnonymouslyAsync();
Once you get a lobby set up you do need to send some kind of heartbeat to unity's services or it will be labeled as expired and can be deleted (although on their documentation it seems the let you leave it up for an hour. I have mine set to 30 seconds).

Show time

Once the host clicks start in the lobby there are quite a few dominos that need to fall in order to get the game running for everyone.

Creating the relay

The host needs to create the relay and then start the network manager. Once that is initialized the other clients will use the Join Relay function

  public async Task CreateRelay()
  {
      try
      {
          Allocation allocation = await RelayService.Instance.CreateAllocationAsync(3);

          string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);

          Debug.Log("Join Code: " + joinCode);

          RelayServerData relayServerData = new RelayServerData(allocation, "dtls");

          NetworkManager.Singleton.GetComponent().SetRelayServerData(relayServerData);
          NetworkManager.Singleton.StartHost();

          Debug.Log("Host started with Player ID: " + AuthenticationService.Instance.PlayerId);
          return joinCode;

      }
      catch(RelayServiceException ex)
      {
          Debug.LogException(ex);
          return null;
      }
  }

To spare on the details since this is already a long post, join relay is pretty similar and the documentation explains its well here: https://docs-multiplayer.unity3d.com/netcode/current/relay/ After creating the relay and before we load the next level, we need to act fast on some things to avoid errors. So the current event manager needs to be destroyed since we will be getting a different one in the next level. The same is true for the main camera, since each player prefab has its own camera attached it.

       if (IsLobbyHost())
       {
           GameObject.Destroy(MainCamera);
           GameObject.Destroy(eventManager);
           Debug.Log("Host confirmed, changing level.");
           //Levels will be dynamic based on playe choices but for this demo we have only made the one level.
           NetworkManager.Singleton.SceneManager.LoadScene("Stronghold_1", LoadSceneMode.Single);
       }
    

Initializing the Game Scene

Joining is actually pretty easy and straight forward since there are a lot of resources online in the manuals and on YouTube that do a good job explain it. where the real challenge begins is initializing all the systems you have made and getting everything ready and working on all clients. One error I encountered on my first couple of test runs was that enemies were not spawning, or a single enemy would spawn into the level. And I thought how could this be? The issue was when and how code was being executed. The "Manager" object I had that was in control of spawning enemies was doing it's budget and spawning calculations before the player's were fully loaded. And part of what the Manager does is update the player's UI with round/wave information. So as it starts going it realizes there is no player UI and would hang. But this isn't an issue that can simply be fixed by delaying when that executes. What if the players load in late, have high ping, or some other issue arises? Where questions I had to consider while I thought how I can fix my class. So my solution was to grab the Network Manager, and if it was null at the start, to start a coroutine that would wait for it to initialize and then the Game mode manager could begin its initialization, and properly update the players, and network manager(this is needed because spawning game objects over the internet means we have to use network prefabs). A similar issue persisted those first few attempts with the player UI and their data class. The two scripts need each other to work properly. And a similar solution of a coroutine got those dominos to fall into place and get everything set up.

       StartCoroutine(WaitForPlayerData());
       playerData = player.GetComponent();

    private void Initialize()
    {
        // Subscribe to importantEvents events
        healthAndArmorComponent.OnHealthChanged += UpdateHealthDisplay;
        healthAndArmorComponent.OnArmorChanged += UpdateArmorDisplay;
        UpdateSoulLabel();
        OnUIInitialized?.Invoke(this);
    }
Using coroutines and events were a God send while figuring out the best way to set up a multiplayer match. Similarly how using interfaces, and creating several small behavior classes for enemy NPCs has allowed for some really cool possible combinations to make challenging unique enemies with different abilities and behaviors. I'm going to cut this off here, there's obviously so much more that went into this. But this kind of gives the gist of how you can quickly get up a working multiplayer demo. Unity Lobby + Relay. And once the game is loaded for all clients, you need to ensure that every script is initialized properly for the server and for the clients. Don't forget to use

if(IsServer) or  if (IsOwner) 
when objects need to be spawned in or when a local player is performing an action only on their characters. If this is something you're working on and somehow find this, if YouTube or the manuals aren't getting you the help you need, feel free to email/ message me on LinkedIn and I'll try to help you troubleshoot. Best of luck in all your projects!