클라이언트 중계 멀티플레이어 #

클라이언트 중계 멀티플레이어라고도 하는 실시간 멀티플레이어를 통해 플레이어 간에 데이터를 신속하게 주고 받습니다. 클라이언트가 대결로 연결되면 서버로 메시지를 전송할 수 있습니다. Nakama의 실시간 멀티플레이어 엔진은 해당 메시지를 대결에 있는 다른 사용자에게 자동으로 전달합니다. 메시지가 수신되면 클라이언트는 메시지를 통해 액션, 업데이트 또는 게임 상태에 대한 변경 사항을 처리합니다.

Nakama는 서버를 대결의 중심에 두는 서버-권한 보유 멀티플레이어도 지원합니다. 모든 클라이언트는 서버와 데이터를 직접 주고 받고, 필요 시 데이터를 처리하여 다른 클라이언트로 전달합니다. 서버-권한 보유 멀티플레이어에서 데이터를 처리하는 방식이 더 유연할 수 있지만, 개발 및 유지가 더 복잡할 수 있습니다.

Pirate Panic의 경우, 실시간 멀티플레이어를 사용합니다.

대결 참여 #

서버는 티켓이 제출되면 매치메이킹을 처리하고, 새로운 대결을 찾았을 때 대결에 참여한 모든 플레이어를 새로운 대결로 지정합니다.

클라이언트 측에서 작동되도록 콜백 함수를 등록하여 장면을 전환하거나 대결 참여를 위해서 게임을 준비할 수 있습니다.

이 경우, 서버가 상대방을 발견한 경우 자동으로 발화되는 ReceivedMatchmakerMatched등록 후크를 사용합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
_connection.Socket.ReceivedMatchmakerMatched += OnMatchmakerMatched;
...
private void OnMatchmakerMatched(IMatchmakerMatched matched)
{
    ...
    _connection.Socket.ReceivedMatchmakerMatched -= OnMatchmakerMatched; // Unregister callback function

    _connection.BattleConnection = new BattleConnection(matched); // Save Matched object to BattleConnection for later use

    SceneManager.LoadScene(GameConfigurationManager.Instance.GameConfiguration.SceneNameBattle); // Switch scene to battle scene
}

대결 화면에서 로드한 경우, 대결에 참여할 때 필요하기 때문에 IMatchmakerMatched 개체를 저장해야 합니다. PiratePanic의 경우, BattleConnection 개체를 생성하여 해당 정보를 저장하고 다양한 클래스에 액세스합니다.

BattleConnection.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class BattleConnection
{
    public string MatchId { get; set; }
    public string HostId { get; set; }
    public string OpponentId { get; set; }
    public IMatchmakerMatched Matched { get; set; }
    // More properties can be stored here!

    public BattleConnection(IMatchmakerMatched matched)
    {
        Matched = matched;
    }
}

대결 장면을 로드한 후에, 매치메이커에서 신원 확인 및 예약 토큰으로 제공되는 매치메이커 토큰을 전달하여 시작 시 JoinMatchAsync을(를) 호출합니다.

이렇게 하면 서버에서 해당 클라이언트가 올바른 대결로 추가되어 다른 플레이어한테 전송되는 메시지가 해당 클라이언트에도 전송되도록 합니다:

1
2
3
4
5
protected async void Start() {
    IMatch match = await _connection.Socket.JoinMatchAsync(_connection.BattleConnection.Matched);
    _connection.BattleConnection.MatchId = match.Id;
    ...
}

플레이어는 matchId이(가) JoinMatchAsync에서 반환되는 ID인 _connection.Socket.LeaveMatchAsync(matchId)을(를) 호출하여 대결을 종료할 수 있습니다.

실시간 데이터 전송 #

플레이어가 대결에 참여한 후에 다른 플레이어에게 정보를 전달합니다. 이 작업은 socket.SendMatchStateAsync() 호출을 통해 처리됩니다.

예를 들어, 게임 도중에 플레이어가 주문을 걸면 다른 플레이어에게 정보(예: 주문 유형, 주문을 건 장소 등)가 전달되어 게임 클라이언트에서 처리됩니다.

MatchMessageSpellActivated.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using Nakama.TinyJson;
...
public class MatchMessageSpellActivated {
    public readonly string OwnerId;
    ... // More message data goes here

    public MatchMessageSpellActivated(string ownerId, ...) {
        OwnerId = ownerId;
        ...
    }
}

그런 다음, 함수 내에서 다음 작업을 수행합니다:

1
2
3
4
long opCode = 5; // custom opcode for spells
MatchMessageSpellActivated message = new MatchMessageSpellActivated(playerId, ...);
string json = JsonWriter.ToJson(message); // Converts C# object to JSON object
_connection.Socket.SendMatchStateAsync(_connection.BattleConnection.MatchId, opCode, json);

여기서는 필요한 정보가 포함되는 MatchMessageSpellActivated(이)라고 하는 새로운 개체를 생성합니다. 이 케이스에서는 JSON과 같은 다른 형식으로 전환될 수 있도록 개체를 표준화해야 합니다. 사용자 지정 클래스를 생성하여 JSON으로 전환하는 것이 모든 유형의 데이터를 전송하는 일반적인 패턴입니다. 원하는 항목을 전송하기 위해서 클래스에 다른 속성을 추가할 수 있습니다.

opCode을(를) 통해 수신 클라이언트는 페이로드의 구조를 살펴보지 않고도 카테고리를 기반으로 메시지를 다르게 처리할 수 있습니다. opcode는 양의 정수이며, Nakama에 의해 어떠한 내재적인 의미도 사전적으로 정의되어 있지 않기 때문에 자체적인 계획을 생성할 수 있습니다.

실시간 데이터 수신 #

대결로 메시지를 전송했기 때문에 대결에 있는 다른 플레이어도 메시지를 받을 수 있어야 합니다. Nakama에 있는 등록 후크 ReceivedMatchState은(는) 메시지가 클라이언트에 수신될 때마다 발화됩니다.

플레이어가 주문을 거는 것과 같은 예시에서 클라이언트에 적용된 주문을 표시하고 피해를 처리하기 위해서 메시지를 수신해야 합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void Start() {
    _connection.Socket.ReceivedMatchState += ReceiveMatchStateMessage; // Bind function to hook
}
...
private void ReceiveMatchStateMessage(IMatchState matchState) {
    string messageJson = System.Text.Encoding.UTF8.GetString(matchState.State);

    if (matchState.opCode == 5) {
        MatchMessageSpellActivated spell = JsonParser.FromJson<MatchMessageSpellActivated>(messageJson);
        // Do stuff with the spell (instantiate object, destroy towers, etc.)
    }
    ...
    // Handle more opcodes, or alternatively create a switch statement
}

여기서는 사용자 지정 함수, ReceiveMatchStateMessage을(를) 생성하고 ReceivedMatchState(으)로 연결하여 메시지가 수신될 때마다 호출될 수 있도록 합니다. 메시지는 matchState.State에 저장되고 opcode 5을(를) 사용하여 지정한 메시지가 MatchMessageSpellActivated 유형이기 때문에 JSON 개체를 C#로 다시 변환하여 속성을 읽을 수 있습니다.

ReceiveMatchStateMessage 함수를 사용하여 각각의 opcodes에 의해 지정된 다양한 메시지 유형을 처리할 수 있습니다.

대결 현재 상태 #

대결에 있는 모든 플레이어는 대결 안에 presence이(가) 있습니다. Nakama는 내부적으로 현재 상태 정보를 대결 로스터로 추적합니다. 플레이어가 게임에 참여하거나 나갈 때마다 업데이트 델타가 클라이언트로 전송되어 클라이언트가 플레이어 목록 보기를 업데이트하거나 게임 내에서 특정한 액션을 취할 수 있습니다.

클라이언트가 최초로 대결에 참여하는 경우, Nakama는 현재 참여 중인 상대방의 목록을 자동으로 제공합니다. 이 목록은 match.Presences 개체를 통해 액세스할 수 있습니다. 참고로, match은(는) JoinMatchAsync에 의해 반환된 IMatch 개체입니다.

또한, ReceivedMatchPresence 후크를 사용하여 플레이어가 참여하거나 나갈 때마다 함수를 실행할 수 있습니다. 참여 목록은 match.Joins, 탈퇴 목록은 match.Leaves을(를) 통해 액세스할 수 있습니다.

예를 들어, 아래의 코드는 플레이어가 경기를 떠났고 다른 플레이어가 남아 있을 때 메시지를 출력합니다:

1
2
3
4
5
6
7
8
9
void Start() {
    _connection.Socket.ReceivedMatchPresence += OnMatchPresence;
}
...
private void OnMatchPresence(IMatchPresenceEvent e) {
    if (e.Leaves.Count() > 0) {
        Debug.LogWarning($"OnMatchPresence() User(s) left the game");
    }
}

또한, 현재 상태 정보를 사용하여 이벤트를 한 번만 처리해야 하는 호스트 플레이어를 지정할 수 있습니다. 예를 들어, 게임을 시작하기 전에 최초 카드를 모든 플레이어에게 배분해야 합니다.

대결에 처음 참여하는 플레이어를 호스트로 지정할 수 있습니다. 대결에서 현재 상태의 수가 0(현재 클라이언트는 현재 상태의 개수에 포함되지 않음)인지 확인하면 됩니다:

1
2
3
4
5
6
7
8
9
if (match.Presences.Count() == 0) {
    _connection.BattleConnection.HostId = _connection.Session.UserId;
    ...
} else {
    string opponentId = match.Presences.First().UserId;
    _connection.BattleConnection.OpponentId = opponentId;
    _connection.BattleConnection.HostId = opponentId;
    ...
}

그런 다음, 호스트의 고유한 액션을 취하기 위해서 호스트 ID가 현재 사용자의 ID와 일치하는지 확인할 수 있습니다:

1
2
3
if (_connection.BattleConnection.HostId == _connection.Session.UserId) {
    // Handle host-only behaviors
}

RPC 사용 #

민감한 계산은 호스트가 실행하도록 하지 않고 서버에서 삭제하기를 원할 수도 있습니다. 예를 들어, 대결이 종료되면 승자에게 보석을 제공하기를 원할 수도 있습니다.

Pirate Panic의 경우, 플레이어의 요새가 파괴되면 서버에 메시지를 전송하여 대결을 종료합니다:

1
2
3
4
private async void OnAfterMainFortDestroyed() {
    ...
    response = await _connection.Client.RpcAsync(_connection.Session, "handle_match_end", matchEndRequest.ToJson());
}

여기서, matchEndRequest은(는) 서버에 전송할 정보를 포함하는 모든 개체입니다. RPC를 통해 전송할 수 있도록 JSON으로 변환한 다음 Typescript의 서버 측으로 디코딩할 수 있습니다:

1
2
3
4
const rpcHandleMatchEnd: nkruntime.RpcFunction = function(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
    let request : MatchEndRequest = JSON.parse(payload);
    ...
}

RPC에서 이러한 행위에 대한 예시는 순위표 섹션을 참조하십시오.

다음 주제 #

순위표

Related Pages