Introduction

When we talk about Teams, we have to keep in mind that other platforms might call this feature something else. Teams can encompass a lot of different multiplayer features of the same name such as Clans, Guilds, Groups, Orgs, Tribes, Factions, etc, so keep this in mind. These other platforms may not call their feature Teams and that might not be what the feature is called for your game.

GameSparks Teams are one of the more complicated features of GameSparks but in essence they provide functionality to group players together into a single data structure. When driven by the SparkTeams API this feature can be used create variety of custom features, but out-of-the-box GameSparks provides the following functionality:

These features are what we need in order to transition Teams from GameSparks to another platform. While not all GameSparks developers use all features of Teams, the basic needs are usually the requests above, and the same functionality through an API. However, there are other features with GameSparks Teams that we will discuss.

Team Chat & Notifications

Team-Chat is a very widely used GameSparks feature. It can be adapted for other features like lobbies or tournaments or even global announcements.

Something important to keep in mind when reviewing these other platforms is that they do not all use websockets like GameSparks does. That means that it is not as simple for that system to send notifications to other team-members when something has changed or a chat message has been sent.

We will look at how these notifications can be reproduced in these platforms where possible.

Team-Chat may not exist at all as an option in these platforms, but we do cover messaging and chat in another topic here.

Team Leaderboards

In GameSparks, Team Leaderboards use the team Id instead of the player Id in order to post scores. This allows any member of the Team to post scores to a Leaderboard.

Some of these alternative platforms will not provide this feature, and without access to the underline API controlling Teams, there will be no way to reproduce this feature.

Team Data

Team data is not a feature of GameSparks, but it is a common feature for GameSparks developers to create themselves when using Teams and Cloud-Code.

Team data is created by using the team id as a field to search for Teams by in a new database collection. By referencing the team id, you can add any information you want to the Team. Common examples might be a “team-chest” where you can add all your Team’s earned currency or shared items. Extra fields for icons, descriptions or rules are also common.

Where possible we will also show how to create this feature, if it is not offered by the platform out-of-the-box.

Beamable

The Teams feature for Beamable is called Groups. It has many of the same features that you would expect from GameSparks Teams, plus a few extra features to manage members, Team data and a team wallet which can be used to deliver Virtual Currency to members.

Beamable does not have a chat system built into their Groups feature, however they do have their own chat service which is covered in the Messaging topic here.

Beamable also does not have Team-Leaderboards as a feature. Since GameSparks uses the team Id for their Team-Leaderboards it is not really possible to recreate the Team-Leaderboard API as it would be in GameSparks.

No configuration or setup is needed to create a Team or a template of a Team like in GameSparks. Instead, all of this setup is done through API calls directly from the client.

In Beamable you can:

Where these features overlap with GameSparks we will cover some simple examples using the Group Service API.

Creating A New Group

As mentioned above, creating a new Group/Team is done using the Group Service APIs. To create a new group we first need a GroupCreateRequest which requires a few settings.

Name, tag and maxSize are all options you should be familiar with, but there are two other options that you will not have seen from GameSparks.

A couple more things to quickly note are that the “tag” field must be unique across the game and is also optional, and the maxSize field has a max value of 50.

The API call for this is very simple, you can see an example of this below.

/// <summary>
/// Creates a new group with the given details
/// </summary>
/// <param name="groupName">Group Name</param>
/// <param name="type"> open, restricted or closed</param>
/// <param name="req"> set to zero to ignore this value</param>
/// <param name="maxSize"> max member size - max 50</param>
async void CreateGroup(string groupName, EnrollmentType type, long req, int maxSize)
{
   Debug.Log($"name: {groupName}, type: {type.ToString()}, req: {req}, maxSize: {maxSize}");
   Debug.Log($"Creating new group - {groupName}");
   // create a new group-request object //
   GroupCreateRequest groupCreateReq = new GroupCreateRequest(groupName, null, type.ToString(), req, maxSize);
   GroupCreateResponse groupCreateResp = await beamableAPI.GroupsService.CreateGroup(groupCreateReq);
   // log details //
   Debug.Log($"New Group ID: {groupCreateResp.@group.id}");
   Debug.Log($"New Group Name: {groupCreateResp.@group.name}");
   Debug.Log($"New Group Tag: {groupCreateResp.@group.tag}");
}

You can see in the response that a new Id will be given to your group indicating a new instance has been created.

Creating & Finding Groups

Now that we have created a group, let's take a look at how we can search for our groups or other groups.

There are a few options you can pass into the search method. These are things that have already been covered while creating a group such as the enrollment-type, the requirement, etc. You can see more details on these options here.

A few interesting points on these fields are below:

Something else to note is that the Search() method uses mongoDB text-search functionality. This means that you can do partial string matches if you search the group-name, but it is not the same as a regex query.

Let's take a look at what happens with a partial search for the group we just created.

/// <summary>
/// Search for a group with the given name
/// </summary>
/// <param name="nameSearchTxt">full or partial match for the group name</param>
async void SearchGroup(string nameSearchTxt)
{
   Debug.Log($"Searching Groups - {nameSearchTxt}");
   GroupSearchResponse searchResp = await beamableAPI.GroupsService.Search(nameSearchTxt);
   // we can go through the groups to get their details //
   foreach (Group group in searchResp.groups)
   {
       Debug.Log($"Group ID: {group.id}");
       Debug.Log($"Group Name: {group.name}");
       foreach (Member member in group.members)
       {
           Debug.Log($"Member ID: {member.gamerTag}");
           Debug.Log($"Member Role: {member.role}");
       }
   }
}

We can see from the Unity console how we can get the data for the groups.

There is actually a lot more data available for the group from this response, but we will cover that later.

Group Management

You can see in the example above that you can get the group member’s role. In the case of this example, it was our player who created the group so they are automatically given the “leader” role. However, you can modify and change these roles in any way you want using the SetRole() method.

This allows you to set up something like a “captain”, “lieutenant”, “private” structure where the creator of the group does not need to be the only one that has permissions to invite members or vet applications to the Group. However, this is all up to you to design, role-privileges are not out-of-the-box in Beamable. This sort of vetting could easily be created through the client or using microservices (for better security).

Joining A Group

When we created the group in this example we made it an “open” group. This means that any player can join without a request or invitation and so, the process for joining is very simple. We have an example below.

/// <summary>
/// Allows the player to join the group or send a request to join if the group is not open
/// </summary>
/// <param name="groupId">The id of the group</param>
async void JoinGroup(long groupId)
{
   Debug.Log($"Joining Group: {groupId}");
   GroupMembershipResponse joinResp = await beamableAPI.GroupsService.JoinGroup(groupId);
   Debug.Log($"Group Join Successful: {joinResp.member}");
}

Using the search-teams example from before we can validate this player has joined. Note that they haven't been given a role in this case.

What about a case where the Group is “restricted” or “closed”?

This request still applies where the Group is restricted. Restricted groups can still be applied to using the JoinGroup() request but your players will not be granted immediate access. They will instead send a message to the leader of the Group asking for permission to join. There are more details on this flow in the section of Group Notifications.

If your Group is closed then no one can apply to join. Instead, you can invite a player to join your Group using the Petition() method. There are more details on this here.

Example: Team Data

Although GameSparks does not have this out-of-the-box, it is a very common feature that is custom built by GameSparks developers on top of the existing Teams API.

This is usually just a “TeamData” collection which uses the team Id as the query field. Using Runtime-collections or GDS you can append any data to a Team that you like.

Since this is a very common use-case in GameSparks, we will cover how to reproduce this here.

There are a limited number of fields which you can modify and they are described here. Below is an example of how to perform this kind of update in code.

GroupUpdateProperties newProps = new GroupUpdateProperties();
newProps.name = "Not an awesome group for lame people";
newProps.slogan = "we suck!";
newProps.clientData = "some custom data";
newProps.tag = "meh";

Then, updating the Group is pretty simple…

/// <summary>
/// Updates the group properties with new data
/// </summary>
/// <param name="groupId"> the id of the group</param>
/// <param name="properties">a GroupUpdateProperties object containing your update data</param>
async void SetGroupData(long groupId, GroupUpdateProperties properties)
{
   // update these properties //
   await beamableAPI.GroupsService.SetGroupProps(groupId, properties);
   // just for this example we will get the group and confirm the update //
   Group groupDetailsAfter = await beamableAPI.GroupsService.GetGroup(groupId);
   Debug.Log($"Updated Client Data...");
   Debug.Log($"ClientData {groupDetailsAfter.clientData}");
   Debug.Log($"Slogan {groupDetailsAfter.slogan}");
   Debug.Log($"Name {groupDetailsAfter.name}");
   Debug.Log($"Tag {groupDetailsAfter.tag}");
}

We created an example using the code above showing the data before and after the update for verification.

Group Notifications

For the example we have provided above, our Group was set to “open” this means that joining was automatic. So what happens if you have a restricted or closed group? How can the leader or admins know that there is a new request to join?

This process actually goes through Beamable’s Mail Service. This differs from GameSparks as notifications and updates about Teams are automatically relayed through the web-socket. Since Beamable does not have a web-socket with Groups, the Mail Service is a replacement that allows leaders players to get updates about their Groups.

We will not cover the Mail Service here as it is covered in the topic on Chat & Messaging here.

Other Features

There are other features of Beamable Groups that do not overlap with GameSparks so we will not cover them in this topic. However, it is handy to know about these features for future development.

Currency Donations

The Groups Service also had functionality for delivering currency to a member of the Team. This works by the member first requesting a donation using the MakeDonationRequest() function. Admins in the Team can then approve this by sending that member the currency using the Donate() function.

AccelByte

In AccelByte, the Groups service is similar to the Teams in GameSparks. However, the AccelByte Groups feature actually provides more functionality when compared to GameSparks.

For example, we can search for the specific group name instead of just the team Id and we can retrieve the list of groups that match with keywords supplied. We can retrieve group information for any specific player or for the player who is calling the API call. We can add and update the Group with custom data which will be discussed in the Team Data section.

With GameSparks, we can create open or private Teams. With AccelByte three are three options: open, public and private.

These options are the same for both platforms except for platforms for the public option.

There are no features like Team Leaderboards or Team Chat from GameSparks in AccelByte, however, there is a Chat feature covered in the topic here which allows you to use the Group feature in conjunction with their Lobbies service to create something like Team chat.

For comparison with GameSparks’ Teams feature, with AccelByte you can:

Note - You cannot delete a Group once we created it in AccelByte while you can drop a Team in GameSparks

Group Configuration

In GameSparks, you create and configure new Teams through the portal. This is the same with AccelByte. However, there are a number of additional configuration steps needed before we can start making a Group.

Group Roles

We need to define the various roles in a group with different permissions for each role. These roles are needed in order to create a new Group. This will be explained in the following section. We can create as many roles as we want to suit our game’s needs.

From the Admin portal, you should go to the Roles section under the Group Management category as shown below. You need to click on the Add Role button on the right-hand side of the window, and supply the role name when prompted by the small popup. We will create two roles named “Captain” and “Private”. We will use the “Captain” role as our Admin or the Group, and everyone else in the Group can be “Private”. These are essentially members without Admin privileges.

Role Permissions

We can set permissions by clicking the view option beside the specific role we want to set, as shown below.

We can assign appropriate permissions to the roles based on the needs as shown below.

There are other ways to mix and match the privileges of these roles depending on your own game. You can check out the guide here for more information.

Group Configuration

Now that we have our roles created we need to configure the new Group in the Admin portal.

Go to the Configurations section under the Group Management category as shown below. Click on the Initiate Configuration button. This will create a default group with a 50 member capacity.

Then, we can create a configuration with all necessary details as shown below.

There are similar fields for GameSparks features like code, name and description.

You can select the roles you created earlier using the Group Admin and Group Member fields. In our example, Captain is the Group Admin Role and Private is the Group Member Role.

Now that we have configured the group in the Admin portal and it is ready to use. Players can create their own instances of this Group from the client SDK.

Creating New Groups

Using the GameSparks SDK, your players can create their own new Team with three fields like TeamId, TeamName, TeamType.

With Accelbyte, we can provide similar details and also include groupRegion, groupDescription and cutsomAttributes. The group-region is a string representation a C# RegionInfo name. Custom attributes will be covered in a section later on Team-Data. This is custom data you want to attribute to your Group. In the example below we added a wallet to our Group so that it can have similar attributes to the player wallet we created in previous topics.

Note - You cannot assign a group Id like team Id in GameSparks and a random one will be generated.

You players can create a new Group using the example below.

CreateGroupRequest createGroupRequest = new CreateGroupRequest
{
            configurationCode = "group-clan",
            groupDescription = "This is My Fabulous Group",
            groupName = "OmegaSquad",
            groupRegion = "IE",
            groupType = GroupType.OPEN,
            customAttributes = new Dictionary<string, object> {
               {"Level", 0 }, {"Coins", 0 }, {"Gems", 0 }, {"XP", 0 }
            }
};

AccelBytePlugin.GetGroup().CreateGroup(createGroupRequest, result =>
{
            Debug.Log("GroupId: " + result1.Value.groupId);
            Debug.Log("GroupName: " + result1.Value.groupName);
            Debug.Log("GroupRegion: " + result1.Value.groupRegion);
});

The configurationCode field shown above is the code we gave to the Group we created through the Admin portal. groupType is specified using an enum. Our group-type is GroupType.OPEN so that anyone can join.

Using this example you should see some logs in the console once your Group is created.

You can confirm the group was created by going to the Lists option under Group Management Category of the Admin portal.

Searching & Finding Groups

In contrast with GameSparks, we can search for groups with groupName, groupRegion instead of just name or type.

Note - AccelByte does not allow you to list all the groups of a certain type or config.

Below is an example of how to get a list of groups from the client SDK.

string groupName = "Omega";
string groupRegion = "IE";
int offset = 0;
int limit = 20;

AccelBytePlugin.GetGroup().SearchGroups(groupName, groupRegion, limit, offset, result =>
{
       foreach (var group in result1.Value.data)
       {
              Debug.Log("GroupId: " + group.groupId);
              Debug.Log("GroupName: " + group.groupName);
              Debug.Log("GroupRegion: " + group.groupRegion);
        }
});

You can also do partial string searches with AccelByte. For example, we searched with groupName as ‘Omega’ and you can see we found our “Omega Squad” group below.

Getting Member Details

There are two APIs to be aware of when it comes to getting member details. First you get the list of members and then, you generally want some more specific information about those members. We will cover that here briefly.

Group Members List

So we have set up a Group with 3 members as shown below.

With GameSparks, we can retrieve Team information with ownerId, teamId, teamType, however, with AccelByte, we just need groupid.

We can retrieve our three users with the below API call.

string groupId = "60c35882c568598bf1bfe622";
abGroup.GetGroupMemberList(groupId,GroupMemberCallback);

private void GroupMemberCallback(Result<PaginatedGroupMemberList> result)
    {
        if (result.IsError)
        {
            Debug.Log("Encountered an error: " + result.Error.Message);
        }
        else
        {
            int i = 0;
            foreach(GroupMemberInformation id in result.Value.data)
            {
                Debug.Log("Group Member" + i++);
                Debug.Log("UserId" + id.userId);
            }
        }
    } 

And we can see our member Ids logged out in the console.

BulkUsersPresence

This is similar to the above API call but this is mainly helpful to retrieve information about the status and presence of a user.

userIds[0] = "b18989ecb007482eb9b138exxxxxxx";
userIds[1] = "004ffcdf69ad47a096a1e2e6xxxxxxx";

AccelBytePlugin.GetLobby().BulkGetUserPresence(userIds, BulkUserPresenceCallback);

private void BulkUserPresenceCallback(Result<BulkUserStatusNotif> result)
{
    if (result.IsError)
    {
        Debug.Log("Encountered an error: " + result.Error.Message);
     }
     else
     {
         foreach (UserStatusNotif BUStatus in result.Value.data)
         {
                Debug.Log("UserId" + BUStatus.userID);
                Debug.Log("namespace" + BUStatus.namespace_);
                Debug.Log("lastseenAt" + BUStatus.lastSeenAt);
                Debug.Log("availability" + BUStatus.availability);
                Debug.Log("activity" + BUStatus.activity);
                Debug.Log("online" + result.Value.online);
                Debug.Log("offline" + result.Value.offline);
                Debug.Log("invisible" + result.Value.invisible);
                Debug.Log("busy" + result.Value.busy);
          }
      }
}

Here you can see the values for these different parameters.

The difference between GetGroupMember() and GetBulkUserPresence() is the type of response you get. If you want to retrieve information regarding Group Member information related to User then you can use GetGroupMember() call. If you want to know the status of the player in the group, you would use GetBulkUserPresence().

Group Interaction

There are a number of different API used in the regular flow of joining, leaving, and managing Groups. These are similar to GameSparks so we will only briefly cover them here.

Join Group

In GameSparks, we have the JoinTeamRequest to send a request to join a team. Similarly, we have the JoinGroup API call for AccelByte.

We will just supply groupId but this is not the case in the GameSparks version where we should supply ownerId, teamId and teamType.

When we call this method from the client, depending on the type of the group (open, public or private), a JoinRequest will be sent out to the Admin.

If the team-type is open (as in our example) then players will join instantly otherwise the Admin has to approve the request.

Below is an example of the API call for JoinGroup.

string groupId = "60c35882c568598bf1bfe622";
abGroup.JoinGroup(groupId, JoinGroupCallback);

private void JoinGroupCallback(Result<JoinGroupResponse> result)
{
        if (result.IsError)
        {
            Debug.Log("Encountered and error: " + result.Error.Message);
        }
        else
        {
            Debug.Log("Joined Group");
            Debug.Log(result.Value.status);
        }
 }

Response would be as shown below.

Leave Group

We can leave the team in GameSparks by using LeaveTeamRequest. In AccelByte, We will call LeaveGroup to achieve the same functionality. We need three parameters, ownerId, teamId and teamType in GameSparks while we do not need any parameters to call the LeaveGroup call in AccelByte.

Below is an example of the API call for LeaveGroup.

abGroup.LeaveGroup(LeaveGroupCallback);
private void LeaveGroupCallback(Result<GroupGeneralResponse> result)
    {
        if (result.IsError)
        {
            Debug.Log("Encountered and error: " + result.Error.Message);
        }
        else
        {
            Debug.Log("Left the Group");
        }
    }

We will get a response as shown below.

Group Management

There are several API calls available for group management and listed below. We will not cover it in detail here as they are not available with GameSparks, however these are great features to use in addition to your current GameSparks implementation and you can find more info about them here.

Admin Calls Some API calls are only usable by Admins of the Group. As discussed earlier these are defined using the roles set on the group-config when it was created.

You can find more information about these Admin calls here.

Example: Team Data / Group Attributes

Although GameSparks does not have Team-Data as a feature out-of-the-box, it is a very common feature that is custom built by GameSparks developers on top of the existing Teams API.

This is usually just a “TeamData” collection that uses the team Id as the query field. Using Runtime-collections or GDS you can append any data to a Team that you like.

Since this is a very common use case in GameSparks, we will cover how to reproduce this here.

In AccelByte, we usually refer to Custom Attributes when we speak about Team Data. We can easily add Custom Attributes to the Group through API calls or when the Group was created by the player as we have shown before.

We have created a group called “OmegaSquad“ in one of the earlier sections. We will show the code below to create a group with custom attributes for reference.

We will add these attributes below.

We can update custom data above with the following API call.

string groupId = "60c35882c568598bfxxxxxx";

var emblem = new List<int>(){ 15, 10, 0 };
customAttributes = new Dictionary<string, object> {
       {"Emblem", emblem },{"Level", 1 },{"XP", 1200}, {"Coins", 123 }, {"Gems", 15 } }


AccelBytePlugin.GetGroup().UpdateGroupCustomAttributes(groupId, customAttributes, result =>
{
      Debug.Log("Update Success!");
});

And you can view those changes in the Groups section of the Admin portal.

After the update, the OmegaSquad group would appear as shown below in the admin portal.

Group Notifications

With AccelByte there is a feature called Group Notifications which is similar to Team Notifications from GameSparks. However, as the Group Service is not using a socket for communication, you cannot get these notifications asynchronously. Instead the notifications system for Groups is tied into the Lobby Service.

We will not cover that setup in this topic, instead you can find more information on Lobbies here and additional details on the notifications API at the end of the section on Groups here.

Nakama

The Teams feature in Nakama is called Groups. It has many of the same features that you would expect from GameSparks Teams, plus a few extra features to manage members and their permissions within a team.

As with GameSparks, Groups have the following functionality:

In addition to the functionality above, Nakama also offer the following functionality for Groups out-of-the-box with no additional configuration or custom code:

We recommend taking a look through Nakama’s documentation on Groups here as these APIs are relatively simple and there are plenty of examples on how to use the APIs listed above on Nakama’s documentation site.

Finding & Listing Groups

If you are using the out-of-the-box ListTeamsRequest in GameSparks to get a list of your teams you will find that there is a similar API in Nakama. However, in Nakama you are also able to do partial string searches by including the “%” wildcard character at the end of the search string.

groupNameSearch += "%";
IApiGroupList groupListResp = await nakamaClient.ListGroupsAsync(session, groupNameSearch, 100);
Debug.Log($"Groups Found [{groupListResp.Groups.Count()}]...");
foreach (var g in groupListResp.Groups)
{
   Debug.Log($"Group name '{g.Name}' count '{g.EdgeCount}'");
}

We created a Group called “Awesome Group For Cool People” and you can see below that we can find that group by adding the wildcard character to the name search parameter. We don't need to include the full Group name in order to find the Group.

Listing Player Groups

This would be something like the GameSparks GetMyTeamsRequest which allows players to get a list of their own Groups. This is a very simple API call and you can see an example of this call with Unity on Nakama’s documentation site here so we don't cover this for this topic.

Listing Group Members

In both examples above we get some basic information about the Group but we don't get a list of the members and their details like we can from GameSparks.

There is however, a request to get a list of member details using Nakama. You can check out more details about that request here. You can get a lot of information about group members from this request so it is very useful.

Joining Groups

Similar to the GameSparks JoinTeamRequest, joining a Group in Nakama requires the group ID.

await client.JoinGroupAsync(session, groupId);

However, if the Group is not open, the player will not be able to join.

Instead, a message will be sent to the Group’s super-admin. By default this is the player who created the Group.

They can use the user ID of the member seeking to join the Group to accept that member’s request. When that action is performed the member seeking to join will also receive a message that their request has been accepted.

You can see an example of those APIs here.

In order to set up notifications you will need to assign a listener to the ReceivedNotification handlers.

You can see a short guide on this here or you can check out our topic on Achievements here for more details on how to create sockets and set up notification listeners.

Group Permissions

Group permissions are not a feature that is available from GameSparks but it is a very common case for modification of the GameSparks Teams feature using Cloud-Code.

Permissions are applied to members of a Group to allow them different privileges within the Group, such as being able to accept new member requests.

There is more information on these permissions and how to use them here.

Group Data

Team-Data is a common feature created by GameSparks developers in order to add additional information to their Team to track Achievements or give their Team a balance they can use for buying their members certain in-game items.

In GameSparks this must be done with a custom collection but with Nakama’s groups there is an out-of-the-box option.

As you may have noticed from some other Nakama topics, many out-of-the-box components of the Nakama’s system have a “metaData” parameter where you can store custom JSON data.

Remember that this data is always public. This doesn't mean that other players can edit the data but it does mean that it is available through public APIs like searching for Groups. This could be very useful in a case where you want to show a team’s level, balance, icon, etc in the list of Teams.

MetaData can only be set from the runtime server so for this example we are going to create a new RPC function which will set this MetaData at the same time as we create a new Group which is a common GameSparks example.

We created a new TypeScript file called “gsGroups.ts” and we are going to create a new class which we can use in future for all Group functionality.

class GSGroups {
   /**
    * Creates a new group with the details provided
    * @param userId
    * @param groupName
    * @param isOpen - if 'false' group is private
    * @param metaData
    * @param maxPlayers
    * @param logger
    * @param nk
    * @returns {string} groupId
    */
   static createGroup(userId: string, groupName: string, isOpen: boolean, metaData: any, maxPlayers: number, logger: nkruntime.Logger, nk: nkruntime.Nakama): string {
       try{
           // Create new group with metadata //
           var groupDetails: nkruntime.Group = nk.groupCreate(userId, groupName, userId, null, null, null, isOpen, metaData, maxPlayers);
           // return new group's Id //
           return JSON.stringify({
               "groupId": groupDetails.id
           });
       }
       catch(e: any){
           logger.error(e.message);
           throw JSON.stringify({
               "code": 123,
               "error": e.message
           });
       }
   }
}

You can see from the example above that we can handle errors by throwing an exception. This will allow us to handle the response we get back in the event of an error server-side.

An example of this during normal gameplay might be a duplicate group-name being created. The Nakama runtime server will not allow this and will throw an error instead so we need to handle that error ourselves.

Next we are going to create a new RPC for this function. This will be simple; all we need to do here is parse the data we would pass in from the client to make sure it is in the correct format for the groupCreate() function.

/**
* Creates a new group for the user with the given parameters
* @param context
* @param logger
* @param nk
* @param payloadString
*/
function rpcCreateNewGroup(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payloadString: string): string {
   logger.info("Data In: " + payloadString);
   const payloadData: any = JSON.parse(payloadString);
   // parse params //
   let groupName: string = payloadData.groupName;
   let isOpen: boolean = (payloadData.isOpen === "True")? true : false;
   let maxPlayers: number = parseInt(payloadData.maxPlayers);
   let metaData: any = JSON.parse(payloadData.metaData);
   // and we can get the userId from the context //
   let userId = context.userId;
   // create the group and return the new group Id //
   return GSGroups.createGroup(userId, groupName, isOpen, metaData, maxPlayers, logger, nk);
}

And lastly we will need to register this RPC function in the InitModule function of the main.ts script.

initializer.registerRpc("createNewGroup", rpcCreateNewGroup);

Reminder - Remember to add file references to the tsconfig.json file and rebuild and redeploy the server before testing.

Note - You can also update Group details using the groupUpdate() function. This takes many of the same parameters as the groupCreate() function so we won't cover this in this topic.

Now that we have the server code prepared, we need to create some C# code so we can create our groups from Unity.

// create metaData //
Dictionary<string, string> metaData = new Dictionary<string, string>()
{
   { "COINS" , "123" },
   { "GEMS" , "55" },
   { "LEVEL" , "10" },
};
// construct request //
string rpcId = "createNewGroup";
Dictionary<string, string> requestPayload = new Dictionary<string, string>()
{
   { "groupName" , newGroupName },
   { "isOpen" , isOpen.ToString() },
   { "maxPlayers" , maxPlayers.ToString() },
   { "metaData" , Nakama.TinyJson.JsonWriter.ToJson(metaData) },
};
var payload = Nakama.TinyJson.JsonWriter.ToJson(requestPayload);
try
{
   IApiRpc responseData = await nakamaClient.RpcAsync(session, rpcId, payload);
   Debug.Log(responseData.Payload); // << response string
}
catch (Exception e)
{
   // handle error
   Debug.LogError(e);
}

This code is simple but it may appear complicated as it contains a couple of C# Dictionaries and a try-catch. So let's break down what is happening in this example.

Below you can see examples of successful and unsuccessful attempts to create a Group using this method.

Success

We get the new Group ID back in a JSON string.

Error

We get some context about the error from the Nakama runtime server when the request failed.

Getting Group MetaData

Group metaData can be retrieved when you search for, or list groups. The metaData object is a JSON string so you will need to parse this data before you can use it in C#.

IApiGroupList groupListResp = await nakamaClient.ListGroupsAsync(session, groupNameSearch, 100);
foreach (var g in groupListResp.Groups)
{
   Debug.Log($"Group name '{g.Name}' count '{g.EdgeCount}'");
   Debug.Log("Parsing MetaData...");
   Dictionary<string, string>  metaData = Nakama.TinyJson.JsonParser.FromJson<Dictionary<string, string>>(g.Metadata);
   foreach (var key in metaData)
   {
       Debug.Log($"{key.Key}:{key.Value}");
   }
}

Custom Group Data

The above example shows how you can attach data to a Group using metaData, but there are cases where you want to have more complex data, for example a larger data-set than metaData can provide (max 16kb) or you want the Group data to be private. In these cases you can do something similar to what you would have done in GameSparks.

When you create a new Group in Cloud-Code, you can get the new Group ID and then create a document for that Group using the Nakama Storage engine.

Let us take a look at creating this new document using our previous code example.

// Create new group with metadata //
var groupDetails: nkruntime.Group = nk.groupCreate(userId, groupName, userId, null, null, null, isOpen, null, maxPlayers);
// create a new doc for this group //
var customGroupDoc: any = {
   wallet: {
      COINS: 123,
      GEMS: 5,
      LEVEL: 10
   },
   items: [{ "diamond_sword" : 2, "diamond_shield" : 1 }]
};
// Save this doc into the Nakama storage engine //
nk.storageWrite([{
   collection: "Custom_Group",
   key: groupDetails.id,
   userId: userId,
   value: customGroupDoc,
   permissionRead: 0, // no public read
   permissionWrite: 0, // no write
}]);

You will notice that we added no read or write permissions to this doc. This doesn't mean that we can never edit the doc, it just means that from the client, no players can edit it, not even the owner. This restricts all edit permissions of the document to the server only.

You can double-check your custom group document was created by going to the Developer Console and clicking on the “Storage” option on the left-hand side menu.

You will be able to see your Group’s data by clicking on the document.

Now we need an example of how to read this data back. This is a simple process. We will create a new function in our GSGroups class, and a new RPC so we can call the RPC from the client.

   /**
    * Returns the group data from the storage engine
    * @param groupId
    * @param logger
    * @param nk {JSON}
    */
   static getGroupData(groupId: string, logger: nkruntime.Logger, nk: nkruntime.Nakama): any {
       let groupDataCurs = nk.storageRead([{
           collection: "Custom_Group",
           key: groupId,
           userId: "00000000-0000-0000-0000-000000000000"
       }]);
       if (groupDataCurs.length > 0) {
           return JSON.stringify(groupDataCurs[0].value);
       }
       // handle errors //
       throw JSON.stringify({
           "code": 123,
           "error": "Group not found"
       });
   }

And our RPC is as follows…

/**
* Returns custom group data for the given groupId
* @param context
* @param logger
* @param nk
* @param payloadString
*/
function rpcGetGroupMetaData(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payloadString: string): string {
   logger.info("Data In: " + payloadString);
   const payloadData: any = JSON.parse(payloadString);
   let groupId: string = payloadData.groupId;
   let groupDetails = GSGroups.getGroupDetails(groupId, logger, nk);
   return JSON.stringify(groupDetails);
}

And lastly we will need to register this RPC function in the InitModule function of the main.ts script.

initializer.registerRpc("getGroupCustomData", rpcGetGroupMetaData);

Reminder - Remember to rebuild and redeploy your server before testing.

We will check out an example of how to test this from the Nakama administrator portal. Since only the server has permission to access the Group’s custom data we don't need a client to test that this is working. Instead we can call this from the API Explorer window of the portal.

Team Leaderboards

Team Leaderboards are possible with Nakama however they are not an out-of-the-box feature and will require some custom code. We have covered how you might approach Team Leaderboard in our Leaderboards topic here.