Nakama JavaScript 클라이언트 가이드 #

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

Sagi-shi gameplay screen
Sagi-shi gameplay

필수 조건 #

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

전체 API 문서 #

전체 API 문서에 대한 자세한 내용은 API 문서를 확인하시기 바랍니다.

설치 #

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

NPM 또는 Yarn을 사용하는 경우 package.json 파일에 종속성만 추가하면 됩니다:

1
2
yarn add "@heroiclabs/nakama-js"
yarn install

클라이언트를 설치한 후 프로젝트로 가져옵니다:

1
import {Client} from "@heroiclabs/nakama-js"

기본 JavaScript 함수에서 클라이언트 객체를 만듭니다.

업데이트 #

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

비동기 프로그래밍 #

JavaScript SDK에서 사용할 수 있는 Nakama API의 많은 메서드는 비동기식 및 비차단식입니다.

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

1
await client.authenticateDevice("<deviceId>");

async 함수await 연산자 참조.

예외 처리 #

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

Sagi-shi의 API 호출은 오류를 정상적으로 처리하기 위해 try 블록과 catch 절로 둘러싸여 있습니다:

1
2
3
4
5
6
try {
    await client.authenticateDevice("<deviceId>");
}
catch (err) {
    console.log("Error authenticating device: %o:%o", err.statusCode, err.message);
}

시작하기 #

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

Nakama 클라이언트 #

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

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

1
var client = new nakamajs.Client("defaultkey", "127.0.0.1", 7350);

요청 제한 시간 구성하기 #

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

1
client.timeout = 10000;

Nakama 소켓 #

Nakama 소켓은 채팅, 파티, 대결 및 RPC와 같은 게임 플레이 및 실시간 대기 시간에 민감한 기능에 사용됩니다.

클라이언트에서 소켓을 생성합니다:

1
2
3
4
const socket = client.createSocket();

var appearOnline = true;
await socket.connect(session, appearOnline);

인증 #

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
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// This import is only required with React Native
var deviceInfo = require('react-native-device-info');

var deviceId = null;
// If the user's device ID is already stored, grab that - alternatively get the System's unique device identifier.
try {
  const value = await AsyncStorage.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.
var create = true;
const session = await client.authenticateDevice(deviceId, create, "mycustomusername");
console.info("Successfully authenticated:", session);

Facebook 인증 #

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

Nakama Facebook 인증을 사용하려면 Unity용 Facebook SDK(외부)를 설치하십시오.

1
2
3
4
5
6
7
8
9
const oauthToken = "<token>";
const importFriends = true;
try {
    const session = await client.authenticateFacebook(oauthToken, true, "mycustomusername", importFriends);
    console.log("Successfully authenticated:", session);
}
catch(err) {
    console.log("Error authenticating with Facebook: %o", err.message);
}

사용자 지정 인증 #

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

예시는 Itch.io 사용자 지정 인증 레시피를 참조하십시오.

연결 인증 #

사용자 인증을 완료한 경우, 사용자는 계정에서 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.
var deviceId = "<uniqueDeviceId>";

// Link Device Authentication to existing player account.
try {
    await client.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
const oauthToken = "<token>";
const import = true;
try {
    const session = await client.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);
}

세션 변수 #

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

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

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

1
2
3
4
5
6
7
8
const vars = {
  deviceId = localStorage.getItem("deviceId"),
  deviceOs = localStorage.getItem("deviceOs"),
  inviteUserId = "<someUserId>",
  // ...
}

const session = await client.authenticateDevice(deviceId, null, true, vars);

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

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

세션 수명 주기 #

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

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

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

나중에 사용하기 위해서 토큰을 저장합니다:

1
2
var authToken = session.token;
var refreshToken = session.refresh_token;

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

1
session = session.restore(authToken, refreshToken);

세션이 만료되었거나 만료가 임박했는지 확인하고 새로 고침을 사용합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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 = await client.sessionRefresh(session);
    } catch (error) {
        // Couldn't refresh the session so reauthenticate.
        session = await client.authenticateDevice(deviceId);
        var refreshToken = session.refresh_token;
    }

    var authToken = session.token;
}

자동 세션 새로 고침 #

JavaScript 클라이언트 라이브러리에는 만료가 임박한 세션을 자동으로 새로 고치는 기능이 포함되어 있습니다.

이 기능은 기본적으로 활성화되어 있지만 다음 매개변수를 사용하여 Nakama 클라이언트를 처음 생성할 때 구성할 수 있습니다:

  • autoRefreshSession - 이 기능이 true기본적으로 활성화되어 있는지 여부를 나타내는 부울 값
  • expiredTimespanMs - 자동 새로고침이 발생하는 세션 만료까지의 시간, 기본값은 300000(5분)(으)로 설정

세션 종료하기 #

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

1
await client.sessionLogout(session);

사용자 계정 #

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

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

Sagi-shi player profile screen
Player profile

사용자 계정 만들기 #

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

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

1
2
3
4
5
const account = await client.getAccount(session);
const user = account.user;
var username = user.username;
var avatarUrl = user.avatarUrl;
var userId = user.id;

사용자 계정 업데이트 #

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

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

1
2
3
4
5
6
7
var newUsername = "NotTheImp0ster";
var newDisplayName = "Innocent Dave";
var newAvatarUrl = "https://example.com/imposter.png";
var newLangTag = "en";
var newLocation = "Edinburgh";
var newTimezone = "BST";
await client.updateAccount(session, newUsername, newDisplayName, newAvatarUrl, newLangTag, newLocation, newTimezone);

사용자 가져오기 #

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

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

1
var users = await client.getUsers(session, ["<AnotherUserId>"]);

메타데이터 저장 #

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

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

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

메타데이터 읽기 #

업데이트된 계정 객체를 가져오고 JSON 메타데이터를 구문 분석합니다:

1
2
3
4
5
6
7
8
9
// Get the updated account object.
var account = await client.getAccount(session);

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

console.log("Title: %o", metadata.title);
console.log("Hat: %o", metadata.hat);
console.log("Skin: %o", metadata.skin);

지갑 #

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

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

지갑에 액세스하기 #

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

1
2
3
4
5
6
7
var account = await client.getAccount(session);
var wallet = JSON.parse(account.wallet);
var keys = wallet.keys;

keys.forEach(function(currency) {
    console.log("%o: %o", currency, wallet[currency].toString())
});

지갑 업데이트하기 #

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

인앱 구매 확인 #

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

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

저장소 엔진 #

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

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

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

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

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

Sagi-shi player items screen
Player items

저장소 객체 읽기 #

모음 이름, 키 및 사용자 ID를 사용하여 새 저장소 객체 ID를 만듭니다. 저장소 객체를 읽고 JSON 데이터를 구문 분석합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var readObjectId = new storageObjectId {
    collection = "Unlocks",
    key = "Hats",
    userId = session.user.id
};

var result = await client.readStorageObjects(session, readObjectId);

if (result.objects.any())
{
    var storageObject = result.objects.first();
    var unlockedHats = JSON.parse(storageObject.value);
    console.log("Unlocked hats: %o", string.join(",", unlockedHats.Hats));
}

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

저장소 개체 작성하기 #

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

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

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

컬렉션 이름, 키, JSON 인코딩 데이터를 사용하여 저장소 개체 작성하기를 생성합니다. 마지막으로, 저장소 엔진에 저장소 객체를 작성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var favoriteHats = new {
    hats = ["cowboy", "alien"]
};

var writeObject = new WriteStorageObject {
    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
};

await client.writeStorageObjects(session, writeObject);

WriteStorageObjectsAsync 메서드에 여러 개의 객체를 전달할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var writeObjects = {
    new WriteStorageObject {
        //...
    },
    new WriteStorageObject
    {
        // ...
    }
};

await client.writeStorageObjects(session, writeObjects);

조건부 작성 #

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

이렇게 하면 데이터 덮어쓰기를 방지할 수 있습니다. 예를 들어, 플레이어가 마지막으로 액세스한 이후에 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)
var writeObject = new WriteStorageObject {
    collection = storageObject.collection,
    key = storageObject.key,
    value = "<NewJSONValue>",
    permissionWrite = 0,
    permissionRead = 1,
    version = storageObject.version
};

try {
    await client.writeStorageObjects(session, writeObjects);
}
catch (error) {
    console.log(error.message);
}

저장소 객체 목록 만들기 #

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var limit = 3;
var cursor = null;
var unlocksObjectList = await client.listStorageObjects(session, "Unlocks", limit, cursor);

unlocksObjectList.objects.forEach(function(unlockStorageObject) {
    switch(unlockStorageObject.key) {
        case "Titles":
            var unlockedTitles = JSON.parse<TitlesStorageObject>(unlockStorageObject.value);
            // Display the unlocked titles
            break;
        case "Hats":
            var unlockedHats = JSON.parse<HatsStorageObject>(unlockStorageObject.value);
            // Display the unlocked hats
            break;
        case "Skins":
            var unlockedSkins = JSON.parse<SkinsStorageObject>(unlockStorageObject.value);
            // Display the unlocked skins
            break;
    }
});

페이지 지정 결과 #

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

예:

  • 커서의 값이 5인 경우, 다섯 번째 객체에서 결과를 얻습니다.
  • 커서가 null인 경우, 첫 번째 객체에서 결과를 얻습니다.
1
objectList = await client.listStorageObjects(session, "<CollectionName>", limit, objectList.cursor);

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

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

원격 프로시저 호출 #

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

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

서버 로직 생성하기 #

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

클라이언트 RPC #

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

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

1
2
3
4
5
6
7
8
try {
    var payload = { "item": "cowboy"};
    var response = await client.rpc(session, "EquipHat", payload);
    console.log("New hat equipped successfully", response);
}
catch (error) {
    console.log("Error: %o", error.message);
}

소켓 RPC #

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

1
var response = await socket.rpc("<rpcId>", "<payloadString>");

친구 #

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

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

Sagi-shi Friends screen
Friends screen

친구 추가하기 #

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

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

1
2
3
4
5
6
7
// Add friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.addFriends(session, usernames);

// Add friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.addFriends(session, ids);

친구 관계 상태 #

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

상태
0서로 아는 친구
1수락 대기 중인 친구 요청 발신
2수락 대기 중인 친구 요청 수신
4금지됨

친구 목록 만들기 #

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

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

1
2
3
4
5
6
7
var limit = 20; // Limit is capped at 1000
var friendshipState = 0;
var result = await client.listFriends(session, friendshipState, limit, cursor: null);

result.forEach((friend) => {
    console.log("ID: %o", friend.user.id);
});

친구 요청 수락하기 #

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

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

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

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

1
2
3
4
5
6
var limit = 1000;
var result = await client.listFriends(session, 2, limit, cursor: null);

result.forEach((friend) => {
    await client.addFriends(session, friend.user.id);
});

친구 삭제하기 #

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

1
2
3
4
5
6
7
// Delete friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.deleteFriends(session, ids});

// Delete friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.deleteFriends(session, null, usernames});

사용자 차단하기 #

Sagi-shi 플레이어는 사용자 이름이나 사용자 ID를 통해 다른 사용자를 차단할 수 있습니다:

1
2
3
4
5
6
7
// Block friends by User ID.
var ids = ["<SomeUserId>", "<AnotherUserId>"];
await client.blockFriends(session, ids);

// Block friends by Username.
var usernames = ["AlwaysTheImposter21", "SneakyBoi"];
await client.blockFriends(session, usernames);

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

차단된 사용자는 친구 목록 보기와 같이 나열할 수 있지만 해당 친구 관계 상태(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
// 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.
var friendsResult = await client.listFriends(session, 0);
var friendIds = [];
friendsResult.friends.forEach(function(friend) {
    friendIds.push(friend.user.id);
});
var result = await socket.followUsers(friendIds);

result.presences.forEach(function(presence){
    console.log("%o is online with status: %o", presence.username, presence.status);
});

사용자 팔로우 취소하기 #

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

1
await socket.unfollowUsers(["<UserId>"]);

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

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

1
await socket.updateStatus("Viewing the Main Menu");

그룹 #

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

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

Sagi-shi groups screen
Groups list screen

그룹 생성하기 #

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

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

1
2
3
4
5
6
7
8
9
const groupName = "Imposters R Us";
const description = "A group for people who love playing the imposter.";

const group = await client.createGroup(session {
    name: groupName,
    description: description,
    open: true, // public group
    maxSize = 100
});

그룹 표시 유형 업데이트 #

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

1
2
3
4
const groupId = "<groupId>";
await client.updateGroup(session, groupId, {
    open: false
});

그룹 크기 업데이트 #

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

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

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 = await client.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.
var nextResults = await client.listGroups(session, name: "imposter%", limit, result.cursor);

그룹 삭제 #

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

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

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

1
2
const groupId = "<groupId>";
await client.deleteGroup(session, groupId);

그룹 메타데이터 #

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const payload = new {
    groupId = "<GroupId>",
    interests = ["Deception", "Sabotage", "Cute Furry Bunnies"],
    activeTimes = ["9am-2pm Weekdays", "9am-10pm Weekends"],
    languages = ["English", "German"]
};

try {
    var result = await client.rpc(session, "UpdateGroupMetadata", JSON.stringify(payload));
    console.log("Successfully updated group metadata");
}
catch (error) {
    console.log("Error: %o", error.message);
}

그룹 구성원 상태 #

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

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

그룹에 가입하기 #

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

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

1
2
const group_id = "<group id>";
await client.joinGroup(session, group_id);

사용자 그룹 목록 만들기 #

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

1
2
3
4
5
const userId = "<user id>";
const groups = await client.listUserGroups(session, userId);
groups.user_groups.forEach(function(userGroup){
  console.log("Group: name '%o' State: '%o'.", userGroup.group.name, userGroup.state);
});

구성원 목록 만들기 #

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

1
2
3
4
5
const groupId = "<group id>";
const groups = await client.listUserGroups(session, groupId);
groups.group_users.forEach(function(groupUser){
  console.log("User: ID '%o' State: '%o'.", groupUser.user.id, groupUser.state);
});

가입 요청 수락하기 #

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

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

1
2
3
4
5
const groupId = "<group id>";
const result = await client.listGroupUsers(session, groupId);
groups.group_users.forEach(function(groupUser){
    await client.addGroupUsers(session, groupId, [groupUser.user.id]);
});

구성원 승격하기 #

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

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

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

  • 구성원이 승격되면 관리자가 됩니다.
  • 관리자가 승격되면 총괄 관리자가 됩니다.
1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.promoteGroupUsers(session, groupId, [userId]);

구성원 강등하기 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.demoteGroupUsers(session, groupId, [userId]);

구성원 추방하기 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.kickGroupUsers(session, groupId, [userId]);

구성원 차단하기 #

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

1
2
3
const groupId = "<group id>";
const userId = "<user id>";
await client.banGroupUsers(session, groupId, [userId]);

그룹 탈퇴하기 #

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

1
2
const groupId = "<group id>";
await client.leaveGroup(session, groupId);

채팅 #

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

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

Sagi-shi chat screen
Sagi-shi Chat

동적 채팅 참여하기 #

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

1
2
3
4
5
6
7
const roomName = "<match id>";
const persistence = false;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.joinChat(1, roomName, persistence, hidden);

console.log("Connected to dynamic room channel: %o", channel.id);

그룹 채팅 참여하기 #

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

1
2
3
4
5
6
7
const groupId = "<group id>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.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
const userId = "<user id>";
const persistence = true;
const hidden = false;
// 1 = Room, 2 = Direct Message, 3 = Group
const channel = await socket.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
var channelId = "<channel id>";
var data = { "message": "I think Red is the imposter!" };
const messageAck = await socket.writeChatMessage(channelId, data);

var emoteData = {
    "emote": "point",
    "emoteTarget": "<redPlayerUserId>"
}
const emoteMessageAck = await socket.writeChatMessage(channelId, emoteData);

메시지 기록 나열하기 #

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

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

1
2
3
4
5
6
7
8
const groupId = "<group id>";
const limit = 100;
const forward = true;

const result = await client.listChannelMessages(session, groupId, limit, forward, cursor: null);
result.messages.forEach((message) => {
  console.log("%o: %o", message.username, message.data);
});

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

1
2
const cursor = result.cacheable_cursor;
const nextResults = await client.listChannelMessages(session, groupId, limit, forward, cursor);

메시지 업데이트 #

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

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

1
2
3
var channelId = "<ChannelId>";
var messageData = {"message": "I think Red is the imposter!" };
const messageSendAck = await socket.writeChatMessage(channelId, messageData);

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

1
2
var newMessageData = {"message": "I think BLUE is the imposter!" };
const messageUpdateAck = await socket.updateChatMessage(channelId, messageSendAck.message.id, newMessageData));

대결 #

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

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

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

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

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

대결 생성하기 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var match = await socket.createMatch();
var friendsList = await client.listFriends(session);
var onlineFriends = [];
friendsList.friends.forEach((friend){
    if (friend.user.online){
        onlineFriends.push(friend.user);
    }
});

onlineFriends.friend.forEach(function(friend){
    var messageData = {"message": "Hey %o, join me for a match!", friends.username},
    var matchId = match.id,
    const channel = await socket.joinChat(2, friend.id),
    const messageAck = await socket.writeChatMessage(channel, messageData)
});

대결 참여하기 #

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

1
2
var matchId = "<MatchId>";
var match = await socket.joinMatch(matchId);

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

1
2
3
4
5
6
7
8
9
socket.onmatchmakermatched = async (matchmakerMatched) => {
    var match = await socket.joinMatch(matchmakerMatched);
};

var minPlayers = 2;
var maxPlayers = 10;
var query = "";

var matchmakingTicket = await socket.addMatchmaker(query, minPlayers, maxPlayers);

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

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

1
2
3
4
5
6
var status = {
    "Status": "Playing a match",
    "MatchId": "<MatchId>"
};

await socket.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){
        var status = JSON.parse(presence.status),
        if (status.hasOwnProperty("MatchId")) {
            await socket.joinMatch(status["MatchId"]);
            break;
        }
    });

대결 나열하기 #

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var minPlayers = 2;
var maxPlayers = 10;
var limit = 10;
var authoritative = true;
var label = "";
var query = "";
const result = await client.listMatches(session, minPlayers, maxPlayers, limit, authoritative, label, query);

result.matches.forEach(function(match){
    console.log("%o: %o/10 players", match.id, match.size);
});

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

1
var label = "AnExactMatchLabel";

고급:

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

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

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

플레이어 생성 #

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

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

1
2
3
4
5
6
7
8
var match = await socket.joinMatch(matchId);

var players = {};

match.presences.forEach(function(presence){
    var go = 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.
        var go = // 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"){
            const index = players.session.id;
            if (index > -1) {
                players.splice(index, 1);
            }
        })
    })
};

대결 상태 전송하기 #

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

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

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

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

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

플레이어 위치 전송

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

1
2
3
4
5
class PositionState {
    static X;
    static Y;
    static Z;
}

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

1
2
3
4
5
6
7
8
9
var state = new PositionState {
    x = transform.position.x,
    y = transform.position.y,
    z = transform.position.z
};

var opCode = 1;

await socket.sendMatchState(match.Id, opCode, JSON.stringify(state));

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

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

1
2
3
4
5
6
class OpCodes {
    static position = 1;
    static vote = 2;
}

await socket.sendMatchState(match.Id, OpCodes.position, JSON.stringify(state));

대결 상태 수신하기 #

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) {
        case opCodes.position:
            // Get the updated position data
            var stateJson = matchState.state;
            var positionState = 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 = new Vector3(positionState.s, positionState.y, positionState.z);
            }
            break;
        default:
            console.log("Unsupported op code");
            break;
    }
};

매치 메이커 #

개발자는 대결 목록 또는 Nakama 매치 메이커를 사용하여 플레이어의 대결을 찾을 수 있습니다. 이를 통해 플레이어는 실시간 매치메이킹 풀에 참여하고 지정된 기준과 일치하는 다른 플레이어와 대결이 성사될 때 알림을 받을 수 있습니다.

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

매치 메이커 추가 #

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

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

1
2
3
4
5
6
var minPlayers = 2;
var maxPlayers = 10;
var query = "+skill:>100 mode:sabotage";
var stringProperties = { "mode": "sabotage"};
var numericProperties = { "skill": 125};
var matchmakerTicket = await socket.addMatchmaker(query, minPlayers, maxPlayers, stringProperties, numericProperties);

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

1
2
3
4
socket.onmatchmakermatched = (matched) => {
  const matchId = null;
  socket.joinMatch(matchId, matched.token);
};

파티 #

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

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

파티 생성하기 #

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

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

1
2
3
var open = false;
var maxPlayers = 4;
const party = await socket.createParty(open, maxPlayers);

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var friendsList = await client.listFriends(session);
var onlineFriends = [];
friendsList.friends.forEach((friend){
    if (friend.user.online){
        onlineFriends.push(friend.user);
    }
});

onlineFriends.friend.forEach(function(friend){
    var messageData = {"message": "Hey %o, wanna join the party?", friends.username};
    var partyId = party.id;
    const channel = await socket.joinChat(2, friend.id);
    const messageAck = await socket.writeChatMessage(channel, messageData);
});

파티 참여하기 #

Safi-shi 플레이어는 메시지에서 파티 ID를 확인하여 채팅 메시지에 있는 파티에 참여할 수 있습니다:

1
2
3
4
5
6
socket.onchannelmessage = async (m) => {
    var content = JSON.parse(m.content);
    if (content.hasOwnProperty("partyId")) {
        await socket.joinParty(content["partyId"]);
    }
};

구성원 승격하기 #

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

1
2
var newLeader = "<user id>";
await socket.promotePartyMember(party.Id, newLeader);

파티 탈퇴하기 #

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

1
await socket.leaveParty(party.Id);

파티로 매치메이킹 #

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

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

1
2
3
socket.onmatchmakermatched = async (matchmakerMatched) => {
    await socket.joinMatch(matchmakerMatched.match.id);
};

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

1
2
3
4
5
var partyId = "<party id>";
var minPlayers = 2;
var maxPlayers = 10;
var query = "";
var matchmakerTicket = await socket.addMatchmakerParty(partyId, query, minPlayers, maxPlayers);

순위표 #

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"};
await client.writeLeaderboardRecord(session, "weekly_imposter_wins", score, subscore, JSON.stringify(metadata));

상위 레코드 나열하기 #

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

1
2
3
4
5
6
7
var limit = 20;
var leaderboardName = "weekly_imposter_wins";
const result = await client.listLeaderboardRecords(session, leaderboardName, ownerIds: null, expiry: null, limit, cursor: null);

result.records.forEach(fuction(record){
    console.log("%o:%o", record.owner.id, record.score);
});

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

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

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

1
2
3
4
5
6
7
8
var userId = session.user.id;
var limit = 20;
var leaderboardName = "weekly_imposter_wins";
var result = await client.listLeaderboardRecordsAroundOwner(session, leaderboardName, userId, expiry: null, limit);

result.records.forEach(fuction(record){
    console.log("%o:%o", record.owner.id, record.score);
});

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var friendsList = await client.ListFriendsAsync(session);
var userIds = [];
friendsList.friends.forEach(function(friend){
    userIds.push(friend.user.id);
});
var recordList = await client.listLeaderboardRecords(session, "weekly_imposter_wins", userIds, expiry: null, 100, cursor: null);

recordList.records.forEach(fuction(record){
    console.log("%o:%o", record.username, record.score);
});

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var groupId = "<group id>";
var groupUserList = await client.listGroupUsers(session, groupId);
var userIds = [];
groupUserList.forEach(function(group_user){
    if (group_user.state < 3){
        userIds.push(group_user.id);
    }
});

var recordList = await client.listLeaderboardRecords(session, "weekly_imposter_wins", userIds, expiry: null, 100, cursor: null);
recordList.records.forEach(fuction(record){
    console.log("%o:%o", record.username, record.score);
});

레코드 삭제하기 #

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

1
2
var leaderboardId = "<leaderboard id>";
await client.deleteLeaderboardRecord(session, leaderboardId);

토너먼트 #

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

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

Sagi-shi tournaments screen
Sagi-shi Tournaments

토너먼트 생성하기 #

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

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

토너먼트 참여하기 #

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

1
2
var id = "<tournament id>";
await await client.joinTournament(session, id);

토너먼트 나열하기 #

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var categoryStart = 1;
var categoryEnd = 2;
var startTime = 1538147711;
var endTime = null; // all tournaments from the start time
var limit = 100; // number to list per page
var cursor = null;
var result = await client.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 토너먼트 등).

레코드 나열하기 #

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

1
2
3
4
5
6
var tournamentName = "weekly_top_detective";
var limit = 20;
var result = await client.listTournamentRecords(session, tournamentName, limit);
result.records.forEach(function(record) {
  console.log("%o:%o", record.owner.id, record.score);
});

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

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

1
2
3
4
5
6
7
var userId = "<user id>";
var limit = 20;
var tournamentName = "weekly_top_detective";
var result = await client.listTournamentRecordsAroundOwner(session, tournamentName, userId, limit);
result.records.forEach(function(record) {
  console.log("%o:%o", record.owner.id, record.score);
});

점수 제출하기 #

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

1
2
3
4
5
var tournamentName = "weekly_top_detective";
var score = 1;
var subscore = 0;
var metadata = { "map": "space_station"};
await client.writeTournamentRecord(session, tournamentName, score, subscore, metadata);

알림 #

게임 서버에서 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
 9
10
11
socket.onnotification = (notification) => {
    const rewardCode = 100;
    switch (notification.code) {
        case rewardCode:
            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;
    }
};

알림 나열하기 #

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

1
2
3
4
5
const result = await client.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);
1
2
3
4
5
6
var limit = 100;
var cacheableCursor = null;
var result = await client.listNotifications(session, limit, cacheableCursor);
result.notification.forEach(function(notification) {
    console.log("Notification: %o:%o\n%o", notification.code, notification.subject, notification.content)
});

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

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

1
const cacheableCursor = result.cacheable_cursor;

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

1
var nextResults = await client.listNotifications(session, limit, cacheableCursor);

알림 삭제하기 #

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

1
2
var notificationId = "<notification id>";
await client.deleteNotifications(session, [notificationId]);