This client library guide will show you how to use the core Nakama features in Defold 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 provides a convenience function for creating and starting a coroutine to run multiple requests synchronously:
1
2
3
4
5
6
7
-- non-blocking coroutinenakama.sync(function()-- blocks in the coroutinelocalclient=nakama.create_client(config)-- blocks in the coroutinelocalnakama.get_account(client)end)
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:
The Nakama Socket is used for gameplay and real-time latency-sensitive features such as chat, parties, matches and RPCs.
Use the client and create a socket:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
localsocket=nakama.create_socket(client)nakama.sync(function()-- connectlocalok,err=nakama.socket_connect(socket)ifokthen-- do socket stuffendiferrthenprint(err.message)endend)
localnakama_session=require"nakama.session"functionauthenticate_with_device()nakama.sync(function()localvars=nillocalcreate=truelocalresult=nakama.authenticate_device(client,defold.uuid(),vars,create,"mycustomusername")ifnotresult.tokenthenprint("Unable to login")returnend-- store the token to use when communicating with the servernakama.set_bearer_token(client,result.token)-- store the toke on disklocalsession=nakama_session.create(token)print(session.user_id)end)end
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.
localnakama_session=require"nakama.session"functionauthenticate_with_facebook()-- Facebook permissionslocalpermissions={"public_profile"}-- you don't need a publishing audience with read permissionslocalaudience=facebook.AUDIENCE_NONEfacebook.login_with_permissions(permissions,audience,function(self,data)localvars=nillocalcreate=truelocalresult=nakama.authenticate_facebook(client,facebook.access_token(),vars,create,"mycustomusername")ifnotresult.tokenthenprint("Unable to login")returnend-- store the token to use when communicating with the servernakama.set_bearer_token(client,result.token)-- store the token on disklocalsession=nakama_session.create(token)print(session.user_id)end)end
functionlink_facebook_authentication()-- Facebook permissionslocalpermissions={"public_profile"}-- you don't need a publishing audience with read permissionslocalaudience=facebook.AUDIENCE_NONEfacebook.login_with_permissions(permissions,audience,function(self,data)localresult=nakama.link_facebook(client,facebook.access_token())ifresult.errorprint(result.error)returnendend)end
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.
Sagi-shi stores the auth token on disk:
1
sys.save(session.token,"token_path")
Restore a session without having to re-authenticate:
1
2
3
4
5
6
7
8
9
10
localnakama_session=require"nakama.session"localtoken=sys.load("token_path")localsession=nakama_session.create(token)ifnakama_session.expired(session)thenprint("Session has expired, re-authenticate.")elsenakama.set_bearer_token(client,session.token)end
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
localtime_one_day=60*60*24-- check whether a session has expired or is close to expiry.ifnakama_session.expired(session)oros.difftime(session.expires,os.time())<time_one_daythenlocalresult=nakama.session_refresh(client,session.token,vars)-- authenticateifresult.errorthenlocalresult=nakama.authenticate_device(client,defold.uuid(),true)endend
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 object, use their user id instead.
Players can only read storage objects they own or that are public (Read permission 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
9
10
11
12
13
14
15
16
17
18
-- Assuming we already have a storage object (storage_object)localobjects={{collection=storage_object.collection,key=storage_object.key,value=json.encode({"cowboy","alien"}),permissionRead=0,permissionWrite=1,version=storage_object.version}}localresult=nakama.write_storage_objects(client,objects)ifresult.errorthenprint(result.message)returnend
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 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
8
9
10
11
12
13
14
locallimit=20-- Limit is capped at 1000localfrienship_state=0localcursor=nillocalresult=nakama.list_friends(client,limit,frienship_state,cursor)ifresult.errorthenprint(result.message)returnendfor_,friendinipairs(result.friends)dopprint(friend)end
locallimit=1000localfrienship_state=2localcursor=nillocallist_friends_result=nakama.list_friends(client,limit,frienship_state,cursor)iflist_friends_result.errorthenprint(list_friends_result.message)returnendlocalids={}for_,friendinipairs(list_friends_result.friends)do-- Collect all idstable.insert(ids,friend.id)end-- Accept all friends requests at oncelocaladd_friends_result=nakama.add_friends(client,ids)
Nakama Status & Presence is 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.
socket.on_statuspresence(function(message)localpressences=message.on_status_presenceifpressencethen-- todo: check if onlinefor_,presenceinipairs(presences)pprint(presence.username.." is online with status: '"..presence.status.."'")endendend)localuser_ids={"<SomeUserId>","<AnotherUserId>"}localresult=socket.status_follow(user_ids)ifresult.errorthenprint(result.error.message)returnend-- todo: check if list of presences received and print
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
localavatar_url="https://example.com/imposter.png"localdescription="A group for people who love playing the imposter."locallang_tag="en"localmax_count=100localname="Imposters R Us"localopen=true-- public grouplocalresult=nakama.create_group(client,avatar_url,description,lang_tag,max_count,name,open)ifresult.errorthenprint(result.message)returnendpprint(result.name)
Nakama allows group superadmin or admin members to update some properties from the client, like the open visibility:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
localavatar_url="https://example.com/imposter.png"localdescription="A group for people who love playing the imposter."localgroup_id="<GroupId>"locallang_tag="en"localname="Imposters R Us"localopen=false-- private grouplocalresult=nakama.update_group(client,group_id,avatar_url,description,group_id,lang_tag,name,open)ifresult.errorthenprint(result.message)returnendpprint(result.open)
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
localname="imposter%"locallimit=20cursor=nillocalresult=nakama.list_groups(client,name,cursor,limit)ifresult.errorthenprint(result.message)returnendfor_,groupinipairs(result.groups)dopprint(group.name.." "..tostring(group.open))end-- Get the next page of results.localnext_result=nakama.list_groups(client,name,result.cursor,limit)
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
7
8
9
10
11
12
13
14
15
localuser_id=account.user.idlocallimit=20localmembership_state=nil-- All membership stateslocalcursor=nillocalresult=list_user_groups(client,user_id,limit,membership_state,cursor)ifresult.errorthenprint(result.message)returnendfor_,groupinipairs(result.groups)dopprint(group.name.." "..tostring(group.state))end
locallimit=20localmembership_state=nil-- All membership stateslocalcursor=nillocalresult=nakama.list_group_users(client,"<GroupId>",limit,membership_state,cursor);ifresult.errorthenprint(result.message)returnendfor_,groupinipairs(result.groups)dopprint(group.name.." "..tostring(group.state))end
locallimit=20localmembership_state=3-- Join requestlocalcursor=nillocallist_group_result=nakama.list_group_users(client,"<GroupId>",limit,membership_state,cursor);iflist_group_result.errorthenprint(list_group_result.message)returnendlocalids={}for_,userinipairs(list_group_result.users)do-- Collect all idstable.insert(ids,user.id)end-- Accept all join requests at oncelocaladd_friends_result=nakama.add_group_users(client,"<GroupId>",ids)
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
localgroup_id="<GroupId>"localpersistence=truelocalhidden=falselocalresult=socket.channel_join(group_id,socket.CHANNELTYPE_GROUP,persistence,hidden)print("Connected to group channel: "..result.channel.id)
Sagi-shi players can also chat privately one-to-one during or after matches and view past messages:
1
2
3
4
5
6
localuser_id="<UserId>"localpersistence=truelocalhidden=falselocalresult=socket.channel_join(user_id,socket.CHANNELTYPE_DIRECT_MESSAGE,persistence,hidden)print("Connected to direct message channel: "..result.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
localchannel_id="<ChannelId>"localcontent={message="I think Red is the imposter!"}localmessage_send_ack=socket.channel_message_send(channel_id,json.encode(content))localemote_content={emote="point",emoteTarget="<RedPlayerUserId>"};localemote_send_ack=socket.channel_message_send(channelId,json.encode(emote_content));
Chat also has cacheable cursors to fetch the most recent messages. Store the cursor in a persistent Lua table using sys.save()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- cache cursor in file 'my_settings'localsettings=sys.load("my_settings")settings["nakama_message_cursor_"..group_id]=result.cacheableCursorsys.save("my_settings",settings)-- get cached cursorlocalsettings=sys.load("my_settings")localcursor=settings["nakama_message_cursor_"..group_id]-- use cursor to get new messagelocalnext_result=nakama.list_channel_messages(client,group_id,limit,forward,cursor)-- update cached cursorsettings["nakama_message_cursor_"..group_id]=next_result.cacheableCursorsys.save("my_settings",settings)
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
localchannelId="<ChannelId>"localmessage_content={message="I think Red is the imposter!"};localmessage_send_ack=socket.channel_message_send(channel_id,json.encode(message_content))
They then quickly edit their message to confuse others:
1
2
3
4
localnew_message_content={message="I think BLUE is the imposter!"}localmessage_update_ack=socket.channel_message_update(channel_id,message_send_ack.message_id,json.encode(new_message_content))
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 update their status when they join a new match:
1
2
3
4
5
6
7
8
9
10
11
localstatus={status="Playing a match",match_id="<MatchId>"}localresult=socket.status_update(status)ifresult.errorthenprint(result.error.message)returnend
When their followers receive the real-time status event they can try and join the match:
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:
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
17
18
19
socket.on_matchdata(function(message)localmatch_data=message.match_datalocalop_code=tonumber(match_data.op_code)localdata=json.decode(match_data.data)ifop_code==op_codes.positoonthen-- todo: check what presence comes inlocalsession_id=match_data.presence.session_idifplayers[session_id]thenlocalplayer=players[session_id]player.x=data.xplayer.ya=data.yendelseifop_code==op_codes.votethen-- vote logicendend)
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:
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
socket.on_party_data(function(message)localparty_data=message.party_datalocalop_code=tonumber(party_data.op_code)localdata=json.decode(party_data.data)ifop_code==vote_op_codethen-- Show a UI dialogue - "<username> has proposed to call a vote for <reason>. Do you agree? Yes/No"endend)
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
localmetadata=json.encode({map="space_station"})-- need to be stringslocalscore="1"localsubscore="0"nakama.write_leaderboard_record(client,"<leaderboard_id>",metadata,nil,score,subscore)
Tournaments have to be created on the server, see the [tournament]../../concepts/tournaments/#create-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.
Categories are filtered using a range, not individual numbers, for performance reasons. 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
socket.on_notification(function(message)localreward_code=100localnotification=message.notificationifnotification.code==reward_codethenprint("Congratulations, you won the tournament!\n"..notification.subject.."\n"..notification.content)elseprint(notification.code.."\n"..notification.subject.."\n"..notification.content)endend)