Nakama Godot 클라이언트 가이드 #

이 클라이언트 라이브리러 가이드에서는 Among Us(외부)에서 영감을 받은 Sagi-shi(“사기꾼"을 뜻하는 일본어)라는 게임에서 Nakama 고유의 부품을 개발하는 방법(전체 게임 로직이나 UI를 사용하지 않음)을 통해 Godot에서 Nakama의 핵심 기능을 사용하는 방법에 대해서 설명합니다.

Sagi-shi gameplay screen
Sagi-shi gameplay

필수 조건 #

진행하기 전에 다음 사항을 확인합니다:

설치 #

다음에서 클라이언트를 사용할 수 있습니다:

클라이언트를 다운로드하면 아카이브가 컨텐츠를 Godot 프로젝트 폴더로 추출합니다.

Project -> Project Settings -> Autoload 메뉴에서 addons/com.heroiclabs.nakama/에 있는 Nakama.gd 싱글톤을 추가합니다.

클라이언트 개체를 생성하여 서버와 상호 작용합니다.

업데이트 #

Nakama Godot 클라이언트의 새로운 버전과 개선사항은 변경 로그에 기록되어 있습니다.

비동기 프로그래밍 #

많은 Nakama API는 비동기적이며 비차단형이고, Godot SDK에서 비동기적 메서드로 사용할 수 있습니다.

Sagi-shi는 게임이 응답성과 효율성을 유지하고 스레드 호출을 차단하지 않도록 await 운영자를 사용하여 비동기적 메서드를 호출합니다.

1
yield(client.authenticate_device_async("<device_id>"), "completed")

코루틴과 await에 대한 자세한 내용은 Godot 공식 문서를 참조하십시오.

예외 처리 #

네트워크 프로그래밍은 연결과 페이로드 문제에 대해서 추가적인 안전 장치가 필요합니다.

Godot은 예외 처리를 지원하지 않기 때문에 비동기적 요청 시 is_exception() 메서드를 사용할 수 있습니다:

1
2
3
4
5
var invalid_session = NakamaSession.new() # An empty session, which will cause an error
var invalid_account = yield(client.get_account_async(invalid_session), "completed")
print(invalid_account) # This will print the exception
if invalid_account.is_exception():
    print("We got an exception.")

데이터 직렬화 및 역직렬화 #

네트워크에서 데이터를 전송하거나 수신하려면 직렬화 및 역직렬화가 적용되어야 합니다. 가장 일반적으로 할 수 있는 두 가지 방법은 JSON이나 바이너리 데이터를 사용하는 것입니다.

두 예시 모두 아래 Dictionary 객체를 직렬화 및 역직렬화하는 방법을 보여주지만 직렬화 가능한 모든 Variant 객체와 함께 사용할 수 있습니다.

1
2
3
4
var data = {
  "Key": "Value",
  "AnotherKey": "AnotherValue"
}

JSON #

Godot에서는 글로벌 JSON 객체를 사용하여 JSON으로 직렬화 및 역직렬화할 수 있는 네이티브 지원이 제공됩니다.

1
2
3
4
5
// Serialize
var serialized = JSON.print(data)

// Deserialize
var deserialized = JSON.parse(serialized).result

바이너리 #

Godot은 글로벌 액세스 가능한 var2bytesbytes2var 함수를 사용하여 byte[] 배열로 직렬화 및 역직렬화가 가능합니다.

1
2
3
4
5
// Serialize
var serialized = var2bytes(data)

// Deserialize
var deserialized = bytes2var(serialized)

시작하기 #

Nakama 클라이언트와 소켓 개체를 통해 Sagi-shi와 게임을 시작하는 방법에 대해서 알아봅니다.

Nakama 클라이언트 #

Nakama 클라이언트를 통해 Nakama 서버로 연결하며 Nakama 기능에 접근할 수 있습니다. 게임별 서버에 한 개의 클라이언트가 있는 것이 좋습니다.

서버 연결 세부내역에서 Sagi-shi 패스에 대한 클라이언트를 생성하려면 다음 사항이 필요합니다:

1
2
3
4
5
6
extends Node

var client : NakamaClient

func _ready():
    client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")

요청 제한 시간 구성하기 #

클라이언트에서 Nakama에 대한 요청은 시간 초과로 간주되기 전에 일정한 시간 내에 완료되어야 합니다. 클라이언트에서 timeout 값을 설정하여 해당 시간(초)을 구성할 수 있습니다:

1
client.timeout = 10

Nakama 소켓 #

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 returns

onready var socket = Nakama.create_socket_from(client)

func _ready():
    var connected : NakamaAsyncResult = yield(socket.connect_async(session), "completed")
    if connected.is_exception():
        print("An error occurred: %s" % connected)
        return
    print("Socket connected.")

인증 #

Nakama에는 많은 인증 방법이 있으며 서버에서 사용자 지정 인증 생성을 지원합니다.

Sagi-shi는 같은 사용자 계정과 연동된 장치 인증과 Facebook 인증을 통해서 사용자가 여러 장치에서 게임을 실행할 수 있도록 합니다.

Sagi-shi login screen
Login screen and Authentication options

장치 인증 #

Nakama 장치 인증은 물리적인 장치의 고유한 식별자를 사용하여 사용자 인증을 용이하게 하고 계정이 없는 경우 계정을 생성할 수 있습니다.

장치 인증만 사용할 경우, 게임이 실행되면 자동으로 인증이 진행되기 때문에 사용자는 로그인 UI가 필요하지 않습니다.

인증은 Nakama 클라이언트에서 액세스할 수 있는 Nakama 기능의 예시입니다.

1
2
3
4
5
6
7
8
9
# Get the System's unique device identifier
var device_id = OS.get_unique_id()

# Authenticate with the Nakama server using Device Authentication
var session : NakamaSession = yield(client.authenticate_device_async(device_id), "completed")
if session.is_exception():
    print("An error occurred: %s" % session)
    return
print("Successfully authenticated: %s" % session)

Facebook 인증 #

Nakama Facebook 인증은 사용자의 Facebook 친구를 선택하여 Nakama 친구 목록에 추가할 수 있는 인증 방법입니다.

1
2
3
4
5
6
7
var oauth_token = "<token>"
var import_friends = true
var session : NakamaSession = yield(client.authenticate_facebook_async(oauth_token, import_friends), "completed")
if session.is_exception():
    print("An error occurred: %s" % session)
    return
print("Successfully authenticated: %s" % session)

사용자 지정 인증 #

Nakama는 사용자 지정 인증 방법을 지원하여 추가적인 신원 확인 서비스와 통합할 수 있습니다.

예시는 Itch.io 사용자 지정 인증 조각을 참조하십시오.

연결 인증 #

사용자 인증을 완료한 경우, 사용자는 계정에서 Nakama 연결 인증 방법을 사용할 수 있습니다.

장치 ID 인증 연결

1
2
3
4
5
6
7
8
var device_id = "<unique_device_id>"

# Link Device Authentication to existing player account.
var linked : NakamaAsyncResult = yield(client.link_custom_async(session, device_id), "completed")
if linked.is_exception():
    print("An error occurred: %s" % linked)
    return
print("Id '%s' linked for user '%s'" % [device_id, session.user_id])

Facebook 인증 연결

1
2
3
4
5
6
7
var oauth_token = "<token>"
var import_friends = true
var session : NakamaSession = yield(client.link_facebook_async(session, oauth_token, import_friends), "completed")
if session.is_exception():
    print("An error occurred: %s" % linked)
    return
print("Facebook authentication linked for user '%s'" % [session.user_id])

세션 변수 #

Nakama 세션 변수는 인증 시 저장될 수 있으며 세션이 활성화되어 있는 경우 클라이언트와 서버에서 사용할 수 있습니다.

Sagi-shi는 세션 변수를 통해 분석, 추천, 보상 프로그램 등을 실행합니다.

인증을 진행하는 과정에서 인수로 전달하여 세션 변수를 저장합니다:

1
2
3
4
5
6
7
8
var vars = {
    "device_os" : OS.get_name,
    "device_model" : OS.get_model_name,
    "invite_user_id" : "<some_user_id>,
    # ...
}

var session : NakamaSession = yield(client.authenticate_device_async("<device_id>", null, true, vars), "completed")

클라이언트에서 세션 변수에 액세스하려면 Session 개체의 Vars 속성을 사용합니다:

1
var device_os = session.vars["device_os"];

세션 수명 주기 #

Nakama 세션은 서버 구성에서 설정한 시간이 지나면 만료됩니다. 비활성 세션을 만료시키는 것은 보안 측면에서 모범적인 사례입니다.

Nakama에서는 세션을 복구할 수 있습니다. 예를 들어, Sagi-shi 플레이어가 게임을 다시 실행하거나 토큰에 대해서 새로 고침을 적용하는 경우, 게임이 실행되는 동안 세션이 활성화 상태로 유지됩니다.

세션을 복구하거나 새로 고치려면 세션의 인증 및 새로 고침 토큰을 사용합니다.

재인증 없이 세션을 복구합니다:

1
2
var auth_token = "restored from save location"
var session = 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 expiry
if session.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)
}

세션 종료하기 #

로그아웃하고 현재 세션을 종료합니다:

1
yield(client.session_logout_async(session), "completed")

사용자 계정 #

Nakama 사용자 계정은 Nakama와 사용자 지정 개발자 메타데이터에 의해 정의된 사용자 정보를 저장합니다.

Sagi-shi에서는 게임 진행률과 게임 아이템과 같은 메타데이터를 저장하고 계정을 편집할 수 있습니다.

Sagi-shi player profile screen
Player profile

사용자 계정 만들기 #

많은 Nakama 기능은 사용자 계정 가져오기와 같은 인증 세션을 통해서 액세스할 수 있습니다.

기본 사용자 정보 및 사용자 ID를 통해 Sagi-shi 플레이어의 전체 사용자 계정을 가져옵니다:

1
2
3
4
var account = yield(client.get_account_async(session), "completed")
var username = account.user.username
var avatar_url = account.user.avatar_url
var user_id = account.user.id

사용자 계정 업데이트 #

Nakama에서는 사용자 계정과 같이 서버에 저장된 리소스를 쉽게 업데이트할 수 있습니다.

Sagi-shi 플레이어는 공용 프로필을 업데이트할 수 있어야 합니다:

1
2
3
4
5
6
7
var new_username = "NotTheImp0ster"
var new_display_name = "Innocent Dave"
var new_avatar_url = "https://example.com/imposter.png"
var new_lang_tag = "en"
var new_location = "Edinburgh"
var new_timezone = "BST"
yield(client.update_account_async(session, new_username, new_display_name, new_avatar_url, new_lang_tag, new_location, new_timezone), "completed")

사용자 가져오기 #

현재 인증된 플레이어의 사용자 계정을 가져오는 방법 외에 Nakama에서 ID나 사용자 이름과 같은 기존 방식을 통해 다른 플레이어의 공용 프로필을 가져올 수 있습니다.

Sagi-shi는 다른 Nakama 기능을 사용할 때 이 메서드로 플레이어의 프로필을 표시합니다:

1
2
var ids = ["userid1", "userid2"]
var users : NakamaAPI.ApiUsers = yield(client.get_users_async(session, ids), "completed")

메타데이터 저장 #

개발자는 Nakama 사용자 메타데이터를 사용하여 사용자 계정을 공용 사용자 필드로 확장할 수 있습니다.

사용자 메타데이터는 서버에서만 업데이트할 수 있습니다. 예시는 사용자 메타데이터 업데이트 레시피를 참조하십시오.

Sagi-shi는 메타데이터를 통해 플레이어가 게임에서 구비한 아이템을 저장합니다:

메타데이터 읽기 #

메타데이터를 설명하고 JSON 메타데이터를 분석하는 클래스를 정의합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class_name metadata

export(String) var title
export(String) var hat
export(String) var skin

# Get the updated account object
var account : NakamaAPI.ApiAccount = yield(client.get_account_async(session), "completed")

# Parse the account user metadata.
var metadata = JSON.parse(account.user.metadata)

Print("Title: %s", metadata.title)
Print("Hat: %s", metadata.hat)
Print("Skin: %s", metadata.skin)

지갑 #

Nakama 사용자 지갑은 여러 디지털 화폐를 문자열/정수의 키/값의 쌍으로 저장할 수 있습니다.

Sagi-shi 플레이어는 게임 내의 가상 화폐를 통해 잠금 해제하거나 제목, 스킨, 모자를 구매할 수 있습니다.

지갑에 액세스하기 #

사용자 계정에서 JSON 지갑 데이터를 분석합니다:

1
2
3
4
var account : NakamaAPI.ApiAccount = yield(client.get_account_async(session), "completed")
var wallet = JSON.parse(account.wallet)
for currency in wallet
    Print("%s, %s" % [currency, wallet[currency].string(int from)])

지갑 업데이트하기 #

지갑은 서버에서만 업데이트할 수 있습니다. 예시는 사용자 계정 가상 지갑 문서를 참조하십시오.

인앱 구매 확인 #

Sagi-shi 플레이어는 서버에서 합법적으로 승인되고 검증된 인앱 구매를 통해 게임 내의 가상 화폐를 구매할 수 있습니다.

예시는 인앱 구매 확인 문서를 참조하십시오.

저장소 엔진 #

Nakama 저장소 엔진은 게임을 위한 분산 및 확장 가능한 문서 기반 저장소 솔루션입니다.

저장소 엔진을 통해 데이터에 쉽게 액세스하고 컬렉션으로 구성할 수 있습니다.

컬렉션은 이름이 지정되며 고유 키와 사용자 ID에 JSON 데이터를 저장합니다.

기본적으로, 플레이어는 자체적인 저장소 객체를 생성, 읽기, 업데이트, 삭제할 수 있는 권한이 있습니다.

Sagi-shi 플레이어는 저장소 엔진에 저장된 항목을 잠금 해제하거나 구매할 수 있습니다.

Sagi-shi player items screen
Player items

저장소 개체 읽기 #

저장소 개체를 읽고 JSON 데이터를 분석합니다:

1
2
3
4
5
6
7
var read_object_id = NakamaStorageObjectId.new("unlocks", "hats", session.user_id)

var result : NakamaAPI.ApiStorageObjects = yield(client.read_storage_objects_async(session, read_object_id), "completed")

print("Unlocked hats: ")
for o in result.objects:
    print("%s" % o)

다른 플레이어의 공용 저장소 개체를 읽기 위해서 user_id을(를) 사용합니다. 플레이어는 본인이 소유하거나 공용인 저장소 개체만 읽을 수 있습니다(2PermissionRead 값).

저장소 개체 작성하기 #

개발자는 Nakama를 통해 클라이언트와 서버에서 저장소 엔진을 작성할 수 있습니다.

쓰기 로직을 어디에 둘 것인지 결정할 때 악의적인 사용자가 게임과 재무 상태에 어떤 역효과를 줄 수 있는지 생각하십시오. 예를 들어 정식으로만 작성해야 하는 데이터(예: 게임 잠금 해제 또는 진행 상황).

Sagi-shi에서 플레이어는 UI를 통해 더 쉽게 액세스할 수 있도록 즐겨찾기 항목을 사용할 수 있으며 클라이언트로부터 이 데이터를 쓰는 것이 안전합니다.

저장소 엔진에 저장소 개체를 작성합니다:

1
2
3
4
5
6
var favorite_hats = ["cowboy", "alien"]
var can_read = 1 # Only the server and owner can read
var can_write = 1 # The server and owner can write

var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("hats", "favorite_hats", can_read, can_write)]), "completed")

write_storage_objects_async 메서드에 여러 개의 개체를 전달할 수 있습니다:

1
2
3
4
var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new(...),
    NakamaWriteStorageObject.new(...)
]), "completed")

조건부 작성 #

저장소 엔진 조건부 작성은 저장소 엔진에 액세스한 후에 개체가 변경되지 않은 경우에만 발생합니다.

이렇게 하면 데이터 덮어쓰기를 방지할 수 있습니다. 예를 들어, 플레이어가 마지막으로 액세스한 이후에 Sagi-shi 서버가 개체를 업데이트 했을 수도 있습니다.

조건부 작성을 실행하려면 버전을 추가하여 가장 최신의 객체 버전에서 저장소 객체를 작성합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Assuming we already have a storage object
var favorite_hats = ["cowboy", "alien"]
var can_read = 1 # Only the server and owner can read
var can_write = 1 # The server and owner can write
var version = <version>

var acks : NakamaAPI.ApiStorageObjectAcks = yield(client.write_storage_objects_async(session, [
    NakamaWriteStorageObject.new("hats", "favorite_hats", can_read, can_write, version)]), "completed")
if acks.is_exception():
    print("An error occurred: %s" % acks)
    return

저장소 객체 목록 만들기 #

개별 키로 다양한 읽기 요청을 수행하지 않고, 컬렉션에서 플레이어가 액세스할 수 있는 모든 저장소 객체의 목록을 만들 수 있습니다.

Sagi-shi는 플레이어가 잠금 해제하거나 구매한 모든 제목, 모자, 스킨을 나열합니다:

1
2
3
4
5
6
7
8
var limit = 3
var unlocks_object_list : NakamaAPI.ApiStorageObjectList = yield(client.list_storage_objects_async(session, "titles", "hats", "skins", session.user_id, limit), "completed")
if unlocks_object_list.is_exception():
    print("An error occurred: %s" % unlocks_object_list)
    return
print("Unlocked objects: ")
for o in unlocks_object_list.objects:
    print("%s" % o)

페이지 지정 결과 #

결과를 나열하는 Nakama 메서드는 컬렉션에서 개체 검색을 시작할 위치를 나타내기 위해 Nakama에 대한 후속 호출에 전달할 수 있는 커서를 반환합니다.

예:

  • 커서의 값이 5인 경우, 다섯 번째 객체에서 결과를 얻습니다.
  • 커서가 null인 경우, 첫 번째 객체에서 결과를 얻습니다.
1
object_list : NakamaAPI.ApiStorageObjectList = yield(client.list_storage_objects_async(session, "<object>", limit, object_list.cursor), "completed")

서버에서 저장소 작업 보호하기 #

Nakama 저장소 엔진 작업은 서버에서 플레이어가 데이터(예: 게임 잠금 해제 또는 진행률)를 수정할 수 없도록 보호할 수 있습니다. 저장소 엔진에 작성하기 레시피를 참조하십시오.

원격 프로시저 호출 #

개발자는 Nakama 서버에서 사용자 지정 로직을 작성하여 클라이언트에게 RPC로 노출시킬 수 있습니다.

Sagi-shi에는 플레이어가 장비를 장착하기 전에 장비를 소유하고 있는지 확인하는 등 서버 내에서 보호해야 하는 다양한 로직이 포함되어 있습니다.

서버 로직 생성하기 #

플레이어가 장비를 장착하기 전에 장비를 소유하고 있는지 확인하는 원격 절차를 만드는 예시에 대해서는 플레이어 장비 정식 취급 레시피를 참조하십시오.

클라이언트 RPC #

클라이언트에서 Nakama 원격 프로시저를 호출하여 JSON 페이로드를 선택할 수 있습니다.

Sagi-shi 클라이언트는 모자를 안전하게 구비하기 위해서 RPC를 만듭니다.

1
2
3
4
5
6
var payload = {"hat": "cowboy"}
var rpc_id = equip_hat
var response : NakamaAPI.ApiRpc = yield(client.rpc_async(session, rpc_id, JSON.print(payload)), "completed")
if response.is_exception():
    print("An error occurred: %s" % response)
    return

소켓 RPC #

Nakama의 실시간 기능에 인터페이스를 설정해야 할 경우 소켓에서 Nakama 원격 프로시저도 호출할 수 있습니다. 이러한 실시간 기능에는 라이브 소켓과 해당 세션 식별자가 필요합니다. 이와 같은 식별자를 포함하는 소켓에서 RPC를 생성할 수 있습니다.

1
var response : NakamaAPI.ApiRpc = yield(socket.rpc_async("<rpc_id>", "<payload>"), "completed")

친구 #

Nakama 친구는 플레이어 간의 우정을 관리하는 완벽한 소셜 그래프 시스템을 제공합니다.

Sagi-shi에서 플레이어는 친구를 추가하고, 관계를 관리하고, 함께 플레이할 수 있습니다.

Sagi-shi Friends screen
Friends screen

친구 추가하기 #

Nakama에서 친구를 추가해도 상호적인 친구 관계가 즉각적으로 추가되지 않습니다. 사용자별 진행중인 친구 요청은 사용자가 승인해야 합니다.

Sagi-shi에서 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 추가할 수 있습니다:

1
2
3
4
5
6
7
8
var ids = ["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")

친구 관계 상태 #

Nakama 친구 관계는 다음의 상태로 구분됩니다.

상태
0서로 아는 친구
1수락 대기 중인 친구 요청 발신
2수락 대기 중인 친구 요청 수신
3사용자에 의해 차단됨

친구 목록 만들기 #

개발자는 Nakama에서 친구 관계 상태를 기반으로 플레이어의 친구 목록을 만들 수 있습니다.

Sagi-shi에서는 20명의 가장 최근 친구가 목록으로 표시됩니다:

1
2
3
4
5
6
7
8
9
var limit = 20 # Limit is capped at 1000
var friendship_state = 0
var list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, limit, friendship_state), "completed")
if list.is_exception():
    print("An error occurred: %s" % list)
    return
for f in list.friends:
    var friend = f as NakamaAPI.ApiFriend
    print("Friends %s [friend.user.id])

친구 요청 수락하기 #

Nakama에서 친구 요청을 수락하면 플레이어는 양방향 친구 관계를 추가합니다.

Nakama는 둘의 상태를 보류에서 상호로 변경하는 작업을 처리합니다.

완전한 게임에서는 플레이어가 개별 요청을 수락할 수 있도록 허용합니다.

Sagi-shi는 들어오는 모든 친구 요청을 가져와 수락합니다:

1
2
3
4
var limit = 1000
var result : NakamaAsyncResult = yield(client.list_friends_async(session, 2, limit, cursor: null)
for f in result.friends:
    yield(client.add_friend_async(session, f.user.id), "completed")

친구 삭제하기 #

Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 친구를 삭제할 수 있습니다:

1
2
3
4
5
6
7
8
var ids = ["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
var ids = ["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")

친구 차단하기와 관련된 관계 상태에 대해서 자세하게 알아봅니다.

차단된 사용자는 친구 목록 보기와 같이 나열할 수 있지만 해당 친구 관계 상태(3)를 사용합니다.

상태 및 현재 상태 #

Nakama 상태는 사용자가 온라인 상태를 설정하고 상태 메시지를 업데이트하며 다른 사용자의 업데이트를 따를 수 있는 실시간 상태 및 현재 상태 서비스입니다.

플레이어는 팔로우하려고 하는 다른 사용자와 친구가 아니어도 됩니다.

Sagi-shi는 상태 메시지와 온라인 현재 상태를 사용하여 친구가 온라인 상태일 때 플레이어에게 알리고 대결을 공유합니다.

Sagi-shi status update screen
Updating player status

사용자 팔로우하기 #

개발자는 Nakama 실시간 API를 사용하여 상태 변경과 같은 소켓의 이벤트를 구독하고 실시간으로 수신할 수 있습니다.

사용자 팔로우 방법은 현재 상태라고 하는 현재 온라인 사용자와 해당 상태도 반환합니다.

Sagi-shi는 플레이어의 친구를 팔로우하며 친구가 온라인 상태가 되면 알림을 제공합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func _ready():
    # Setup the socket and subscribe to the status event
    socket.connect("received_status_presence", self, "_on_status_presence")

func _on_status_presence(p_presence : NakamaRTAPI.StatusPresenceEvent):
    print(p_presence)
    for j in p_presence.joins:
        print("%s is online with status: %s" % [j.user_id, j.status])
    for j in p_presence.leaves:
        print("%s went offline" % [j.user_id])

# Follow mutual friends and get the initial Status of any that are currently online
var friends_result = yield(client.list_friends_async(session, 0), "completed")
var friend_ids = []
for friend in friends_result:
	var f = friend as NakamaAPI.ApiFriend
	if not f or not f.user.online:
		continue
	friend_ids.append(f.user)
var result : NakamaAsyncResult = yield(socket.follow_users_async(friend_ids)

for p in result.presences:
    print("%s is online with status: %s" % [presence.user_id, presence.status])

사용자 팔로우 취소하기 #

Sagi-shi 플레이어는 팔로우를 취소할 수 있습니다:

1
yield(socket.unfollow_users_async("<user_id>"), "completed")

플레이어 상태 업데이트하기 #

Sagi-shi 플레이어는 상태를 변경하고 팔로워에게 공개할 수 있습니다:

1
yield(socket.update_status_async("Viewing the Main Menu"), "completed")

그룹 #

Nakama 그룹은 공용/개인 표시, 사용자 구성원 및 권한, 메타데이터 및 그룹 채팅을 제공하는 그룹 또는 클랜 시스템입니다.

Sagi-shi에서 플레이어는 그룹을 만들고 가입하여 협력하고 경쟁할 수 있습니다.

Sagi-shi groups screen
Groups list screen

그룹 생성하기 #

그룹에는 공용 또는 개인 “공개” 표시가 있습니다. 누구나 공용 그룹에 가입할 수 있지만 가입을 요청하고 비공개 그룹의 최고 관리자/관리자가 수락해야 합니다.

Sagi-shi 플레이어는 공통의 관심사를 기반으로 그룹을 생성할 수 있습니다:

1
2
3
4
5
6
var name = "Imposters R Us"
var description = "A group for people who love playing the imposter."
var open = true # public group
var max_size = 100

var group : NakamaAPI.ApiGroup = yield(client.create_group_async(session, name, description, open, max_size), "completed")

그룹 표시 유형 업데이트 #

Nakama를 사용하면 그룹 총괄 관리자 또는 관리자 구성원이 공개 표시와 같은 클라이언트의 일부 속성을 업데이트할 수 있습니다:

1
2
var open = false
yield(client.update_group_async(session, "<group_id>", name: null, open), "completed")

그룹 크기 업데이트 #

그룹의 최대 구성원 숫자와 같은 다른 속성은 서버에서만 변경할 수 있습니다.

예시를 보려면 그룹 크기 업데이트 레시피를 참조하고 서버에서 그룹을 업데이트하는 방법을 자세히 알아보려면 그룹 서버 함수 참조를 참조하십시오.

Sagi-shi group edit screen
Sagi-shi group edit

그룹 목록 및 필터링 #

그룹은 다른 Nakama 리소스와 같이 목록을 만들고 와일드카드 그룹 이름으로 필터링할 수 있습니다.

Sagi-shi 플레이어는 그룹 목록과 필터링을 통해 기존 그룹을 검색할 수 있습니다:

1
2
3
4
5
6
7
8
9
var limit = 20
var result : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, "imposter%", limit), "completed")

for g in result.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name &s, open %s", [group.name, group.open])

$ Get the next page of results
var next_results : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, name: "imposter%", limit, result.cursor)

그룹 삭제 #

총괄 관리자는 Nakama를 통해 그룹을 삭제할 수 있습니다.

개발자는 해당 기능 전체를 사용하지 않을 수 있습니다. Nakama API를 보호하는 다양한 방법에 대한 예시는 API 보호 가이드를 참조하십시오.

Sagi-shi 플레이어는 관리자로 되어 있는 그룹을 삭제할 수 있습니다:

1
yield(client.delete_group_async(session, "<group_id>"), "completed")

그룹 메타데이터 #

사용자 계정과 마찬가지로, 그룹에는 공용 메타데이터가 있을 수 있습니다.

Sagi-shi는 그룹 메타데이터를 사용하여 그룹의 관심사, 활성 플레이어 시간, 언어를 저장합니다.

그룹 메타데이터는 서버에서만 업데이트할 수 있습니다. 예시는 그룹 메타데이터 업데이트 레시피를 참조하십시오.

Sagi-shi 클라이언트는 그룹 메타데이터 페이로드를 통해 RPC를 생성합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var payload = {
    group_id = "<group_id>",
    interests = ["Deception", "Sabotage", "Cute Furry Bunnies"],
    active_times = ["9am-2pm Weekdays", "9am-10pm Weekends"],
    languages = ["English", "German"],
}

var result : NakamaAsyncResult = yield(client.rpc_async(session, "update_group_metadata", JSON.stringify(payload))
if result.is_exception():
    print("An error occurred: %s" % result)
    return
print("Successfully updated group metadata")

그룹 구성원 상태 #

Nakama 그룹 구성원은 다음의 상태로 구분합니다:

코드목적
0총괄 관리자모든 그룹에는 최소 1명의 총괄 관리자가 있어야 합니다. 총괄 관리자는 모든 관리자 권한이 부여되며, 그룹을 삭제하고 관리자 구성원을 승격시킬 수 있습니다.
1관리자관리자는 2명 이상일 수 있습니다. 관리자는 그룹을 업데이트하거나 구성원을 수락, 추방, 승격, 강등, 금지, 추가할 수 있습니다.
2구성원일반 그룹 구성원 새로운 사용자의 가입 요청을 수락할 수 없습니다.
3가입 요청새로운 사용자로부터 가입 요청이 있습니다. 이것은 그룹의 최대 구성원 숫자에 포함되지 않습니다.

그룹에 가입하기 #

플레이어가 공개 그룹에 가입하는 경우, 즉각적으로 구성원으로 되지만, 개인 그룹에 가입하려고 하는 경우에는 그룹 관리자가 수락해야 합니다.

Sagi-shi 플레이어는 그룹에 가입할 수 있습니다:

1
yield(client.join_group_async(session, "<group_id>"), "completed")

사용자 그룹 목록 만들기 #

Sagi-shi 플레이어는 구성원인 그룹의 목록을 만들 수 있습니다:

1
2
3
4
5
6
var user_id = "<user id>"
var result : NakamaAPI.ApiUserGroupList = yield(client.list_user_groups_async(session, user_id), "completed")

for ug in result.user_groups:
    var g = ug.group as NakamaAPI.ApiGroup
    print("Group %s role %s", g.id, ug.state)

구성원 목록 만들기 #

Sagi-shi 플레이어는 그룹의 구성원 목록을 만들 수 있습니다:

1
2
3
4
5
6
var group_id = "<group id>"
var member_list : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, group_id), "completed")

for ug in member_list.group_users:
    var u = ug.user as NakamaAPI.ApiUser
    print("User %s role %s" % [u.id, ug.state])

가입 요청 수락하기 #

개인 그룹 관리자 또는 총괄 관리자는 사용자를 그룹에 추가하여 가입 요청을 수락할 수 있습니다.

Sagi-shi는 먼저 가입 요청 상태의 모든 사용자를 나열한 다음 루프를 통해 그룹에 추가합니다:

1
2
3
4
5
var result : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, "<group_id>", 3), "completed")

for gu in result.group_users:
    var u = gu.user as NakamaAPI.ApiUser
    yield(client.add_group_users_async(session, "<group_id>", u), "completed"))

구성원 승격하기 #

Nakama 그룹 구성원은 관리자 또는 총괄 관리자 역할로 승격되어 성장하는 그룹을 관리하거나 구성원이 탈퇴할 경우 인수할 수 있습니다.

관리자는 다른 구성원을 관리자로 승격시킬 수 있고, 총괄 관리자는 다른 구성원을 총괄 관리자로 승격시킬 수 있습니다.

구성원은 한 단계 승격됩니다. 예:

  • 구성원이 승격되면 관리자가 됩니다.
  • 관리자가 승격되면 총괄 관리자가 됩니다.
1
yield(client.promote_group_users_async(session, "<group_id>", "<user_id>")

구성원 강등하기 #

Sagi-shi 그룹 관리자와 총괄 관리자는 구성원을 강등시킬 수 있습니다:

1
yield(client.demote_group_users_async(session, "<group_id>", "<user_id>")

구성원 추방하기 #

Sagi-shi 그룹 관리자와 총괄 관리자는 그룹의 구성원을 삭제할 수 있습니다:

1
yield(client.kick_group_users_async(session, "<group_id>", "<user_id>")

구성원 차단하기 #

Sagi-shi 그룹 관리자와 총괄 관리자는 강등이나 탈퇴 외에 사용자를 차단할 수도 있습니다:

1
yield(client.ban_group_users_async(session, "<group_id>", "<user_id>")

그룹 탈퇴하기 #

Sagi-shi 플레이어는 그룹에서 탈퇴할 수 있습니다:

1
yield(client.leave_group_async(session, "<group_id>")

채팅 #

Nakama 채팅은 그룹, 개인/직접 메시지 및 동적 채팅방을 위한 실시간 채팅 시스템입니다.

Sagi-shi는 그룹 채팅 및 개인/다이렉트 메시지를 사용하며 대결 중 동적 채팅을 사용하여 플레이어는 서로를 오도하고 사기꾼이 누구인지 이야기할 수 있습니다.

Sagi-shi chat screen
Sagi-shi Chat

동적 채팅 참여하기 #

Sagi-shi 대결에는 플레이어가 이야기할 수 있는 임시 대화방이 있습니다:

1
2
3
4
5
6
7
var roomname = "<match_id>"
var persistence = false
var hidden = false
var type = NakamaSocket.ChannelType.Room
var channel : NakamaRTAPI.Channel = yield(socket.join_chat_async(roomname, type, persistence, hidden), "completed")

print("Connected to dynamic room channel: '%s'" % [channel.id])

그룹 채팅 참여하기 #

Sagi-shi 그룹 구성원은 지속적인 그룹 채팅 채널에서 플레이 세션 동안 대화할 수 있습니다:

1
2
3
4
5
6
7
var group_id = "<group_id>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.Group
var channel : 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
var user_id = "<user_id>"
var persistence = true
var hidden = false
var type = NakamaSocket.ChannelType.DirectMessage
var channel : 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
var channel_id = "<channel_id>"

var message_content = { "message": "I think Red is the imposter!" }

var message_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, message_content), "completed")

var emote_content = {
    "emote": "point",
    "emoteTarget": "<red_player_user_id>",
    }

var emote_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, emote_content), "completed")

메시지 기록 나열하기 #

메시지 목록에는 메시지를 최신 순 또는 역순으로 표시할 수 있는 매개변수가 있습니다.

Sagi-shi 플레이어는 그룹의 메시지 기록을 나열할 수 있습니다:

1
2
3
4
5
6
7
8
var limit = 100
var forward = true
var group_id = "<group_id>"
var result : NakamaAPI.ApiChannelMessageList = yield(client.list_channel_messages_async(session, group_id, limit, forward), "completed")

for m in result.messages:
    var message : NakamaAPI.ApiChannelMessage = m as NakamaAPI.ApiChannelMessage
    print(message.user_id, message.content)

채팅에는 캐시 가능한 커서가 있어 가장 최근 메시지를 가져올 수 있습니다. 캐시 가능한 커서에 대한 자세한 내용은 목록 알림 문서를 참조하십시오.

메시지 업데이트 #

Nakama는 메시지 업데이트 기능도 지원합니다. 이 기능은 원하는 경우 사용할 수 있지만 Sagi-shi와 같은 속임수 게임에서는 속임수가 추가될 수 있습니다.

예를 들어, 플레이어는 다음의 메시지를 전송합니다:

1
2
3
4
var channel_id = "<channel_id>"
var message_content = {"message": "I think Red is the imposter!" }

var message_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.write_chat_message_async(channel_id, message_content), "completed")

다른 플레이어에게 혼동을 주기 위해서 메시지를 빠르게 편집할 수 있습니다:

1
2
3
var new_message_content = { "message": "I think BLUE is the imposter!" }

var message_update_ack : NakamaRTAPI.ChannelMessageAck = yield(socket.update_chat_message_async(channel_id, new_message_content), "completed")

대결 #

Nakama는 Server AuthoritativeServer Relayed 멀티플레이어 대결을 지원합니다.

서버 정식 대결에서 서버는 게임 플레이 루프를 제어하고 모든 클라이언트를 게임의 현재 상태인 최신 상태로 유지해야 합니다.

서버 중계 대결의 경우 클라이언트가 제어하고 서버는 연결된 다른 클라이언트에게 정보만 릴전달합니다.

Sagi-shi와 같은 경쟁 게임에서는 클라이언트가 승인되지 않은 방식으로 게임과 상호 작용하는 것을 방지하기 위해 서버 정식 대결이 사용될 수 있습니다.

이 가이드에서는 쉽게 설명하기 위해 서버 중계 모델을 사용합니다.

대결 생성하기 #

Sagi-shi 플레이어는 대결을 생성하여 온라인 친구들이 참여하도록 초대할 수 있습니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var match : NakamaRTAPI.Match = yield(socket.create_match_async(), "completed")
var friends_list = yield(client.list_friends_async(session, 0, 100)
var online_friends = []
for friend in friends_list:
    var f = friend as NakamaAPI.ApiFriend
	if not f or not f.user.online:
		continue
    online_friends.append(f.user)

for f in online_friends:
    var content = {
        "message": "Hey %s, join me for a match!",
        match_id = match.id,
    }
    var channel = yield(socket.join_chat_async(f.id, NakamaSocket.ChannelType.DirectMessage), "completed")
    var message_ack = yield(socket.write_chat_message_async(channel.id, content), "completed")

대결 이름으로 대결 생성하기

Sagi-shi 플레이어는 특정한 대결 이름을 사용하여 대결을 생성하여 친구에게 대결의 이름을 알려주고 초대할 수 있습니다. 이름으로 대결을 만들 때(임의의 이름이고 정식 대결 핸들러에 연결되지 않은 것) 대결은 항상 정식 대결이 아니라 중계된 대결이라는 점에 유의해야 합니다.

1
2
var match_name = "NoImpostersAllowed"
var match : NakamaRTAPI.Match = yield(socket.create_match_async(match_name), "completed")

대결 참여하기 #

Sagi-shi 플레이어는 ID를 알고 있는 경우 기존 대결에 참여할 수 있습니다:

1
2
var match_id = "<matchid>"
var match = yield(socket.join_match_async(match_id), "completed")

또는 실시간 매치 메이커 리스너를 설정하고 매치 메이커에 자신을 추가합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

var min_players = 2
var max_players = 10
var query = ""

var matchmaking_ticket : NakamaRTAPI.MatchmakerTicket = yield(
  socket.add_matchmaker_async(query, min_players, max_players),
  "completed"
)

플레이어 상태로 대결 참여하기

Sagi-shi 플레이어는 새로운 대결에 참여할 경우 상태를 업데이트할 수 있습니다:

1
2
3
4
5
6
var status = {
    "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 status
    for j in p_presence.joins:
        var status = JSON.parse(p_presence.status)
        if matchid in status:
            yield(socket.join_match_async(status["matchid"]), "completed")

대결 나열하기 #

대결 목록은 플레이어 수, 대결 레이블 및 더 복잡한 검색 쿼리를 제공하는 옵션을 포함하여 대결을 필터링하기 위해 여러 기준을 사용합니다.

Sagi-shi 대결은 로비 상태에서 시작합니다. 대결은 서버상에 존재하지만, 플레이어의 숫자가 충분하지 않을 경우 실제 게임이 시작되지 않습니다.

Sagi-shi에서는 더 많은 플레이어를 기다리고 있는 대결 목록이 표시됩니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var min_players = 2
var max_players = 10
var limit = 10
var authoritative = true
var label = ""
var query = ""
var result : NakamaRTApi.Match = yield(client.list_matches_async(session, min_players, max_players, limit, authoritative, label, query)

for m in result.matches:
    print("%s: %s/10 players", match.match_id, match.size)

"an_exact_match_label"과(와) 같은 레이블이 있는 대결 항목을 찾으려면:

1
var label = "an_exact_match_label"

고급:

더 복잡한 구조의 쿼리를 사용하려면 대결 레이블이 JSON 형식으로 작성되어야 합니다.

플레이어의 기술 수준이 >100과(와) 같을 것으로 예상하고 선택적으로 "sabotage" 게임 모드가 있는 대결을 찾으려면 다음을 수행합니다:

1
var query = "+label.skill:>100 label.mode:sabotage"

플레이어 생성 #

대결 개체에는 현재 온라인 사용자의 현재 상태가 목록으로 나열됩니다.

Sagi-shi는 대결 현재 상태를 사용하여 클라이언트에서 플레이어를 생성합니다:

1
2
3
4
5
6
7
8
var match = yield(socket.join_match_async(match_id), "completed")

var players = {}

for p in match.presences:
    // Spawn a player for this presence and store it in a dictionary by session id.
    var go = <player_node>.new()
    players.add(presence.session_id, go)

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...
    for p in p_presence.joins:
        # Spawn a player for this presence and store it in a dictionary by session id.
        var go = <player_node>.new()
        players.add(p_presence.session_id, go)
    # For each player that has left in this event...
    for p in p_presence.leaves:
        # Remove the player from the game if they've been spawned
        if presence.session_id in players:
            <player_node>.remove_and_skip()
            players.remove(presence.session_id)

대결 상태 전송하기 #

Nakama는 실시간 네트워킹을 통해 플레이어가 게임 세계에서 이동하고 상호 작용할 때 대결 상태를 보내고 받습니다.

대결이 진행되는 동안 Sagi-shi 클라이언트는 다른 클라이언트와 연결되는 서버로 대결 상태를 전송합니다.

대결 상태에는 수신 중인 데이터를 수신인에게 알려주는 연산 코드가 포함되어 있어 수신 중인 데이터를 역직렬화하고 게임 보기를 업데이트할 수 있습니다.

Sagi-shi에서 사용되는 연산 코드 예시:

  • 1: 플레이어 위치
  • 2: 플레이어 호출 투표

플레이어 위치 전송

Sagi-shi 플레이어 위치 상태를 표시하는 클래스를 정의합니다:

1
2
3
4
5
class_name position_state

var X
var Y
var Z

플레이어의 변환에서 인스턴스를 만들고 연산 코드를 설정하고 JSON 인코딩 상태를 보냅니다:

1
2
3
4
5
6
7
8
9
var state = {
    X = transform.x,
    Y = transform.y,
    Z = transform.z,
}

var op_code = 1

yield(socket.send_match_state_async(match.id, op_code, JSON.print(state), "completed")

정적 클래스로서의 연산 코드

Sagi-shi에는 네트워크 게임 액션이 많습니다. 연산 코드에 대해 정적 상수 클래스를 사용하면 코드를 더 쉽게 준수하고 유지 관리할 수 있습니다:

1
2
3
4
5
6
class_name op_codes

const position = 1
const vote = 2

yield(socket.send_match_state_async(match.id, op_codes.position, JSON.print(state), "completed")

대결 상태 수신하기 #

Sagi-shi 플레이어는 대결 상태 수신 이벤트를 구독하여 다른 연결된 클라이언트로부터 대결 데이터를 수신할 수 있습니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func _on_match_state(p_state : NakamaRTAPI.MatchData):
    match match_state.op_code:
        op_code.position:
        # Get the updated position data
        var position_state = JSON.parse(match_state.state)
        # Update the game object associated with that player
        var user = match_state.user_presence.session_id
        if user in players:
            # 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 매치 메이커를 사용하여 플레이어의 대결을 찾을 수 있습니다. 이를 통해 플레이어는 실시간 매치메이킹 풀에 참여하고 지정된 기준과 일치하는 다른 플레이어와 대결이 성사될 때 알림을 받을 수 있습니다.

매치메이킹을 통해 플레이어는 서로를 찾을 수 있지만 대결을 생성하지는 않습니다. 이러한 분리는 의도된 것이므로 게임 대결을 찾는 것 이상으로 매치메이킹을 사용할 수 있습니다. 예를 들어, 소셜 경험을 만들고 있는 경우 매치메이킹을 사용하여 채팅할 다른 사람을 찾을 수 있습니다.

매치 메이커 추가 #

매치메이킹 기준은 그냥 2명의 플레이어만 찾거나 아니면 더 복잡하게 특정 게임 모드에 관심이 있는 최소 기술 수준을 가진 2-10명의 플레이어를 찾을 수 있습니다.

Sagi-shi에서는 플레이어가 매치메이킹 풀에 참여하고 서버를 통해 다른 플레이어와의 대결을 만들 수 있습니다:

1
2
3
4
5
6
7
var min_players = 2
var max_players = 10
var query = "+skill:>100 mode:sabotage"
var string_properties = { "mode": "sabotage" }
var numeric_properties = { "skill": 125 }
var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
  socket.add_matchmaker_async(query, min_players, max_players, string_properties, numeric_properties)

제공된 기준에 따라 대결이 성공한 후에 플레이어는 대결에 참여할 수 있습니다:

1
2
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

파티 #

Nakama 파티는 플레이어가 모든 플레이어의 연결이 끊긴 후에도 지속되지 않는 단기 파티를 구성할 수 있는 실시간 시스템입니다.

Sagi-shi를 통해 친구들은 함께 파티를 맺고 대결할 수 있습니다.

파티 생성하기 #

파티를 생성하는 플레이어가 파티의 리더가 됩니다. 파티에는 최대 플레이어 수가 지정되며 공개된 경우 자동으로 플레이어가 수락되고 비공개인 경우 파티 리더가 들어오는 참가 요청을 수락해야 합니다.

Sagi-shi는 최대 4명의 플레이어가 있는 비공개 파티를 사용합니다:

1
2
3
var open = false
var max_players = 4
var party = yield(socket.create_party_async(open, max_players), "completed")

Sagi-shi에서는 개인/직접 메시지를 통해 친구와 파티 ID를 공유합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var friends_list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, limit, friendship_state), "completed")
var online_friends = []
for friend in friends_list:
    var f = friend as NakamaAPI.ApiFriend
    if not f or not f.user.online:
        continue
    online_friends.append(f.user)

for f in online_friends:
    var content = {
        "message": "Hey %s, wanna join the party?",
        party_id = party.id,
    }
    var channel = yield(socket.join_chat_async(f.id, NakamaSocket.ChannelType.DirectMessage), "completed")
    var message_ack = yield(socket.write_chat_message_async(channel.id, content), "completed")

파티 참여하기 #

Safi-shi 플레이어는 메시지에서 파티 ID를 확인하여 채팅 메시지에 있는 파티에 참여할 수 있습니다. 먼저, 소켓의 received_channel_message 신호를 연결해야 합니다.

1
socket.connect("received_channel_message", self, "_on_received_channel_message")

신호가 수신되면 메시지 컨텐츠를 확인하고 파티에 참여할 수 있습니다.

1
2
3
4
5
6
func _on_received_channel_message(message):
    var data = JSON.parse(message.content)
    if data.result.party_id:
        var join = yield(socket.join_party_async(data.result.party_id), "completed")
        if join.is_exception():
            print("error joining party)

구성원 승격하기 #

Sagi-shi 파티 구성원은 파티의 리더로 승격될 수 있습니다:

1
2
3
var new_leader = "<user_id>"
var party_id = "<party_id>"
var leader: NakamaAsyncResult = yield(socket.received_party_leader(party_id, new_leader), "completed)

파티 탈퇴하기 #

Sagi-shi 플레이어는 파티에서 탈퇴할 수 있습니다:

1
2
var party_id = "<party_id>"
var party: NakamaAsyncResult = yield(socket.leave_party_async(party_id), "completed")

파티로 매치메이킹 #

파티에 가입하면 모든 플레이어가 매치메이킹 풀에 함께 참여할 수 있는 이점이 있습니다.

Sagi-shi 플레이어는 매치메이커 대결 이벤트를 들은 후 발견하면 해당 대결에 참여할 수 있습니다:

1
2
func _on_matchmaker_matched(p_matched : NakamaRTAPI.MatchmakerMatched):
  var joined_match : NakamaRTAPI.Match = yield(socket.join_match_async(p_matched), "completed")

파티 리더는 파티 상대 찾기를 시작합니다:

1
2
3
4
5
var party_id = "<party_id>"
var min_players = 2
var max_players = 10
var query = ""
var matchmaker_ticket = yield(socket.add_matchmaker_party_async(party_id, query, min_players, max_Players)

순위표 #

Nakama 순위표는 게임에 경쟁적인 측면을 만들고 플레이어 참여도와 유지율을 높입니다.

Sagi-shi에는 주간 사기꾼 승리 순위표가 있습니다. 여기서 플레이어는 승리할 때마다 점수가 높아지며 마찬가지로 주간 직원 승리에 대한 순위표가 있습니다.

Sagi-shi leaderboard screen
Sagi-shi Leaderboard

순위표 생성하기 #

순위표는 서버에서 생성해야 합니다. 순위표 생성에 대한 자세한 내용은 순위표 문서를 참조하십시오.

점수 제출하기 #

플레이어가 점수를 제출하면, Nakama는 플레이어의 기존 점수에 제출된 점수값을 더합니다.

Nakama에는 점수값 외에 점수가 동일할 때 순위를 매길 수 있는 항목별 점수도 있습니다.

Sagi-shi 플레이어는 점수를 달성한 맵과 같은 상황별 메타데이터를 포함하여 순위표에 점수를 제출할 수 있습니다:

1
2
3
4
var score = 1
var subscore = 0
var metadata = { "map": "space_station" }
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, "weekly_imposter_wins", score, subscore, JSON.print(metadata), "completed")

상위 레코드 나열하기 #

Sagi-shi 플레이어는 순위표에서 상위 레코드를 나열할 수 있습니다:

1
2
3
4
5
6
var limit = 20
var leaderboard_name = "weekly_imposter_wins"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_name, owner_ids: null, expiry: null, limit, cursor: null), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)

사용자에 대한 레코드 나열하기

개발자는 Nakama에서 플레이어에 대한 순위표 레코드를 나열할 수 있습니다.

Sagi-shi는 플레이어에게 주변 플레이어에 대해 본인이 어떻게 하고 있는지에 대한 스냅샷을 제공합니다:

1
2
3
4
5
6
var limit = 20
var leaderboard_name = "weekly_imposter_wins"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_name, session.user_id, expiry: null, limit), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)

사용자 목록에 대한 레코드 나열하기

Sagi-shi 플레이어는 소유자 ID 매개변수로 사용자 ID를 제출하여 친구의 점수를 얻을 수 있습니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var friends_list : NakamaAPI.ApiFriendList = yield(client.list_friends_async(session, 0, 100, cursor: null), "completed")
var user_ids = []
for friend in friends_list.friends:
    var f = friend as NakamaAPI.ApiFriend
    user_ids.append(f.user.id)

var record_list : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, "weekly_imposter_wins", user_ids, expiry: null, 100, cursor: null), "completed")

for record in record_list.records:
    print("%s scored %s", record.username, record.score)

소유자 ID 매개변수로 사용자 ID를 제출하여 그룹 구성원의 점수도 얻을 수 있습니다:

1
2
3
4
5
6
7
8
var group_id = "<groupid>"
var group_user_list : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, group_id, 100, cursor: null), "completed")
var user_ids = []
for gu in group_user_list.group_users:
    var u = gu as NakamaAPI.ApiUser
    user_ids.append(u.user.id)

var record_list : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, "weekly_imposter_wins", user_ids, expiry: null, 100, cursor: null), "completed")

레코드 삭제하기 #

Sagi-shi 플레이어는 본인의 순위표 레코드를 삭제할 수 있습니다:

1
yield(client.delete_leaderboard_record_async(session, "<leaderboard_id>"), "completed")

토너먼트 #

Nakama 토너먼트는 플레이어가 상을 받기 위해서 경쟁하는 단기간의 경기입니다.

Sagi-shi 플레이어는 진행 중인 토너먼트를 보고 필터링하고 참가할 수 있습니다.

Sagi-shi tournaments screen
Sagi-shi Tournaments

토너먼트 생성하기 #

토너먼트는 서버에서 생성해야 합니다. 토너먼트 생성에 대한 자세한 내용은 토너먼트 문서를 참조하십시오.

Sagi-shi에는 가장 정확한 사기꾼 투표를 얻기 위해 플레이어들이 도전할 수 있는 주간 토너먼트가 있습니다. 한 주가 끝난 후에 상위 플레이어는 게임 화폐를 상금으로 받습니다.

토너먼트 참여하기 #

기본적으로, Nakama 플레이어는 점수를 제출하기 전에 토너먼트에 참여하지 않아도 되지만 Sagi-shi에서는 점수를 의무적으로 제출해야 합니다:

1
yield(client.join_tournament_async(session, "<id>"), "completed")

토너먼트 나열하기 #

Sagi-shi 플레이어는 다양한 기준으로 토너먼트를 나열하고 필터링할 수 있습니다:

1
2
3
4
5
6
7
8
9
var category_start = 1
var category_end = 2
int start_time = null
int end_time = null
var limit = 100
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, category_start, category_end, start_time, end_time, limit, cursor: null), "completed")

for t in result.tournaments:
    print("%s:%s", tournament.id, tournament.title)

성능 상의 이유로 카테고리는 개별 숫자가 아닌 범위를 사용하여 필터링됩니다. 이를 활용하기 위해 카테고리를 구성하십시오(예: 1XX 범위의 모든 PvE 토너먼트, 2XX 범위의 모든 PvP 토너먼트 등).

레코드 나열하기 #

Sagi-shi 플레이어는 토너먼트 레코드를 나열할 수 있습니다:

1
2
3
4
5
6
var limit = 20
var tournament_name = "weekly_top_detective"
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, tournament_name, owner_ids: null, expiry: null, limit, cursor: null), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)

사용자에 대한 레코드 나열하기

순위표와 유사하게 Sagi-shi 플레이어는 다른 플레이어의 스코어를 얻을 수 있습니다:

1
2
3
4
5
6
var limit = 20
var tournament_name = "weekly_top_detective"
var result : NakamaAPI.ApiTournamentRecordList = yield(client.list_tournament_records_async(session, tournament_name, session.user_id, expiry: null, limit), "completed")

for r in result.records:
    print("%s:%s", record.owner_id, record.score)

점수 제출하기 #

Sagi-shi 플레이어는 점수, 항목별 점수, 메타데이터를 토너먼트에 제출할 수 있습니다:

1
2
3
4
5
var score = 1
var subscore = 0
var metadata = JSON.print({
    "map": "space_station" })
var new_record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_tournament_record_async(session, "weekly_top_detective", score, subscore, metadata), "completed")

알림 #

게임 서버에서 Nakama 알림을 통해 플레이어에게 실시간 메시지를 전달할 수 있습니다.

알림은 영구적(플레이어가 볼 때까지 남음)이거나 일시적(플레이어가 현재 온라인인 경우에만 수신)일 수 있습니다.

Sagi-shi는 알림을 통해 토너먼트의 승자에게 승리 여부를 알려줍니다.

Sagi-shi notification screen
Sagi-shi notifications

알림 수신하기 #

알림은 서버에서 전송해야 합니다.

Nakama는 알림을 구별하기 위해서 코드를 사용합니다. 0 이하의 코드는 Nakama 내부 직원을 위해 예약된 시스템입니다.

Sagi-shi 플레이어는 알림 수신 이벤트를 구독할 수 있습니다. Sagi-shi는 토너먼트 우승 100 코드를 사용합니다:

1
2
3
4
5
6
7
8
cont reward_code = 100

func _on_notification(p_notification : NakamaAPI.ApiNotification):
    match notification.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)

알림 나열하기 #

Sagi-shi 플레이어는 오프라인 상태였을 때 수신된 알림을 나열할 수 있습니다:

1
2
3
4
5
var limit = 100
var result : NakamaAPI.ApiNotificationList = yield(client.list_notifications_async(session, limit), "completed")

for n in result.notifications:
    print("Notification: %s:{%s\n%s", notification.code, notification.subject, notification.content)

페이지 지정 및 캐시 가능한 커서

다른 목록 방법과 마찬가지로 알림 결과는 결과에서 커서 또는 캐시 가능한 커서를 사용하여 페이지를 지정할 수 있습니다.

1
2
var result : NakamaAPI.ApiNotificationList = yield(client.list_notifications_async(session, 1), "completed")
var cacheable_cursor = result.cacheable_cursor

다음에 플레이어가 로그인하면 캐시 가능한 커서를 읽지 않은 알림을 나열하는 데 사용할 수 있습니다.

1
var next_results = yield(client.list_notifications_async(session, limit, cacheable_cursor)

알림 삭제하기 #

Sagi-shi 플레이어는 알림을 읽은 후에 삭제할 수 있습니다:

1
2
var notification_ids = ["<notification-id>"]
var delete : NakamaAsyncResult = yield(client.delete_notifications_async(session, notification_ids), "completed")