This client library guide will show you how to use the core Nakama features in C++ by showing you how to develop the Nakama specific parts (without full game logic or UI) of an Among Us (external) inspired game called Sagi-shi (Japanese for “Imposter”).
Console Support
The C++ SDK includes console support for Sony, Microsoft, and Nintendo platforms. To obtain the C++ client for console platforms please contact us.
booldone=false;autologinFailedCallback=[&done](constNError&error){cout<<"Failed to login"<<endl;cout<<error.message<<endl;done=true;};autologinSucceededCallback=[&done,&rtClient](NSessionPtrsession){cout<<"Login successful"<<endl;cout<<session->getAuthToken()<<endl;rtClient->connect(session,true);};stringdeviceId="e872f976-34c1-4c41-88fe-fd6aef118782";client->authenticateDevice(deviceId,opt::nullopt,opt::nullopt,{},loginSucceededCallback,loginFailedCallback);
The alternative method is to use the std::future version of the function (postfixed with Async):
Network programming requires additional safeguarding against connection and payload issues.
API calls in Sagi-shi gracefully handle errors:
1
2
3
4
5
6
7
8
9
10
11
autoerrorCallback=[](constNError&error){cout<<"An error occurred: "<<error.message<<endl;if(error.code==ErrorCode::ConnectionError){cout<<"The server is currently unavailable. Check internet connection."<<endl;}};client->getAccount(session,successCallback,errorCallback);
You can also handle errors when using futures:
1
2
3
4
5
6
7
8
9
10
11
12
13
try{autoaccount=client->getAccountAsync(session).get();}catch(constNException&e){cout<<"An error occurred: "<<e.error.message>><<endl;if(e.error.code==ErrorCode::ConnectionError){cout<<"The server is currently unavailable. Check internet connection."<<endl;}}
The Nakama Client connects to a Nakama Server and is the entry point to access Nakama features. It is recommended to have one client per server per game.
To create a client for Sagi-shi pass in your server connection details:
// Typically you would get the system's unique device identifier here.
stringdeviceId="e872f976-34c1-4c41-88fe-fd6aef118782";autologinFailedCallback=[&done](constNError&error){cout<<"An error occurred: "<<error.message<<endl;};autologinSucceededCallback=[&done,&rtClient](NSessionPtrsession){cout<<"Successfully authenticated: "<<session->getAuthToken()<<endl;};// Authenticate with the Nakama server using Device Authentication.
client->authenticateDevice(deviceId,opt::nullopt,opt::nullopt,{},loginSucceededCallback,loginFailedCallback);
Nakama Facebook Authentication is an easy to use authentication method which lets you optionally import the player’s Facebook friends and add them to their Nakama Friends list.
1
2
3
4
5
6
7
8
9
10
11
12
// Authenticate with the Nakama server using Facebook Authentication.
stringaccessToken="<Token>";boolimportFriends=true;client->authenticateFacebook(accessToken,"mycustomusername",true,importFriends,{},loginSucceededCallback,loginFailedCallback);
Nakama allows players to Link Authentication methods to their account once they have authenticated.
Linking Device ID authentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autolinkSuccessCallback=[](){cout<<"Successfully linked Device ID authentication to existing player account"<<endl;};autolinkErrorCallback=[](constNError&error){cout<<"Error linking Device ID: "<<error.message<<endl;};// Link Device Authentication to existing player account.
client->linkDevice(session,deviceId,linkSuccessCallback,linkErrorCallback);
Linking Facebook authentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autolinkSuccessCallback=[](){cout<<"Successfully linked Facebook authentication to existing player account"<<endl;};autolinkErrorCallback=[](constNError&error){cout<<"Error linking Facebook: "<<error.message<<endl;};client->linkFacebook(session,accessToken,importFriends,linkSuccessCallback,linkErrorCallback);
Nakama Sessions expire after a time set in your server configuration. Expiring inactive sessions is a good security practice.
Nakama provides ways to restore sessions, for example when Sagi-shi players re-launch the game, or refresh tokens to keep the session active while the game is being played.
Use the auth and refresh tokens on the session object to restore or refresh sessions.
Restore a session without having to re-authenticate:
1
session=restoreSession(authToken,refreshToken);
Check if a session has expired or is close to expiring and refresh it to keep it alive:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Check whether a session has expired or is close to expiry.
if(session->isExpired()||session->isExpired(time(0)+24*60*60)){autorefreshSuccessCallback=[](NSessionPtrsession){cout<<"Session successfully refreshed"<<endl;};autorefreshErrorCallback=[](constNError&error){// Couldn't refresh the session so reauthenticate.
// client->authenticateDevice(...)
};// Refresh the existing session
client->authenticateRefresh(session,refreshSuccessCallback,refreshErrorCallback);}
Many of Nakama’s features are accessible with an authenticated session, like fetching a user account.
Get a Sagi-shi player’s full user account with their basic user information and user id:
1
2
3
4
5
6
7
8
9
10
11
12
13
autosuccessCallback=[](constNAccount&account){stringusername=account.user.username;stringavatarUrl=account.user.avatarUrl;stringuserId=account.user.id;};autoerrorCallback=[](constNError&error){cout<<"Failed to get user account: "<<error.message<<endl;};client->getAccount(session,successCallback,errorCallback);
In addition to getting the current authenticated player’s user account, Nakama has a convenient way to get a list of other players’ public profiles from their ids or usernames.
Sagi-shi uses this method to display player profiles when engaging with other Nakama features:
Define a class that describes the storage object and create a new storage object id with the collection name, key and user id. Finally, read the storage objects and parse the JSON data:
To read other players’ public storage objects use their UserId instead. Remember that players can only read storage objects they own or that are public (PermissionRead value of 2).
Nakama allows developers to write to the Storage Engine from the client and server.
Consider what adverse effects a malicious user can have on your game and economy when deciding where to put your write logic, for example data that should only be written authoritatively (i.e. game unlocks or progress).
Sagi-shi allows players to favorite items for easier access in the UI and it is safe to write this data from the client.
Create a write storage object with the collection name, key and JSON encoded data. Finally, write the storage objects to the Storage Engine:
autosuccessCallback=[](constNStorageObjectAcks&storageObjectAcks){cout<<"Success writing storage object"<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error writing storage object: "<<error.message<<endl;};hatsStorageObjectfavoriteHats{{"cowboy","alien"}};jsonj;j["hats"]=favoriteHats.hats;NStorageObjectWritewriteObject{collection:"favorites",key:"hats",value:j.dump(),permissionRead:NStoragePermissionRead::OWNER_READ,// Only the server and owner can read
permissionWrite:NStoragePermissionWrite::OWNER_WRITE// The server and owner can write
};client->writeStorageObjects(session,{writeObject},successCallback,errorCallback);
Storage Engine Conditional Writes ensure that write operations only happen if the object hasn’t changed since you accessed it.
This gives you protection from overwriting data, for example the Sagi-shi server could have updated an object since the player last accessed it.
To perform a conditional write, add a version to the write storage object with the most recent object version:
1
2
3
4
5
6
7
8
9
10
11
// Assuming we already have a storage object (storageObject).
NStorageObjectWritewriteObject{collection:"favorites",key:"hats",value:"<NewJsonValue>",permissionRead:NStoragePermissionRead::OWNER_READ,// Only the server and owner can read
permissionWrite:NStoragePermissionWrite::OWNER_WRITE,// The server and owner can write
version:storageObject.version};client->writeStorageObjects(session,{writeObject},successCallback,errorCallback);
Nakama methods that list results return a cursor which can be passed to subsequent calls to Nakama to indicate where to start retrieving objects from in the collection.
For example:
If the cursor has a value of 5, you will get results from the fifth object.
If the cursor is null, you will get results from the first object.
Nakama Storage Engine operations can be protected on the server to protect data the player shouldn’t be able to modify (i.e. game unlocks or progress). See the writing to the Storage Engine authoritatively recipe.
Adding a friend in Nakama does not immediately add a mutual friend relationship. An outgoing friend request is created to each user, which they will need to accept.
Sagi-shi allows players to add friends by their usernames or user ids:
Nakama Status is a real-time status and presence service that allows users to set their online presence, update their status message and follow other user’s updates.
Players don’t have to be friends with others they want to follow.
Sagi-shi uses status messages and online presences to notify players when their friends are online and share matches.
// Subscribe to the Status event.
NRtDefaultClientListenerlistener;rtClient->setListener(&listener);listener.setStatusPresenceCallback([](constNStatusPresenceEvent&statusPresenceEvent){for(NUserPresencepresence:statusPresenceEvent.joins){cout<<presence.username<<" is online with status: "<<presence.status<<endl;}for(NUserPresencepresence:statusPresenceEvent.leaves){cout<<presence.username<<"went offline"<<endl;}});// Follow mutual friends and get the initial Status of any that are currently online.
autosuccessCallback=[&rtClient](NFriendListPtrfriendList){autofollowSuccessCallback=[](constNStatus&status){for(NUserPresencepresence:status.presences){cout<<presence.username<<" is online with status: "<<presence.status<<endl;}};autofollowErrorCallback=[](constNRtError&error){cout<<"Error following friends: "<<error.message<<endl;};for(NFriendf:friendList->friends){rtClient->followUsers({f.user.id},followSuccessCallback,followErrorCallback);}};autoerrorCallback=[](constNError&error){cout<<"Error listing friends: "<<error.message<<endl;};// Follow mutual friends and get the initial Status of any that are currently online.
client->listFriends(session,1000,NFriend::State::FRIEND,"",successCallback,errorCallback);
Sagi-shi players can change and publish their status to their followers:
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](){cout<<"Successfully updated status"<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error updating status: "<<error.message<<endl;};rtClient->updateStatus("Viewing the Main Menu",successCallback,errorCallback);
Groups have a public or private “open” visibility. Anyone can join public groups, but they must request to join and be accepted by a superadmin/admin of a private group.
Sagi-shi players can create groups around common interests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
autosuccessCallback=[](constNGroup&group){cout<<"Successfully created group: "<<group.id<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error creating group: "<<error.message<<endl;};stringname="Imposters R Us";stringdescription="A group for people who love playing the imposter.";stringavatarUrl="";stringlangTag="";boolopen=true;// public group
intmaxSize=100;client->createGroup(session,name,description,avatarUrl,langTag,open,maxSize,successCallback,errorCallback);
Groups can be listed like other Nakama resources and also filtered with a wildcard group name.
Sagi-shi players use group listing and filtering to search for existing groups to join:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
autosuccessCallback=[&session](NGroupListPtrgroupList){for(NGroupgroup:groupList->groups){cout<<group.name<<": "<<(group.open?"Public":"Private")<<endl;}// Get the next page of results using groupList->cursor.
};autoerrorCallback=[](constNError&error){cout<<"Error listing groups: "<<error.message<<endl;};intlimit=20;stringcursor="";client->listGroups(session,"imposter%",limit,cursor,successCallback,errorCallback);
Nakama group memberships are categorized with the following states:
Code
Purpose
0
Superadmin
There must at least be 1 superadmin in any group. The superadmin has all the privileges of the admin and can additionally delete the group and promote admin members.
1
Admin
There can be one of more admins. Admins can update groups as well as accept, kick, promote, demote, ban or add members.
2
Member
Regular group member. They cannot accept join requests from new users.
3
Join request
A new join request from a new user. This does not count towards the maximum group member count.
Private group admins or superadmins can accept join requests by re-adding the user to the group.
Sagi-shi first lists all the users with a join request state and then loops over and adds them to the group:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autosuccessCallback=[&client](NGroupUserListPtrgroupUserList){for(NGroupUsergroupUser:groupUserList->groupUsers){client->addGroupUsers(session,"<GroupId>",{groupUser.user.id},nullptr,nullptr);}};autoerrorCallback=[](constNError&error){cout<<"Error listing group users: "<<error.message<<endl;};autolimit=opt::nullopt;NUserGroupStatestate=NUserGroupState::JOIN_REQUEST;stringcursor="";client->listGroupUsers(session,"",limit,state,cursor,successCallback,errorCallback);
Nakama group members can be promoted to admin or superadmin roles to help manage a growing group or take over if members leave.
Admins can promote other members to admins, and superadmins can promote other members up to superadmins.
The members will be promoted up one level. For example:
Promoting a member will make them an admin
Promoting an admin will make them a superadmin
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](){cout<<"Successfully promoted group users"<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error promoting group users: "<<error.message<<endl;};client->promoteGroupUsers(session,"<GroupId>",{"<UserId>"},successCallback,errorCallback);
Sagi-shi group admins and superadmins can demote members:
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](){cout<<"Successfully demoted group users"<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error demoting group users: "<<error.message<<endl;};client->demoteGroupUsers(session,"<GroupId>",{"<UserId>"},successCallback,errorCallback);
Sagi-shi group admins and superadmins can remove group members:
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](){cout<<"Successfully kicked group users"<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error kicking group users: "<<error.message<<endl;};client->kickGroupUsers(session,"<GroupId>",{"<UserId>"},successCallback,errorCallback);
Nakama Chat is a real-time chat system for groups, private/direct messages and dynamic chat rooms.
Sagi-shi uses dynamic chat during matches, for players to mislead each other and discuss who the imposters are, group chat and private/direct messages.
Sagi-shi matches have a non-persistent chat room for players to communicate in:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
autosuccessCallback=[](NChannelPtrchannel){cout<<"Connected to dynamic room channel: "<<channel->id<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error connecting to channel: "<<error.message<<endl;};stringroomName="<MatchId>";boolpersistence=false;boolhidden=false;rtClient->joinChat(roomName,NChannelType::ROOM,persistence,hidden,successCallback,errorCallback);
Sagi-shi group members can have conversations that span play sessions in a persistent group chat channel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
autosuccessCallback=[](NChannelPtrchannel){cout<<"Connected to group channel: "<<channel->id<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error connecting to channel: "<<error.message<<endl;};stringgroupId="<GroupId>";boolpersistence=false;boolhidden=false;rtClient->joinChat(groupId,NChannelType::GROUP,persistence,hidden,successCallback,errorCallback);
Sagi-shi players can also chat privately one-to-one during or after matches and view past messages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
autosuccessCallback=[](NChannelPtrchannel){cout<<"Connected to direct message channel: "<<channel->id<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error connecting to channel: "<<error.message<<endl;};stringuserId="<UserId>";boolpersistence=true;boolhidden=false;rtClient->joinChat(userId,NChannelType::DIRECT_MESSAGE,persistence,hidden,successCallback,errorCallback);
autosuccessCallback=[](constNChannelMessageAck&messageAck){cout<<"Successfully sent message: "<<messageAck.messageId<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error sending message: "<<error.message<<endl;};stringchannelId="<ChannelId>";jsonj;j["message"]="I think Red is the imposter!";rtClient->writeChatMessage(channelId,j.dump(),successCallback,errorCallback);jsonj2;j2["emote"]="point";j2["emoteTarget"]="<RedPlayerUserId>";rtClient->writeChatMessage(channelId,j2.dump(),successCallback,errorCallback);
Nakama also supports updating messages. It is up to you whether you want to use this feature, but in a game of deception like Sagi-shi it can add an extra element of deception.
For example a player sends the following message:
1
2
3
4
5
stringchannelId="<ChannelId>";jsonj;j["message"]="I think Red is the imposter!";rtClient->writeChatMessage(channelId,j.dump(),successCallback,errorCallback);
They then quickly edit their message to confuse others:
1
2
3
4
5
6
autosuccessCallback=[&rtClient](constNChannelMessageAck&messageAck){jsonj;j["message"]="I think BLUE is the imposter!";rtClient->updateChatMessage(messageAck.channelId,messageAck.messageId,j.dump(),nullptr,nullptr);};
In server authoritative matches the server controls the gameplay loop and must keep all clients up to date with the current state of the game.
In server relayed matches the client is in control, with the server only relaying information to the other connected clients.
In a competitive game such as Sagi-shi, server authoritative matches would likely be used to prevent clients from interacting with your game in unauthorized ways.
For the simplicity of this guide, the server relayed model is used.
autoerrorRtCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error: "<<error.message<<endl;};autolistFriendsSuccessCallback=[&rtClient,&errorRtCallback,&match](NFriendListPtrfriendList){for(NFriendf:friendList->friends){if(!f.user.online){continue;}autojoinChannelSuccessCallback=[&rtClient,&f,&match](NChannelPtrchannel){jsonj;j["message"]="Hey "+f.user.username+", join me for a match!";j["matchId"]=match.matchId;rtClient->writeChatMessage(channel->id,j.dump(),nullptr,nullptr);};boolpersistence=false;boolhidden=false;rtClient->joinChat(f.user.id,NChannelType::DIRECT_MESSAGE,persistence,hidden,joinChannelSuccessCallback,errorRtCallback);}};intlimit=100;NFriend::Statestate=NFriend::State::FRIEND;stringcursor="";client->listFriends(session,limit,state,cursor,listFriendsSuccessCallback,errorCallback);
Sagi-shi players can try to join existing matches if they know the id:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
autosuccessCallback=[](constNMatch&match){cout<<"Successfully joined match: "<<match.matchId<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};stringmatchId="<MatchId>";NStringMapmetadata={{"Region","EU"}};rtClient->joinMatch(matchId,metadata,successCallback,errorCallback);
Or set up a real-time matchmaker listener and add themselves to the matchmaker:
listener.setMatchmakerMatchedCallback([&rtClient](NMatchmakerMatchedPtrmatchmakerMatched){autosuccessCallback=[](constNMatchmatch){cout<<"Successfully joined match: "<<match.matchId<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};rtClient->joinMatch(matchmakerMatched->matchId,{},successCallback,errorCallback);});autosuccessCallback=[](constNMatchmakerTicket&matchmakerTicket){cout<<"Successfully joined matchmaker: "<<matchmakerTicket.ticket<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};intminPlayers=2;intmaxPlayers=10;stringquery="";NStringMapstringProperties={};NStringDoubleMapnumericProperties={};autocountMultiple=opt::nullopt;rtClient->addMatchmaker(minPlayers,maxPlayers,query,stringProperties,numericProperties,countMultiple,successCallback,errorCallback);
Joining matches from player status
Sagi-shi players can update their status when they join a new match:
1
2
3
4
5
jsonj;j["status"]="Playing a match";j["matchId"]="<MatchID>";rtClient->updateStatus(j.dump(),successCallback,errorCallback);
When their followers receive the real-time status event they can try and join the match:
listener.setStatusPresenceCallback([&rtClient](constNStatusPresenceEvent&statusPresence){for(NUserPresencepresence:statusPresence.joins){autosuccessCallback=[](constNMatch&match){cout<<"Successfully joined match: "<<match.matchId<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};jsonj=json::parse(presence.status);if(j.contains("matchId")){NStringMapmetadata={};rtClient->joinMatch(j["matchId"],metadata,successCallback,errorCallback);}}});
Match Listing takes a number of criteria to filter matches by including player count, a match label and an option to provide a more complex search query.
Sagi-shi matches start in a lobby state. The match exists on the server but the actual gameplay doesn’t start until enough players have joined.
Sagi-shi can then list matches that are waiting for more players:
The match object has a list of current online users, known as presences.
Sagi-shi uses the match presences to spawn players on the client:
1
2
3
4
5
6
7
8
9
// Assuming a GameObject type
//class GameObject { };
map<string,GameObject*>players={};for(NUserPresenceuserPresence:match.presences){GameObject*gameObject=spawnPlayer();// Instantiate player object
players.insert({userPresence.sessionId,gameObject});}
Sagi-shi keeps the spawned players up-to-date as they leave and join the match using the match presence received event:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
listener.setMatchPresenceCallback([&players](constNMatchPresenceEventmatchPresence){// For each player that has joined in this event...
for(NUserPresencepresence:matchPresence.joins){// Spawn a player for this presence and store it in the dictionary by session id.
GameObject*gameObject=spawnPlayer();players.insert({presence.sessionId,gameObject});}// For each player that has left in this event...
for(NUserPresencepresence:matchPresence.leaves){// Remove the player from the game if they've been spawned
if(players.count(presence.sessionId)>0){players.erase(presence.sessionId);}}});
Nakama has real-time networking to send and receive match state as players move and interact with the game world.
During the match, each Sagi-shi client sends match state to the server to be relayed to the other clients.
Match state contains an op code that lets the receiver know what data is being received so they can deserialize it and update their view of the game.
Example op codes used in Sagi-shi:
1: player position
2: player calling vote
Sending player position
Define a class to represent Sagi-shi player position states:
1
2
3
4
5
structpositionState{floatx;floaty;floatz;};
Create an instance from the player’s transform, set the op code and send the JSON encoded state:
1
2
3
4
5
6
7
8
9
// Assuming a position variable
jsonj;j["x"]=position.x;j["y"]=position.y;j["z"]=position.z;intopCode=1;rtClient->sendMatchData(match.matchId,opCode,j.dump());
Op Codes as a static class
Sagi-shi has many networked game actions. Using a static class of constants for op codes will keep your code easier to follow and maintain:
listener.setMatchDataCallback([&players](constNMatchData&matchData){switch(matchData.opCode){caseOpCodes::POSITION:{// Get the updated position data
jsonj=json::parse(matchData.data);positionStateposition{j["x"].get<float>(),j["y"].get<float>(),j["z"].get<float>()};// Update the GameObject associated with that player.
if(players.count(matchData.presence.sessionId)>0){// Here we would normally do something like smoothly interpolate to the new position, but for this example let's just set the position directly.
players[matchData.presence.sessionId].position=newVector3(position.x,position.y,position.z);}}default:cout<<"Unsupported opcode";break;}});
Developers can find matches for players using Match Listing or the Nakama Matchmaker, which enables players join the real-time matchmaking pool and be notified when they are matched with other players that match their specified criteria.
Matchmaking helps players find each other, it does not create a match. This decoupling is by design, allowing you to use matchmaking for more than finding a game match. For example, if you were building a social experience you could use matchmaking to find others to chat with.
The player who creates the party is the party’s leader. Parties have maximum number of players and can be open to automatically accept players or closed so that the party leader can accept incoming join requests.
Sagi-shi uses closed parties with a maximum of 4 players:
1
2
3
4
5
6
7
8
9
10
11
12
13
autosuccessCallback=[](constNParty&party){cout<<"Successfully created party: "<<party.id<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};boolopen=false;intmaxPlayers=4;rtClient->createParty(open,maxPlayers,successCallback,errorCallback);
Sagi-shi shares party ids with friends via private/direct messages:
Safi-shi players can join parties from chat messages by checking for the party id in the message:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
listener.setChannelMessageCallback([&rtClient](constNChannelMessage&channelMessage){autosuccessCallback=[](){cout<<"Successfully joined party"<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};jsonj=json::parse(channelMessage.content);if(j.contains("partyId")){rtClient->joinParty(j["partyId"],successCallback,errorCallback);}});
Sagi-shi party members can be promoted to the party leader:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autosuccessCallback=[](){cout<<"Successfully promoted party member"<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};for(NUserPresencepresence:party.presences){if(presence.sessionId!=party.leader.sessionId){rtClient->promotePartyMember(party.id,presence,successCallback,errorCallback);}}
autosuccessCallback=[](){cout<<"Successfully left party"<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};rtClient->leaveParty(party.id,successCallback,errorCallback);
One of the main benefits of joining a party is that all the players can join the matchmaking pool together.
Sagi-shi players can listen to the the matchmaker matched event and join the match when one is found:
1
2
3
4
5
6
7
8
9
10
11
12
13
listener.setMatchmakerMatchedCallback([&rtClient](NMatchmakerMatchedPtrmatchmakerMatched){autosuccessCallback=[](constNMatch&match){cout<<"Successfully joined match: "<<match.matchId<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};rtClient->joinMatch(matchmakerMatched->matchId,{},successCallback,errorCallback);});
The party leader will start the matchmaking for their party:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autosuccessCallback=[](constNPartyMatchmakerTicket&partyMatchmakerTicket){cout<<"Successfully joined matchmaker as party: "<<partyMatchmakerTicket.ticket<<endl;};autoerrorCallback=[](constNRtError&error){cout<<"Error: "<<error.message<<endl;};intminPlayers=2;intmaxPlayers=10;stringquery="";NStringMapstringProperties={};NStringDoubleMapnumericProperties={};autocountMultiple=opt::nullopt;rtClient->addMatchmakerParty(party.id,query,minPlayers,maxPlayers,stringProperties,numericProperties,countMultiple,successCallback,errorCallback);
Sagi-shi players can receive party data from other party members by subscribing to the party data event.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
listener.setPartyDataCallback([](constNPartyData&partyData){switch(partyData.opCode){caseOpCodes::PARTY_CALL_VOTE:{// Get the username of the player calling the vote and the reason
jsonj=json::parse(partyData.data);stringusername=j["username"].get<string>();stringreason=j["reason"].get<string>();// Show a UI dialogue - "<username> has proposed to call a vote for <reason>. Do you agree? Yes/No"
}default:cout<<"Unsupported opcode";break;}});
Nakama Leaderboards introduce a competitive aspect to your game and increase player engagement and retention.
Sagi-shi has a leaderboard of weekly imposter wins, where player scores increase each time they win, and similarly a leaderboard for weekly crew member wins.
Tournaments have to be created on the server, see the tournament documentation for details on how to create a tournament.
Sagi-shi has a weekly tournament which challenges players to get the most correct imposter votes. At the end of the week the top players receive a prize of in-game currency.
By default in Nakama players don’t have to join tournaments before they can submit a score, but Sagi-shi makes this mandatory:
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[](){cout<<"Successfully joined tournament"<<endl;};autoerrorCallback=[](constNError&error){cout<<"Error: "<<error.message<<endl;};client->joinTournament(session,"<TournamentId>",successCallback,errorCallback);
For performance reasons categories are filtered using a range, not individual numbers. Structure your categories to take advantage of this (e.g. all PVE tournaments in the 1XX range, all PVP tournaments in the 2XX range, etc.).
Nakama uses a code to differentiate notifications. Codes of 0 and below are system reserved for Nakama internals.
Sagi-shi players can subscribe to the notification received event. Sagi-shi uses a code of 100 for tournament winnings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
listener.setNotificationsCallback([](constNNotificationList¬ificationList){constintrewardCode=100;for(NNotificationn:notificationList.notifications){switch(n.code){caserewardCode:cout<<"Congratulations, you won the tournament!"<<endl<<n.subject<<endl<<n.content<<endl;break;default:cout<<"Other notification:"<<endl<<n.subject<<endl<<n.content<<endl;break;}}});