이 클라이언트 라이브리러 가이드에서는 Among Us(외부)에서 영감을 받은 Sagi-shi(“사기꾼"을 뜻하는 일본어)라는 게임에서 Nakama 고유의 부품을 개발하는 방법(전체 게임 로직이나 UI를 사용하지 않음)을 통해 Java에서 Nakama의 핵심 기능을 사용하는 방법에 대해서 설명합니다.
repositories{maven{url'https://jitpack.io'}}dependencies{implementation'com.github.heroiclabs.nakama-java:nakama-java:<commit>'// or, depend on the fat Jar which bundles all of the Nakama Java dependencies into a single Jar.
// implementation 'com.github.heroiclabs.nakama-java:nakama-java-all:<commit>'
}
// Create a single thread executorExecutorServiceexecutor=Executors.newSingleThreadExecutor();// Get a ListenableFuture with a Session resultListenableFuture<Session>authFuture=client.authenticateDevice(deviceId);// Setup the success and failure callbacks, specifying which executor to useFutures.addCallback(authFuture,newFutureCallback<Session>(){@OverridepublicvoidonSuccess(@NullableDeclSessionsession){logger.debug("Authenticated user id: "+session.getUserId());executor.shutdown();}@OverridepublicvoidonFailure(Throwablethrowable){logger.error(throwable.getMessage());executor.shutdown();}},executor);// Wait for the executor to finish all taskstry{executor.awaitTermination(5,TimeUnit.SECONDS);}catch(InterruptedExceptione){logger.error(e.getMessage());}
비동기식 호출을 함께 연결하려면 AsyncFunction<> 객체와 Futures.transformAsync 함수를 사용하면 됩니다.
// Get a ListenableFuture with a Session resultListenableFuture<Session>authFuture=client.authenticateDevice(deviceId);// Create an AsyncFunction to get the Account of a user using a Session objectAsyncFunction<Session,Account>accountFunction=session->client.getAccount(session);// Get a ListenableFuture from Futures.transformAsync by first passing the original Future, followed by the extended Future and finally an exectorListenableFuture<Account>accountFuture=Futures.transformAsync(authFuture,accountFunction,executor);// Setup the success and failture callbacks as usualFutures.addCallback(accountFuture,newFutureCallback<>(){@OverridepublicvoidonSuccess(@NullableDeclAccountaccount){logger.debug("Got account for user id: "+account.getUser().getId());executor.shutdown();}@OverridepublicvoidonFailure(Throwablethrowable){logger.error(throwable.getMessage());executor.shutdown();}},executor);
간결함을 위해 이 가이드의 코드 샘플에서는 더 간단하지만 스레드 차단 .get() 함수를 대신 사용합니다.
Nakama 클라이언트를 통해 Nakama 서버로 연결하며 Nakama 기능에 접근할 수 있습니다. 게임별 서버에 한 개의 클라이언트가 있는 것이 좋습니다.
서버 연결 세부내역에서 Sagi-shi 패스에 대한 클라이언트를 생성하려면 다음 사항이 필요합니다:
1
2
3
4
// explictly passing the defaultsClientclient=newDefaultClient("defaultkey","127.0.0.1",7349,false)// or same as aboveClientclient=newDefaultClient("defaultkey");
Nakama Java SDK는 gRPC를 통해 Nakama 서버와 직접 통신하므로 Nakama 서버에 대해 구성한 gRPC 포트 번호를 사용하는 것이 좋습니다. 기본적으로 이것은 7349입니다.
사용자 인증을 완료한 경우, 사용자는 계정에서 Nakama 연결 인증 방법을 사용할 수 있습니다.
장치 ID 인증 연결
1
2
3
StringdeviceId=UUID.randomUUID().toString();client.linkDevice(session,deviceId);logger.info("Linked device id {} for user {}",deviceId,session.getUserId());
Facebook 인증 연결
1
2
3
4
StringfacebookAccessToken="...";booleanimportFriends=true;client.linkFacebook(session,facebookAccessToken,importFriends);logger.info("Linked facebook authentication for user {}",deviceId,session.getUserId());
현재 인증된 플레이어의 사용자 계정을 가져오는 방법 외에 Nakama에서 ID나 사용자 이름, Facebook ID와 같은 기존 방식을 통해 다른 플레이어의 공용 프로필을 가져올 수 있습니다.
Sagi-shi는 다른 Nakama 기능을 사용할 때 이 메서드로 플레이어의 프로필을 표시합니다:
1
2
3
4
5
6
7
8
9
List<String>ids=Arrays.asList("userid1","userid2");List<String>usernames=Arrays.asList("username1","username1");String[]facebookIds=newString[]{"facebookid1"};Usersusers=client.getUsers(session,ids,usernames,facebookIds).get();for(Useruser:users.getUsersList()){logger.info("User id {} username {}",user.getId(),user.getUsername());}
// Get the updated account object.Accountaccount=client.getAccount(session).get();// Parse the account user metadata and log the result.Gsongson=newGson();Metadatametadata=gson.fromJson(account.getUser().getMetadata(),Metadata.class);logger.info("Title: {}, Hat: {}, Skin: {}",metadata.Title,metadata.Hat,metadata.Skin);
쓰기 로직을 어디에 둘 것인지 결정할 때 악의적인 사용자가 게임과 재무 상태에 어떤 역효과를 줄 수 있는지 생각하십시오. 예를 들어 정식으로만 작성해야 하는 데이터(예: 게임 잠금 해제 또는 진행 상황).
Sagi-shi에서 플레이어는 UI를 통해 더 쉽게 액세스할 수 있도록 즐겨찾기 항목을 사용할 수 있으며 클라이언트로부터 이 데이터를 쓰는 것이 안전합니다.
컬렉션 이름, 키, JSON 인코딩 데이터를 사용하여 저장소 개체 작성하기를 생성합니다. 마지막으로, 저장소 엔진에 저장소 객체를 작성합니다.
1
2
3
4
5
6
7
8
// Serialize your object as JSONMap<String,List<String>>favoriteHats=newHashMap<>();favoriteHats.put("hats",Arrays.asList("cowboy","alien"));StringfavoriteHatsJson=newGson().toJson(favoriteHats);StorageObjectWritewriteObject=newStorageObjectWrite("favorites","Hats",favoriteHatsJson,PermissionRead.OWNER_READ,PermissionWrite.OWNER_WRITE);StorageObjectAcksacks=client.writeStorageObjects(session,writeObject).get();logger.info("Stored objects {}",acks.getAcksList());
client.writeStorageObjects 메서드에 여러 개의 객체를 전달할 수 있습니다.
저장소 엔진 조건부 작성은 저장소 엔진에 액세스한 후에 개체가 변경되지 않은 경우에만 발생합니다.
이렇게 하면 데이터 덮어쓰기를 방지할 수 있습니다. 예를 들어, 플레이어가 마지막으로 액세스한 이후에 Sagi-shi 서버가 개체를 업데이트 했을 수도 있습니다.
조건부 작성을 실행하려면 버전을 추가하여 가장 최신의 객체 버전에서 저장소 객체를 작성합니다:
1
2
3
4
5
6
// Assuming we already got an object (`obj`)// Create a new object with the same Collection and Key and use the previous object's VersionStorageObjectWritewriteObject=newStorageObjectWrite(obj.getCollection(),obj.getKey(),newJsonValue,PermissionRead.OWNER_READ,PermissionWrite.OWNER_WRITE);writeObject.setVersion(obj.getVersion())// ... then write it to the Storage Engine as shown above
Nakama에서 친구를 추가해도 상호적인 친구 관계가 즉각적으로 추가되지 않습니다. 사용자별 진행중인 친구 요청은 사용자가 승인해야 합니다.
Sagi-shi에서 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 추가할 수 있습니다:
1
2
3
4
5
6
7
// Add friends by User ID.List<String>ids=Arrays.asList("AlwaysTheImposter21","SneakyBoi");// Add friends by Username.String[]usernames=newString[]{"<SomeUserId>","<AnotherUserId>"};client.addFriends(session,ids,usernames).get();
개발자는 Nakama에서 친구 관계 상태를 기반으로 플레이어의 친구 목록을 만들 수 있습니다.
Sagi-shi에서는 20명의 가장 최근 친구가 목록으로 표시됩니다:
1
2
3
4
5
6
intfriendshipState=0;intlimit=20;// Limit is capped at 1000Stringcursor=null;FriendListfriends=client.listFriends(session,friendshipState,limit,cursor).get();friends.getFriendsList().forEach(friend->logger.info("Friend Id: {}",friend.getUser().getId()));
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 삭제할 수 있습니다:
1
2
3
4
5
6
7
// Delete friends by User ID.List<String>ids=Arrays.asList("<SomeUserId>","<AnotherUserId>");// Delete friends by Username.String[]usernames=newString[]{"<SomeUsername>","<AnotherUsername>"};client.deleteFriends(session,ids,usernames).get();
Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 다른 사용자를 차단할 수 있습니다:
1
2
3
4
5
6
7
// Block friends by User ID.List<String>ids=Arrays.asList("<SomeUserId>","<AnotherUserId>");// Block friends by Username.String[]usernames=newString[]{"<SomeUsername>","<AnotherUsername>"};client.blockFriends(session,ids,usernames).get();
// If one doesn't already exist, create a socket listener and handle status presence eventsSocketListenersocketListener=newSocketListener(){@OverridepublicvoidonStatusPresence(StatusPresenceEvente){if(e.getJoins()!=null){e.getJoins().forEach(presence->logger.info("{} is online with status {}",presence.getUsername(),presence.getStatus()));}if(e.getLeaves()!=null){e.getLeaves().forEach(presence->logger.info("{} went offline",presence.getUsername()));}}// ... other required overrides (e.g. onChannelMessage etc)}// Then create and connect a socket connectionSocketClientsocket=client.createSocket();socket.connect(session,socketListener);// Follow mutual friends and get the initial Status of any that are currently onlineintfriendshipState=0;// Mutual friendsintlimit=20;// Limit is capped at 1000FriendListfriends=client.listFriends(session,friendshipState,limit,null).get();List<String>friendIds=newArrayList<>();for(Friendfriend:friends.getFriendsList()){friendIds.add(friend.getUser().getId());}Statusstatus=socket.followUsers(friendIds).get();if(status.getPresences()!=null){status.getPresences().forEach(presence->logger.info("{} is online with status {}",presence.getUsername(),presence.getStatus()));}
그룹에는 공용 또는 개인 “공개” 표시가 있습니다. 누구나 공용 그룹에 가입할 수 있지만 가입을 요청하고 비공개 그룹의 최고 관리자/관리자가 수락해야 합니다.
Sagi-shi 플레이어는 공통의 관심사를 기반으로 그룹을 생성할 수 있습니다:
1
2
3
4
5
6
7
Stringname="Imposters R Us";Stringdescription="A group for people who love playing the imposter.";StringavatarUrl="";StringlangTag="";Booleanopen=true;// Public groupintmaxSize=100;Groupgroup=client.createGroup(session,name,description,avatarUrl,langTag,open,maxSize).get();
그룹은 다른 Nakama 리소스와 같이 목록을 만들고 와일드카드 그룹 이름으로 필터링할 수 있습니다.
Sagi-shi 플레이어는 그룹 목록과 필터링을 통해 기존 그룹을 검색할 수 있습니다:
1
2
3
4
5
6
intlimit=20;GroupListgroupList=client.listGroups(session,"imposter%",limit).get();groupList.getGroupsList().forEach(group->logger.info("{} [{}]",group.getName(),group.hasOpen()&&group.getOpen().getValue()?"Public":"Private"));// Get the next page of results.GroupListnextGroupListResults=client.listGroups(session,"imposter%",limit,groupList.getCursor()).get();
Stringuserid="<user id>";UserGroupListuserGroups=client.listUserGroups(session,userid).get();for(UserGroupList.UserGroupuserGroup:userGroups.getUserGroupsList()){System.out.format("Group name %s role %d",userGroup.getGroup().getName(),userGroup.getState());}
Sagi-shi 그룹 구성원은 지속적인 그룹 채팅 채널에서 플레이 세션 동안 대화할 수 있습니다:
1
2
3
4
5
StringgroupId="<GroupId>";booleanpersistence=true;booleanhidden=false;Channelchannel=socket.joinChat(groupId,ChannelType.GROUP,persistence,hidden).get();logger.info("Connected to group channel: {}",channel.getId());
Sagi-shi 플레이어는 대결 중 또는 대결 후에 개인적으로 1:1 채팅을 하고 이전 메시지를 볼 수도 있습니다:
1
2
3
4
5
StringuserId="<UserId>";booleanpersistence=true;booleanhidden=false;Channelchannel=socket.joinChat(userId,ChannelType.DIRECT_MESSAGE,persistence,hidden).get();logger.info("Connected to direct message channel: {}",channel.getId());
모든 유형의 채팅 채널에서 메시지를 보내는 방법은 동일합니다. 메시지에는 채팅 문자와 이모티콘이 포함되며 JSON 직렬 데이터로 전송됩니다:
1
2
3
4
5
6
7
8
9
10
StringchannelId="<ChannelId>";Map<String,String>messageContent=newHashMap<>();messageContent.put("message","I think Red is the imposter!");ChannelMessageAckmessageSendAck=socket.writeChatMessage(channelId,newGson().toJson(messageContent,messageContent.getClass())).get();Map<String,String>emoteContent=newHashMap<>();emoteContent.put("emote","point");emoteContent.put("emoteTarget","<RedPlayerUserId>");ChannelMessageAckemoteSendAck=socket.writeChatMessage(channelId,newGson().toJson(emoteContent,emoteContent.getClass())).get();
채팅에는 캐시 가능한 커서가 있어 가장 최근 메시지를 가져올 수 있으며 이런 메시지는 원하는 위치에 저장할 수 있습니다.
1
2
3
4
5
// Store this in whichever way suits your applicationStringcacheableCursor=channelMessageList.getCacheableCursor();// ...then use the cacheable cursor later to retrieve the next resultsChannelMessageListnextResults=client.listChannelMessages(session,groupId,limit,cacheableCursor,forward);
Nakama는 메시지 업데이트 기능도 지원합니다. 이 기능은 원하는 경우 사용할 수 있지만 Sagi-shi와 같은 속임수 게임에서는 속임수가 추가될 수 있습니다.
예를 들어, 플레이어는 다음의 메시지를 전송합니다:
1
2
3
4
5
6
7
Map<String,String>messageContent=newHashMap<>();messageContent.put("message","I think Red is the imposter!");ChannelMessageAckack=socket.writeChatMessage(channelId,newGson().toJson(messageContent,messageContent.getClass())).get();Map<String,String>updatedMessageContent=newHashMap<>();updatedMessageContent.put("message","I think BLUE is the imposter!");ChannelMessageAckupdateAck=socket.updateChatMessage(channelId,ack.getMessageId(),newGson().toJson(updatedMessageContent,updatedMessageContent.getClass())).get();
Sagi-shi 플레이어는 대결을 생성하여 온라인 친구들이 참여하도록 초대할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
Matchmatch=socket.createMatch().get();FriendListfriendList=client.listFriends(session,0,100,"").get();for(Friendfriend:friendList.getFriendsList()){if(friend.getUser().getOnline()){Map<String,String>messageContent=newHashMap<>();messageContent.put("message",String.format("Hey %s, join me for a match!",friend.getUser().getUsername()));messageContent.put("matchId",match.getMatchId());Channelchannel=socket.joinChat(friend.getUser().getId(),ChannelType.DIRECT_MESSAGE).get();ChannelMessageAckack=socket.writeChatMessage(channel.getId(),newGson().toJson(messageContent,messageContent.getClass())).get();}}
// In the SocketListener, override the onMatchmakerMatched function@OverridepublicvoidonMatchmakerMatched(MatchmakerMatchedmatchmakerMatched){Matchmatch=socket.joinMatch(matchmakerMatched.getMatchId()).get();}// ...then, elsewhereintminPlayers=2;intmaxPlayers=10;Stringquery="";MatchmakerTicketmatchmakingTicket=socket.addMatchmaker(minPlayers,maxPlayers,query).get();
플레이어 상태로 대결 참여하기
Sagi-shi 플레이어는 새로운 대결에 참여할 경우 상태를 업데이트할 수 있습니다:
1
2
3
4
5
Map<String,String>status=newHashMap<>();status.put("Status","Playing a match");status.put("MatchId","<MatchId>");socket.updateStatus(newGson().toJson(status,status.getClass())).get();
Sagi-shi 플레이어는 대결 상태 수신 이벤트를 구독하여 다른 연결된 클라이언트로부터 대결 데이터를 수신할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@OverridepublicvoidonMatchData(MatchDatamatchData){if(matchData.getOpCode()==OpCodes.Position){// Get the updated position dataStringjson=newString(matchData.getData());PositionStateposition=newGson().fromJson(json,newTypeToken<PositionState>(){}.getType());// Update the GameObject associated with that playerif(players.containsKey(matchData.getPresence().getSessionId())){Playerplayer=players.get(matchData.getPresence().getSessionId());// Here we would normally do something like smoothly interpolate to the new position, but for this example let's just set the position directly.player.position=position;}}}
개발자는 대결 목록 또는 Nakama 매치 메이커를 사용하여 플레이어의 대결을 찾을 수 있습니다. 이를 통해 플레이어는 실시간 매치메이킹 풀에 참여하고 지정된 기준과 일치하는 다른 플레이어와 대결이 성사될 때 알림을 받을 수 있습니다.
매치메이킹을 통해 플레이어는 서로를 찾을 수 있지만 대결을 생성하지는 않습니다. 이러한 분리는 의도된 것이므로 게임 대결을 찾는 것 이상으로 매치메이킹을 사용할 수 있습니다. 예를 들어, 소셜 경험을 만들고 있는 경우 매치메이킹을 사용하여 채팅할 다른 사람을 찾을 수 있습니다.
Nakama는 알림을 구별하기 위해서 코드를 사용합니다. 0 이하의 코드는 Nakama 내부 직원을 위해 예약된 시스템입니다.
Sagi-shi 플레이어는 소켓 리스너를 사용하여 알림 수신 이벤트를 구독할 수 있습니다. Sagi-shi는 토너먼트 우승 100 코드를 사용합니다:
1
2
3
4
5
6
7
8
9
10
11
@OverridepublicvoidonNotifications(NotificationListnotificationList){finalintrewardCode=100;for(Notificationnotification:notificationList.getNotificationsList()){if(notification.getCode()==rewardCode){logger.info("Congratulations, you won the tournament!\n{}\n{}",notification.getSubject(),notification.getContent());}else{logger.info("Other notification: {}:{}\n{}",notification.getCode(),notification.getSubject(),notification.getContent());}}}
다른 목록 방법과 마찬가지로 알림 결과는 결과에서 커서 또는 캐시 가능한 커서를 사용하여 페이지를 지정할 수 있습니다.
캐시 가능 항목이 저장되었다고 가정하고 다음에 플레이어가 로그인하면 캐시 가능한 커서를 읽지 않은 알림을 나열하는 데 사용할 수 있습니다.
1
2
3
// Assuming this has been saved and loadedStringcacheableCursor="";NotificationListnextResults=client.listNotifications(session,limit,cacheableCursor);