이 클라이언트 라이브리러 가이드에서는 Among Us(외부)에서 영감을 받은 Sagi-shi(“사기꾼"을 뜻하는 일본어)라는 게임에서 Nakama 고유의 부품을 개발하는 방법(전체 게임 로직이나 UI를 사용하지 않음)을 통해 JavaScript에서 Nakama의 핵심 기능을 사용하는 방법에 대해서 설명합니다.
// This import is only required with React Native
vardeviceInfo=require('react-native-device-info');vardeviceId=null;// If the user's device ID is already stored, grab that - alternatively get the System's unique device identifier.
try{constvalue=awaitAsyncStorage.getItem('@MyApp:deviceKey');if(value!==null){deviceId=value}else{deviceId=deviceInfo.getUniqueID();// Save the user's device ID so it can be retrieved during a later play session for re-authenticating.
AsyncStorage.setItem('@MyApp:deviceKey',deviceId).catch(function(error){console.log("An error occurred: %o",error);});}}catch(error){console.log("An error occurred: %o",error);}// Authenticate with the Nakama server using Device Authentication.
varcreate=true;constsession=awaitclient.authenticateDevice(deviceId,create,"mycustomusername");console.info("Successfully authenticated:",session);
사용자 인증을 완료한 경우, 사용자는 계정에서 Nakama 연결 인증 방법을 사용할 수 있습니다.
장치 ID 인증 연결
1
2
3
4
5
6
7
8
9
10
11
// Acquiring the unique device ID has been shortened for brevity, see previous example.
vardeviceId="<uniqueDeviceId>";// Link Device Authentication to existing player account.
try{awaitclient.linkDevice(session,deviceId);console.log("Successfully linked Device ID authentication to existing player account");}catch(err){console.log("Error linking Device ID: %o",err.message);}
Facebook 인증 연결
1
2
3
4
5
6
7
8
9
constoauthToken="<token>";constimport=true;try{constsession=awaitclient.linkFacebook(session,oauthToken,true,import);console.log("Successfully linked Facebook authentication to existing player account");}catch(err){console.log("Error authenticating with Facebook: %o",err.message);}
// Check whether a session has expired or is close to expiry.
if(session.isexpired||session.isexpired(Date.now+1){try{// Attempt to refresh the existing session.
session=awaitclient.sessionRefresh(session);}catch(error){// Couldn't refresh the session so reauthenticate.
session=awaitclient.authenticateDevice(deviceId);varrefreshToken=session.refresh_token;}varauthToken=session.token;}
// Get the updated account object.
varaccount=awaitclient.getAccount(session);// Parse the account user metadata.
varmetadata=JSON.parse(account.user.metadata);console.log("Title: %o",metadata.title);console.log("Hat: %o",metadata.hat);console.log("Skin: %o",metadata.skin);
쓰기 로직을 어디에 둘 것인지 결정할 때 악의적인 사용자가 게임과 재무 상태에 어떤 역효과를 줄 수 있는지 생각하십시오. 예를 들어 정식으로만 작성해야 하는 데이터(예: 게임 잠금 해제 또는 진행 상황).
Sagi-shi에서 플레이어는 UI를 통해 더 쉽게 액세스할 수 있도록 즐겨찾기 항목을 사용할 수 있으며 클라이언트로부터 이 데이터를 쓰는 것이 안전합니다.
컬렉션 이름, 키, JSON 인코딩 데이터를 사용하여 저장소 개체 작성하기를 생성합니다. 마지막으로, 저장소 엔진에 저장소 객체를 작성합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
varfavoriteHats=new{hats=["cowboy","alien"]};varwriteObject=newWriteStorageObject{collection="favorites",ley="Hats",value=JSON.stringify(favoriteHats),permissionRead=1,// Only the server and owner can read
permissionWrite=1// The server and owner can write
};awaitclient.writeStorageObjects(session,writeObject);
WriteStorageObjectsAsync 메서드에 여러 개의 객체를 전달할 수 있습니다.
저장소 엔진 조건부 작성은 저장소 엔진에 액세스한 후에 개체가 변경되지 않은 경우에만 발생합니다.
이렇게 하면 데이터 덮어쓰기를 방지할 수 있습니다. 예를 들어, 플레이어가 마지막으로 액세스한 이후에 Sagi-shi 서버가 개체를 업데이트 했을 수도 있습니다.
조건부 작성을 실행하려면 버전을 추가하여 가장 최신의 객체 버전에서 저장소 객체를 작성합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Assuming we already have a storage object (storageObject)
varwriteObject=newWriteStorageObject{collection=storageObject.collection,key=storageObject.key,value="<NewJSONValue>",permissionWrite=0,permissionRead=1,version=storageObject.version};try{awaitclient.writeStorageObjects(session,writeObjects);}catch(error){console.log(error.message);}
클라이언트에서 Nakama 원격 프로시저를 호출하여 JSON 페이로드를 선택할 수 있습니다.
Sagi-shi 클라이언트는 모자를 안전하게 구비하기 위해서 RPC를 만듭니다.
1
2
3
4
5
6
7
8
try{varpayload={"item":"cowboy"};varresponse=awaitclient.rpc(session,"EquipHat",payload);console.log("New hat equipped successfully",response);}catch(error){console.log("Error: %o",error.message);}
Nakama에서 친구를 추가해도 상호적인 친구 관계가 즉각적으로 추가되지 않습니다. 사용자별 진행중인 친구 요청은 사용자가 승인해야 합니다.
Sagi-shi에서 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 추가할 수 있습니다:
1
2
3
4
5
6
7
// Add friends by Username.
varusernames=["AlwaysTheImposter21","SneakyBoi"];awaitclient.addFriends(session,usernames);// Add friends by User ID.
varids=["<SomeUserId>","<AnotherUserId>"];awaitclient.addFriends(session,ids);
개발자는 Nakama에서 친구 관계 상태를 기반으로 플레이어의 친구 목록을 만들 수 있습니다.
Sagi-shi에서는 20명의 가장 최근 친구가 목록으로 표시됩니다:
1
2
3
4
5
6
7
varlimit=20;// Limit is capped at 1000
varfriendshipState=0;varresult=awaitclient.listFriends(session,friendshipState,limit,cursor:null);result.forEach((friend)=>{console.log("ID: %o",friend.user.id);});
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 삭제할 수 있습니다:
1
2
3
4
5
6
7
// Delete friends by User ID.
varids=["<SomeUserId>","<AnotherUserId>"];awaitclient.deleteFriends(session,ids});// Delete friends by Username.
varusernames=["AlwaysTheImposter21","SneakyBoi"];awaitclient.deleteFriends(session,null,usernames});
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 다른 사용자를 차단할 수 있습니다:
1
2
3
4
5
6
7
// Block friends by User ID.
varids=["<SomeUserId>","<AnotherUserId>"];awaitclient.blockFriends(session,ids);// Block friends by Username.
varusernames=["AlwaysTheImposter21","SneakyBoi"];awaitclient.blockFriends(session,usernames);
// Subscribe to the Status event.
socket.onstatuspresence=(e)=>{e.joins.forEach(function(presence){console.log("%o is online with status: %o",presence.username,presence.status);})e.leaves.forEach(function(presence){console.log("%o went offline",presence.username);})};// Follow mutual friends and get the initial Status of any that are currently online.
varfriendsResult=awaitclient.listFriends(session,0);varfriendIds=[];friendsResult.friends.forEach(function(friend){friendIds.push(friend.user.id);});varresult=awaitsocket.followUsers(friendIds);result.presences.forEach(function(presence){console.log("%o is online with status: %o",presence.username,presence.status);});
그룹에는 공용 또는 개인 “공개” 표시가 있습니다. 누구나 공용 그룹에 가입할 수 있지만 가입을 요청하고 비공개 그룹의 최고 관리자/관리자가 수락해야 합니다.
Sagi-shi 플레이어는 공통의 관심사를 기반으로 그룹을 생성할 수 있습니다:
1
2
3
4
5
6
7
8
9
constgroupName="Imposters R Us";constdescription="A group for people who love playing the imposter.";constgroup=awaitclient.createGroup(session{name:groupName,description:description,open:true,// public group
maxSize=100});
그룹은 다른 Nakama 리소스와 같이 목록을 만들고 와일드카드 그룹 이름으로 필터링할 수 있습니다.
Sagi-shi 플레이어는 그룹 목록과 필터링을 통해 기존 그룹을 검색할 수 있습니다:
1
2
3
4
5
6
7
8
9
varlimit=20;varresult=awaitclient.ListGroupsAsync(session,"imposter%",limit);result.groups.forEach(function(group){console.log("%o group is %o",group.name,group.open);});// Get the next page of results.
varnextResults=awaitclient.listGroups(session,name:"imposter%",limit,result.cursor);
Sagi-shi 그룹 구성원은 지속적인 그룹 채팅 채널에서 플레이 세션 동안 대화할 수 있습니다:
1
2
3
4
5
6
7
constgroupId="<group id>";constpersistence=true;consthidden=false;// 1 = Room, 2 = Direct Message, 3 = Group
constchannel=awaitsocket.joinChat(3,groupId,persistence,hidden);console.log("Connected to group channel: %o",channel.id);
Sagi-shi 플레이어는 대결 중 또는 대결 후에 개인적으로 1:1 채팅을 하고 이전 메시지를 볼 수도 있습니다:
1
2
3
4
5
6
7
constuserId="<user id>";constpersistence=true;consthidden=false;// 1 = Room, 2 = Direct Message, 3 = Group
constchannel=awaitsocket.joinChat(2,userId,persistence,hidden);console.log("Connected to direct message channel: %o",channel.id);
모든 유형의 채팅 채널에서 메시지를 보내는 방법은 동일합니다. 메시지에는 채팅 문자와 이모티콘이 포함되며 JSON 직렬 데이터로 전송됩니다:
1
2
3
4
5
6
7
8
9
varchannelId="<channel id>";vardata={"message":"I think Red is the imposter!"};constmessageAck=awaitsocket.writeChatMessage(channelId,data);varemoteData={"emote":"point","emoteTarget":"<redPlayerUserId>"}constemoteMessageAck=awaitsocket.writeChatMessage(channelId,emoteData);
Nakama는 메시지 업데이트 기능도 지원합니다. 이 기능은 원하는 경우 사용할 수 있지만 Sagi-shi와 같은 속임수 게임에서는 속임수가 추가될 수 있습니다.
예를 들어, 플레이어는 다음의 메시지를 전송합니다:
1
2
3
varchannelId="<ChannelId>";varmessageData={"message":"I think Red is the imposter!"};constmessageSendAck=awaitsocket.writeChatMessage(channelId,messageData);
다른 플레이어에게 혼동을 주기 위해서 메시지를 빠르게 편집할 수 있습니다:
1
2
varnewMessageData={"message":"I think BLUE is the imposter!"};constmessageUpdateAck=awaitsocket.updateChatMessage(channelId,messageSendAck.message.id,newMessageData));
Sagi-shi 플레이어는 대결을 생성하여 온라인 친구들이 참여하도록 초대할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
varmatch=awaitsocket.createMatch();varfriendsList=awaitclient.listFriends(session);varonlineFriends=[];friendsList.friends.forEach((friend){if(friend.user.online){onlineFriends.push(friend.user);}});onlineFriends.friend.forEach(function(friend){varmessageData={"message":"Hey %o, join me for a match!",friends.username},varmatchId=match.id,constchannel=awaitsocket.joinChat(2,friend.id),constmessageAck=awaitsocket.writeChatMessage(channel,messageData)});
varstatus={"Status":"Playing a match","MatchId":"<MatchId>"};awaitsocket.updateStatus(JSON.stringify(status));
팔로워가 실시간 상태 이벤트를 수신한 경우, 대결에 참여할 수 있습니다:
1
2
3
4
5
6
7
8
9
socket.onstatuspresence=async(e)=>{// Join the first match found in a friend's status
e.joins.forEach(function(presence){varstatus=JSON.parse(presence.status),if(status.hasOwnProperty("MatchId")){awaitsocket.joinMatch(status["MatchId"]);break;}});
varmatch=awaitsocket.joinMatch(matchId);varplayers={};match.presences.forEach(function(presence){vargo=spawnPlayer();// Instantiate player object
players.push(presence.session.id,go);});
Sagi-shi는 대결 현재 상태 수신 이벤트를 사용하여 생성된 플레이어를 최신 상태로 유지합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
socket.onmatchpresence=(matchPresenceEvent)=>{// For each player that has joined in this event...
matchPresenceEvent.joins.forEach(function(presence){// Spawn a player for this presence and store it in a dictionary by session id.
vargo=// Instantiate player object;
players.push(presence.session.id,go);})// For each player that has left in this event...
matchPresenceEvent.leaves.forEach(function(presence){// Remove the player from the game if they've been spawned
if(players.hasOwnProperty("SessionId"){constindex=players.session.id;if(index>-1){players.splice(index,1);}})})};
Sagi-shi 플레이어는 대결 상태 수신 이벤트를 구독하여 다른 연결된 클라이언트로부터 대결 데이터를 수신할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
socket.onmatchdata=(matchState)=>{switch(matchState.opCode){caseopCodes.position:// Get the updated position data
varstateJson=matchState.state;varpositionState=JSON.parse(stateJson);// Update the GameObject associated with that player
if(players.hasOwnProperty(matchState.user_presence.session.id)){// 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);}break;default:console.log("Unsupported op code");break;}};
개발자는 대결 목록 또는 Nakama 매치 메이커를 사용하여 플레이어의 대결을 찾을 수 있습니다. 이를 통해 플레이어는 실시간 매치메이킹 풀에 참여하고 지정된 기준과 일치하는 다른 플레이어와 대결이 성사될 때 알림을 받을 수 있습니다.
매치메이킹을 통해 플레이어는 서로를 찾을 수 있지만 대결을 생성하지는 않습니다. 이러한 분리는 의도된 것이므로 게임 대결을 찾는 것 이상으로 매치메이킹을 사용할 수 있습니다. 예를 들어, 소셜 경험을 만들고 있는 경우 매치메이킹을 사용하여 채팅할 다른 사람을 찾을 수 있습니다.
varcategoryStart=1;varcategoryEnd=2;varstartTime=1538147711;varendTime=null;// all tournaments from the start time
varlimit=100;// number to list per page
varcursor=null;varresult=awaitclient.listTournaments(session,categoryStart,categoryEnd,startTime,endTime,limit,cursor);result.tournaments.forEach(function(tournament){console.log("%o:%o",tournament.id,tournament.title);});
성능 상의 이유로 카테고리는 개별 숫자가 아닌 범위를 사용하여 필터링됩니다. 이를 활용하기 위해 카테고리를 구성하십시오(예: 1XX 범위의 모든 PVE 토너먼트, 2XX 범위의 모든 PVP 토너먼트 등).
Nakama는 알림을 구별하기 위해서 코드를 사용합니다. 0 이하의 코드는 Nakama 내부 직원을 위해 예약된 시스템입니다.
Sagi-shi 플레이어는 알림 수신 이벤트를 구독할 수 있습니다. Sagi-shi는 토너먼트 우승 100 코드를 사용합니다:
1
2
3
4
5
6
7
8
9
10
11
socket.onnotification=(notification)=>{constrewardCode=100;switch(notification.code){caserewardCode:console.log("Congratulations, you won the tournament!\n%o\n%o",notification.subject,notification.content);break;default:console.log("Other notification: %o:%o\n%o",notification.code,notification.subject,notification.content);break;}};
constresult=awaitclient.listNotifications(session,10);result.notifications.forEach(notification=>{console.info("Notification code %o and subject %o.",notification.code,notification.subject);});console.info("Fetch more results with cursor:",result.cacheable_cursor);