Skip to main content

Asynchronous multiplayer: Lobby functionality

Creating the lobby manager

In LobbyUIManager let’s create references to all of the UI elements and a private reference to our PersistentLobbyManager, with a public method to set its value.

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Elympics.Models.Authentication;
using ElympicsLobbyPackage.Authorization;
using ElympicsLobbyPackage.Session;
using ElympicsLobbyPackage;
using Elympics;

public class LobbyUIManager : MonoBehaviour
{
[SerializeField] private Image playerAvatar;
[SerializeField] private TextMeshProUGUI playerNickname;
[SerializeField] private TextMeshProUGUI playerEthAddress;

[SerializeField] private List<(TextMeshProUGUI, TextMeshProUGUI)> leaderboardContent = new List<(TextMeshProUGUI, TextMeshProUGUI)>();
[SerializeField] private TextMeshProUGUI leaderboardTimer;
[SerializeField] private TextMeshProUGUI playButtonText;

[SerializeField] private GameObject connectWalletButton;
[SerializeField] private GameObject authenticationInProgressScreen;
[SerializeField] private GameObject matchmakingInProgressScreen;
[SerializeField] private GameObject errorScreen;
[SerializeField] private TextMeshProUGUI errorMessage;

private string playQueue = null;
private string leaderboardQueue = null;
private LeaderboardClient leaderboardClient = null;
private PersistentLobbyManager persistentLobbyManager = null;
public void SetPersistentLobbyManager(PersistentLobbyManager newValue) => persistentLobbyManager = newValue;
}

In the PersistantLobbyManager let’s create references to Web3Wallet and SessionManager.

Let’s create a method for setting the LobbyUIManager reference and setting its PersistentLobbyManager.

In Start() let’s get their values from the ElympicsExternalCommunicator instance and call the LobbyUIManager set up method.

After that let’s call ElympicsExternalCommunicator.Instance.GameStatusCommunicator.ApplicationInitialized() to let the PlayPad know that our game application has loaded.

using UnityEngine;
using ElympicsLobbyPackage.Blockchain.Wallet;
using ElympicsLobbyPackage.Session;
using ElympicsLobbyPackage;
using Elympics;
using Cysharp.Threading.Tasks;

public class PersistentLobbyManager : MonoBehaviour
{
private SessionManager sessionManager = null;
private Web3Wallet web3Wallet = null;
private LobbyUIManager lobbyUIManager = null;

private void Start()
{
GameObject elympicsExternalCommunicator = ElympicsExternalCommunicator.Instance.gameObject;
sessionManager = elympicsExternalCommunicator.GetComponent<SessionManager>();
web3Wallet = elympicsExternalCommunicator.GetComponent<Web3Wallet>();
SetLobbyUIManager();

ElympicsExternalCommunicator.Instance.GameStatusCommunicator.ApplicationInitialized();
}

private void SetLobbyUIManager()
{
lobbyUIManager = FindObjectOfType<LobbyUIManager>();
lobbyUIManager.SetPersistentLobbyManager(this);
}
}

In LobbyUIManager let’s create a method for turning the authentication screen on and off.

public void SetAuthenticationScreenActive(bool newValue) => authenticationInProgressScreen.SetActive(newValue);

In the PersistentLobbyManager let's create an asynchronous method that will turn on the Authentication screen and attempt authenticating the player. If it doesn't succeed it will await identification, and only then turn off the authentification screen.

private async UniTask AttemptStartAuthenticate()
{
lobbyUIManager.SetAuthenticationScreenActive(true);
if (!ElympicsLobbyClient.Instance.IsAuthenticated || ElympicsLobbyClient.Instance.WebSocketSession.IsConnected)
{
await sessionManager.AuthenticateFromExternalAndConnect();
}
lobbyUIManager.SetAuthenticationScreenActive(false);
}

We then call this method in Start() without awaiting result.

private void Start()
{
GameObject elympicsExternalCommunicator = ElympicsExternalCommunicator.Instance.gameObject;
sessionManager = elympicsExternalCommunicator.GetComponent<SessionManager>();
web3Wallet = elympicsExternalCommunicator.GetComponent<Web3Wallet>();
SetLobbyUIManager();

ElympicsExternalCommunicator.Instance.GameStatusCommunicator.ApplicationInitialized();

AttemptStartAuthenticate().Forget();
}

In LobbyUIManager let’s create a method that will set correct ui values and remembered queue names based on authentication methods.

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Elympics.Models.Authentication;
using ElympicsLobbyPackage.Authorization;
using ElympicsLobbyPackage.Session;
using ElympicsLobbyPackage;
using Elympics;

public class LobbyUIManager : MonoBehaviour
{
[SerializeField] private Image playerAvatar;
[SerializeField] private TextMeshProUGUI playerNickname;
[SerializeField] private TextMeshProUGUI playerEthAddress;

[SerializeField] private List<(TextMeshProUGUI, TextMeshProUGUI)> leaderboardContent = new List<(TextMeshProUGUI, TextMeshProUGUI)>();
[SerializeField] private TextMeshProUGUI leaderboardTimer;
[SerializeField] private TextMeshProUGUI playButtonText;

[SerializeField] private GameObject connectWalletButton;
[SerializeField] private GameObject authenticationInProgressScreen;
[SerializeField] private GameObject matchmakingInProgressScreen;
[SerializeField] private GameObject errorScreen;
[SerializeField] private TextMeshProUGUI errorMessage;

private string playQueue = null;
private string leaderboardQueue = null;
private LeaderboardClient leaderboardClient = null;
private PersistentLobbyManager persistentLobbyManager = null;
public void SetPersistentLobbyManager(PersistentLobbyManager newValue) => persistentLobbyManager = newValue;

public void SetAuthenticationScreenActive(bool newValue) => authenticationInProgressScreen.SetActive(newValue);

public void SetLobbyUIVariant( SessionManager sessionManager)
{
// gathering info for further evaluation
Capabilities capabilities = sessionManager.CurrentSession.Value.Capabilities;
var currentAuthType = ElympicsLobbyClient.Instance.AuthData.AuthType;
bool isGuest = currentAuthType is AuthType.ClientSecret or AuthType.None;

// UI elements adjustments logic
playButtonText.text = isGuest ? "Train now" : "Play now";
playerAvatar.gameObject.SetActive(!isGuest);
playerNickname.gameObject.SetActive(!isGuest);
if (!isGuest)
{
playerNickname.text = sessionManager.CurrentSession.Value.AuthData.Nickname;

}
playerEthAddress.gameObject.SetActive(!isGuest && !capabilities.IsTelegram());
connectWalletButton.SetActive((capabilities.IsEth() || capabilities.IsTon()) && isGuest);


// adjusting queues
playQueue = currentAuthType switch
{
AuthType.Telegram => "telegram",
AuthType.EthAddress => "eth",
_ => "training",
};

leaderboardQueue = currentAuthType == AuthType.Telegram ? "telegram" : "eth";
}

After that let’s create methods that will create a leaderboard client, fetch top 5 entries and display them.

public void CreateLeaderboardClient()
{
var pageSize = 5; // Depends on display design - in this game we show top 5
var gameVersion = LeaderboardGameVersion.All; // Worth changing to Current if new version contains important balance changes
var leaderboardType = LeaderboardType.BestResult; // Adjust to the type of game
var customTimeScopeFrom = "2023-07-07T12:00:00+02:00";
var customTimeScopeTo = "2023-07-14T12:00:00+02:00";

var timeScopeObject = new LeaderboardTimeScope(DateTimeOffset.Parse(customTimeScopeFrom), DateTimeOffset.Parse(customTimeScopeTo));
leaderboardClient = new LeaderboardClient(pageSize, timeScopeObject, leaderboardQueue, gameVersion, leaderboardType);
}


public void FetchLeaderboardEntries() => leaderboardClient.FetchFirstPage(DisplayTop5Entries);

private void DisplayTop5Entries(LeaderboardFetchResult result)
{
//reseting leaderboard
foreach(var leaderboardRow in leaderboardContent)
{
leaderboardRow.Item1.text = "";
leaderboardRow.Item2.text = "";
}

for (int i = 0; i < 5 && i < result.Entries.Count; i++)
{
leaderboardContent[i].Item1.text = result.Entries[i].Nickname;
leaderboardContent[i].Item2.text = result.Entries[i].Score.ToString();
}
}

When setting up a lobby variant let’s create a leaderboardClient and fetch the leaderboardEntries.

public void SetLobbyUIVariant( SessionManager sessionManager)
{
// gathering info for further evaluation
Capabilities capabilities = sessionManager.CurrentSession.Value.Capabilities;
var currentAuthType = ElympicsLobbyClient.Instance.AuthData.AuthType;
bool isGuest = currentAuthType is AuthType.ClientSecret or AuthType.None;

// UI elements adjustments logic
playButtonText.text = isGuest ? "Train now" : "Play now";
playerAvatar.gameObject.SetActive(!isGuest);
playerNickname.gameObject.SetActive(!isGuest);
if (!isGuest)
{
playerNickname.text = sessionManager.CurrentSession.Value.AuthData.Nickname;

}
playerEthAddress.gameObject.SetActive(!isGuest && !capabilities.IsTelegram());
connectWalletButton.SetActive(capabilities.IsEth() || capabilities.IsTon() && isGuest);


// adjusting queues
playQueue = currentAuthType switch
{
AuthType.Telegram => "telegram",
AuthType.EthAddress => "eth",
_ => "training",
};

leaderboardQueue = currentAuthType == AuthType.Telegram ? "telegram" : "eth";

CreateLeaderboardClient();
FetchLeaderboardEntries();
}

In PersistentLobbyManager we call the UI method to set its variant based on the authentication method.

private async UniTask AttemptStartAuthenticate()
{
lobbyUIManager.SetAuthenticationScreenActive(true);
if (!ElympicsLobbyClient.Instance.IsAuthenticated || ElympicsLobbyClient.Instance.WebSocketSession.IsConnected)
{
await sessionManager.AuthenticateFromExternalAndConnect();
}
lobbyUIManager.SetAuthenticationScreenActive(false);

lobbyUIManager.SetLobbyUIVariant(sessionManager);
web3Wallet.WalletConnectionUpdated += ReactToAuthenticationChange;
}

Creating button functionalities

Authentication and reauthentication

In the PersistentLobbyManager let’s create a method for connecting to a Web3Wallet.

public async void ConnectToWallet()
{
await sessionManager.ConnectToWallet();
}

In the LobbyUIManager, let's create a method that will tell our PersistentManager to connect our wallet and turn on the authenticationScreen, which we will call by pressing the button on the UI.

public void ConnectToWallet()
{
persistentLobbyManager.ConnectToWallet();
authenticationInProgressScreen.SetActive(true);
}

In our scene, find the created earlier ConnectToWallet button, and in its OnClick() event call the ConnectToWallet() method from the LobbyUIManager object.

We will turn the authentication screen off when the authentication is completed. We detect it by subscribing to the Web3Wallet.WalletConnectionUpdated event in our PersistentLobbyManager.

Since we want it to run only in the lobby and while not in the matchmaking, let’s create a variable that will store the applications current state. Let’s also create a public setter for it, which will detect if we’ve set the state to Lobby and attempt reauthentication.

public enum AppState { Lobby, Matchmaking, Gameplay}
private AppState appState = AppState.Lobby;
public void SetAppState(AppState newState)
{
appState = newState;
if (appState == AppState.Lobby)
{
SetLobbyUIManager();
AttemptReAuthenticate();
}
}

Let’s create a method that we will subscribe to the WalletConnectionUpdated event in the AttemptStartAuthenticate() method.

private async UniTask AttemptStartAuthenticate()
{
lobbyUIManager.SetAuthenticationScreenActive(true);
if (!ElympicsLobbyClient.Instance.IsAuthenticated || ElympicsLobbyClient.Instance.WebSocketSession.IsConnected)
{
await sessionManager.AuthenticateFromExternalAndConnect();
}
lobbyUIManager.SetAuthenticationScreenActive(false);

lobbyUIManager.SetLobbyUIVariant(sessionManager);
web3Wallet.WalletConnectionUpdated += ReactToAuthenticationChange;
}

public void ReactToAuthenticationChange(WalletConnectionStatus status)
{
if (appState == AppState.Lobby)
{
AttemptReAuthenticate();
}
}
public async void AttemptReAuthenticate()
{
await sessionManager.TryReAuthenticateIfWalletChanged();
lobbyUIManager.SetLobbyUIVariant(sessionManager);
lobbyUIManager.SetAuthenticationScreenActive(false);
}

Play button

In our LobbyUIManager let’s create a method for starting the game, which we will call by clicking the button on the UI. It should block the input with the matchmaking screen. It should also set our application state to Matchmaking.

public async void PlayGame()
{
ElympicsLobbyClient.Instance.RoomsManager.StartQuickMatch(playQueue);
persistentLobbyManager.SetAppState(PersistentLobbyManager.AppState.Matchmaking);
matchmakingInProgressScreen.SetActive(true);
}

In our scene find the Play button and in its OnClick() event, call the PlayGame() method from the LobbyUIManager object.

Account info button

After that let’s create a method that will get the PlayPad to show our account information. We will call it by clicking on the player avatar.

public void ShowAccountInfo()
{
ElympicsExternalCommunicator.Instance.WalletCommunicator.ExternalShowAccountInfo();
}

In our scene find the Player avatar, which should be a button, and in it's OnClick() event, call the ShowAccountInfo() method from the LobbyUIManager object.

Calling lobby methods from gameplay

In our GameManager if the game has started or ended, the client instance needs to notify our Persistent lobby manager. For that we will make our GameManager implement the IClientHandler interface. It contains the OnMatchEnded method, in which we will send the message to the PlayPad.

public void OnMatchEnded(Guid matchId)
{
ElympicsExternalCommunicator.Instance.GameStatusCommunicator.GameFinished(score.Value);
}

In our Initialize method, let’s tell our PersistantLobbyManager to set the app state to gameplay.

public void Initialize()
{
timer.Value = initialTimerValue;
if (Elympics.IsClient)
ElympicsExternalCommunicator.Instance.gameObject.GetComponent<PersistantLobbyManager>().SetAppState(PersistentLobbyManager.AppState.Gameplay);
}

Let’s add a button to the gameOver screen that will let us return to the lobby, and create a method for it in the DisplayManager.

public void ReturnToLobby()
{
SceneManager.LoadScene(0);
ElympicsExternalCommunicator.Instance.gameObject.GetComponent<PersistentLobbyManager>().SetAppState(PersistentLobbyManager.AppState.Lobby);
}

In our DisplayManager let's create a private asynchronous method for getting and displaying the respect earned by the player during the match. We’ll start by checking if the player is authenticated with Wallet or Telegram, and if they’re not, we’ll set the respect message to notify the player that as a guest they cannot earn respect.

If they are logged in, we need to create a RespectService object and fetch the respect value from it.

private async void DisplayRespect()
{
if (ElympicsLobbyClient.Instance == null
|| !ElympicsLobbyClient.Instance.IsAuthenticated
|| ElympicsLobbyClient.Instance.AuthData.AuthType is AuthType.None or AuthType.ClientSecret)
{
respectText.text = "Log in to earn respect";
}
else
{
var respectService = new RespectService(ElympicsLobbyClient.Instance, ElympicsConfig.Load());
var matchId = FindObjectOfType<PersistentLobbyManager>().CachedMatchId;
var respectValue = await respectService.GetRespectForMatch(matchId);

respectText.text = "Respect earned: " + respectValue.Respect.ToString();
}
}

You can access matchId by subscribing to ElympicsLobbyClient.Instance.RoomsManager.MatchDataReceived in the lobby scene and caching received data.
For example:

public class PersistentLobbyManager : MonoBehaviour
{
. . .

public Guid CachedMatchId { get; private set; }

private void Start()
{
ElympicsLobbyClient.Instance!.RoomsManager.MatchDataReceived += RememberMatchId;
. . .
}

. . .
}

Finally, let's call this method at the end of the DisplayGameoverScreen() method.