이 클라이언트 라이브리러 가이드에서는 Among Us(외부)에서 영감을 받은 Sagi-shi(“사기꾼"을 뜻하는 일본어)라는 게임에서 Nakama 고유의 부품을 개발하는 방법(전체 게임 로직이나 UI를 사용하지 않음)을 통해 Godot에서 Nakama의 핵심 기능을 사용하는 방법에 대해서 설명합니다.
Godot은 예외 처리를 지원하지 않기 때문에 비동기적 요청 시 is_exception() 메서드를 사용할 수 있습니다:
1
2
3
4
5
varinvalid_session=NakamaSession.new()# An empty session, which will cause an errorvarinvalid_account=yield(client.get_account_async(invalid_session),"completed")print(invalid_account)# This will print the exceptionifinvalid_account.is_exception():print("We got an exception.")
Nakama 소켓은 채팅, 파티, 대결 및 RPC와 같은 게임 플레이 및 실시간 대기 시간에 민감한 기능에 사용됩니다.
클라이언트에서 소켓을 생성합니다:
1
2
3
4
5
6
7
8
9
10
# Make this a node variable or it will disconnect when the function that creates it returnsonreadyvarsocket:=Nakama.create_socket_from(client)func_ready():varconnected:NakamaAsyncResult=yield(socket.connect_async(session),"completed")ifconnected.is_exception():print("An error occurred: %s"%connected)returnprint("Socket connected.")
Nakama 장치 인증은 물리적인 장치의 고유한 식별자를 사용하여 사용자 인증을 용이하게 하고 계정이 없는 경우 계정을 생성할 수 있습니다.
장치 인증만 사용할 경우, 게임이 실행되면 자동으로 인증이 진행되기 때문에 사용자는 로그인 UI가 필요하지 않습니다.
인증은 Nakama 클라이언트에서 액세스할 수 있는 Nakama 기능의 예시입니다.
1
2
3
4
5
6
7
8
9
# Get the System's unique device identifiervardevice_id=OS.get_unique_id()# Authenticate with the Nakama server using Device Authenticationvarsession:NakamaSession=yield(client.authenticate_device_async(device_id),"completed")ifsession.is_exception():print("An error occurred: %s"%session)returnprint("Successfully authenticated: %s"%session)
사용자 인증을 완료한 경우, 사용자는 계정에서 Nakama 연결 인증 방법을 사용할 수 있습니다.
장치 ID 인증 연결
1
2
3
4
5
6
7
8
vardevice_id="<unique_device_id>"# Link Device Authentication to existing player account.varlinked:NakamaAsyncResult=yield(client.link_custom_async(session,device_id),"completed")iflinked.is_exception():print("An error occurred: %s"%linked)returnprint("Id '%s' linked for user '%s'"%[device_id,session.user_id])
Facebook 인증 연결
1
2
3
4
5
6
7
varoauth_token="<token>"varimport_friends=truevarsession:NakamaSession=yield(client.link_facebook_async(session,oauth_token,import_friends),"completed")ifsession.is_exception():print("An error occurred: %s"%linked)returnprint("Facebook authentication linked for user '%s'"%[session.user_id])
Nakama 세션은 서버 구성에서 설정한 시간이 지나면 만료됩니다. 비활성 세션을 만료시키는 것은 보안 측면에서 모범적인 사례입니다.
Nakama에서는 세션을 복구할 수 있습니다. 예를 들어, Sagi-shi 플레이어가 게임을 다시 실행하거나 토큰에 대해서 새로 고침을 적용하는 경우, 게임이 실행되는 동안 세션이 활성화 상태로 유지됩니다.
세션을 복구하거나 새로 고치려면 세션의 인증 및 새로 고침 토큰을 사용합니다.
재인증 없이 세션을 복구합니다:
1
2
varauth_token="restored from save location"varsession=NakamaClient.restore_session(auth_token)
세션이 만료되었거나 만료가 임박했는지 확인하고 새로 고침을 사용합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Check whether a session has expired or is close to expiryifsession.expired:# Attempt to refresh the existing session.session=yield(client.session_refresh_async(session),"completed)
if session.is_exception():
# Couldn't refresh the session so reauthenticate.
session = yield(client.authenticate_device_async(device_id), "completed")
# Save the new refresh token
<save_file>.set_value("refresh_token", session.refresh_token)
}
# Save the new auth token
<save_file>.set_value("auth_token", session.auth_token)
}
class_namemetadataexport(String)vartitleexport(String)varhatexport(String)varskin# Get the updated account objectvaraccount:NakamaAPI.ApiAccount=yield(client.get_account_async(session),"completed")# Parse the account user metadata.varmetadata=JSON.parse(account.user.metadata)Print("Title: %s",metadata.title)Print("Hat: %s",metadata.hat)Print("Skin: %s",metadata.skin)
쓰기 로직을 어디에 둘 것인지 결정할 때 악의적인 사용자가 게임과 재무 상태에 어떤 역효과를 줄 수 있는지 생각하십시오. 예를 들어 정식으로만 작성해야 하는 데이터(예: 게임 잠금 해제 또는 진행 상황).
Sagi-shi에서 플레이어는 UI를 통해 더 쉽게 액세스할 수 있도록 즐겨찾기 항목을 사용할 수 있으며 클라이언트로부터 이 데이터를 쓰는 것이 안전합니다.
저장소 엔진에 저장소 개체를 작성합니다:
1
2
3
4
5
6
varfavorite_hats=["cowboy","alien"]varcan_read=1# Only the server and owner can readvarcan_write=1# The server and owner can writevaracks:NakamaAPI.ApiStorageObjectAcks=yield(client.write_storage_objects_async(session,[NakamaWriteStorageObject.new("hats","favorite_hats",can_read,can_write)]),"completed")
write_storage_objects_async 메서드에 여러 개의 개체를 전달할 수 있습니다:
저장소 엔진 조건부 작성은 저장소 엔진에 액세스한 후에 개체가 변경되지 않은 경우에만 발생합니다.
이렇게 하면 데이터 덮어쓰기를 방지할 수 있습니다. 예를 들어, 플레이어가 마지막으로 액세스한 이후에 Sagi-shi 서버가 개체를 업데이트 했을 수도 있습니다.
조건부 작성을 실행하려면 버전을 추가하여 가장 최신의 객체 버전에서 저장소 객체를 작성합니다:
1
2
3
4
5
6
7
8
9
10
11
# Assuming we already have a storage objectvarfavorite_hats=["cowboy","alien"]varcan_read=1# Only the server and owner can readvarcan_write=1# The server and owner can writevarversion=<version>varacks:NakamaAPI.ApiStorageObjectAcks=yield(client.write_storage_objects_async(session,[NakamaWriteStorageObject.new("hats","favorite_hats",can_read,can_write,version)]),"completed")ifacks.is_exception():print("An error occurred: %s"%acks)return
Nakama에서 친구를 추가해도 상호적인 친구 관계가 즉각적으로 추가되지 않습니다. 사용자별 진행중인 친구 요청은 사용자가 승인해야 합니다.
Sagi-shi에서 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 추가할 수 있습니다:
1
2
3
4
5
6
7
8
varids=["some_user_id","another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]
# Add friends by username
var result : NakamaAsyncResult = yield(client.add_friends_async(session, usernames), "completed")
# Add friends by user id
var result : NakamaAsyncResult = yield(client.add_friends_async(session, ids), "completed")
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 삭제할 수 있습니다:
1
2
3
4
5
6
7
8
varids=["some_user_id","another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]
# Delete friends by username
var result : NakamaAsyncResult = yield(client.delete_friends_async(session, usernames), "completed")
# Delete friends by user id
var result : NakamaAsyncResult = yield(client.delete_friends_async(session, ids), "completed")
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 다른 사용자를 차단할 수 있습니다:
1
2
3
4
5
6
7
8
varids=["some_user_id","another_user_id]
var usernames = ["AlwaysTheImposter21", "SneakyBoi"]
# Block friends by username
var result : NakamaAsyncResult = yield(client.block_friends_async(session, usernames), "completed")
# Block friends by user id
var result : NakamaAsyncResult = yield(client.block_friends_async(session, ids), "completed")
func_ready():# Setup the socket and subscribe to the status eventsocket.connect("received_status_presence",self,"_on_status_presence")func_on_status_presence(p_presence:NakamaRTAPI.StatusPresenceEvent):print(p_presence)forjinp_presence.joins:print("%s is online with status: %s"%[j.user_id,j.status])forjinp_presence.leaves:print("%s went offline"%[j.user_id])# Follow mutual friends and get the initial Status of any that are currently onlinevarfriends_result=yield(client.list_friends_async(session,0),"completed")varfriend_ids=[]forfriendinfriends_result:varf=friendasNakamaAPI.ApiFriendifnotfornotf.user.online:continuefriend_ids.append(f.user)varresult:NakamaAsyncResult=yield(socket.follow_users_async(friend_ids)forpinresult.presences:print("%s is online with status: %s"%[presence.user_id,presence.status])
그룹에는 공용 또는 개인 “공개” 표시가 있습니다. 누구나 공용 그룹에 가입할 수 있지만 가입을 요청하고 비공개 그룹의 최고 관리자/관리자가 수락해야 합니다.
Sagi-shi 플레이어는 공통의 관심사를 기반으로 그룹을 생성할 수 있습니다:
1
2
3
4
5
6
varname="Imposters R Us"vardescription="A group for people who love playing the imposter."varopen=true# public groupvarmax_size=100vargroup:NakamaAPI.ApiGroup=yield(client.create_group_async(session,name,description,open,max_size),"completed")
그룹은 다른 Nakama 리소스와 같이 목록을 만들고 와일드카드 그룹 이름으로 필터링할 수 있습니다.
Sagi-shi 플레이어는 그룹 목록과 필터링을 통해 기존 그룹을 검색할 수 있습니다:
1
2
3
4
5
6
7
8
9
varlimit=20varresult:NakamaAPI.ApiGroupList=yield(client.list_groups_async(session,"imposter%",limit),"completed")forginresult.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name &s, open %s",[group.name,group.open])$Getthenextpageofresultsvarnext_results:NakamaAPI.ApiGroupList=yield(client.list_groups_async(session,name:"imposter%",limit,result.cursor)
varuser_id="<user id>"varresult:NakamaAPI.ApiUserGroupList=yield(client.list_user_groups_async(session,user_id),"completed")foruginresult.user_groups:varg=ug.groupasNakamaAPI.ApiGroupprint("Group %s role %s",g.id,ug.state)
vargroup_id="<group id>"varmember_list:NakamaAPI.ApiGroupUserList=yield(client.list_group_users_async(session,group_id),"completed")foruginmember_list.group_users:varu=ug.userasNakamaAPI.ApiUserprint("User %s role %s"%[u.id,ug.state])
Sagi-shi 그룹 구성원은 지속적인 그룹 채팅 채널에서 플레이 세션 동안 대화할 수 있습니다:
1
2
3
4
5
6
7
vargroup_id="<group_id>"varpersistence=truevarhidden=falsevartype=NakamaSocket.ChannelType.Groupvarchannel:NakamaRTAPI.Channel=yield(socket.join_chat_async(group_id,type,persistence,hidden),"completed")print("Connected to group channel: '%s'"%[channel.id])
Sagi-shi 플레이어는 대결 중 또는 대결 후에 개인적으로 1:1 채팅을 하고 이전 메시지를 볼 수도 있습니다:
1
2
3
4
5
6
7
varuser_id="<user_id>"varpersistence=truevarhidden=falsevartype=NakamaSocket.ChannelType.DirectMessagevarchannel:NakamaRTAPI.Channel=yield(socket.join_chat_async(user_id,type,persistence,hidden),"completed")print("Connected to direct message channel: '%s'"%[channel.id])
모든 유형의 채팅 채널에서 메시지를 보내는 방법은 동일합니다. 메시지에는 채팅 문자와 이모티콘이 포함되며 JSON 직렬 데이터로 전송됩니다:
1
2
3
4
5
6
7
8
9
10
11
12
varchannel_id="<channel_id>"varmessage_content={"message":"I think Red is the imposter!"}varmessage_ack:NakamaRTAPI.ChannelMessageAck=yield(socket.write_chat_message_async(channel_id,message_content),"completed")varemote_content={"emote":"point","emoteTarget":"<red_player_user_id>",}varemote_ack:NakamaRTAPI.ChannelMessageAck=yield(socket.write_chat_message_async(channel_id,emote_content),"completed")
Nakama는 메시지 업데이트 기능도 지원합니다. 이 기능은 원하는 경우 사용할 수 있지만 Sagi-shi와 같은 속임수 게임에서는 속임수가 추가될 수 있습니다.
예를 들어, 플레이어는 다음의 메시지를 전송합니다:
1
2
3
4
varchannel_id="<channel_id>"varmessage_content={"message":"I think Red is the imposter!"}varmessage_ack:NakamaRTAPI.ChannelMessageAck=yield(socket.write_chat_message_async(channel_id,message_content),"completed")
다른 플레이어에게 혼동을 주기 위해서 메시지를 빠르게 편집할 수 있습니다:
1
2
3
varnew_message_content={"message":"I think BLUE is the imposter!"}varmessage_update_ack:NakamaRTAPI.ChannelMessageAck=yield(socket.update_chat_message_async(channel_id,new_message_content),"completed")
Sagi-shi 플레이어는 대결을 생성하여 온라인 친구들이 참여하도록 초대할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
varmatch:NakamaRTAPI.Match=yield(socket.create_match_async(),"completed")varfriends_list=yield(client.list_friends_async(session,0,100)varonline_friends=[]forfriendinfriends_list:varf=friendasNakamaAPI.ApiFriendifnotfornotf.user.online:continueonline_friends.append(f.user)forfinonline_friends:varcontent={"message":"Hey %s, join me for a match!",match_id=match.id,}varchannel=yield(socket.join_chat_async(f.id,NakamaSocket.ChannelType.DirectMessage),"completed")varmessage_ack=yield(socket.write_chat_message_async(channel.id,content),"completed")
대결 이름으로 대결 생성하기
Sagi-shi 플레이어는 특정한 대결 이름을 사용하여 대결을 생성하여 친구에게 대결의 이름을 알려주고 초대할 수 있습니다. 이름으로 대결을 만들 때(임의의 이름이고 정식 대결 핸들러에 연결되지 않은 것) 대결은 항상 정식 대결이 아니라 중계된 대결이라는 점에 유의해야 합니다.
varstatus={"status":"Playing a match","matchid":"<match_id>",}yield(socket.update_status_async(status),"completed")
팔로워가 실시간 상태 이벤트를 수신한 경우, 대결에 참여할 수 있습니다:
1
2
3
4
5
6
func_on_status_presence(p_presence:NakamaRTAPI.StatusPresenceEvent):# Join the first match found in a friend's statusforjinp_presence.joins:varstatus=JSON.parse(p_presence.status)ifmatchidinstatus:yield(socket.join_match_async(status["matchid"]),"completed")
Sagi-shi는 대결 현재 상태 수신 이벤트를 사용하여 생성된 플레이어를 최신 상태로 유지합니다:
1
2
3
4
5
6
7
8
9
10
11
12
func_on_match_presence(p_presence:NakamaRTApi.MatchPresenceEvent):# For each player that has joined in this event...forpinp_presence.joins:# Spawn a player for this presence and store it in a dictionary by session id.vargo=<player_node>.new()players.add(p_presence.session_id,go)# For each player that has left in this event...forpinp_presence.leaves:# Remove the player from the game if they've been spawnedifpresence.session_idinplayers:<player_node>.remove_and_skip()players.remove(presence.session_id)
Sagi-shi 플레이어는 대결 상태 수신 이벤트를 구독하여 다른 연결된 클라이언트로부터 대결 데이터를 수신할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
func_on_match_state(p_state:NakamaRTAPI.MatchData):matchmatch_state.op_code:op_code.position:# Get the updated position datavarposition_state=JSON.parse(match_state.state)# Update the game object associated with that playervaruser=match_state.user_presence.session_idifuserinplayers:# 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[user].transform.Vector3=vec(position_state.x,position_state.y,position_state.z)_:print("Unsupported op code.")
개발자는 대결 목록 또는 Nakama 매치 메이커를 사용하여 플레이어의 대결을 찾을 수 있습니다. 이를 통해 플레이어는 실시간 매치메이킹 풀에 참여하고 지정된 기준과 일치하는 다른 플레이어와 대결이 성사될 때 알림을 받을 수 있습니다.
매치메이킹을 통해 플레이어는 서로를 찾을 수 있지만 대결을 생성하지는 않습니다. 이러한 분리는 의도된 것이므로 게임 대결을 찾는 것 이상으로 매치메이킹을 사용할 수 있습니다. 예를 들어, 소셜 경험을 만들고 있는 경우 매치메이킹을 사용하여 채팅할 다른 사람을 찾을 수 있습니다.
Nakama는 알림을 구별하기 위해서 코드를 사용합니다. 0 이하의 코드는 Nakama 내부 직원을 위해 예약된 시스템입니다.
Sagi-shi 플레이어는 알림 수신 이벤트를 구독할 수 있습니다. Sagi-shi는 토너먼트 우승 100 코드를 사용합니다:
1
2
3
4
5
6
7
8
contreward_code=100func_on_notification(p_notification:NakamaAPI.ApiNotification):matchnotification.code:reward_code:print("Congratulations, you won the tournament!\n%s\n%s",notification.subject,notification.content)_:print("Other notification: %s:%s\n%s",notification.code,notification.subject,notification.content)