# 권한 있는 멀티플레이어

**URL:** https://heroiclabs.com/docs/kr/nakama/concepts/multiplayer/authoritative/
**Summary:** 권한 보유 멀티플레이어는 게임 서버에서 관리하는 중앙 상태에 따라 달라지는 게임플레이에 적합합니다. Nakama의 권한 보유 멀티플레이어 엔진에는 고정된 틱 속도로 사용자 지정 대결 로직을 실행하는 방법이 사용됩니다. 메시지의 유효성이 검사되고 상태 변경 사항이 연결된 피어로 브로드캐스트될 수 있습니다.

---


# 권한 보유 멀티플레이어

[중계 멀티플레이어](../relayed/) 외에도 Nakama는 서버 권한 보류 멀티플레이어 모델도 지원하므로 게임에 가장 적합한 접근 방식을 자유롭게 결정할 수 있습니다.

서버 권한 보유 멀티플레이어에서 교환되는 모든 게임 플레이 데이터는 서버에서 검증되고 브로드캐스트됩니다. 이 모델에서는 Nakama에 의해 적용될 게임플레이 규칙에 대한 사용자 지정 서버 런타임 코드를 작성합니다(즉, 몇 명의 플레이어가 가입할 수 있는지, 진행 중인 대결에 가입할 수 있는지 여부 등).

다른 것보다 중계 또는 권한 보유 접근 방식을 필요로 하는 강력한 결정 요소는 없으며 원하는 게임 플레이를 기반으로 한 디자인에 따른 결정입니다. 권한 보유 멀티플레이어는 게임 서버에서 관리하는 중앙 상태에 의존하는 게임 플레이, 대결당 플레이어 수가 더 많은 게임 플레이, 게임 클라이언트를 신뢰하지 않고 대신 게임 플레이 규칙을 더 엄격하게 제어하여 부정 행위를 최소화하려는 경우 등에 더 적합합니다.

서버에서 유지 관리되는 상태를 변경하기 위해 데이터 메시지를 필요로 하는 멀티플레이어 게임 디자인을 지원하기 위해 권한 보유 멀티플레이어 엔진을 사용하면 고정된 틱 속도로 사용자 지정 대결 로직을 실행할 수 있습니다. 메시지의 유효성이 검사되고 상태 변경 사항이 연결된 피어로 브로드캐스트될 수 있습니다. 이를 통해 다음을 만들 수 있음:

1. **비동기식 실시간 권위 보유 멀티플레이어**: 빠르게 진행되는 실시간 멀티플레이어. 메시지는 서버로 전송되고, 서버는 환경과 플레이어의 변경 사항을 계산하고, 데이터는 관련 피어에게 브로드캐스트됩니다. 일반적으로 게임 플레이가 응답성을 느끼려면 높은 틱 속도가 필요합니다.
2. **능동 차례 기반 멀티플레이어**: 두 명 이상의 플레이어가 연결되어 빠른 차례 기반 대결을 수행하는 Stormbound 또는 Clash Royale을 예시로 들 수 있습니다. 플레이어는 즉시 차례에 응답해야 합니다. 서버는 입력을 수신하여 유효성을 검사하고 플레이어에게 브로드캐스트합니다. 주고 받는 메시지의 속도가 낮기 때문에 예상되는 틱 속도는 상당히 낮습니다.
3. **수동 차례 기반 멀티플레이어**: 게임 플레이가 몇 시간에서 몇 주에 걸쳐 진행될 수 있는 모바일에서 수행하는 Words With Friends를 예시로 들 수 있습니다. 서버는 입력을 수신하고 입력을 검증하고 입력을 데이터베이스에 저장하고 다음 게임 플레이 시퀀스까지 서버 루프를 종료하기 전에 연결된 모든 피어에 변경 사항을 브로드캐스트합니다.
4. **세션 기반 멀티플레이어**: 실제 수행을 서버 측에서 실행하려는 복잡한 게임플레이용(예: Unity 헤드리스 인스턴스). Nakama는 오케스트레이션 계층을 통해 이러한 헤드리스 인스턴스를 관리할 수 있으며 [매치 메이킹](../matchmaker/), 대결 완료 시 플레이어 이동, 대결 결과 보고에 사용할 수 있습니다.

서버에서 권한 보유 멀티플레이어 게임을 구축할 때 즉시 사용 가능한 일반적인 시나리오는 없으므로 주의합니다. 사용자 지정 [런타임 코드](../../../server-framework/)를 작성하여 게임 플레이(대결 당 플레이어 수, 진행 중 가입 허용 여부, 대결 종료 방법 등)를 정의해야 합니다.

권한 보유 멀티플레이어 기능을 구현하기로 결정할 때 주의해야 할 몇 가지 개념이 있습니다.

## 대결 핸들러

대결 핸들러는 게임 입력을 처리하고 이를 작동하기 위해 함께 그룹화된 모든 서버 측 기능을 나타냅니다. 이를 대결이 인스턴스화되는 "청사진"으로 생각하십시오. 대결 핸들러는 대결에 대한 게임 플레이 규칙을 설정하며, 게임에는 여러 플레이 모드(예: 깃발 뺏기, 데스매치, 모두 무료 등)가 있을 수 있으므로 각 게임 모드에 하나씩 대결 핸들러가 여러 개 필요할 수 있습니다.

모든 대결 핸들러에는 7가지 기능이 필요합니다. 이러한 기능은 Nakama에서만 호출되며 클라이언트 또는 다른 런타임 코드에서 직접 호출**할 수 없습니다**.

* 대결 초기화
* 대결 가입 시도
* 대결 가입
* 대결 종료
* 대결 루프
* 대결 완료
* 대결 신호

자세한 내용은 [대결 핸들러](../../../server-framework/typescript-runtime/function-reference/match-handler/) 및 [대결 런타임](../../../server-framework/typescript-runtime/function-reference/match-runtime/) 함수 참조를 참조하세요.

이러한 함수들에 의해 하드웨어 및 플레이어 수에 따라 수천 개의 대결을 실행할 수 있는 단일 Nakama 노드로 주어진 대결의 상태와 수명 주기가 정의됩니다. 대결 핸들러와 주어진 대결의 상태는 특정 Nakama 인스턴스에 저장되며 해당 인스턴스는 해당 대결의 _호스트_가 됩니다.

단일 노드이므로 최고 수준의 상태 액세스 및 업데이트 일관성이 보장되고 분산 상태 조정의 잠재적 지연이 방지됩니다.

{{< note "important" "Nakama 회사만" >}}
대결 현재 상태는 복제되므로 클러스터의 모든 노드는 대결 목록과 대결 참가자에 대한 세부 정보 모두에 즉시 액세스할 수 있습니다. 노드 간의 균형 조정은 자동으로 수행되며 가장 적절한 노드에서 새로운 대결이 생성되고 종료된 노드에서는 생성되지 않습니다.

Nakama Open-Source에서 Nakama Enterprise로 원활하게 마이그레이션할 수 있으며 클라이언트 또는 서버 측 코드의 변경은 필요하지 않습니다. 현재 상태 복제, 클러스터 간 데이터 교환 및 메시지 라우팅은 모두 단일 인스턴스 클러스터에서 작동하는 것처럼 대결 핸들러에 투명하게 발생합니다.
{{< /note >}}

실행 중인 모든 대결은 자체 포함되며 다른 대결과 통신하거나 다른 대결에 영향을 미칠 수 없습니다. 대결과의 통신은 대결 데이터를 보내는 클라이언트를 **통해서만** 이루어집니다. Nakama는 각 대결에 대한 CPU 일정 및 메모리 할당을 내부적으로 관리하여 단일 인스턴스 또는 클러스터의 모든 인스턴스에 대해 공정하고 균형 잡힌 부하 분산을 보장합니다.

{{< note "중ㅇ" >}}
대결 신호 기능을 사용하여 제한된 방식으로 이를 수행할 수 있습니다: 지정된 플레이어를 위해 대결에서 한 자리를 예약하거나 플레이어/데이터를 다른 대결로 넘겨줍니다. 이는 표준 사례가 아닌 **드문 예외**로만 사용해야 합니다.
{{< /note >}}

대결 핸들러는 연결되거나 활성화된 현재 상태가 없는 경우에도 실행됩니다. 대결 런타임 로직에서 유휴 또는 빈 대결 처리를 고려해야 합니다.

{{< code type="server" >}}
```go
type LobbyMatch struct{}

type LobbyMatchState struct {
	presences  map[string]runtime.Presence
	emptyTicks int
}

func (m *LobbyMatch) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
	state := &LobbyMatchState{
		emptyTicks: 0,
		presences:  map[string]runtime.Presence{},
	}
	tickRate := 1 // 1 tick per second = 1 MatchLoop func invocations per second
	label := ""
	return state, tickRate, label
}

func (m *LobbyMatch) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	lobbyState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid lobby state object")
		return nil
	}

	for i := 0; i < len(presences); i++ {
		lobbyState.presences[presences[i].GetSessionId()] = presences[i]
	}

	return lobbyState
}

func (m *LobbyMatch) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
	lobbyState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid lobby state object")
		return nil
	}

	for i := 0; i < len(presences); i++ {
		delete(lobbyState.presences, presences[i].GetSessionId())
	}

	return lobbyState
}

func (m *LobbyMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
	lobbyState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid lobby state object")
		return nil
	}

	// If we have no presences in the match according to the match state, increment the empty ticks count
	if len(lobbyState.presences) == 0 {
		lobbyState.emptyTicks++
	}

	// If the match has been empty for more than 100 ticks, end the match by returning nil
	if lobbyState.emptyTicks > 100 {
		return nil
	}

	return lobbyState
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
const matchInit = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, params: {[key: string]: string}): {state: nkruntime.MatchState, tickRate: number, label: string} {
  return {
    state: { presences: {}, emptyTicks: 0 },
    tickRate: 1, // 1 tick per second = 1 MatchLoop func invocations per second
    label: ''
  };
};

const matchJoin = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
  presences.forEach(function (p) { 
    state.presences[p.sessionId] = p;
  });

  return {
    state
  };
}

const matchLeave = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
  presences.forEach(function (p) {
    delete(state.presences[p.sessionId]);
  });

  return {
    state
  };
}

const matchLoop = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, messages: nkruntime.MatchMessage[]) : { state: nkruntime.MatchState} | null {
  // If we have no presences in the match according to the match state, increment the empty ticks count
  if (state.presences.length === 0) {
    state.emptyTicks++;
  }

  // If the match has been empty for more than 100 ticks, end the match by returning null
  if (state.emptyTicks > 100) {
    return null;
  }

  return {
    state
  };
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
local M = {}

function M.match_init(context, initial_state)
	local state = {
		presences = {},
		empty_ticks = 0
	}
	local tick_rate = 1 -- 1 tick per second = 1 MatchLoop func invocations per second
	local label = ""

	return state, tick_rate, label
end

function M.match_join(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.session_id] = presence
	end

	return state
end

function M.match_leave(context, dispatcher, tick, state, presences)
	for _, presence in ipairs(presences) do
		state.presences[presence.session_id] = nil
	end

	return state
end

function M.match_loop(context, dispatcher, tick, state, messages)
  -- Get the count of presences in the match
  local totalPresences = 0
  for k, v in pairs(state.presences) do
    totalPresences = totalPresences + 1
  end

	-- If we have no presences in the match according to the match state, increment the empty ticks count
	if totalPresences == 0 then
		state.empty_ticks = state.empty_ticks + 1
	end

	-- If the match has been empty for more than 100 ticks, end the match by returning nil
	if state.empty_ticks > 100 then
		return nil
	end

	return state
end
```
{{< / code >}}

대결은 외부에서 중지할 수 없으며 수명 주기 함수 중 하나가 `nil` 상태를 반환할 때만 종료됩니다.

대결 핸들러를 사용하려면 등록해야 합니다.

{{< code type="server">}}
```go
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
	if err := initializer.RegisterMatch("lobby", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) {
		return &LobbyMatch{}, nil
	}); err != nil {
		logger.Error("unable to register: %v", err)
		return err
	}
}
```
{{< / code >}}

{{< code type="server">}}
```typescript
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerMatch('lobby', {
    matchInit,
    matchJoinAttempt,
    matchJoin,
    matchLeave,
    matchLoop,
    matchSignal,
    matchTerminate
  });
}
```
{{< / code >}}

{{< code type="server">}}
```lua
-- the name must be the same as the match handler file (e.g. lobby.lua)
nk.register_matchmaker_matched(function(context, matched_users)
    local match_id, err = nk.match_create("lobby", { invited = matched_users })
    return match_id
end)
```
{{< / code >}}

## 틱 속도

대부분의 대결 핸들러 함수는 사용자의 행동이나 내부 서버 프로세스로 인해 호출되지만, 처리를 기다리는 입력이 없는 경우에도 서버는 **주기적으로 대결 루프 함수**를 호출합니다. 이 로직은 필요에 따라 게임 상태를 진전시킬 수 있으며 들어오는 입력을 검증하고 비활성 플레이어를 추방할 수도 있습니다.

틱 속도는 서버가 대결 루프 함수를 호출하는 원하는 빈도(초당)를 나타냅니다. 즉, 대결이 업데이트되어야 하는 빈도입니다. 예를 들어 `10` 속도는 초당 대결 루프에 대한 10틱을 나타냅니다.

서버는 항상 _시작_점 간격을 균일하게 유지하려고 합니다. `10` 예시의 틱 속도를 사용하여 각 루프는 마지막 루프가 _시작된_ 후 `100ms`을(를) 시작합니다. 가장 좋은 방법은 루프 사이에 가능한 한 많은 시간을 두어 불규칙하게 호출된 비루프 함수가 루프 사이에서 실행되도록 만드는 것입니다.

게임 루프 로직과 구성된 틱 속도로 인해 서버가 뒤처지지 않도록 하는 것이 중요합니다. 즉, 다음 루프가 예약되기 전에 각 루프를 완료할 수 있어야 합니다(예시에서 `100ms`보다 작음). 대결 루프가 뒤처지면 서버는 먼저 가능한 한 빨리 다음 루프를 시작하여 "따라잡기"를 시도합니다. 너무 많은 루프가 뒤쳐지는 경우(일반적으로 루프 로직 설계가 불량한 결과) 서버는 대결을 종료합니다.

틱 속도는 구성 가능하며 일반적인 빈도 범위는 차례 기반 게임의 경우 초당 한 번에서 빠르게 진행되는 게임 플레이의 경우 초당 수십 번입니다. 틱 속도를 선택할 때 염두에 두어야 할 몇 가지 고려 사항:

* 허용 가능한 플레이어 경험을 제공하는 가능한 가장 낮은 틱 속도를 선택합니다(지연 없음 등).
* 틱 속도가 높을수록 대결 루프 사이의 간격이 줄어들고 플레이어의 반응성 "느낌"이 많아집니다.
* 항상 낮은 속도로 시작하여 원하는 경험이 달성될 때까지 작은 증분(1-2)으로 높입니다
* 틱 속도가 낮으면 CPU 코어당 동시에 실행할 수 있는 것보다 더 많은 일치가 생깁니다
* 대결 핸들러마다 다양한 게임 모드에 대해 다양한 틱 속도가 가능합니다.

## 일치 상태

Nakama는 권한 보유 대결이 대결 중 상태를 저장하는 데 사용할 메모리 내 영역을 노출합니다. 이 영역에는 대결 중 게임 데이터 및 클라이언트 동작을 계속 추적하는 데 필요한 모든 정보가 포함될 수 있습니다.

각 대결은 고유한 개별 격리 상태가 유지됩니다. 이 상태는 유효성 검사 후 사용자 입력 루프에 따라 초기 상태에 적용된 연속 변환의 결과입니다. 이러한 상태 변경 사항은 연결된 클라이언트에 자동으로 전송되지 **않습니다**. 적절한 작업 코드와 데이터를 [브로드캐스트](#broadcast-message)하여 대결 핸들러 로직 내에서 수동으로 이 작업을 수행해야 합니다.

### 데이터 메시지 보내기

[중계된 멀티플레이어에서 메시지를 보내는 것](../relayed/#send-data-messages)과 달리 권한 보유 대결에서 수신된 메시지는 연결된 다른 모든 클라이언트에 자동으로 다시 브로드캐스트되지 않습니다. 대결 논리에서 [`broadcast` 함수](../../../server-framework/typescript-runtime/function-reference/match-runtime/#BroadcastMessage)를 명시적으로 호출하여 메시지를 보내야 합니다.

각 메시지에는 [작업 코드](#op-codes)와 페이로드가 포함됩니다. 

{{< code type="server">}}
```go
const MATCH_START_OPCODE = 7

matchStartData := &map[string]interface{} {
  "started": true,
  "roundTimer": 100,
}

data, err := json.Marshal(matchStartData)
if err != nil {
  logger.Error("error marshaling match start data", err)
  return nil
}

reliable := true
dispatcher.BroadcastMessage(MATCH_START_OPCODE, data, nil, nil, reliable)
```
{{< / code >}}

{{< code type="server">}}
```typescript
const matchStartOpcode = 7

const matchStartData = {
  started: true,
  roundTimer: 100
};

dispatcher.broadcastMessage(matchStartOpcode, json.stringify(matchStartData), null, null, true);
```
{{< / code >}}

{{< code type="server">}}
```lua
local match_start_opcode = 7

match_start_data = {
    started = true,
    round_timer = 100
}

dispatcher.broadcast_message(match_start_opcode, nk.json_encode(match_start_data), nil, nil)
```
{{< / code >}}


각 데이터 메시지 내의 바이너리 컨텐츠(페이로드)는 `1500` 바이트라는 최대 전송 단위(MTU) 내에서 **최대한 작아야** 합니다. JSON을 사용하는 것이 일반적이며 [프로토콜 버퍼](https://developers.google.com/protocol-buffers/) 또는 [FlatBuffers](https://google.github.io/flatbuffers/)와 같은 컴팩트 바이너리 형식을 사용하는 것이 좋습니다. 

메시지 크기 및/또는 빈도를 더 줄일 수 없는 경우 **메시지를 더 적게** 보내는 게 가장 좋습니다. 예를 들어, 초당 `1000` 바이트 메시지 1개가 초당 `200` 바이트 메시지 5개보다 낫습니다.

클라이언트 메시지는 수신된 순서대로 서버에 의해 버퍼링되고 다음 대결 루프가 실행될 때 배치로 전달됩니다. 모범 사례는 틱 당, 서버에 대한 현재 상태 당, 서버에서 각 현재 상태까지 동일한 메시지를 1개 이하로 유지하려고 시도하는 것입니다.

구성된 틱 속도에 대한 메시지가 너무 많으면 일부 메시지가 서버에 의해 삭제될 수 있으며 오류가 기록됩니다. 지속적인 메시지 삭제를 방지하려면 다음을 시도합니다:

* 클라이언트에서 서버로의 메시지 전송 속도 감소
* 메시지가 더 자주 소비되도록 틱 속도 증가
* [버퍼 크기](../../../getting-started/configuration/#match.input_queue_size) 늘리기

#### 작업 코드

작업 코드는 전송된 메시지 유형의 숫자 식별자입니다. 메시지를 디코딩하기 전에 작업 코드를 통해 메시지의 목적과 내용을 쉽게 확인할 수 있습니다. 

작업 코드를 사용하여 다음과 같은 특정 사용자 작업에 속하는 게임플레이 내에서 명령을 정의할 수 있습니다:

* 초기 상태 동기화
* 준비 상태
* Ping / Pong
* 게임 상태 업데이트
* 이모티콘

비트 연산을 사용하여 데이터를 인코딩하면 작업 코드 필드에 추가 정보를 포함시킬 수도 있습니다.

구현에 관한 예는 [Fish Game 튜토리얼](../../../tutorials/unity/fishgame/#operation-codes)을 참조하세요.

#### 브로드캐스트 대 브로드캐스트 지연

[대결 핸들러 함수](../../../server-framework/typescript-runtime/function-reference/match-handler/)에 전달된 `dispatcher` 유형을 사용하면 대결에서 _해당 대결_에 있는 하나 이상의 현재 상태로 데이터를 보낼 수 있습니다.

데이터를 보내는 데 사용할 수 있는 두 가지 방법 `Broadcast` 및 `BroadcastDeferred`.

`Broadcast` 함수 당 여러 번 호출할 수 있지만 가장 좋은 방법은 나가는 데이터를 각 루프의 현재 상태 당 하나의 메시지로 제한하는 것입니다. 루프 당 여러 호출을 사용하는 것은 각 현재 상태에 다른 메시지를 보내야 하는 경우에만 권장됩니다.

`Broadcast` 및 `BroadcastDeferred`의 한 가지 사용 상 차이점은 전자는 호출 시 즉시 데이터를 보내고 후자는 루프가 끝날 때까지 데이터를 보내지 않는 것입니다.

너무 많은 데이터를 전송/브로드캐스트하고 클라이언트에 대한 다운로드 연결이 대결 데이터 전송 속도보다 느린 경우, 클라이언트 연결의 전송 버퍼 큐를 채우고 메모리 오버플로를 방지하기 위해 서버가 연결을 끊도록 만들 수 있습니다.

### 데이터 메시지 수신

서버는 클라이언트에서 보내는 데이터 메시지를 처리하는 순서대로 데이터를 전달합니다. 클라이언트는 수신되는 대결 데이터 메시지에 대한 콜백을 추가할 수 있습니다. 이것은 대결을 [가입](#join-a-match)하고 [종료](#leave-a-match)하기 전에 수행되어야 합니다.

{{< code type="client" >}}
```javascript
socket.onmatchdata = (result) => {
  var content = result.data;

  switch (result.op_code) {
    case 101:
      console.log("A custom opcode.");
      break;
    default:
      console.log("User %o sent %o", result.presence.user_id, content);
  }
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// Use whatever decoder for your message contents.
var enc = System.Text.Encoding.UTF8;
socket.ReceivedMatchState += newState =>
{
    var content = enc.GetString(newState.State);

    switch (newState.OpCode)
    {
        case 101:
            Console.WriteLine("A custom opcode.");
            break;
        default:
            Console.WriteLine("User '{0}'' sent '{1}'", newState.UserPresence.Username, content);
    }
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtListener->setMatchDataCallback([](const NMatchData& data)
{
    switch (data.opCode)
    {
        case 101:
            std::cout << "A custom opcode." << std::endl;
            break;
        default:
            std::cout << "User " << data.presence.userId << " sent " << data.data << std::endl;
            break;
    }
});
```
{{< / code >}}

{{< code type="client" >}}
```java
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onMatchData(final MatchData matchData) {
        System.out.format("Received match data %s with opcode %d", matchData.getData(), matchData.getOpCode());
    }
};
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.connect("received_match_state", self, "_on_match_state")

func _on_match_state(p_state : NakamaRTAPI.MatchData):
    print("Received match state with opcode %s, data %s" % [p_state.op_code, parse_json(p_state.data)])
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
socket.on_matchdata(function(message)
  local data = json.decode(message.match_data.data)
  local op_code = tonumber(message.match_data.op_code)
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

## 대결 레이블

대결 레이블을 사용하여 대결이 Nakama와 플레이어 기반에 광고하려는 내용을 강조 표시합니다. 여기에는 게임 모드, 열림 또는 닫힘 여부, 플레이어 수, 대결 상태 등과 같은 세부 정보가 포함될 수 있습니다.  

대결 레이블은 **단순 문자열** 또는 **JSON** 값일 수 있습니다. 이런 레이블은 [대결 목록 API](../match-listing/)를 통해 대결을 필터링하는 데 사용할 수 있습니다.  

**JSON 값**이 있는 대결 레이블에 대해서만 **[검색 쿼리](../query-syntax/)**를 사용할 수 있습니다. **단순 문자열 값**(예: `"team-deathmatch"`)이 있는 대결 레이블의 경우 `label` 매개변수를 사용하여 **정확한 일치**만 수행할 수 있습니다. 

인덱싱된 쿼리는 대결 목록에서 더 효과적이고 유용하므로 JSON 대결 레이블을 사용하는 것이 좋습니다. 주의해야 할 몇 가지 모범 사례:

* 대결 레이블에는 2kb 크기 제한 있음
* 레이블 가능한 업데이트하지 않음(즉, 틱 당 한 번 이하).
* 레이블이 배치로 업데이트되어 특정 시점 보기가 생성됨

## 대결 관리

### 대결 생성

권한 보유 대결은 [수동으로](#manually) 또는 [매치메이커](#matchmaker)를 통해 서버에서 생성할 수 있습니다.

#### 수동

일부 사용자 ID를 서버에 제출하고 대결을 생성하는 RPC 함수를 사용할 수 있습니다.

인앱 알림이나 푸시 메시지(또는 둘 다)와 함께 플레이어에게 보낼 수 있는 대결 ID가 생성됩니다. 이 접근 방식은 수동으로 대결을 만들고 특정 사용자와 경쟁하려는 경우에 유용합니다.

{{< code type="server" >}}
```lua
local nk = require("nakama")

local function create_match(context, payload)
  local modulename = "pingpong"
  local setupstate = { initialstate = payload }
  local matchid = nk.match_create(modulename, setupstate)

  return nk.json_encode({ matchid = matchid })
end

nk.register_rpc(create_match, "create_match_rpc")
```
{{< / code >}}

{{< code type="server" >}}
```go
func CreateMatchRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
    params := make(map[string]interface{})

    if err := json.Unmarshal([]byte(payload), &params); err != nil {
        return "", err
    }

    modulename := "pingpong" // Name with which match handler was registered in InitModule, see example above.

    if matchId, err := nk.MatchCreate(ctx, modulename, params); err != nil {
        return "", err
    } else {
        return matchId, nil
    }
}

// Register as RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("create_match_rpc", CreateMatchRPC); err != nil {
    logger.Error("Unable to register: %v", err)
    return err
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
function rpcCreateMatch(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, payload: string): string {
  var matchId = nk.matchCreate('pingpong', payload);
  return JSON.stringify({ matchId });
}
```
{{< / code >}}

#### 매치 메이커

[매치메이커](../matchmaker/)를 사용하여 상대방을 찾고 서버에서 매치메이커 대결 콜백을 사용하여 권한 보유 대결을 생성하고 대결 ID를 반환합니다. 여기에서는 클라이언트에서 표준 매치메이커 API를 사용합니다.

클라이언트는 대결 ID가 있는 매치메이커 콜백을 정상적으로 수신합니다.

{{< code type="server" >}}
```lua
local nk = require("nakama")

local function makematch(context, matched_users)
  -- print matched users
  for _, user in ipairs(matched_users) do
    local presence = user.presence
    nk.logger_info(("Matched user '%s' named '%s'"):format(presence.user_id, presence.username))
    for k, v in pairs(user.properties) do
      nk.logger_info(("Matched on '%s' value '%s'"):format(k, v))
    end
  end

  local modulename = "pingpong"
  local setupstate = { invited = matched_users }
  local matchid = nk.match_create(modulename, setupstate)
  return matchid
end

nk.register_matchmaker_matched(makematch)
```
{{< / code >}}

{{< code type="server" >}}
```go
func MakeMatch(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
    for _, e := range entries {
        logger.Info("Matched user '%s' named '%s'", e.GetPresence().GetUserId(), e.GetPresence().GetUsername())

        for k, v := range e.GetProperties() {
            logger.Info("Matched on '%s' value '%v'", k, v)
        }
    }

    matchId, err := nk.MatchCreate(ctx, "pingpong", map[string]interface{}{"invited": entries})

    if err != nil {
        return "", err
    }

    return matchId, nil
}

// Register as matchmaker matched hook, this call should be in InitModule.
if err := initializer.RegisterMatchmakerMatched(MakeMatch); err != nil {
    logger.Error("Unable to register: %v", err)
    return err
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
function matchmakerMatched(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, matches: nkruntime.MatchmakerResult[]): string {
  matches.forEach(function (match) {
    logger.info("Matched user '%s' named '%s'", match.presence.userId, match.presence.username);

    Object.keys(match.properties).forEach(function (key) {
      logger.info("Matched on '%s' value '%v'", key, match.properties[key])
    });
  });

  try {
    const matchId = nk.matchCreate("pingpong", { invited: matches });
    return matchId;
  } catch (err) {
    logger.error(err);
    throw (err);
  }
}

// ...

initializer.registerMatchmakerMatched(matchmakerMatched);
```
{{< / code >}}

매치메이커 대결 후크는 대결 ID 또는 `nil`을(를) 반환해야 하거나 대결이 중계된 멀티플레이어로 진행되어야 하는지 여부를 반환해야 합니다.

match create 함수에 전달된 문자열은 사용된 서버 런타임 언어에 따라 다릅니다:

- _Lua_의 경우 모듈 이름이어야 합니다. 이 예제에서는 이름이 `pingpong.lua`인 파일이므로 대결 모듈은 `pingpong`입니다.
- _Go_ 및 _TypeScript_의 경우 대결 핸들러 함수의 등록된 이름이어야 합니다. 위의 예에서 `InitModule` 함수에서 `initializer.RegisterMatch`을(를) 호출할 때 `pingpong`(으)로 등록했습니다.

### 대결 가입

플레이어는 매치메이커가 대결시킨 후에도 참가할 때까지 대결에 참가하는 것은 아닙니다. 이를 통해 플레이어는 플레이하지 않기로 결정한 대결에서 나올 수 있습니다.

이는 중계된 멀티플레이어와 동일한 방식으로 클라이언트에서 수행할 수 있습니다. 이 작업을 수행하는 방법에 대한 전체 예는 [여기](../relayed/#join-a-match)를 참조하세요.

### 대결 종료

사용자는 언제든지 대결을 종료할 수 있습니다. 이는 중계된 멀티플레이어와 동일한 방식으로 클라이언트에서 수행할 수 있습니다. 이 작업을 수행하는 방법에 대한 전체 예는 [여기](../relayed/#leave-a-match)를 참조하세요.

대결을 탈퇴할 때 `LeaveMatch` 수명 대결 핸들러 함수가 호출되고 플레이어가 대결을 탈퇴했는지 아니면 연결이 끊겼는지에 대한 이유가 추가됩니다. 연결이 끊긴 경우 임시로 자리를 보존할 수 있습니다.

중계 대결과 달리, 권한 보유 대결의 경우 모든 플레이어가 탈퇴하여도 종료되지 **않습니다**. 이는 정상적인 현상이며 게임 세계가 계속 진행하는 동안 플레이어가 일시적으로 연결을 끊을 수 있는 사용 사례를 지원할 수 있도록 만든 것입니다.

권한 보유 대결 핸들러는 콜백 중 하나라도 `nil` 상태를 반환할 때만 중지됩니다. 게임에 연결된 플레이어가 있는지 여부에 관계없이 대결이 진행되는 동안 언제든지 이 작업을 수행하도록 선택할 수 있습니다.

### 대결 마이그레이션

{{< note "important" "Nakama 회사만" >}}
{{< / note >}}

Nakama 인스턴스의 [정상적인 종료 시작](../../../getting-started/configuration/#shutdown_grace_sec)으로 인해 대결이 종료되면 이 유예 기간을 사용하여 플레이어를 새 대결로 마이그레이션할 수 있습니다.

먼저 새로운 대결을 만들거나 [대결 목록](../match-listing/)을 통해 참여할 수 있는 기존 대결을 찾습니다. 그런 다음 이 새로운 대결 정보가 포함된 [발송자 브로드캐스트](#send-data-messages)를 영향을 받는 클라이언트에 보냅니다. 마지막으로 현재 대결에서 해당 플레이어가 나가기를 기다리거나 필요한 경우 강제로 추방할 수 있습니다.

{{< code type="server" >}}
```go
// Define an op code for sending a new match id to remaining presences
const newMatchOpCode = 999

func (m *LobbyMatch) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
	logger.Debug("match will terminate in %d seconds", graceSeconds)

	var matchId string

	// Find an existing match for the remaining connected presences to join
	limit := 1
	authoritative := true
	label := ""
	minSize := 2
	maxSize := 4
	query := "*"
	availableMatches, err := nk.MatchList(ctx, limit, authoritative, label, minSize, maxSize, query)
	if err != nil {
		logger.Error("error listing matches", err)
		return nil
	}

	if len(availableMatches) > 0 {
		matchId = availableMatches[0].MatchId
	} else {
		// No available matches, create a new match instead
		matchId, err = nk.MatchCreate(ctx, "match", nil)
		if err != nil {
			logger.Error("error creating match", err)
			return nil
		}
	}

	// Broadcast the new match id to all remaining connected presences
	data := map[string]string{
		matchId: matchId,
	}

	dataJson, err := json.Marshal(data)
	if err != nil {
		logger.Error("error marshaling new match message")
		return nil
	}
	
	dispatcher.BroadcastMessage(newMatchOpCode, dataJson, nil, nil, true)

	return state
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
// Define an op code for sending a new match id to remaining presences
const newMatchOpCode = 999;

const matchTerminate = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, graceSeconds: number) : { state: nkruntime.MatchState} | null {
  logger.debug(`Match will terminate in ${graceSeconds} seconds.`);

  let matchId = null;
  
  // Find an existing match for the remaining connected presences to join
  const limit = 1;
  const authoritative = true;
  const label = "";
  const minSize = 2;
  const maxSize = 4;
  const query = "*";
  const availableMatches = nk.matchList(limit, authoritative, label, minSize, maxSize, query);
  
  if (availableMatches.length > 0) {
    matchId = availableMatches[0].matchId;
  } else {
    // No available matches, create a new match instead
    matchId = nk.matchCreate("match", { invited: state.presences });
  }

  // Broadcast the new match id to all remaining connected presences
  dispatcher.broadcastMessage(newMatchOpCode, JSON.stringify({ matchId }), null, null, true);

  return {
    state
  };
}
```
{{< / code >}}

{{< code type="server" >}}
```lua
-- Define an op code for sending a new match id to remaining presences
local new_match_op_code = 999

function M.match_terminate(context, dispatcher, tick, state, grace_seconds)
	local message = "Server shutting down in " .. grace_seconds .. " seconds"

	local match_id

	-- Find an existing match for the remaining connected presences to join
	local limit = 1;
	local authoritative = true;
	local label = "";
	local min_size = 2;
	local max_size = 4;
	local query = "*";
	local available_matches = nk.match_list(limit, authoritative, label, min_size, max_size, query);

	if #available_matches > 0 then
		match_id = available_matches[0].match_id;
	else
		-- No available matches, create a new match instead
		match_id = nk.match_create("match", { invited = state.presences });
	end

	-- Broadcast the new match id to all remaining connected presences
	dispatcher.broadcast_message(new_match_op_code, nk.json_encode({ ["matchId"] = match_id }))

	return state
end
```
{{< / code >}}

## 모범 사례

### 대결 상태 데이터 저장

{{< code type="server" >}}
```go
type LobbyMatchState struct {
	presences  map[string]runtime.Presence
	started bool
}

func (m *LobbyMatch) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
	state := &LobbyMatchState{
		presences:  map[string]runtime.Presence{},
		started: false,
	}
	tickRate := 1
	label := ""
	return state, tickRate, label
}

func (m *LobbyMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
	lobbyState, ok := state.(*LobbyMatchState)
	if !ok {
		logger.Error("state not a valid lobby state object")
	}
	
  if (len(lobbyState.presences) > 2) {
		lobbyState.started = true;
	}

	return lobbyState
}
```
{{< / code >}}

{{< code type="server" >}}
```typescript
const matchInit = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, params: {[key: string]: string}): {state: nkruntime.MatchState, tickRate: number, label: string} {
  return {
    state: { presences: {}, started: false },
    tickRate,
    label: ''
  };
};

const matchLoop = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, messages: nkruntime.MatchMessage[]) : { state: nkruntime.MatchState} | null {
   if (state.presences.length > 2) {
    state.started = true;
  }

  return {
    state
  };
}

```
{{< / code >}}

{{< code type="server" >}}
```lua
local M = {}

function M.match_init(context, initial_state)
	local state = {
		presences = {},
		started = false
	}
	local tick_rate = 1
	local label = ""

	return state, tick_rate, label
end

function M.match_loop(context, dispatcher, tick, state, messages)
  -- Get the count of presences in the match
  local totalPresences = 0
  for k, v in pairs(state.presences) do
    totalPresences = totalPresences + 1
  end

	if totalPresences > 2 then
		state.started = true
	end

	return state
end
```
{{< / code >}}

