친구 #

Nakama의 핵심 기능은 소셜 네트워크로 개별적인 연결에 더 쉽게 액세스할 수 있는 친구 시스템입니다.

초대 시스템과 플레이어의 친구 목록 기능이 지원되기 때문에 친구들은 채팅을 통해 서로 교류하고, 순위표에서 경쟁하거나 게임에 함께 참여할 수 있습니다.

여기서, Nakama의 친구 기능을 사용하는 방법에 대해서 학습합니다:

  • 특정한 사람에 대한 플레이어 데이터베이스 검색
  • 새 친구 추가 및 초대 관리
  • 다이렉트 메시지 친구
  • 오프라인 상태에서 친구로부터 메시지 받기

Pirate Panic에서는 다음과 같이 나타납니다:

친구 패널
친구 패널

이 예시에서 코드는 Unity와 직접 인터페이스로 연결되는 클라이언트 측 코드ServerModules 폴더에 존재하고 Typescript로 작성되는 서버 측 코드로 구분됩니다.

이러한 구분을 통해 민감한 정보를 중심 위치에서 관리하고 저장할 수 있으며, 플레이어가 볼 수 있는 부분을 제어할 수 있습니다. 예를 들어, 모든 플레이어의 데이터베이스와 서버 연결을 저장해야 할 수도 있지만, 플레이어는 개인의 친구들만 볼 수 있습니다.

친구 찾기 #

클라이언트와 서버 사이에서 데이터를 주고 받기 위해서 RPC의 원격 프로시저 호출을 작성할 수 있습니다.

RPC 코드의 공통적인 구조는 JSON.parse을(를) 사용하여 JSON 문자열로 읽고, 입력에서 작동하고, JSON.stringify을(를) 사용하여 문자열을 반환하는 것입니다. JSON을 사용하여 데이터 형식을 표준화하면 다양한 엔진과 언어 사이에서 더 쉽게 의사소통할 수 있습니다.

예를 들어, RPC를 사용하면 플레이어가 사용자 이름을 검색하여 친구를 추가할 수 있습니다. 클라이언트가 다른 플레이어의 정보에 직접 액세스할 수 없도록 하려면 다음과 같이 처리할 수 있습니다:

  1. 플레이어가 검색하려고 하는 구문을 입력합니다.
  2. 클라이언트는 RPC를 사용하여 해당 구문을 서버로 전송합니다.
  3. 서버가 대결이 있는지 확인하기 위해서 데이터베이스에 직접 쿼리를 적용합니다.
  4. 서버가 대결을 클라이언트에 반환하여 표시합니다.

서버에 RPC 등록하기 #

클라이언트가 호출할 수 있도록 RPC를 서버 측에서 실행합니다:

main.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const rpcSearchUsernameFn: nkruntime.RpcFunction = function(
  ctx: nkruntime.Context,
  logger: nkruntime.Logger,
  nk: nkruntime.Nakama,
  payload: string): string {

  const input: any = JSON.parse(payload)

  // NOTE: Must be very careful with custom SQL queries to performance check them.
  const query = `
  SELECT id, username FROM users WHERE username ILIKE concat($1, '%')
  `
  const result = nk.sqlQuery(query, [input.username]);

  return JSON.stringify(result);
}

이 RPC는 JSON 개체를 클라이언트로 출력하는 함수입니다. 컨텍스트 개체에 포함된 여러 유용한 속성은 호출이 어디에서 발생했는지 알려줍니다. Nakama 인스턴스는 Nakama 런타임 API에 인터페이스를 적용하고 페이로드는 클라이언트에서 전송된 사용자 지정 정보를 포함하는 JSON 개체입니다.

예를 들어, “BobNorris101"이라고 하는 친구를 찾는 경우, 페이로드는 {username: "BobNorris101"}와(과) 같이 표시됩니다.

JSON을 분석한 후, 해당 정보를 사용하여 SQL 쿼리를 통해 데이터베이스에 직접 인터페이스를 적용할 수 있습니다. nk.sqlQuery에 대한 호출은 {{id: '3ea5608a-43c3-11e7-90f9-7b9397165f34', username: 'BobNorris101'} ...}와(과) 같이 표시되는 개체를 반환합니다.

이 함수를 명시적으로 호출하지 않고, 이벤트로 RPC를 등록하기만 하면 됩니다:

1
initializer.registerRpc('search_username', rpcSearchUsernameFn);

클라이언트가 search_username을(를) 호출할 때마다 함수가 실행되어 자동으로 결과를 클라이언트로 반환합니다.

클라이언트에서 RPC 호출 #

Unity 클라이언트에서 위에서 정의한 RPC를 호출하려면 다음과 같이 합니다:

UsernameSearcher.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private struct SearchResult {
    public string username;
}
...
private async void SearchUsers(string text)
{
    ... // Do some error handling (see source for examples)

    // Construct a payload
    var searchRequest = new Dictionary<string, string>() { { "username", text } };

    // Convert the payload to JSON for delivery
    string payload = searchRequest.ToJson();
    //RPC method ID from server
    string rpcid = "search_username";

    Task<IApiRpc> searchTask; // Can also put this in a try/catch to handle errors

    //creating search task - sending request for running "search_username" method on server with text parameter
    searchTask = _connection.Client.RpcAsync(_connection.Session, rpcid, payload);

    //awaiting for server returning value
    IApiRpc searchResult = await searchTask;

    //unpacking results to SearchResult struct object
    SearchResult[] usernames = searchResult.Payload.FromJson<SearchResult[]>();

    ...
}

주요 함수는 Nakama 클라이언트 세션, RPC 식별자 search_username, 페이로드를 사용하는 RpcAsync입니다. 비동기화 네트워크 요청이기 때문에 요청사항이 도달하면 출력을 처리할 수 있도록 await searchTask을(를) 사용하여 요청사항을 비동기적으로 처리해야 합니다.

RPC는 데이터베이스 쿼리를 JSON 문자열로 변환 및 반환하기 때문에 작업을 하려면 해당 정보를 C# 개체로 변환해야 합니다. 이렇게 하기 위해서는 JSON을 포함할 형식인 필드 username(이)가 있는 사용자 지정 SearchResult struct을(를) 생성합니다.

친구 추가 및 초대 #

서버에서 반환된 사용자 이름에 대해서 유효성 검사를 수행했기 때문에 AddFriendsAsync 함수를 사용하여 추가할 수 있습니다:

FriendsMenuUI.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public async void AddFriend()
{
    // This is from the UsernameSearcher we made earlier
    string[] usernames = new[] { _usernameSearcher.InputFieldValue };
    await _connection.Client.AddFriendsAsync(_connection.Session, new string[] { }, usernames);
    var friends = await _connection.Client.ListFriendsAsync(_connection.Session);
    // Do stuff with friends list (friends[i].State...)
    ...
}
...

여기서는 ID 또는 사용자 이름을 사용하여 친구 목록을 추가할 수 있습니다. 이 예시에서는 사용자 이름을 사용하기 때문에 ID 목록이 공란으로 표시됩니다.

친구를 추가하고 난 후에 ListFriendsAsync을(를) 사용하여 새로운 상태를 표시할 수 있도록 UI를 업데이트합니다. 이 함수는 IApiFriend 개체 목록을 비동기적으로 반환합니다. 각 IApiFriend에는 User 속성과 State 속성이 포함되어 각각의 친구에 대한 정보를 표시합니다.

User 속성은 사용자(ID, 사용자 이름 등)에 대한 공통적인 정보를 전부 포함합니다.

State 속성은 플레이어와 친구 사이의 관계를 설명합니다:

  • 상태 0은(는) 서로 친구임을 의미합니다.
  • 상태 1은(는) A가 초대를 보냈으나, B가 아직 초대를 수락하지 않았음을 의미합니다.
  • 상태 2은(는) 1와(과) 반대로, B가 A에게 초대를 보냈다는 것을 의미합니다.
  • 상태 3은(는) B가 A에 의해 차단되었음을 의미합니다.

AddFriendsAsync을(를) 호출하면 플레이어 상태가 1이(가) 됩니다. 다른 플레이어는 AddFriendsAsync을(를) 호출하여 두 상태를 0(으)로 전환할 수 있습니다.

마지막으로, 인터페이스를 생성하여 플레이어가 초대를 볼 수 있도록 하고, AddFriendsAsync을(를) 호출하여 ‘수락’ 버튼을 누를 수 있도록 합니다.

이 작업과 관련되는 코드는 친구를 추가하는 코드와 매우 유사하며, FriendPanel.cs를 참조할 수 있습니다.

친구 차단 및 차단 해제 #

플레이어는 친구를 차단할 수도 있습니다. 이 액션은 아래와 같이 처리됩니다:

FriendsPanel.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private async void BlockOrUnblockThisFriend()
{
    //Checking if player status is blocked.
    if (!_blocked)
    {
      //if is not blocked, then block this friend
        string[] ids = new[] { _friend.User.Id };

        await _connection.Client.BlockFriendsAsync(_connection.Session, ids);
        OnDataChanged();
    }
    else
    {
        //unblock this friend
        await _connection.Client.DeleteFriendsAsync(_connection.Session, new[] { _friend.User.Id });
    }
}

여기서, _friend은(는) FriendPanel 클래스로 전달되는 IApiFriend 개체이며, _blocked은(는) 친구가 차단되거나 차단 해제될 때마다 전환되는 부울입니다. 중요한 부분은 ID의 목록을 통해 모든 친구 상태를 3(으)로 설정하는 BlockFriendsAsync 함수입니다.

차단을 해제하기 위해서 DeleteFriendsAsync을(를) 호출하는 것이 직관에 반하는 것처럼 보일 수 있습니다. 친구 상태를 초대(1 또는 2) 또는 친구 관계(0)로 변경하는 것이 대안이 될 수 있습니다. 대신에, 친구 개체를 전체 삭제할 수 있습니다. 그런 다음, 친구를 다시 초대할 수 있습니다.

DeleteFriendsAsync 차단이 해제되지 않은 친구를 제거하는 것과 같은 방식으로 사용할 수 있습니다.

사용자를 차단하는 것과 금지시키는 것에는 차이가 있습니다. 금지는 서버 측에서 실행되며 플레이어가 게임 전체에 참여하지 못하도록 합니다. 금지에 대한 자세한 내용을 살펴보십시오.

친구와 채팅하기 #

Nakama를 통해 친구들과 쉽게 채팅할 수 있습니다.

서버 내에서 채팅은 특정 ID를 통한 채널로 나뉘어집니다. 개인적인 채팅을 원하는 경우, 채팅을 하려고 하는 플레이어의 ID(고유한 ID)를 전달합니다.

JoinChatAsync을(를) 사용하여 채팅 채널에 참여할 수 있습니다.

FriendsMenuUI.cs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private async void StartChatWithUser(string userId, string username)
{
    IChannel chatChannel;

    ...
    chatChannel = await _connection.Socket.JoinChatAsync(userId, ChannelType.DirectMessage, persistence: true, hidden: true);
    ...

    _chatChannelUI.SetChatChannel(chatChannel);
    _chatChannelUI.gameObject.SetActive(true);
}

JoinChatAsync 함수의 매개 변수를 살펴보겠습니다:

채팅 채널에는 3가지 유형이 있습니다:

  • DirectMessage: 두 명의 플레이어 사이에서 개인적인 채팅방을 만들 때 사용합니다.
  • Group: 같은 클랜에 있는 여러 명의 구성원 사이에서 개인적인 채팅을 지원할 때 사용합니다.
  • Room: 모든 사람이 참여할 수 있는 동적인 공용 채팅을 생성합니다. 모든 사람이 채팅방을 나가면 다른 사람이 다시 참여할 때까지 채널은 유지되지 않습니다.
    • 세 가지 채널에 대한 차이점은 여기를 참조하십시오.

Persistent 메시지가 데이터베이스로 저장되어 나중에 메시지 내역 목록을 사용할 수 있다는 의미입니다. 또한, hidden 구성원은 구성원 목록에 표시되지 않습니다.

플레이어가 채널에 참여하였기 때문에 채팅 메시지를 전송할 수 있습니다:

ChatChannelUI.cs

1
2
var content = new Dictionary<string, string>(){{"content", _chatInputField.text}}.ToJson();
_connection.Socket.WriteChatMessageAsync(_chatChannel.Id, content);

여기서, _chatChannel은(는) 이전 함수에서 JoinChatAsync(으)로 생성한 것과 같은 채널입니다. 전달되는 content 형식은 사용자 지정 필드(이 경우, content)를 키로 사용하는 사전으로 반환되고, 값은 전송하려고 하는 정보입니다.

그룹으로 전송된 메시지를 받기 위해서 ReceivedChannelMessage 핸들러 등으로 함수를 추가할 수 있습니다:

1
_connection.Socket.ReceivedChannelMessage += AddMessage;

여기서, AddMessage(을)를 통해 수신된 메시지를 처리합니다. 예:

ChatChannelUI.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private void AddMessage(IApiChannelMessage message)
{
    // Manage message prefabs, etc. (not shown in this snippet)
    ...

    // If the message is from the same user as newest we should hide his username on this message
    bool hideUsername = (message.Username == _lastMessageUsername) && !message.Persistent;

    // Initializing message with ID, username, message content, time of creation, and whether or not we should hide it
    messageUI.InitMessage(message.MessageId, message.Username,
    message.Content.FromJson<Dictionary<string, string>>()["content"], message.CreateTime, hideUsername);

    ...

    //If message is historical change order in hierarchy, the latest historical message is the oldest
    if (message.Persistent)
    {
        messageUI.transform.SetSiblingIndex(1);
    }
}

MessagePrefab 또는messageUI와(과) 같은 구조는 이 튜토리얼에서만 사용하며, 필요에 따라서 사용자 지정으로 사용할 수 있습니다. 위의 코드에서는 ReceivedChannelMessage 핸들러에 의해 전달된 IApiChannelMessage 개체를 사용하는 방법에 대한 예시를 제공합니다.

message 개체에는 여러 가지 속성이 있습니다:

  • SenderId: 해당 메시지를 전송한 클라이언트
  • Username: 전송한 플레이어의 이름
  • Persistent: 메시지가 저장되었는지 알려주는 부울
  • CreateTime: 메시지가 전송된 때
  • Content: 실제 사용 중인 컨텐츠로 전환될 수 있는 JSON 개체 message.Content.FromJson<Dictionary<string, string>>()["content"]

채널 구독 #

기본적으로, 클라이언트가 연결 해제되어 서버로 다시 연결되는 경우(예: 플레이어가 게임에서 나가는 경우), 채팅 채널이 자동으로 다시 구독되지 않습니다.

게임 내에서 해당 작업(예: 메시지를 받을 때 플레이어에게 알림을 제공)을 수행하려고 할 경우, 클라이언트가 시작 시 채팅 채널에 다시 참여하는지 확인합니다.

모든 친구 채널을 구독하려면 다음과 같이 합니다:

Scene01MainMenuController.cs

1
2
3
4
5
6
7
8
9
private async void Start() {
    var friends = await _connection.Client.ListFriendsAsync(_connection.Session);

    foreach(IApiFriend friend in friends) {
        var userId = friend.User.Id;
        await _connection.Socket.JoinChatAsync(userId, ChannelType.DirectMessage, persistence: true, hidden: true);
    }
  ...
}

이 코드는 앱 시작 시 친구 목록에 있는 친구를 살펴보고, 친구의 다이렉트 메시지 채널에 참여하여 친구로부터 전달된 채팅이 자동으로 수신되도록 합니다.

다음 주제 #

클랜

Related Pages