Unity Getting Started Guide

Overview

To help you get started learning about our services and their implementation, you can download Light Fantastic. Light Fantastic is our sample game which shows off a wide range of AccelByte services. By looking at our sample game and this guide, you’ll be able to see how our services can be implemented in your own game.

Tutorials

Build Light Fantastic

Prerequisites

Before you build Light Fantastic, make sure you have the following:

Build the Light Fantastic Game Client

  1. Open Light Fantastic in Unity.
  2. Ensure the Assets/Resources/AccelByteSDKConfig.json fields are all correct, especially the ClientId and ClientSecret.
{
"PublisherNamespace": "accelbyte",
"Namespace": "lightfantastic",
"UseSessionManagement": true,
"BaseUrl": "https://api.demo.accelbyte.io",
"LoginServerUrl": "https://api.demo.accelbyte.io",
"IamServerUrl": "https://api.demo.accelbyte.io/iam",
"PlatformServerUrl": "https://api.demo.accelbyte.io/platform",
"BasicServerUrl": "https://api.demo.accelbyte.io/basic",
"LobbyServerUrl": "wss://demo.accelbyte.io/lobby/",
"CloudStorageServerUrl": "https://api.demo.accelbyte.io/binary-store",
"TelemetryServerUrl": "https://api.demo.accelbyte.io/telemetry",
"GameProfileServerUrl": "https://api.demo.accelbyte.io/soc-profile",
"StatisticServerUrl": "https://api.demo.accelbyte.io/statistic",
"LeaderboardServerUrl": "https://api.demo.accelbyte.io/leaderboard",
"AgreementServerUrl": "https://api.demo.accelbyte.io/agreement",
"ClientId": "CONTACT_US",
"ClientSecret": "CONTACT_US",
"RedirectUri": "http://127.0.0.1"
}
  1. Modify GAME_VERSION (and DS_TARGET_VERSION) in Assets\Scripts\AccelByte\LightFantasticConfig.cs. If there are no changes in the gameplay and server version, DS_TARGET VERSION doesn't have to be updated.

  2. In the Unity Editor, open the File menu and select BuildSettings. Select PC, Mac & Linux Standalone as the platform, then select your architecture from the dropdown list. Ensure that Server Build is cleared and that Development Build is selected.

    unity-guide

  3. Click Build.

Build the Light Fantastic Game Server

  1. Open Light Fantastic in Unity.
  2. Ensure the Assets/Resources/AccelByteServerSDKConfig.json fields are all correct, especially the ClientId and ClientSecret.
{
"PublisherNamespace": "accelbyte",
"Namespace": "lightfantastic",
"BaseUrl": "https://demo.accelbyte.io",
"DSMServerUrl": "http://justice-dsm-service/dsm",
"IamServerUrl": "https://demo.accelbyte.io/iam",
"ClientId": "CONTACT_US",
"ClientSecret": "CONTACT_US",
"RedirectUri": "http://127.0.0.1"
}
  1. In the Unity Editor, open the File menu and select BuildSettings. Select PC, Mac & Linux Standalone as the platform, then choose Linux from the Target Platform dropdown list. Ensure that Server Build is selected.

    unity-guide

  2. Click Build.

Build the Light Fantastic Local Game Server

  1. Open Light Fantastic in Unity.
  2. Ensure the Assets/Resources/AccelByteServerSDKConfig.json fields are all correct, especially the ClientId and ClientSecret.
{
"PublisherNamespace": "accelbyte",
"Namespace": "lightfantastic",
"BaseUrl": "https://demo.accelbyte.io",
"DSMServerUrl": "https://demo.accelbyte.io/dsmcontroller",
"IamServerUrl": "https://demo.accelbyte.io/iam",
"ClientId": "CONTACT_US",
"ClientSecret": "CONTACT_US",
"RedirectUri": "http://127.0.0.1"
}
  1. In the Unity Editor, open the File menu and select BuildSettings. Select PC, Mac & Linux Standalone as the platform, then choose Windows from the Target Platform dropdown list. Ensure that Server Build is selected.

    unity-guide

  2. Click Build.

  3. To run the server, add the parameter localds when running the executable.

    LightFantastic.exe localds

Create and Register a Player Account

In Light Fantastic, you can create a player account to play the game. As a developer you can work with accounts in several ways, including assigning different accounts different roles and permissions.

Our User Account Management service includes many ways of authenticating a user, such as headless auth via unique identifiers, or login with third parties such asSteam, Epic, Xbox, or Playstation.

In this guide, we’ll focus on the conventional email registration, verification, and authentication method.

One helpful feature of the AccelByte SDK is that it manages its own Unity GameObject instance. As such, you don’t need to create an empty GameObject to manage AccelByte services. When you call an AccelByte value or function, the SDK will create and manage itself in the active scene:

unity-guide

Here’s what happens when a player opens our game:

using AccelByte.Api;
using AccelByte.Models;
using AccelByte.Core;
public class AccelByteAuthenticationLogic : MonoBehaviour
{
private User abUser;
private UserData abUserData;
void Awake()
{
abLobbyLogic = GetComponent<AccelByteLobbyLogic>();
abUserProfileLogic = GetComponent<AccelByteUserProfileLogic>();
abUserStatisticLogic = GetComponent<AccelByteStatisticLogic>();
abLeaderboardLogic = GetComponent<AccelByteLeaderboardLogic>();
//Initialize AccelByte Plugin
abUser = AccelBytePlugin.GetUser();
useSteam = cmdLine.ParseCommandLine();
}
}

The first thing we do is initialize our plugin in the scene, using the GetUser function. This will retrieve an empty user object, which we can then log into to input the necessary data. However, before we log a user in we must first register that user.

Registration requires just two functions. First is the registration code:

public void Register()
{
System.DateTime dob = new System.DateTime(int.Parse(UIHandlerAuthComponent.registerDobYear.text),
int.Parse(UIHandlerAuthComponent.registerDobMonth.text), int.Parse(UIHandlerAuthComponent.registerDobDay.text));
string country = AccelByteManager.Instance.countryObjectsCache[UIHandlerAuthComponent.registerCountryDropdown.value].code;
UIHandlerAuthComponent.registerErrorText.text = " ";
abUser.Register(UIHandlerAuthComponent.registerEmail.text, UIHandlerAuthComponent.registerPassword.text, UIHandlerAuthComponent.registerDisplayName.text,
country, dob, OnRegister);
}

This code is hooked up to a few input fields and dropdown menus. First, we convert the date of birth text fields to System.DateTime format. Then, we call the User.Register function, which sends the player’s information to the server. By default, to register a player we need:

  • A valid email address
  • The player’s desired password
  • The player’s desired display name
  • Their current region in ISO 3166–1 alpha-2 format
  • Their date of birth in DateTime format
  • Callback to receive the data

The user registration form in Light Fantastic looks like this:

unity-guide

Our Register() function is tied to the Register button’s OnClick EventListener. When the event is fired, it sends the registration data to the server and the server sends back a response. We need a callback to interpret the data sent back from the server, as seen below:

private void OnRegister(Result<RegisterUserResponse> result)
{
if (result.IsError)
{
UIHandlerAuthComponent.registerErrorText.text = result.Error.Message;
Debug.Log("Register failed:" + result.Error.Message);
Debug.Log("Register Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Register successful.");
UIHandlerAuthComponent.loginEmail.text = UIHandlerAuthComponent.registerEmail.text;
UIHandlerAuthComponent.loginPassword.text = UIHandlerAuthComponent.registerPassword.text;
Login();
//Show Verification Panel
UIElementHandler.ShowExclusivePanel(ExclusivePanelType.REGISTER);
}
}

Our registration callback takes one generic class of Result which we populate with various other member classes depending on the task at hand. In this case, the result will be populated with UserData. If there are any errors in the user data, information about the error is displayed for the player. Several errors are possible, such as their email address already being registered or one of the fields containing invalid characters. If the request was successful, the registration flow continues. By default, all new players must verify their email address to complete registration. Newly registered players will receive the email seen below:

unity-guide

Before we can call our Verify() function we must log our player in so they are authorized to make more calls against the service. You can see in our Register code that if a login is successful, we take the username and password from the registration UI fields and make a LoginWithUsername call.

private void OnRegister(Result<RegisterUserResponse> result)
{
if (result.IsError)
{
UIHandlerAuthComponent.registerErrorText.text = result.Error.Message;
Debug.Log("Register failed:" + result.Error.Message);
Debug.Log("Register Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Register successful.");
UIHandlerAuthComponent.loginEmail.text = UIHandlerAuthComponent.registerEmail.text;
UIHandlerAuthComponent.loginPassword.text = UIHandlerAuthComponent.registerPassword.text;
Login();
//Show Verification Panel
UIElementHandler.ShowExclusivePanel(ExclusivePanelType.REGISTER);
}
}

Then the Registration panel fades out and lets our Login Callback handle things. In this case, the Login Callback checks for errors and attempts to GetUserDetails:

public void GetUserDetails()
{
abUser.GetData(OnGetUserData);
}
private void OnGetUserData(Result<UserData> result)
{
if (result.IsError)
{
Debug.Log("GetUserData failed:" + result.Error.Message);
Debug.Log("GetUserData Response Code: " + result.Error.Code);
}
else if (!result.Value.eligible)
{
UIElementHandler.HideLoadingPanel();
gameObject.GetComponent<AccelByteAgreementLogic>().GetUserPolicy();
}
else
{
abUserData = result.Value;
UIHandlerAuthComponent.displayName.text = "DisplayName: " + abUserData.displayName;
UIHandlerAuthComponent.userId.text = "UserId: " + abUserData.userId;
UIHandlerAuthComponent.sessionId.text = "SessionId: " + abUser.Session.AuthorizationToken;
if (!abUserData.emailVerified && !useSteam)
{
UIElementHandler.HideNonExclusivePanel(NonExclusivePanelType.LOADING);
UIElementHandler.ShowExclusivePanel(ExclusivePanelType.VERIFY);
}
else
{
//Progress to Main Menu
if (!useSteam)
{
UIElementHandler.HideLoadingPanel();
}
UIElementHandler.ShowExclusivePanel(ExclusivePanelType.MAIN_MENU);
UIElementHandler.ShowNonExclusivePanel(NonExclusivePanelType.PARENT_OF_OVERLAY_PANELS);
abUserProfileLogic.Init();
abLeaderboardLogic.Init();
abUserStatisticLogic.UpdatePlayerStatisticUI();
abLobbyLogic.ConnectToLobby();
}
}
}

We use the abUserData object to store and reference the relevant UserData that the server sends back. In this case, we display a UserId and SessionId and then check that the player has verified their email. If they have, we send them to the Main Menu of the game; if not we send them to the Verification panel:

unity-guide

To verify we simply copy the verification code from the email we received into the box and click Verify, which is hooked to our Verify() function via the button’s OnClick EventListener. There are two verification functions: the event that sends the code to the server, and an event that requests a new code be emailed to a player:

public void VerifyRegister()
{
abUser.Verify(UIHandlerAuthComponent.verificationCode.text, OnVerify);
}
public void ResendVerification()
{
abUser.SendVerificationCode(OnResendVerification);
}

You can see the same pattern we’ve been following. Every request parses it’s response through a callback:

private void OnVerify(Result result)
{
if (result.IsError)
{
Debug.Log("Verification failed:" + result.Error.Message);
Debug.Log("Verification Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Verification successful.");
UIElementHandler.ShowExclusivePanel(ExclusivePanelType.MAIN_MENU);
UIElementHandler.ShowNonExclusivePanel(NonExclusivePanelType.PARENT_OF_OVERLAY_PANELS);
abUserProfileLogic.Init();
abLeaderboardLogic.Init();
abUserStatisticLogic.UpdatePlayerStatisticUI();
abLobbyLogic.ConnectToLobby();
}
}

We check for errors, and if there are none, progress the player to our main menu where we have finished the registration process. Next time the player logs in, we’ll take their entered email and password from the UI fields, call the Login function, and send the email and password to the server.

public void Login()
{
if (string.IsNullOrEmpty(UIHandlerAuthComponent.loginEmail.text))
{
if (string.IsNullOrEmpty(UIHandlerAuthComponent.loginPassword.text))
ShowErrorMessage(true, "Please fill your email address and password");
else
ShowErrorMessage(true, "Please fill your email address");
}
else if (string.IsNullOrEmpty(UIHandlerAuthComponent.loginPassword.text))
{
ShowErrorMessage(true, "Please fill your password");
}
else
{
loginType = E_LoginType.Username;
abUser.LoginWithUsername(UIHandlerAuthComponent.loginEmail.text, UIHandlerAuthComponent.loginPassword.text, OnLogin);
UIElementHandler.ShowLoadingPanel();
}
}
private void OnLogin(Result result)
{
if (result.IsError)
{
if (!useSteam)
{
UIElementHandler.HideLoadingPanel();
}
Debug.Log("Login failed:" + result.Error.Message);
Debug.Log("Login Response Code: " + result.Error.Code);
//Show Error Message
if (loginType == E_LoginType.Launcher)
{
ShowErrorMessage(true, "Can't login from launcher.");
}
else if (loginType == E_LoginType.Username)
{
ShowErrorMessage(true, "Incorrect email address or password.");
}
else if (loginType == E_LoginType.Steam)
{
ShowErrorMessage(true, "Can't login from steam");
}
}
else
{
Debug.Log("Login successful. Getting User Data");
//Show Login Successful
//Show "Getting User Details"
GetUserDetails();
ShowErrorMessage(false);
}
}

This checks if the player has been verified, and if they have, sends them straight to the main menu so they can start playing.

unity-guide

Define Player Profiles

In Light Fantastic, we create custom attributes for player profiles that show players’ equipped gear.

unity-guide

All of the player profile logic is managed in: Assets/Scripts/Accelbyte/AccelByteUserProfileLogic.cs

Here we have set up player profiles to update after a match ends. If the player doesn’t have a profile yet, one will be created.

// reference to user profile service
private UserProfiles abUserProfiles;
// hold reference user profile content
private UserProfile myProfile;
private List<UserProfile> userProfilesCache = new List<UserProfile>();
// gameobject that store the class that handles the UI elements
private GameObject UIHandler;
// all the ui elements that used on leaderboard prefab
private UILeaderboardComponent UIHandlerLeaderboardComponent;
// class that handles all shorts of functions on UI
private UIElementHandler UIElementHandler;
public void Init()
{
// hold the userprofile service reference
if(abUserProfiles == null) abUserProfiles = AccelBytePlugin.GetUserProfiles();
// Create default user profile
var defaultUserProfile = new CreateUserProfileRequest{language = "en"};
abUserProfiles.CreateUserProfile(defaultUserProfile, OnCreateUserProfile);
// Update player profile info UI
}
// callback on create user profile
private void OnCreateUserProfile(Result<UserProfile> result)
{
if (!result.IsError)
{
myProfile = result.Value;
UpdatePlayerProfileUI();
}
else if (result.Error.Code == ErrorCode.UserProfileConflict)
{
abUserProfiles.GetUserProfile(OnGetMyProfile);
}
}

If the player already has a profile, it will be set as the current profile for the player.

// callback on getuserprofile
private void OnGetMyProfile(Result<UserProfile> result)
{
if (!result.IsError)
{
myProfile = result.Value;
UpdatePlayerProfileUI();
}
}
// update player profile UI
public void UpdatePlayerProfileUI()
{
UIHandlerUserProfileComponent.PlayerProfilePrefab.GetComponent<PlayerStatusPrefab>().UpdatePlayerProfile();
}

Player profiles can store custom attributes, which we use here to store the equipment used by the player. This utilizes Assets/Scripts/Accelbyte/AccelByteEntitlementLogic.cs

public void UpdateMine(UpdateUserProfileRequest request, ResultCallback<UserProfile> callback)
{
abUserProfiles.UpdateUserProfile(request, callback);
}
// Save equipment settings to the user profile service
public void UploadEquipment()
{
UpdateUserProfileRequest savedEquipment = new UpdateUserProfileRequest();
savedEquipment.customAttributes = activeEquipmentList.ToCustomAttribute();
UIHandlerEntitlementComponent.abUserProfileLogic.UpdateMine(savedEquipment, result =>
{
if (result.IsError)
{
Debug.Log("Failed to save current equipment!");
}
else
{
Debug.Log("Current equipment saved!");
}
});
}

UpdateUserProfileRequest in Assets/Accelbyte/Models/BasicModels.cs contains the data model for player profiles. Here, we want to save the equipment settings to customAttributes.

public class UpdateUserProfileRequest
{
[DataMember] public string firstName { get; set; }
[DataMember] public string lastName { get; set; }
[DataMember] public string language { get; set; }
[DataMember] public string avatarSmallUrl { get; set; }
[DataMember] public string avatarUrl { get; set; }
[DataMember] public string avatarLargeUrl { get; set; }
[DataMember] public string timeZone { get; set; }
[DataMember] public string dateOfBirth { get; set; }
[DataMember] public object customAttributes { get; set; }
}

Now every time a player equips gear from their inventory, their selection will be saved to their user profile. This gear will appear on the player’s character during a match.

Enable Friends

Accelbyte offers a full suite of Social features to help you build community between players of your game, including Friends, Groups, Chat, Notifications, and more. You can see how our Friends service works in Light Fantastic.

We use Websocket to run our social services, which allows us to send and receive messages along an established connection with a lot less overhead than a standard REST request. Websocket also allows for asynchronous communication, which is what allows features like chat and notifications to work.

Friends List

Here’s what our friends list looks like in Light Fantastic:

unity-guide

The following setup is required to connect to the AccelByte Social service:

public class AccelByteLobbyLogic : MonoBehaviour
{
public Lobby abLobby;
private AccelByteManager accelByteManager;
matchmakingLogic = gameObject.GetComponent<AccelByteMatchmakingLogic>();
partyLogic = gameObject.GetComponent<AccelBytePartyLogic>();
friendsLogic = gameObject.GetComponent<AccelByteFriendsLogic>();
chatLogic = gameObject.GetComponent<AccelByteChatLogic>();
private void Awake()
{
chatList = new List<string>();
accelByteManager = gameObject.GetComponent<AccelByteManager>();
//Initialize our Lobby object
abLobby = AccelBytePlugin.GetLobby();
matchmakingLogic = gameObject.GetComponent<AccelByteMatchmakingLogic>();
partyLogic = gameObject.GetComponent<AccelBytePartyLogic>();
friendsLogic = gameObject.GetComponent<AccelByteFriendsLogic>();
chatLogic = gameObject.GetComponent<AccelByteChatLogic>();
}
public void ConnectToLobby()
{
// Reset lobby to prevent dual session callback
// Each time user connect to lobby after login, it needs to renew the lobby.
abLobby = new Lobby(AccelBytePlugin.Config.LobbyServerUrl, new WebSocket(), AccelBytePlugin.GetUser().Session, new CoroutineRunner());
//Establish connection to the lobby service
abLobby.Connect();
if (abLobby.IsConnected)
{
//If we successfully connected, load our friend list.
Debug.Log("Successfully Connected to the AccelByte Lobby Service");
abLobby.SetUserStatus(UserStatus.Availabe, "OnLobby", OnSetUserStatus);
friendsLogic.ClearFriendList();
SetupLobbyUI();
}
else
{
//If we don't connect Retry.
// TODO: use coroutine to day the call to avoid spam
Debug.LogWarning("Not Connected To Lobby. Attempting to Connect...");
ConnectToLobby();
}
}
}

First thing we do is initialize our Lobby with the AccelBytePlugin.GetLobby function. This is the object we’ll use to manage our lobby connection, which we need for friends to work.

Next is our ConnectToLobby function, which handles the initial websocket connection. If this connection fails Social services will be unavailable, so upon failure we’ll try to reconnect until a connection is established. In production, it would be best to put the retry on a timer so that the service isn’t hit with too many requests.

Once we’re connected,the friends list inside AccelByteFriendsLogic.cs will load. Request:

friendsLogic.LoadFriendsList();
public void LoadFriendsList()
{
lobbyLogic.abLobby.LoadFriendsList(OnLoadFriendsListRequest);
UIHandlerLobbyComponent.friendsTabButton.interactable = false;
UIHandlerLobbyComponent.invitesTabButton.interactable = true;
}

Response:

private void OnLoadFriendsListRequest(Result<Friends> result)
{
if (result.IsError)
{
Debug.Log("LoadFriends failed:" + result.Error.Message);
Debug.Log("LoadFriends Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Loaded friends list successfully.");
for (int i = 0; i < result.Value.friendsId.Length; i++)
{
Debug.Log(result.Value.friendsId[i]);
if (!friendList.ContainsKey(result.Value.friendsId[i]))
{
friendList.Add(result.Value.friendsId[i], new FriendData(result.Value.friendsId[i], "Loading...", new DateTime(2000, 12, 30), "0"));
lastFriendUserId = result.Value.friendsId[i];
}
}
isLoadFriendDisplayName = true;
}
}

LoadFriendsList returns an array of Friend IDs, which is all the game’s code needs, but it’s helpful for players to show their friends’ usernames as well. For each friendId we receive, we can get their info using GetUserByUserId.

Request:

public void GetFriendInfo(string friendId, ResultCallback<UserData> callback)
{
AccelBytePlugin.GetUser().GetUserByUserId(friendId, callback);
}

Response:

private void OnGetFriendInfoRequest(Result<UserData> result)
{
if (result.IsError)
{
Debug.Log("OnGetFriendInfoRequest failed:" + result.Error.Message);
Debug.Log("OnGetFriendInfoRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("OnGetFriendInfoRequest sent successfully.");
string friendUserId = result.Value.userId;
friendList[friendUserId] = new FriendData(friendUserId, result.Value.displayName, friendList[friendUserId].LastSeen, friendList[friendUserId].IsOnline);
// if this is the last friend id, then continue to get friend status
if (lastFriendUserId == friendUserId)
{
ListFriendsStatus();
}
}
}

If there is an error it’s printed to the console, otherwise we can proceed. In this case, we add our friend’s display name to an array of friends.

The next thing our LoadFriendsList function does is retrieves our friends’ status.

Request:

private void ListFriendsStatus()
{
lobbyLogic.abLobby.ListFriendsStatus(OnListFriendsStatusRequest);
}

Response:

private void OnListFriendsStatusRequest(Result<FriendsStatus> result)
{
if (result.IsError)
{
Debug.Log("ListFriendsStatusRequest failed:" + result.Error.Message);
Debug.Log("ListFriendsStatusRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
if (friendList.Count > 0)
{
Debug.Log("ListFriendsStatusRequest sent successfully.");
for (int i = 0; i < result.Value.friendsId.Length; i++)
{
string friendUserId = result.Value.friendsId[i];
friendList[friendUserId] = new FriendData(friendUserId, friendList[friendUserId].DisplayName, result.Value.lastSeenAt[i], result.Value.availability[i]);
}
RefreshFriendsUI();
// Update chat list UI
if (lobbyLogic.chatLogic.activePlayerChatUserId == lobbyLogic.partyLogic.GetPartyUserId())
{
lobbyLogic.chatLogic.RefreshDisplayNamePartyChatListUI();
}
else
{
lobbyLogic.chatLogic.RefreshDisplayNamePrivateChatListUI();
}
}
}
}

First, we need to clean up the previous instance of our friends list. This involves deleting all Child UI objects in our ScrollContent, seen below:

unity-guide

Next we check for errors in retrieving our friend’s list. If there are any errors a message is shown, if not the Reset Value is assigned to our FriendsStatus object and the UI is refreshed.

internal void RefreshFriendsUI()
{
ClearFriendsUIPrefabs();
foreach (KeyValuePair<string, FriendData> friend in friendList)
{
int lastSeen = System.DateTimeOffset.Now.Subtract(friend.Value.LastSeen).Days;
string timeInfo = " days ago";
if (lastSeen == 0)
{
lastSeen = System.DateTimeOffset.Now.Subtract(friend.Value.LastSeen).Hours;
timeInfo = " hours ago";
}
if (lastSeen == 0)
{
lastSeen = System.DateTimeOffset.Now.Subtract(friend.Value.LastSeen).Minutes;
timeInfo = " minutes ago";
}
FriendPrefab friendPrefab = Instantiate(UIHandlerLobbyComponent.friendPrefab, Vector3.zero, Quaternion.identity).GetComponent<FriendPrefab>();
friendPrefab.transform.SetParent(UIHandlerLobbyComponent.friendScrollContent, false);
if (friend.Value.IsOnline == "0")
{
friendPrefab.GetComponent<FriendPrefab>().SetupFriendUI(friend.Value.DisplayName, lastSeen.ToString() + timeInfo, friend.Value.UserId);
friendPrefab.GetComponent<FriendPrefab>().SetInviterPartyStatus(lobbyLogic.partyLogic.GetIsLocalPlayerInParty());
}
else
{
bool isNotInParty = true;
if (lobbyLogic.partyLogic.GetPartyMemberList().ContainsKey(friend.Value.UserId))
{
isNotInParty = false;
}
friendPrefab.GetComponent<FriendPrefab>().SetupFriendUI(friend.Value.DisplayName, "Online", friend.Value.UserId, isNotInParty);
friendPrefab.GetComponent<FriendPrefab>().SetInviterPartyStatus(lobbyLogic.partyLogic.GetIsLocalPlayerInParty());
}
UIHandlerLobbyComponent.friendScrollView.Rebuild(CanvasUpdate.Layout);
}
}

For every friend we have in our FriendStatus array we do the following:

  • Calculate the days since they were last seen
  • Instantiate a prefab friend tile gameobject (see above)
  • Set the parent of that friend tile to scroll view If the days since the friend was last seen is 0, then the hours and minutes will also be checked. If all values are 0, the friend is shown as Online. If the days, hours, or minutes since they were last seen is greater than 0, the amount of time since they’ve been online will be displayed.

For this we instantiate a prefab of our friend tile, to which the FriendPrefab script is assigned.

public class FriendPrefab : MonoBehaviour
{
[SerializeField]
private Text usernameText;
[SerializeField]
private Text lastSeenText;
[SerializeField]
private Transform InviteButton;
private string userID;
private bool hasParty;
public void SetupFriendUI(string username, string lastSeen, string userId, bool isOnline = false)
{
usernameText.text = username;
lastSeenText.text = lastSeen;
userID = userId;
SetInviteButtonVisibility(isOnline);
}
}

We store the userId for later use with Chat, Party, or Matchmaking services. We also store the username, isonline and last seen values to display to the player. Lastly, the SetupFriendUI function assigns the string values passed in from the ListFriendsStatus response to the actual Unity UI components.

With this our friends list is displayed! Now that we can display our friends list, we need to be able to invite other players to be our friend.

Sending Invites

Sending invites consists of two components: searching for and inviting players. In Light Fantastic, the Find Friends UI looks like this:

unity-guide

By default, we offer a few ways to find other players:

  • User ID
  • Email Address
  • Third Party Platform ID

In this tutorial, we’ll use email address as our example.

To search for a player we can use GetUserByLoginId. Request:

private void FindAFriendRequest()
{
AccelBytePlugin.GetUser().SearchUsers(UIHandlerLobbyComponent.emailToFind.text, OnFindAFriendRequest);
}

Response:

private void OnFindAFriendRequest(Result<PagedPublicUsersInfo> result)
{
for (int i = 0; i < UIHandlerLobbyComponent.friendSearchScrollContent.childCount; i++)
{
Destroy(UIHandlerLobbyComponent.friendSearchScrollContent.GetChild(i).gameObject);
}
if (result.IsError)
{
Debug.Log("GetUserData failed:" + result.Error.Message);
Debug.Log("GetUserData Response Code: " + result.Error.Code);
}
else
{
if (result.Value.data.Length > 0)
{
Debug.Log("Search Results:");
Debug.Log("Display Name: " + result.Value.data[0].displayName);
Debug.Log("UserID: " + result.Value.data[0].userId);
SearchFriendPrefab friend = Instantiate(UIHandlerLobbyComponent.friendSearchPrefab, Vector3.zero, Quaternion.identity).GetComponent<SearchFriendPrefab>();
friend.transform.SetParent(UIHandlerLobbyComponent.friendSearchScrollContent, false);
// Get only the first user
// TODO: display all the user search result on the list
friend.GetComponent<SearchFriendPrefab>().SetupFriendPrefab(result.Value.data[0].displayName, result.Value.data[0].userId);
UIHandlerLobbyComponent.friendSearchScrollView.Rebuild(CanvasUpdate.Layout);
}
else
{
Debug.Log("Search Results: Not Found!");
}
}
}

First, we need to clean up the previous instance of our friends list. This involves deleting all Child UI objects in our ScrollContent.

Then we check for errors, if none are found we instantiate a prefab search result UI tile as a child of the Search UI’s ScrollContent object. Similar to our friend UI tile, the search result is populated with a Display Name and UserId.

The script on the search result is a little different however, as the search result object is responsible for sending the friend request:

unity-guide

Here’s what the script looks like:

public class SearchFriendPrefab : MonoBehaviour
{
[SerializeField]
private Text usernameText;
[SerializeField]
private Button addFriendButton;
private string userId;
public void SetupFriendPrefab(string username, string id)
{
usernameText.text = username;
userId = id;
}
public void AddFriend()
{
AccelByteManager.Instance.LobbyLogic.friendsLogic.SendFriendRequest(userId, SendFriendRequestCallback);
}
private void SendFriendRequestCallback(AccelByte.Core.Result result)
{
if (result.IsError)
{
Debug.Log("SendFriendRequest failed:" + result.Error.Message);
Debug.Log("SendFriendRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Request sent successfully.");
addFriendButton.GetComponentInChildren<Text>().text = "Sent";
addFriendButton.interactable = false;
}
}
}

As you can see, it’s quite similar to the FriendPrefab in that it holds a player’s info and is responsible for updating its own UI text objects. This prefab, however, also contains a button for sending a friend request.

The AddFriend function is hooked to the button’s EventTrigger in the Unity Editor. When clicked, it triggers SendFriendRequest, which takes a userId and the callback to send the response.

The script manages its own response in the SendFriendRequestCallback function. We check for errors, and if there are none we change the button’s UI Text to say the request was sent and mark it as unclickable.

unity-guide

Managing Invites

Now we need a way to see the invites we’ve both sent and received.

Our screen looks like this:

unity-guide

For this example we’ll look at both the Incoming Friend Requests and the Outgoing Friend Requests.

First, let’s look at outgoing friend requests:

Request:

private void GetOutgoingFriendsRequest()
{
lobbyLogic.abLobby.ListOutgoingFriends(OnGetOutgoingFriendsRequest);
}

Response:

private void OnGetOutgoingFriendsRequest(Result<Friends> result)
{
if (result.IsError)
{
Debug.Log("GetGetOutgoingFriendsRequest failed:" + result.Error.Message);
Debug.Log("GetGetOutgoingFriendsRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Loaded outgoing friends list successfully.");
foreach (string friendId in result.Value.friendsId)
{
Debug.Log("Outgoing Friend Id: " + friendId);
//Get person's name, picture, etc
Transform friend = Instantiate(UIHandlerLobbyComponent.sentInvitePrefab, Vector3.zero, Quaternion.identity);
friend.transform.SetParent(UIHandlerLobbyComponent.friendScrollContent, false);
friend.GetComponent<InvitationPrefab>().SetupInvitationPrefab(friendId);
}
}
UIHandlerLobbyComponent.friendScrollView.Rebuild(CanvasUpdate.Layout);
}

We need to clean up any existing UI Prefabs in our UI, then check for errors. For every friend in our outgoing friend requests, we instantiate an Invitation prefab, which consists of a Username and a Sent indicator.

Similar to before, each of these prefabs has its own script to manage the UI tile’s instance, since they each have some level of interaction.

Next we have Incoming Invites, where we can choose to either accept or decline a friend:

Request:

private void GetIncomingFriendsRequest()
{
lobbyLogic.abLobby.ListIncomingFriends(OnGetIncomingFriendsRequest);
}

Response:

private void OnGetIncomingFriendsRequest(Result<Friends> result)
{
if (result.IsError)
{
Debug.Log("GetIncomingFriendsRequest failed:" + result.Error.Message);
Debug.Log("GetIncomingFriendsRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Loaded incoming friends list successfully.");
foreach (string friendId in result.Value.friendsId)
{
Debug.Log("Incoming Friend Id: " + friendId);
Transform friend = Instantiate(UIHandlerLobbyComponent.friendInvitePrefab, Vector3.zero, Quaternion.identity);
friend.transform.SetParent(UIHandlerLobbyComponent.friendScrollContent, false);
friend.GetComponent<InvitationPrefab>().SetupInvitationPrefab(friendId);
}
}
UIHandlerLobbyComponent.friendScrollView.Rebuild(CanvasUpdate.Layout);
}

Once again, we clean the existing UI, check for errors, and repopulate the UI with the relevant tiles.

The difference here is in the Invitation script:

public class InvitationPrefab : MonoBehaviour
{
[SerializeField]
private Text usernameText;
[SerializeField]
private Button acceptInviteButton;
[SerializeField]
private Button declineInviteButton;
private string userId;
public void SetupInvitationPrefab(string id)
{
AccelByteManager.Instance.LobbyLogic.friendsLogic.GetFriendInfo(id, OnGetFriendInfoRequest);
userId = id;
}
private void OnGetFriendInfoRequest(Result<AccelByte.Models.UserData> result)
{
if (result.IsError)
{
Debug.Log("OnGetFriendInfoRequest failed:" + result.Error.Message);
Debug.Log("OnGetFriendInfoRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("OnGetFriendInfoRequest sent successfully.");
if (!usernameText.IsDestroyed())
{
usernameText.text = result.Value.displayName;
}
}
}
public void AcceptInvite()
{
AccelByteManager.Instance.LobbyLogic.friendsLogic.AcceptFriendRequest(userId, OnAcceptFriendRequest);
}
public void DeclineInvite()
{
AccelByteManager.Instance.LobbyLogic.friendsLogic.DeclineFriendRequest(userId, OnAcceptFriendRequest);
}
private void OnAcceptFriendRequest(Result result)
{
if (result.IsError)
{
Debug.Log("AcceptFriendRequest failed:" + result.Error.Message);
Debug.Log("AcceptFriendRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("AcceptFriendRequest sent successfully.");
AccelByteManager.Instance.LobbyLogic.friendsLogic.LoadFriendsList();
Destroy(this.gameObject);
}
}
private void OnDeclineFriendRequest(Result result)
{
if (result.IsError)
{
Debug.Log("AcceptFriendRequest failed:" + result.Error.Message);
Debug.Log("AcceptFriendRequest Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("AcceptFriendRequest sent successfully.");
Destroy(this.gameObject);
}
}
}

Similar to previous Prefab scripts, we want to set up our Invitation tile, but the only identifying information we have for the players is their User ID. We need easily identifiable names to show our end users. To get these we’ll use the GetFriendInfo function, which is just GetUserByUserId by a different name.

In OnGetFriendInfoRequest we check for errors, and if there are none we update the UI text element with the player’s Display Name.

Next we have AcceptInvite and DeclineInvite and their callbacks. These are hooked up to the Unity UI button’s event trigger:

unity-guide

When fired, these will add the player to the friends list, or remove the invite from the pending invites list.

With that completed, our new friend afif7 is now added to our overall friends list:

unity-guide

For player afif7, the invited player afif1 also shows up in their friends list.

unity-guide

Set Up Parties

Now that we’ve enabled friends, let’s set up parties. Parties allow players to play Light Fantastic together. The main actions players can take here are Creating a Party, Inviting Players to a Party, Kicking Players from a Party, and Leaving a Party.

Create Party

Whoever creates a party will become the party leader. Only the party leader can invite other players to the party.

Party LeaderTarget PlayerOther Party Members
CreateParty()--

First, we need to make sure that the player is already logged-in and in the lobby. Most of the interaction between party members will be located in AccelbytePartyLogic.cs.

public class AccelBytePartyLogic : MonoBehaviour
{
private UILobbyLogicComponent UIHandlerLobbyComponent;
private AccelByteLobbyLogic lobbyLogic;
private AccelByteManager accelByteManager;
public void Init(UILobbyLogicComponent uiLobbyLogicComponent, AccelByteLobbyLogic lobbyLogic)
{
UIHandlerLobbyComponent = uiLobbyLogicComponent;
this.lobbyLogic = lobbyLogic;
accelByteManager = lobbyLogic.GetComponent<AccelByteManager>();
}
}

Then implement the CreateParty lobby function along with the callback that contains PartyInfo as it’s parameter. PartyInfo contains PartyID, leaderID, members, invitees, and invitationToken.

private void CreateParty(ResultCallback<PartyInfo> callback)
{
lobbyLogic.abLobby.CreateParty(callback);
}
private void OnPartyCreated(Result<PartyInfo> result)
{
if (result.IsError)
{
Debug.Log("OnPartyCreated failed:" + result.Error.Message);
Debug.Log("OnPartyCreated Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnPartyCreated Party successfully created with party ID: " + result.Value.partyID);
abPartyInfo = result.Value;
SetIsLocalPlayerInParty(true);
isReadyToInviteToParty = true;
}
}

The CreateParty function can also be implemented right after the player presses the Invite To Party button in their friend list and before calling InviteToParty function. It will check if the inviter already has a party, and if not a new party will be created.

Invite To Party

unity-guide

The party leader is the only one that can invite other players to the party via their friend list. Only friends that are online can be invited to join a party. In Light Fantastic, the party slots are shown in the bottom-right corner of the main menu of the game. Empty party slots have plus signs in them, and when clicked the player can add a friend to their party.

unity-guide

unity-guide

public void InviteToParty(string id, ResultCallback callback)
{
string invitedPlayerId = id;
lobbyLogic.abLobby.InviteToParty(invitedPlayerId, callback);
}
private void OnInviteParty(Result result)
{
if (result.IsError)
{
Debug.Log("OnInviteParty failed:" + result.Error.Message);
Debug.Log("OnInviteParty Response Code::" + result.Error.Code);
// If the player already in party then notify the user
PopupManager.Instance.ShowPopupWarning("Invite to Party Failed", " " + result.Error.Message, "OK");
}
else
{
Debug.Log("OnInviteParty Succeded on Inviting player to party");
PopupManager.Instance.ShowPopupWarning("Invite to Party Success", "Waiting for invitee acceptance", "OK");
}
}

The callback, OnInviteParty(), located in the FriendPrefab, is assigned to each friend in the friend list. For invited players, the InvitedToParty event callback that needs to be set up.

public void SetupPartyCallbacks()
{
lobbyLogic.abLobby.InvitedToParty += result => OnInvitedToParty(result);
...
}
public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.InvitedToParty -= OnInvitedToParty;
...
}

The invitation arrives to the invited player through the InvitedToParty event. The PartyInvitation should be cached to a variable so it can be used with the JoinParty function in the next step. The abPartyInfo variable will be used to store information about the party that will be useful to update our UI later on.

private PartyInvitation abPartyInvitation;
private static bool isReadyToInviteToParty;
private static bool isLocalPlayerInParty;
...
private void OnInvitedToParty(Result<PartyInvitation> result)
{
if (result.IsError)
{
Debug.Log("OnInvitedToParty failed:" + result.Error.Message);
Debug.Log("OnInvitedToParty Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnInvitedToParty Received Invitation from " + result.Value.from);
// Party invitation will be used for accepting the invitation
abPartyInvitation = result.Value;
isReceivedPartyInvitation = true;
}
}
private void Update()
{
if (isReceivedPartyInvitation)
{
isReceivedPartyInvitation = false;
AccelBytePlugin.GetUser().GetUserByUserId(abPartyInvitation.from, OnGetUserOnInvite);
}
}
private void OnGetUserOnInvite(Result<UserData> result)
{
if (result.IsError)
{
Debug.Log("OnGetUserOnInvite failed:" + result.Error.Message);
Debug.Log("OnGetUserOnInvite Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnGetUserOnInvite UserData retrieved: " + result.Value.displayName);
PopupManager.Instance.ShowPopup("Party Invitation", "Received Invitation From " + result.Value.displayName, "Accept", "Decline", OnAcceptPartyClicked, OnDeclinePartyClicked);
}
}

The From value in PartyInvitation is a user ID, so we need to call GetUserData in order to get the displayname to show who the invitation is from. After the popup is shown to the player they’ll be given two options, to either Accept or Decline the invitation.

unity-guide

Declining the invitation closes the popup invitation by calling SetActive to false on the popup’s gameobject. Accepting the invitation will call the JoinParty function and the callback will update the player’s current party with the latest info. The UI party slot will also be updated once the partyInfo is retrieved successfully. Below you can see how to assign the AcceptPartyButton OnClick event to OnAcceptPartyClicked.

unity-guide

And here is what it looks like when the button is clicked.

private void OnAcceptPartyClicked()
{
if (abPartyInvitation != null)
{
lobbyLogic.abLobby.JoinParty(abPartyInvitation.partyID, abPartyInvitation.invitationToken, OnJoinedParty);
}
else
{
Debug.Log("OnJoinPartyClicked Join party failed abPartyInvitation is null");
}
}

The callback:

private void OnJoinedParty(Result<PartyInfo> result)
{
if (result.IsError)
{
Debug.Log("OnJoinedParty failed:" + result.Error.Message);
Debug.Log("OnJoinedParty Response Code::" + result.Error.Code);
}
else
{
// On joined should change the party slot with newer players info
Debug.Log("OnJoinedParty Joined party with ID: " + result.Value.partyID + result.Value.leaderID);
SetIsLocalPlayerInParty(true);
abPartyInfo = result.Value;
ClearPartySlots();
GetPartyInfo();
PopupManager.Instance.ShowPopupWarning("Join a Party", "You are just joined a party!", "OK");
}
}

The party slot displays all of the party members from abPartyInfo.members visually. It also records the player’s userId, display name, and email. Below are the player slots as they appear in Light Fantastic (in the bottom-right corner of the screen) and in Unity:

unity-guide

unity-guide

Of the four slots, there is one PlayerButton and three AddFriendButtons. To use the AddFriendButtons, first we must reference them with a transform array.

public class UILobbyLogicComponent : MonoBehaviour
{
public Transform[] partyMemberButtons;
}

Next, create a partyMemberList using the PartyData.

...
private IDictionary<string, PartyData> partyMemberList;
private readonly string partyUserId = LightFantasticConfig.PARTY_CHAT;
...
public struct PartyData
{
public string UserID;
public string PlayerName;
public string PlayerEmail;
public PartyData(string userId, string playerName, string playerEmail)
{
this.UserID = userId;
this.PlayerName = playerName;
this.PlayerEmail = playerEmail;
}
}

After that, use ClearPartySlots and ClearPartyProfile to remove any old party data, so that we can input the new party data.

private void ClearPartySlots()
{
// Clear the party slot buttons
for (int i = 0; i < UIHandlerLobbyComponent.partyMemberButtons.Length; i++)
{
UIHandlerLobbyComponent.partyMemberButtons[i].GetComponent<PartyPrefab>().OnClearProfileButton();
UIHandlerLobbyComponent.partyMemberButtons[i].GetComponent<Button>().onClick.AddListener(() => lobbyLogic.UIElementHandler.ShowExclusivePanel(ExclusivePanelType.FRIENDS));
}
partyMemberList.Clear();
}
public void OnClearProfileButton()
{
if (isInitiated)
{
Destroy(playerImage.gameObject);
partyData.UserID = "";
partyData.PlayerName = "";
partyData.PlayerEmail = "";
partyLeaderID = "";
isInitiated = false;
gameObject.GetComponent<Button>().onClick.RemoveListener(OnProfileButtonClicked);
}
}

After the party slot data is cleaned up, we then call GetPartyMemberInfo to get the player information that we need to fill the partyMemberList.

private void GetPartyMemberInfo(string friendId)
{
AccelBytePlugin.GetUser().GetUserByUserId(friendId, OnGetPartyMemberInfo);
}
private void OnGetPartyMemberInfo(Result<UserData> result)
{
// add party member to party member list
if (result.IsError)
{
Debug.Log("OnGetPartyMemberInfo failed:" + result.Error.Message);
Debug.Log("OnGetPartyMemberInfo Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("OnGetPartyMemberInfo sent successfully.");
// Add the member info to partymemberlist
UserData data = AccelByteManager.Instance.AuthLogic.GetUserData();
string ownId = data.userId;
if (!partyMemberList.ContainsKey(result.Value.userId) && (result.Value.userId != ownId))
{
Debug.Log("OnGetPartyMemberInfo member with id: " + result.Value.userId + " DisplayName: " + result.Value.displayName);
partyMemberList.Add(result.Value.userId, new PartyData(result.Value.userId, result.Value.displayName, result.Value.emailAddress));
}
RefreshPartySlots();
}
}

After all of the members’ information has been collected, call RefreshPartySlots() to execute updates to the UI.

private void RefreshPartySlots()
{
if (partyMemberList.Count > 0)
{
int j = 0;
foreach (KeyValuePair<string, PartyData> member in partyMemberList)
{
Debug.Log("RefreshPartySlots Member names entered: " + member.Value.PlayerName);
UIHandlerLobbyComponent.partyMemberButtons[j].GetComponent<Button>().onClick.RemoveAllListeners();
UIHandlerLobbyComponent.partyMemberButtons[j].GetComponent<PartyPrefab>().OnClearProfileButton();
UIHandlerLobbyComponent.partyMemberButtons[j].GetComponent<PartyPrefab>().SetupPlayerProfile(member.Value, abPartyInfo.leaderID);
j++;
}
if (lobbyLogic.chatLogic.activePlayerChatUserId == partyUserId)
{
lobbyLogic.chatLogic.RefreshDisplayNamePartyChatListUI();
}
}
lobbyLogic.friendsLogic.RefreshFriendsUI();
}
public void SetupPlayerProfile(PartyData data, string leaderID)
{
playerImage = Instantiate(playerProfile,transform).transform;
partyLeaderID = leaderID;
partyData.UserID = data.UserID;
partyData.PlayerName = data.PlayerName;
partyData.PlayerEmail = data.PlayerEmail;
partyData = data;
if (GetIsPartyLeader())
{
playerImage.GetComponent<Image>().color = Color.magenta;
}
else
{
playerImage.GetComponent<Image>().color = Color.white;
}
isInitiated = true;
gameObject.GetComponent<Button>().onClick.AddListener(OnProfileButtonClicked);
}

After the UI is updated, the party members will receive the JoinedParty event. Just like InvitedToParty, we need to set up the listener for this event.

Too add the listener to the event:

public void SetupPartyCallbacks()
{
lobbyLogic.abLobby.InvitedToParty += result => OnInvitedToParty(result);
lobbyLogic.abLobby.JoinedParty += result => OnMemberJoinedParty(result);
...
}

To remove the listener from the event:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.InvitedToParty -= OnInvitedToParty;
lobbyLogic.abLobby.JoinedParty -= OnMemberJoinedParty;
...
}

The OnMemberJoinedParty function updates the party info variable in the AccelbyteLobbyLogic, and updates the UI party slot once the partyInfo has been retrieved successfully.

private void OnMemberJoinedParty(Result<JoinNotification> result)
{
if (result.IsError)
{
Debug.Log("OnMemberJoinedParty failed:" + result.Error.Message);
Debug.Log("OnMemberJoinedParty Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnMemberJoinedParty Retrieved successfully");
isMemberJoinedParty = true;
// let the player knows that ther is a new party member joined in
MainThreadTaskRunner.Instance.Run(delegate
{
PopupManager.Instance.ShowPopupWarning("A New Party Member", "A new member just joined the party!", "OK");
});
}
}
private void GetPartyInfo()
{
lobbyLogic.abLobby.GetPartyInfo(OnGetPartyInfo);
}

The callback from the PartyInfo update:

private void OnGetPartyInfo(Result<PartyInfo> result)
{
if (result.IsError)
{
Debug.Log("OnGetPartyInfo failed:" + result.Error.Message);
Debug.Log("OnGetPartyInfo Response Code::" + result.Error.Code);
if (result.Error.Code == ErrorCode.PartyInfoSuccessGetUserPartyInfoEmpty)
{
SetIsLocalPlayerInParty(false);
lobbyLogic.friendsLogic.RefreshFriendsUI();
lobbyLogic.chatLogic.ClearActivePlayerChat();
lobbyLogic.chatLogic.OpenEmptyChatBox();
}
}
else
{
Debug.Log("OnGetPartyInfo Retrieved successfully");
abPartyInfo = result.Value;
for (int i = 0; i < result.Value.members.Length; i++)
{
Debug.Log("OnGetPartyInfo adding new party member: " + result.Value.members[i]);
// Get member info
GetPartyMemberInfo(result.Value.members[i]);
}
if (result.Value.members.Length == 1)
{
lobbyLogic.chatLogic.ClearActivePlayerChat();
lobbyLogic.chatLogic.OpenEmptyChatBox();
}
SetIsLocalPlayerInParty(true);
}
}

Kick From Party

unity-guide

Party leaders can kick members from their party. Upon being kicked, the player will receive a notification.

unity-guide

When the party leader hovers over a player’s party slot, their profile will appear as a popup along with the Kick From Party button. Here’s what the popup looks like in Unity:

unity-guide

There are a few grouped game objects, such as:

  • LocalLeaderCommand (will be shown when the playerButton triggers the onMouseHover event if the local player is a party leader)
  • LocalMemberCommand (will be shown when the playerButton triggers the onMouseHover event if the local player is a party member)
  • MemberCommand (will be shown when any AddFriendButton triggers the onMouseHover event if the local player is a party leader)

ShowPlayerProfile is located in AccelbyteLobbyLogic.cs.It will show the party controls in the lobby menu.

public void ShowPlayerProfile(PartyData memberData, bool isLocalPlayerButton = false)
{
// If visible then toogle it off to refresh the data
if (UIHandlerLobbyComponent.popupPartyControl.gameObject.activeSelf)
{
UIHandlerLobbyComponent.popupPartyControl.gameObject.SetActive(!UIHandlerLobbyComponent.popupPartyControl.gameObject.activeSelf);
memberCommand.GetComponentInChildren<Button>().onClick.RemoveAllListeners();
}
if (partyLogic.GetIsLocalPlayerInParty())
{
localLeaderCommand.gameObject.SetActive(false);
localmemberCommand.gameObject.SetActive(false);
memberCommand.gameObject.SetActive(false);
PlayerNameText.GetComponent<Text>().text = memberData.PlayerName;
playerEmailText.GetComponent<Text>().text = memberData.PlayerEmail;
UserData data = AccelByteManager.Instance.AuthLogic.GetUserData();
bool isPartyLeader = data.userId == partyLogic.GetAbPartyInfo().leaderID;
if (isPartyLeader && isLocalPlayerButton)
{
localLeaderCommand.gameObject.SetActive(true); ;
}
else if (!isPartyLeader && isLocalPlayerButton)
{
localmemberCommand.gameObject.SetActive(true);
}
else if (isPartyLeader && !isLocalPlayerButton)
{
memberCommand.gameObject.SetActive(true);
}
memberCommand.GetComponentInChildren<Button>().onClick.AddListener(() => partyLogic.OnKickFromPartyClicked(memberData.UserID));
// Show the popup
UIHandlerLobbyComponent.popupPartyControl.gameObject.SetActive(!UIHandlerLobbyComponent.popupPartyControl.gameObject.activeSelf);
}
}

On the memberCommand gameObject, add a listener for the kickFromParty button to kick each userId registered to each PartyPrefab.

internal void OnKickFromPartyClicked(string userId)
{
Debug.Log("OnKickFromPartyClicked Usertokick userId");
KickPartyMember(userId);
}
public void KickPartyMember(string id)
{
lobbyLogic.abLobby.KickPartyMember(id, OnKickPartyMember);
HidePopUpPartyControl();
}

Here’s the callback:

private void OnKickPartyMember(Result result)
{
if (result.IsError)
{
Debug.Log("OnKickPartyMember failed:" + result.Error.Message);
Debug.Log("OnKickPartyMember Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnKickPartyMember Retrieved successfully");
ClearPartySlots();
GetPartyInfo();
PopupManager.Instance.ShowPopupWarning("Kick a Party Member", "You are just kicked one of the party member!", "OK");
}
}

The kicked player will receive the KickedFromParty notification right after being kicked by the party leader. The callback of this event will be used to clear the party slot. Register KickedFromParty as the listener:

public void SetupPartyCallbacks()
{
lobbyLogic.abLobby.InvitedToParty += result => OnInvitedToParty(result);
lobbyLogic.abLobby.JoinedParty += result => OnMemberJoinedParty(result);
lobbyLogic.abLobby.KickedFromParty += result => OnKickedFromParty(result);
...
}

Then remove the listener:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.InvitedToParty -= OnInvitedToParty;
lobbyLogic.abLobby.JoinedParty -= OnMemberJoinedParty;
lobbyLogic.abLobby.KickedFromParty -= OnKickedFromParty;
...
}

The party slot is cleared by the callback as seen below:

private void OnKickedFromParty(Result<KickNotification> result)
{
if (result.IsError)
{
Debug.Log("OnKickedFromParty failed:" + result.Error.Message);
Debug.Log("OnKickedFromParty Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnKickedFromParty party with ID: " + result.Value.partyID);
isMemberKickedParty = true;
MainThreadTaskRunner.Instance.Run(delegate
{
PopupManager.Instance.ShowPopupWarning("Kicked from The Party", "You are just kicked from the party!", "OK");
});
}
}

All the other members will receive the LeaveFromParty event when a player is kicked. This updates the PartyInfo and the UI party slot to reflect that the kicked player is gone.

The LeaveFromParty even registration:

public void SetupPartyCallbacks()
{
lobbyLogic.abLobby.InvitedToParty += result => OnInvitedToParty(result);
lobbyLogic.abLobby.JoinedParty += result => OnMemberJoinedParty(result);
lobbyLogic.abLobby.KickedFromParty += result => OnKickedFromParty(result);
lobbyLogic.abLobby.LeaveFromParty += result => OnMemberLeftParty(result);
}

Remove the listener after use:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.InvitedToParty -= OnInvitedToParty;
lobbyLogic.abLobby.JoinedParty -= OnMemberJoinedParty;
lobbyLogic.abLobby.KickedFromParty -= OnKickedFromParty;
lobbyLogic.abLobby.LeaveFromParty -= OnMemberLeftParty;
}

Update PartyInfo and the party slots:

private void OnMemberLeftParty(Result<LeaveNotification> result)
{
if (result.IsError)
{
Debug.Log("OnMemberLeftParty failed:" + result.Error.Message);
Debug.Log("OnMemberLeftParty Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnMemberLeftParty a party member has left the party" + result.Value.userID);
isMemberLeftParty = true;
MainThreadTaskRunner.Instance.Run(delegate
{
PopupManager.Instance.ShowPopupWarning("A Member Left The Party", "A member just left the party!", "OK");
});
}
}

Leave Party

unity-guide

unity-guide

Any party member, including the leader, can leave the party. Just like when a player is kicked, a member leaving calls the LeaveParty function, while other members receive the LeaveFromParty event.

unity-guide

Here is the LeaveParty function. After this function is called, the callback function clears the party slots, marking that the player has left the party.

private void OnLeavePartyButtonClicked()
{
if (accelByteManager.AuthLogic.GetUserData().userId == abPartyInfo.leaderID)
{
lobbyLogic.localLeaderCommand.gameObject.SetActive(false);
}
else
{
lobbyLogic.localmemberCommand.gameObject.SetActive(false);
}
UIHandlerLobbyComponent.popupPartyControl.gameObject.SetActive(false);
LeaveParty();
}
private void LeaveParty()
{
lobbyLogic.abLobby.LeaveParty(OnLeaveParty);
HidePopUpPartyControl();
lobbyLogic.chatLogic.ClearActivePlayerChat();
lobbyLogic.chatLogic.OpenEmptyChatBox();
}
private void OnLeaveParty(Result result)
{
if (result.IsError)
{
Debug.Log("OnLeaveParty failed:" + result.Error.Message);
Debug.Log("OnLeaveParty Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnLeaveParty Left a party");
ClearPartySlots();
SetIsLocalPlayerInParty(false);
lobbyLogic.friendsLogic.RefreshFriendsUI();
PopupManager.Instance.ShowPopupWarning("Leave The Party", "You are just left the party!", "OK");
}
}

After the event, the remaining party members will have their PartyInfo updated just as if a member had been kicked. If the member that left was the party leader, the leadership will be transferred to the second player on the PartyInfo.members[] list.

Set Up Matchmaking

Light Fantastic comes with matchmaking, which matches players in either a 1v1 or 4 player match.

Start Matchmaking

In Light Fantastic, a player can start matchmaking right away for a 1v1 match or after making a party for 4 player free for all (FFA). StartMatchmaking is called to initiate the matchmaking process. The code related to matchmaking can be found in AccelByteMatchmakingLogic.cs.

unity-guide

Here’s what happens when the Find Match button is clicked:

rivate void FindMatchButtonClicked()
{
if (!lobbyLogic.partyLogic.GetIsLocalPlayerInParty())
{
lobbyLogic.abLobby.CreateParty(OnPartyCreatedFindMatch);
}
else
{
FindMatch();
}
}

Here’s the callback if the local player isn’t in a party yet:

rivate void OnPartyCreatedFindMatch(Result<PartyInfo> result)
{
if (result.IsError)
{
Debug.Log("OnPartyCreated failed:" + result.Error.Message);
Debug.Log("OnPartyCreated Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnPartyCreated Party successfully created with party ID: " + result.Value.partyID);
lobbyLogic.partyLogic.SetAbPartyInfo(result.Value);
lobbyLogic.partyLogic.SetIsLocalPlayerInParty(true);
FindMatch();
}
}

And here’s the callback when the player is in a party:

private void OnFindMatch(Result<MatchmakingCode> result)
{
if (result.IsError)
{
Debug.Log("OnFindMatch failed:" + result.Error.Message);
Debug.Log("OnFindMatch Response Code::" + result.Error.Code);
OnFailedMatchmaking("Couldn't do a matchmaking");
}
else
{
allowToConnectServer = true;
Debug.Log("OnFindMatch Finding matchmaking with gameMode: " + gameMode + " . . .");
// show matchmaking board and start count down on finding match
ShowMatchmakingBoard(true);
UIHandlerLobbyComponent.matchmakingBoard.StartCountdown(MatchmakingWaitingPhase.FindMatch,
delegate
{
OnFailedMatchmaking("Timeout to finding match");
});
// setup current game mode based on player's selected game mode
UIHandlerLobbyComponent.matchmakingBoard.SetGameMode(gameModeEnum);
}
}

After the callback is received, the matchmaking board that includes information about the match will be shown.

Matchmaking Completed

MatchmakingCompleted occurs when a match has been found. The event carries MatchmakingNotif that contains the match status (i.e. Start, Cancel, Done) and the matchId.

Registering a callback on MatchmakingCompleted:

public void SetupMatchmakingCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted += result => OnFindMatchCompleted(result);
...
}

Unregistering the callback:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted -= OnFindMatchCompleted;
...
}

Callback on Start, Done, and Cancel:

private void OnFindMatchCompleted(Result<MatchmakingNotif> result)
{
if (result.IsError)
{
Debug.Log("OnFindMatchCompleted failed:" + result.Error.Message);
Debug.Log("OnFindMatchCompleted Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnFindMatchCompleted Finding matchmaking Completed");
Debug.Log("OnFindMatchCompleted Match Found: " + result.Value.matchId);
Debug.Log(" Match status: " + result.Value.status);
Debug.Log(" Expected match status: " + MatchmakingNotifStatus.done.ToString());
abMatchmakingNotif = result.Value;
lobbyLogic.abLobby.ConfirmReadyForMatch(abMatchmakingNotif.matchId, OnReadyForMatchConfirmation);
UIHandlerLobbyComponent.matchmakingBoard.StartCountdown(MatchmakingWaitingPhase.ConfirmingMatch,
delegate
{
OnFailedMatchmaking("Timeout to confirm matchmaking");
});
// if the player is in a party and the match is complete
if (result.Value.status == MatchmakingNotifStatus.done.ToString())
{
lobbyLogic.WriteInDebugBox(" Match Found: " + result.Value.matchId);
}
// if the player is in a party and the party leader start a matchmaking
else if (result.Value.status == MatchmakingNotifStatus.start.ToString())
{
MainThreadTaskRunner.Instance.Run(delegate
{
ShowMatchmakingBoard(true);
});
UIHandlerLobbyComponent.matchmakingBoard.StartCountdown(MatchmakingWaitingPhase.FindMatch,
delegate
{
OnFailedMatchmaking("Timeout to finding match");
});
UIHandlerLobbyComponent.matchmakingBoard.SetGameMode(gameModeEnum);
}
// if the player is in a party and the party leader cancel the current matchmaking
else if (result.Value.status == MatchmakingNotifStatus.cancel.ToString())
{
MainThreadTaskRunner.Instance.Run(delegate
{
ShowMatchmakingBoard(false);
});
}
}
}

Start occurs when the party leader starts the matchmaking process, Cancel indicates that the party leader has canceled the matchmaking process, and Done occurs when the matchmaking process is finished.

Confirm Ready For Match

ReadyForMatchConfirmation is an event that allows players to choose whether they want to join the match or not. In Light Fantastic, matches are automatically accepted when MatchmakingNotif returns Done as its status. Below is the callback for match confirmation:

private void OnReadyForMatchConfirmation(Result result)
{
abMatchmakingNotif = null;
if (result.IsError)
{
Debug.Log("OnReadyForMatchConfirmation failed:" + result.Error.Message);
Debug.Log("OnReadyForMatchConfirmation Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnReadyForMatchConfirmation Waiting for other player . . .");
lobbyLogic.WriteInDebugBox("Waiting for other players . . .");
}
}

Ready For Match Confirmed

ReadyForMatchConfirmed is an event that tracks which players have confirmed that they’re ready for the match.

Register the event:

public void SetupMatchmakingCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted += result => OnFindMatchCompleted(result);
lobbyLogic.abLobby.ReadyForMatchConfirmed += result => OnGetReadyConfirmationStatus(result);
...
}

Unregister the event:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted -= OnFindMatchCompleted;
lobbyLogic.abLobby.ReadyForMatchConfirmed -= OnGetReadyConfirmationStatus;
...
}

The callback:

private void OnGetReadyConfirmationStatus(Result<ReadyForMatchConfirmation> result)
{
if (result.IsError)
{
Debug.Log("OnGetReadyConfirmationStatus failed:" + result.Error.Message);
Debug.Log("OnGetReadyConfirmationStatus Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnGetReadyConfirmationStatus Ready confirmation completed");
Debug.Log("OnGetReadyConfirmationStatus: " + result.Value.userId + " is ready");
lobbyLogic.WriteInDebugBox("Player " + result.Value.userId + " is ready");
}
}

Cancel Matchmaking

A player can use cancel matchmaking by clicking on the Cancel button in the Finding Match panel. The cancel matchmaking command is stackable on the backend, so be sure that it’s only called once when the matchmaking starts. If Cancel is clicked twice, the second click will negate the next start matchmaking command.

unity-guide

The cancel matchmaking function:

private void FindMatchCancelClicked()
{
lobbyLogic.abLobby.CancelMatchmaking(gameMode, OnFindMatchCanceled);
}

And its callback:

private void OnFindMatchCanceled(Result<MatchmakingCode> result)
{
if (result.IsError)
{
Debug.Log("OnFindMatchCanceled failed:" + result.Error.Message);
Debug.Log("OnFindMatchCanceled Response Code::" + result.Error.Code);
}
else
{
Debug.Log("OnFindMatchCanceled The Match is canceled");
ShowMatchmakingBoard(false);
lobbyLogic.WriteInDebugBox(" Match Canceled");
}
}

If the party leader cancels the matchmaking, all of the party members will receive a MatchmakingNotif with Cancel as its value, as seen above.

DS Updated

When the matchmaking is done, all of the players will receive status updates from the Distributed Server (DS). The first status will be Creating, which indicates that the DS is being created. When the DS is ready to use a second callback will change the status from Creating to Ready. This notification will also include the IP and Port that the game client can use to connect to the DS. When the DS has accepted the players, its status will be changed to Busy.

If the players are connected to a local DS (connectToLocal), the DS status will always be Busy.

unity-guide

Register the callback to the DSUpdated event. Upon callback, you will need to connect the player to the DS using the IP and Port information received from DsNotif.

Registering the event:

public void SetupMatchmakingCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted += result => OnFindMatchCompleted(result);
lobbyLogic.abLobby.ReadyForMatchConfirmed += result => OnGetReadyConfirmationStatus(result);
lobbyLogic.abLobby.DSUpdated += result => OnSuccessMatch(result);
}

Unregistering the event:

public void UnsubscribeAllCallbacks()
{
lobbyLogic.abLobby.MatchmakingCompleted -= OnFindMatchCompleted;
lobbyLogic.abLobby.ReadyForMatchConfirmed -= OnGetReadyConfirmationStatus;
lobbyLogic.abLobby.DSUpdated -= OnSuccessMatch;
}

The callback with Create and Ready statuses:

private void OnSuccessMatch(Result<DsNotif> result)
{
if (result.IsError)
{
Debug.Log("OnSuccessMatch failed:" + result.Error.Message);
Debug.Log("OnSuccessMatch Response Code::" + result.Error.Code);
MainThreadTaskRunner.Instance.Run(delegate
{
OnFailedMatchmaking("An error occurs in the dedicated server manager");
});
}
else
{
if (result.Value.isOK == "false" && !connectToLocal)
{
MainThreadTaskRunner.Instance.Run(delegate
{
OnFailedMatchmaking("Failed to create a dedicated server");
});
return;
}
UIHandlerLobbyComponent.matchmakingBoard.StartCountdown(MatchmakingWaitingPhase.WaitingDSM,
delegate { OnFailedMatchmaking("Spawning a dedicated server timed out"); });
Debug.Log("OnSuccessMatch success match completed");
// DSM on process creating DS
if (result.Value.status == DSNotifStatus.CREATING.ToString())
{
Debug.Log("Waiting for the game server!");
}
// DS is ready
else if (result.Value.status == DSNotifStatus.READY.ToString())
{
// Set IP and port to persistent and connect to the game
Debug.Log("Entering the game!");
Debug.Log("Lobby OnSuccessMatch Connect");
MainThreadTaskRunner.Instance.Run(() => { StartCoroutine(WaitForGameServerReady(result.Value.ip, result.Value.port.ToString())); });
}
else if (result.Value.status == DSNotifStatus.BUSY.ToString())
{
Debug.Log("Entering the game!");
Debug.Log("Lobby OnSuccessMatch Connect");
Debug.Log("ip: " + result.Value.ip + "port: " + result.Value.port);
MainThreadTaskRunner.Instance.Run(() => { StartCoroutine(WaitForGameServerReady(result.Value.ip, result.Value.port.ToString())); });
}
Debug.Log("OnSuccessMatch ip: " + result.Value.ip + "port: " + result.Value.port);
lobbyLogic.WriteInDebugBox("Match Success status " + result.Value?.status + " isOK " + result.Value.isOK + " pod: " + result.Value.podName);
lobbyLogic.WriteInDebugBox("Match Success IP: " + result.Value.ip + " Port: " + result.Value.port);
ShowMatchmakingBoard(true, true);
abDSNotif = result.Value;
}
}

Set Up Statistics

Player statistics is a feature that lets players see their records in the game. In Light Fantastic, the player statistics available include the total number of wins, losses, matches, and distance traveled when playing the game.

Update the Statistics Value from the Game Server

In Light Fantastic, there are four player statistics that are set by the game server after a match is finished. These include number of wins (total-win), number of losses (total-lose), number of matches (total-match), and total distance traveled (total-distance). Because this information is managed by the game server, it can be found in AccelByteServerLogic.cs.

public class AccelByteServerLogic : MonoBehaviour
{
private ServerStatistic abServerStatistic;
private void Start()
{
abServerStatistic = AccelByteServerPlugin.GetStatistic();
...
}
}
public void UpdateUserStatItem(float distance, string userId, bool isWinner)
{
if (playerStatUpdatedCount < playerCount)
{
if (distance > LightFantasticConfig.FINISH_LINE_DISTANCE)
{
distance = LightFantasticConfig.FINISH_LINE_DISTANCE;
}
var createStatItemRequest = new CreateStatItemRequest[4];
createStatItemRequest[0] = new CreateStatItemRequest() { statCode = LightFantasticConfig.StatisticCode.total };
createStatItemRequest[1] = new CreateStatItemRequest() { statCode = LightFantasticConfig.StatisticCode.win };
createStatItemRequest[2] = new CreateStatItemRequest() { statCode = LightFantasticConfig.StatisticCode.lose };
createStatItemRequest[3] = new CreateStatItemRequest() { statCode = LightFantasticConfig.StatisticCode.distance };
abServerStatistic.CreateUserStatItems(userId, createStatItemRequest, OnCreateUserStatItems);
var statItemOperationResult = new StatItemIncrement[3];
//Update total-match
statItemOperationResult[0] = new StatItemIncrement()
{
statCode = createStatItemRequest[0].statCode,
inc = 1
};
//Update total-lose or total-win
if (isWinner)
{
statItemOperationResult[1] = new StatItemIncrement()
{
statCode = createStatItemRequest[1].statCode,
inc = 1
};
}
else
{
statItemOperationResult[1] = new StatItemIncrement()
{
statCode = createStatItemRequest[2].statCode,
inc = 1
};
}
//Update total-distance
statItemOperationResult[2] = new StatItemIncrement()
{
statCode = createStatItemRequest[3].statCode,
inc = distance
};
abServerStatistic.IncrementUserStatItems(userId, statItemOperationResult, OnIncrementUserStatItems);
playerStatUpdatedCount += 1;
}
}

The UpdateUserStatItem function will be called from game code when the match is over. This function is located in BaseGameManager.cs.

private void EndTheGame()
{
uint winnerNetId = DecideWinner();
foreach (var player in players)
{
var isWinner = player.Key == winnerNetId;
AccelByteManager.Instance.ServerLogic.UpdateUserStatItem(player.Value.Character.transform.position.x, player.Value.Character.UserId, isWinner);
}
onGameEnd?.Invoke();
networkObject.SendRpc(RPC_BROADCAST_END_GAME, Receivers.Others, winnerNetId);
}

Get the Statistics Value from the Game Client

The player can see their total number of wins, losses, matches, and distance traveled using the GetUserStatItems function.

public class AccelByteStatisticLogic : MonoBehaviour
{
private Statistic abStatistic;
ICollection<string> playerStatistic;
...
private void Start()
{
playerStatistic = new List<string>
{
LightFantasticConfig.StatisticCode.win,
LightFantasticConfig.StatisticCode.lose,
LightFantasticConfig.StatisticCode.total,
LightFantasticConfig.StatisticCode.distance
};
...
}
...
}
public void UpdatePlayerStatisticUI()
{
if (abStatistic == null) abStatistic = AccelBytePlugin.GetStatistic();
abStatistic.GetUserStatItems(playerStatistic, null, GetStatisticCallback);
}

The callback:

public void GetStatisticCallback(Result<PagedStatItems> result)
{
if (result.IsError)
{
Debug.Log("Get Statistic failed:" + result.Error.Message);
Debug.Log("Get Statistic Response Code: " + result.Error.Code);
//Show Error Message
}
else
{
Debug.Log("Get Statistic successful.");
foreach (var data in result.Value.data)
{
if (data.statCode == LightFantasticConfig.StatisticCode.win)
{
UIHandlerStatisticsComponent.totalWinText.text = data.value.ToString();
}
else if (data.statCode == LightFantasticConfig.StatisticCode.lose)
{
UIHandlerStatisticsComponent.totalLoseText.text = data.value.ToString();
}
else if (data.statCode == LightFantasticConfig.StatisticCode.total)
{
UIHandlerStatisticsComponent.totalMatchText.text = data.value.ToString();
}
else if (data.statCode == LightFantasticConfig.StatisticCode.distance)
{
UIHandlerStatisticsComponent.totalDistanceText.text = data.value.ToString();
}
}
}
}

unity-guide

Set Up Leaderboards

Leaderboards contain information about the player ranking system, based on information made available by our Statistics service.

unity-guide

To retrieve the leaderboard data from the AccelByte SDK in Light Fantastic, use the code below. All leaderboard logic is managed by AccelByteLeaderboardLogic.cs.

First, run the Leaderboard service and hold its reference.

private Leaderboard abLeaderboard;
// the leaderboard code from leaderboard creation
private string leaderboardCode = “alltimetotalwin”;
// the datalist for ui
private IDictionary<string, RankData> playerRankList;
private string lastPlayerRank;
// gameobject that store the class that handles the UI elements
private GameObject UIHandler;
// all the ui elements that used on leaderboard prefab
private UILeaderboardComponent UIHandlerLeaderboardComponent;
// class that handles all shorts of functions on UI
private UIElementHandler UIElementHandler;
public struct RankData
{
public string userId;
public string rank;
public string playerName;
public float winStats;
public RankData(string userId, string rank, string playerName, float winStats)
{
this.userId = userId;
this.rank = rank;
this.playerName = playerName;
this.winStats = winStats;
}
}
public void Init()
{
if (abLeaderboard == null) abLeaderboard = AccelBytePlugin.GetLeaderboard();
playerRankList = new Dictionary<string, RankData>();
RefreshUIHandler();
}

Then, retrieve the top 10 ranking from the leaderboard by using QueryAllTimeLeaderboardRankingData. The parameters are the leaderboardCode that we received when we created the leaderboard, the start and end query list of players whose data we want to retrieve, and the callback.

private void GetTopTenRanking()
{
abLeaderboard.QueryAllTimeLeaderboardRankingData(leaderboardCode, 0, 10, OnGetTopTenRanking);
}
// the callback
private void OnGetTopTenRanking(Result<LeaderboardRankingResult> result)
{
if (result.IsError)
{
Debug.Log("Query leaderboard failed with LeaderboardCode: " + leaderboardCode + " Error : " + result.Error.Message);
}
else
{
for (int i = 0; i < result.Value.data.Length; i++)
{
if (!playerRankList.ContainsKey(result.Value.data[i].userId))
{
var playerResult = result.Value.data[i];
string playerRankResult = (i + 1).ToString();
if (i < 9)
{
playerRankResult = "0" + playerRankResult;
}
playerRankList.Add(playerResult.userId, new RankData(playerResult.userId, playerRankResult, "Loading . . .", playerResult.point));
lastPlayerRank = playerResult.userId;
}
}
isLeaderboardUpdate = true;
RefreshLeaderboardUIPrefabs();
}
}

On the callback, we need to sort the data and update the UI. After the userID data is retrieved from the leaderboard, we need to retrieve each user’s display name by calling GetUserByUserId().

private void Update()
{
if (isLeaderboardUpdate)
{
isLeaderboardUpdate = false;
foreach (var data in playerRankList.Keys)
{
AccelBytePlugin.GetUser().GetUserByUserId(playerRankList[data].userId, OnGetUserDisplayName);
}
}
if (isCheckingDisplayName)
{
isCheckingDisplayName = false;
RefreshLeaderboardUIPrefabs();
}
}
private void RefreshLeaderboardUIPrefabs()
{
ClearLeaderboardUIPrefabs();
foreach (KeyValuePair<string, RankData> player in playerRankList)
{
RankPrefab rankPrefab = Instantiate(UIHandlerLeaderboardComponent.rankPrefab, Vector3.zero, Quaternion.identity).GetComponent<RankPrefab>();
rankPrefab.transform.SetParent(UIHandlerLeaderboardComponent.leaderboardScrollContent, false);
rankPrefab.GetComponent<RankPrefab>().SetupLeaderboardUI(player.Value.rank, player.Value.playerName, player.Value.winStats.ToString());
UIHandlerLeaderboardComponent.leaderboardScrollView.Rebuild(CanvasUpdate.Layout);
if (player.Value.playerName == "Loading . . .")
{
isCheckingDisplayName = true;
}
}
}
private void ClearLeaderboardUIPrefabs()
{
if (UIHandlerLeaderboardComponent.leaderboardScrollContent.childCount > 0)
{
for (int i = 0; i < UIHandlerLeaderboardComponent.leaderboardScrollContent.childCount; i++)
{
Destroy(UIHandlerLeaderboardComponent.leaderboardScrollContent.GetChild(i).gameObject);
}
}
}

Next, we’ll retrieve the users’ rankings.

private void GetMyRanking()
{
abLeaderboard.GetUserRanking(AccelByteManager.Instance.AuthLogic.GetUserData().userId, leaderboardCode ,OnGetMyRanking);
}
// the callback
private void OnGetMyRanking(Result<UserRankingData> result)
{
if (result.IsError)
{
Debug.Log("Get user ranking failed with LeaderboardCode: " + leaderboardCode + " Error : " + result.Error.Message);
}
else
{
string rankPlayer = result.Value.allTime.rank.ToString();
if (result.Value.allTime.rank < 9)
{
rankPlayer = "0" + rankPlayer;
}
UIHandlerLeaderboardComponent.myNumberText.text = rankPlayer;
UIHandlerLeaderboardComponent.myWinStatsText.text = result.Value.allTime.point.ToString();
}
}

After the data is retrieved, the UI will update automatically. Specifically, current player rank, total wins, and user’s display name will update.

Manage Entitlements

In Light Fantastic, player entitlements appear when a player opens up their inventory from the main menu. Here, they can see the items they have and equip any of those items to their avatar. They also appear during gameplay, where player avatars are seen wearing the equipped items.

While entitlements are private information, items currently equipped by a player are saved to their UserProfile, which has public attributes. Other players can see what items the player has equipped when looking at their profile or playing in a match with them.

The entitlements logic are stored inside AccelByteEntitlementLogic.cs. To begin, retrieve the entitlement from the reference and store it in abEntitlements.

public class AccelByteEntitlementLogic : MonoBehaviour
{
public delegate void GetEntitlementCompletion(bool inMenum, Error error);
public event GetEntitlementCompletion OnGetEntitlementCompleted;
private Entitlement abEntitlements;
private Items abItems;
private void Start()
{
abEntitlements = AccelBytePlugin.GetEntitlements();
abItems = AccelBytePlugin.GetItems();
...
}
...
}

The GetEntitlement function includes the inMenu parameter, which indicates whether the entitlement information is needed for the player’s inventory in the main menu or in-game. The variable allItemInfo contains all of the item data available in the store and retrieved from the entitlement service. After the items’ data has been retrieved, proceed to QueryUserEntitlements.

The first parameter of QueryUserEntitlements is the entitlement name which in Light Fantastic is left empty, as it is optional. The second parameter is item ID which is also optional (and also empty here), and the third one is offset, which refers to the offset of the list that has been sliced based on the limit parameter. The value for the offset in Light Fantastic is 0. The fourth and last parameter is limit, which has a value of 99 in Light Fantastic. This means that in Light Fantastic, users can have a maximum of 99 entitlements.

public void GetEntitlement(bool inMenu)
{
activeEquipmentList = null;
originalEquipmentList = null;
if (inMenu)
{
UIElementHandler.ShowNonExclusivePanel(NonExclusivePanelType.LOADING);
HidePromptPanel();
}
if (allItemInfo.data == null)
{
abItems.GetItemsByCriteria(ALL_ITEM_CRITERIA, result =>
{
if (!result.IsError)
{
allItemInfo.data = result.Value.data;
if (inMenu)
{
abEntitlements.QueryUserEntitlements("", "", 0, 99, OnGetEntitlement);
}
else
{
abEntitlements.QueryUserEntitlements("", "", 0, 99, OnGetEntitlementNoMenu);
}
}
else
{
OnGetEntitlementCompleted?.Invoke(inMenu, result.Error);
}
});
}
else
{
if (inMenu)
{
abEntitlements.QueryUserEntitlements("", "", 0, 99, OnGetEntitlement);
}
else
{
abEntitlements.QueryUserEntitlements("", "", 0, 99, OnGetEntitlementNoMenu);
}
}
}

From here the callback is different based on the inMenu parameter.

Get Entitlements for the Inventory

The callback for retrieving entitlements for the inventory is different than the one used for in-game. As mentioned above, we use the User Profile service to save a player’s equipped items.

private void OnGetEntitlement(Result<EntitlementPagingSlicedResult> result)
{
UIElementHandler.HideNonExclusivePanel(NonExclusivePanelType.LOADING);
if (result.IsError)
{
// handle
OnGetEntitlementCompleted?.Invoke(true, result.Error);
}
else
{
// Get saved custom attributes(attached equipments/ activeEquipmentList) in UserProfile
UIHandlerEntitlementComponent.abUserProfileLogic.GetMine(profileResult =>
{
if (!profileResult.IsError)
{
//originalEquipmentList = null;
activeEquipmentList = new Equipments.EquipmentList();
PopulateInventories(result.Value.data);
if (profileResult.Value.customAttributes != null)
{
originalEquipmentList = Equipments.ListFromCustomAttributes(
profileResult.Value.customAttributes.ToJsonString(), allItemInfo.data);
if (originalEquipmentList != null)
{
activeEquipmentList = (Equipments.EquipmentList)originalEquipmentList.Clone();
EquipFromList(originalEquipmentList);
OnGetEntitlementCompleted?.Invoke(true, result.Error);
}
else
{
OnGetEntitlementCompleted?.Invoke(true, new Error(ErrorCode.UnknownError, "Null Equipment list"));
}
}
else
{
OnGetEntitlementCompleted?.Invoke(true, new Error(ErrorCode.UnknownError, "Custom attributes field is empty"));
}
UIHandlerEntitlementComponent.buttonHat.SetEnable(false);
ShowHatInventories(true);
UIHandlerEntitlementComponent.buttonEffect.SetEnable(true);
ShowEffectInventories(false);
}
else
{
OnGetEntitlementCompleted?.Invoke(true, profileResult.Error);
}
});
}
}

Get Entitlements for In-game

The in-game callback is used to display the equipped items on the player’s avatar while they play. Like getting entitlements for the inventory, this also relies on the User Profile service to determine which entitlements are equipped.

private void OnGetEntitlementNoMenu(Result<EntitlementPagingSlicedResult> result)
{
//TODO: fix FadeLoadingOut
//uiHandler.FadeLoading();
if (result.IsError)
{
// handle
OnGetEntitlementCompleted?.Invoke(false, result.Error);
}
else
{
// Get saved custom attributes(attached equipments/ activeEquipmentList) in UserProfile
UIHandlerEntitlementComponent.abUserProfileLogic.GetMine(profileResult =>
{
if (!profileResult.IsError)
{
activeEquipmentList = new Equipments.EquipmentList();
if (profileResult.Value.customAttributes != null)
{
originalEquipmentList = Equipments.ListFromCustomAttributes(
profileResult.Value.customAttributes.ToJsonString(), allItemInfo.data);
if (originalEquipmentList != null)
{
activeEquipmentList = (Equipments.EquipmentList)originalEquipmentList.Clone();
OnGetEntitlementCompleted?.Invoke(false, null);
}
else
{
OnGetEntitlementCompleted?.Invoke(false, new Error(ErrorCode.UnknownError, "Null Equipment list"));
}
}
else
{
OnGetEntitlementCompleted?.Invoke(false, new Error(ErrorCode.UnknownError, "Custom attributes field is empty"));
}
}
else
{
OnGetEntitlementCompleted?.Invoke(false, profileResult.Error);
}
});
}
}

When the player’s entitlements have been retrieved BasePlayerPawn.cs is triggered and the OnGetSelfEntitlementCompleted function will equip the items to the player’s avatar.

private void GetCloudData()
{
...
AccelByteEntitlementLogic abEntitlement = AccelByteManager.Instance.EntitlementLogic;
abEntitlement.OnGetEntitlementCompleted += OnGetSelfEntitlementCompleted;
abEntitlement.GetEntitlement(false);
}
private void OnGetSelfEntitlementCompleted(bool inMenu, AccelByte.Core.Error error)
{
if (inMenu)
{
return;
}
if (error != null)
{
Debug.Log("[" + error.Code + "] " + error.Message);
isInitialized = true;
return;
}
AccelByteEntitlementLogic abEntitlement = AccelByteManager.Instance.EntitlementLogic;
abEntitlement.OnGetEntitlementCompleted -= OnGetSelfEntitlementCompleted;
Equipments.EquipmentList activeEquipments = abEntitlement.GetActiveEquipmentList();
if (activeEquipments != null)
{
hatTitle_ = activeEquipments.hat != null ? activeEquipments.hat.title : "NULL";
effectTitle_ = activeEquipments.effect != null ? activeEquipments.effect.title : "NULL";
hatSetter.SetHatSprite(hatTitle_);
particleSetter.SetItem(effectTitle_);
networkObject.SendRpc(RPC_SET_ACTIVE_EQUIPMENT, Receivers.Others, new object[] { hatTitle_, effectTitle_ });
}
else
{
Debug.LogError("No Active Equipment");
}
isInitialized = true;
}

What’s Next?

  • Now that you’ve learned how our services can be implemented in your game, go download our Unity SDK and try it for yourself.