This client library guide will show you how to use the core Nakama features in Swift 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”).
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:
Each request to Nakama from the client must complete in a certain period of time before it is considered to have timed out. You can configure how long this period is (in seconds) by passing the value for deadlineAfter parameter on the client init. It defaults to 20:
Nakama Device Authentication uses the physical device’s unique identifier to easily authenticate a user and create an account if one does not exist.
When using only device authentication, you don’t need a login UI as the player can automatically authenticate when the game launches.
Authentication is an example of a Nakama feature accessed from a Nakama Client instance.
In this example we use UIDevice API available in iOS to get device unique id:
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.
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
// Acquiring the unique device ID has been shortened for brevity, see previous example.vardeviceId="<uniqueDeviceId>"// Link Device Authentication to existing player account.do{tryawaitclient.linkDevice(session:session,deviceId:deviceId)print("Successfully linked Device ID authentication to existing player account")}catch{debugPrint(error)}
Linking Facebook authentication
1
2
3
4
5
6
7
8
9
varoauthToken="<token>"varimportFriends=truedo{tryawaitclient.linkFacebook(session:session,oauthToken:oauthToken,importFriends:importFriends)print("Successfully linked Facebook authentication to existing player account")}catch{debugPrint(error)}
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.
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
ifsession.hasExpired(offset:Date().addingTimeInterval(5*60)){// Session about to expire in 5 minutesdo{session=tryawaitclient.refreshSession(session:session)}catch{// Couldn't refresh the session so reauthenticatesession=tryawaitclient.authenticateDevice(deviceId:deviceId)varrefreshToken=session.refresh_token}varauthToken=session.token}
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:
Get the updated account object and parse the JSON metadata:
1
2
3
4
5
6
7
8
9
10
11
// Get the updated account object.varaccount=tryawaitclient.getAccount(session:session)// Parse the account user metadata.varmetadata=account.user.metadataprint("Title: \(metadata["title"])\n"+"Hat: \(metadata["hat"])\n"+"Skin: \(metadata["skin"])\n")
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:
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
// Assuming we already have a storage object (storageObject)varwriteObject=WriteStorageObject(collection:storageObject.collection,key:storageObject.key,value:"<NewJSONValue>",permissionRead:1,permissionWrite:0,version:storageObject.version)do{tryawaitclient.writeStorageObjects(session:session,objects:[writeObject])}catch{debugPrint(error)}
Instead of doing multiple read requests with separate keys you can list all the storage objects the player has access to in a collection.
Sagi-shi lists all the player’s unlocked or purchased titles, hats and skins:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
varlimit=3varcursor:String?=nilvarunlocksObjectList=tryawaitclient.listStorageObjects(session:session,collection:"Unlocks",limit:limit,cursor:cursor)unlocksObjectList.objects.forEach{unlockStorageObjectinswitchunlockStorageObject.key{case"Titles":letunlockedTitles=tryJSONSerialization.jsonObject(with:unlockStorageObject.value.data(using:.utf8)!,options:[])as![String:[String]]// Display the unlocked titlescase"Hats":letunlockedHats=tryJSONSerialization.jsonObject(with:unlockStorageObject.value.data(using:.utf8)!,options:[])as![String:[String]]// Display the unlocked hatscase"Skins":letunlockedSkins=tryJSONSerialization.jsonObject(with:unlockStorageObject.value.data(using:.utf8)!,options:[])as![String:[String]]// Display the unlocked skinsdefault:break}}
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 nil, 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.
Nakama Remote Procedures can be called from the client and take optional JSON payloads.
The Sagi-shi client makes an RPC to securely equip a hat:
1
2
3
4
5
6
7
varpayload=["item":"cowboy"]do{letresponse=tryawaitclient.rpc(session:session,id:"EquipHat",payload:payload)print("New hat equipped successfully",response)}catch{debugPrint(error)}
Nakama Remote Procedures can also be called from the socket when you need to interface with Nakama’s real-time functionality. These real-time features require a live socket (and corresponding session identifier). RPCs can be made on the socket carrying this same identifier.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Add friends by Username.varusernames=["AlwaysTheImposter21","SneakyBoi"]do{tryawaitclient.addFriends(session:session,usernames:usernames)}catch{debugPrint(error)}// Add friends by User ID.varids=["<SomeUserId>","<AnotherUserId>"]do{tryawaitclient.addFriends(session:session,ids:ids)}catch{debugPrint(error)}
Nakama allows developers to list the player’s friends based on their friendship state.
Sagi-shi lists the 20 most recent mutual friends:
1
2
3
4
5
6
7
letlimit=1000// Limit is capped at 1000letfriendshipState=0varresult=tryawaitclient.listFriends(session:session,limit:limit)result.friends.forEach{friendinprint("ID: \(friend.user.id)")}
Sagi-shi players can remove friends by their username or user id:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Delete friends by User ID.varids=["<SomeUserId>","<AnotherUserId>"]do{tryawaitclient.deleteFriends(session:session,ids:ids)}catch{debugPrint(error)}// Delete friends by Username.varusernames=["AlwaysTheImposter21","SneakyBoi"]do{tryawaitclient.deleteFriends(session:session,ids:[],usernames:usernames)}catch{debugPrint(error)}
Sagi-shi players can block others by their username or user id:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Block friends by User ID.varids=["<SomeUserId>","<AnotherUserId>"]do{tryawaitclient.blockFriends(session:session,ids:ids)}catch{debugPrint(error)}// Block friends by Username.varusernames=["AlwaysTheImposter21","SneakyBoi"]do{tryawaitclient.blockFriends(session:session,usernames:usernames)}catch{debugPrint(error)}
Nakama Status & Presence has 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.socket.onStatusPresence={presenceEventinpresenceEvent.joins.forEach{presenceinprint("\(presence.username) is online with status: \(presence.status)")}presenceEvent.leaves.forEach{presenceinprint("\(presence.username) went offline")}}// Follow mutual friends and get the initial Status of any that are currently online.varfriendsResult=tryawaitclient.listFriends(session:session,state:0,limit:1000,cursor:nil)varfriendIds=friendsResult.friends.map{friendinreturnfriend.user.id}varresult=tryawaitsocket.followUsers(userIds:friendIds)result.presences.forEach{presenceinprint("\(presence.username) is online with status: \(presence.status)")}
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
vargroupName="Imposters R Us"vardescription="A group for people who love playing the imposter."do{letgroup=tryawaitclient.createGroup(session:session,name:groupName,description:description,avatarUrl:nil,langTag:nil,open:true,maxCount:100)print("Group created successfully",group)}catch{debugPrint(error)}
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
varlimit=20varresult=tryawaitclient.listGroups(session:session,name:"imposter%",limit:limit,cursor:nil)result.groups.forEach{groupinprint("\(group.name) group is \(group.open)")}// Get the next page of results.varnextResults=tryawaitclient.listGroups(session:session,name:"imposter%",limit:limit,cursor:result.cursor)
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.
Sagi-shi players can list groups they are a member of:
1
2
3
4
5
6
varuserId="<userId>"varresult=tryawaitclient.listUserGroups(session:session,userId:userId)result.userGroups.forEach{userGroupinprint("Group: name \(userGroup.group.name) State: \(userGroup.state)")}
vargroupId="<groupId>"varresult=tryawaitclient.listGroupUsers(session:session,groupId:groupId)result.groupUsers.forEach{groupUserinprint("User: ID \(groupUser.user.id) State: \(groupUser.state)")}
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 group members can have conversations that span play sessions in a persistent group chat channel:
1
2
3
4
5
6
7
vargroupId="<groupId>"varpersistence=truevarhidden=false// 1 = Room, 2 = Direct Message, 3 = Groupvarchannel=tryawaitsocket.joinChat(groupId:groupId,type:3,persistence:persistence,hidden:hidden)print("Connected to group channel: \(channel.id)")
Sending messages is the same for every type of chat channel. Messages contain chat text and emotes and are sent as JSON serialized data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
varchannelId="<channel id>"vardata=["message":"I think Red is the imposter!"]do{letmessageAck=tryawaitsocket.writeChatMessage(channelId:channelId,data:data)print("Message sent successfully",messageAck)}catch{debugPrint(error)}varemoteData=["emote":"point","emoteTarget":"<redPlayerUserId>"]do{letmessageAck=tryawaitsocket.writeChatMessage(channelId:channelId,data:emoteData)print("Emote sent successfully",messageAck)}catch{debugPrint(error)}
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
6
7
8
varchannelId="<ChannelId>"vardata=["message":"I think Red is the imposter!"]do{letmessageAck=tryawaitsocket.writeChatMessage(channelId:channelId,data:data)print("Message sent successfully",messageAck)}catch{debugPrint(error)}
They then quickly edit their message to confuse others:
1
2
3
4
5
6
7
8
vardata=["message":"I think BLUE is the imposter!"]do{letmessageAck=tryawaitsocket.updateChatMessage(channelId:channelId,messageId:messageSendAck.message.id,data:data)print("Message updated successfully",messageAck)}catch{debugPrint(error)}
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.
Sagi-shi players can create their own matches and invite their online friends to join:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
varmatch=tryawaitsocket.createMatch()varfriendsList=tryawaitclient.listFriends(session:session)varonlineFriends=friendsList.friends.filter{friendinreturnfriend.user.online}onlineFriends.forEach{friendinvarmessageData=["message":"Hey \(friend.username), join me for a match!"]varmatchId=match.iddo{letchannel=tryawaitsocket.joinChat(userId:friend.user.id,type:2,persistence:true,hidden:false)letmessageAck=tryawaitsocket.writeChatMessage(channelId:channel.id,data:messageData)print("Message sent successfully",messageAck)}catch{debugPrint(error)}}
Creating a match by match name
Sagi-shi players can also create matches with a specific match name, this allows them to invite their friends by telling them the name of the match. It should be noted that when creating a match by name (which is an arbitrary name and not something tied to authoritative match handlers), the match will always be a relayed match rather than an authoritative match.
Sagi-shi players can try to join existing matches if they know the id:
1
2
3
4
5
6
7
varmatchId="<MatchId>"do{letmatch=tryawaitsocket.joinMatch(matchId:matchId)print("Joined match successfully",match)}catch{debugPrint(error)}
Or set up a real-time matchmaker listener and add themselves to the matchmaker:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
socket.onmatchmakermatched={matchmakerMatchedindo{letmatch=tryawaitsocket.joinMatch(matchmakerMatched)print("Joined match successfully",match)}catch{debugPrint(error)}}varminPlayers=2varmaxPlayers=10varquery=""do{letmatchmakingTicket=tryawaitsocket.addMatchmaker(query:query,minCount:minPlayers,maxCount:maxPlayers)print("Matchmaking ticket added successfully",matchmakingTicket)}catch{debugPrint(error)}
Joining matches from player status
Sagi-shi players can update their status when they join a new match:
1
2
3
4
5
6
7
8
9
10
11
varstatus=["Status":"Playing a match","MatchId":"<MatchId>"]do{tryawaitsocket.updateStatus(status:status)print("Status updated successfully")}catch{debugPrint(error)}
When their followers receive the real-time status event they can try and join the match:
1
2
3
4
5
6
7
8
9
10
11
12
13
socket.onstatuspresence={presenceEventinpresenceEvent.joins.forEach{presenceinletstatus=presence.statusas![String:String]ifstatus["MatchId"]!=nil{do{letmatch=tryawaitsocket.joinMatch(matchId:status["MatchId"]!)print("Joined match successfully",match)}catch{debugPrint(error)}}}}
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
varmatch=tryawaitsocket.joinMatch(matchId:matchId)varplayers=[String:GameObject]()match.presences.forEach{presenceinvargo=spawnPlayer()// Instantiate player objectplayers[presence.session.id]=go}
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
socket.onmatchpresence={matchPresenceEventin// For each player that has joined in this event...matchPresenceEvent.joins.forEach{presencein// Spawn a player for this presence and store it in a dictionary by session id.vargo=spawnPlayer()// Instantiate player objectplayers[presence.session.id]=go}// For each player that has left in this event...matchPresenceEvent.leaves.forEach{presencein// Remove the player from the game if they've been spawnedifplayers[presence.session.id]!=nil{players[presence.session.id]=nil}}}
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
10
11
12
13
14
varstate=PositionState(){X=transform.position.x,Y=transform.position.y,Z=transform.position.z}varopCode=1do{letmessageAck=tryawaitsocket.sendMatchState(matchId:match.id,opCode:opCode,data:state)print("Match state sent successfully",messageAck)}catch{debugPrint(error)}
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:
1
2
3
4
5
6
7
8
9
10
11
classOpCodes{staticvarposition=1staticvarvote=2}do{letmessageAck=tryawaitsocket.sendMatchState(matchId:match.id,opCode:OpCodes.position,data:state)print("Match state sent successfully",messageAck)}catch{debugPrint(error)}
Sagi-shi players can receive match data from the other connected clients by subscribing to the match state received event:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
socket.onmatchdata={matchStateinswitchmatchState.opCode{caseOpCodes.position:// Get the updated position dataletstateJson=matchState.stateletpositionState=JSON.parse(stateJson)// Update the GameObject associated with that playerifplayers[matchState.user_presence.session.id]!=nil{// 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[matchState.user_presence.session.id].transform.position=newVector3(positionState.s,positionState.y,positionState.z)}default:print("Unsupported op code")}}
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.
After being successfully matched according to the provided criteria, players can join the match:
1
2
3
4
5
6
7
8
socket.onmatchmakermatched={matchmakerMatchedindo{letmatch=tryawaitsocket.joinMatch(matchmakerMatched)print("Joined match successfully",match)}catch{debugPrint(error)}}
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
varopen=falsevarmaxPlayers=4do{letparty=tryawaitsocket.createParty(open:open,maxCount:maxPlayers)print("Party created successfully",party)}catch{debugPrint(error)}
Sagi-shi shares party ids with friends via private/direct messages:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
varfriendsList=tryawaitclient.listFriends(session:session)varonlineFriends=friendsList.friends.filter{friendinreturnfriend.user.online}onlineFriends.forEach{friendinvarmessageData=["message":"Hey \(friend.username), wanna join the party?"]varpartyId=party.iddo{letchannel=tryawaitsocket.joinChat(userId:friend.user.id,type:2,persistence:true,hidden:false)letmessageAck=tryawaitsocket.writeChatMessage(channelId:channel.id,data:messageData)print("Message sent successfully",messageAck)}catch{debugPrint(error)}}
Sagi-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
socket.onchannelmessage={channelMessageinletcontent=channelMessage.contentas![String:String]ifcontent["partyId"]!=nil{do{letparty=tryawaitsocket.joinParty(partyId:content["partyId"]!)print("Joined party successfully",party)}catch{debugPrint(error)}}}
Sagi-shi party members can be promoted to the party leader:
1
2
3
4
5
6
7
varnewLeader="<user id>"do{tryawaitsocket.promotePartyMember(partyId:party.id,newLeader:newLeader)print("Party member promoted successfully")}catch{debugPrint(error)}
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
socket.onmatchmakermatched={matchmakerMatchedindo{letmatch=tryawaitsocket.joinMatch(matchmakerMatched)print("Joined match successfully",match)}catch{debugPrint(error)}}
The party leader will start the matchmaking for their party:
Sagi-shi players can send data to other members of their party to indicate they wish to start a vote.
1
2
3
4
5
6
7
8
9
10
11
varstate=["username":"<Username>","reason":"Emergency"]do{letmessageAck=tryawaitsocket.sendPartyData(partyId:party.id,opCode:OpCodes.partyCallVote,data:state)print("Party data sent successfully",messageAck)}catch{debugPrint(error)}
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
socket.onpartydata={partyDatainswitchpartyData.opCode{caseOpCodes.partyCallVote:// Get the vote dataletstateJson=partyData.dataletvoteState=JSON.parse(stateJson)// Show a UI dialogue - "<username> has proposed to call a vote for <reason>. Do you agree? Yes/No"default:print("Unsupported op code")}}
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.
When players submit scores, Nakama will increment the player’s existing score by the submitted score value.
Along with the score value, Nakama also has a subscore, which can be used for ordering when the scores are the same.
Sagi-shi players can submit scores to the leaderboard with contextual metadata, like the map the score was achieved on:
1
2
3
4
5
6
7
8
9
varscore=1varsubscore=0varmetadata=["map":"space_station"]do{letleaderboardRecord=tryawaitclient.writeLeaderboardRecord(session:session,leaderboardId:"weekly_imposter_wins",score:score,subscore:subscore,metadata:metadata)print("Leaderboard record written successfully",leaderboardRecord)}catch{debugPrint(error)}
Sagi-shi players can delete their own leaderboard records:
1
2
3
4
5
6
7
varleaderboardId="<leaderboard id>"do{tryawaitclient.deleteLeaderboardRecord(session:session,leaderboardId:leaderboardId)print("Leaderboard record deleted successfully")}catch{debugPrint(error)}
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
vartournamentId="<tournament id>"do{tryawaitclient.joinTournament(session:session,tournamentId:tournamentId)print("Tournament joined successfully")}catch{debugPrint(error)}
Sagi-shi players can list and filter tournaments with various criteria:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
varcategoryStart=1varcategoryEnd=2varstartTime=1538147711varendTime=nil// all tournaments from the start timevarlimit=100// number to list per pagevarcursor=nildo{letresult=tryawaitclient.listTournaments(session:session,categoryStart:categoryStart,categoryEnd:categoryEnd,startTime:startTime,endTime:endTime,limit:limit,cursor:cursor)result.tournaments.forEach{tournamentinprint("\(tournament.id): \(tournament.title)")}}catch{debugPrint(error)}
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.).
Sagi-shi players can submit scores, subscores and metadata to the tournament:
1
2
3
4
5
6
7
8
9
10
11
vartournamentName="weekly_top_detective"varscore=1varsubscore=0varmetadata=["map":"space_station"]do{lettournamentRecord=tryawaitclient.writeTournamentRecord(session:session,tournamentId:tournamentName,score:score,subscore:subscore,metadata:metadata)print("Tournament record written successfully",tournamentRecord)}catch{debugPrint(error)}
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
socket.onnotification={notificationinletrewardCode=100switchnotification.code{caserewardCode:print("Congratulations, you won the tournament!\n\(notification.subject)\n\(notification.content)")default:print("Other notification: \(notification.code):\(notification.subject)\n\(notification.content)")}}
Sagi-shi players can list the notifications they received while offline:
1
2
3
4
5
6
7
varresult=tryawaitclient.listNotifications(session:session,limit:limit,cacheableCursor:nil)result.notifications.forEach{notificationinprint("Notification: \(notification.code):\(notification.subject)\n\(notification.content)")}print("Fetch more results with cursor:",result.cacheable_cursor)