중계 멀티플레이어 외에도 Nakama는 서버 권한 보류 멀티플레이어 모델도 지원하므로 게임에 가장 적합한 접근 방식을 자유롭게 결정할 수 있습니다.
서버 권한 보유 멀티플레이어에서 교환되는 모든 게임 플레이 데이터는 서버에서 검증되고 브로드캐스트됩니다. 이 모델에서는 Nakama에 의해 적용될 게임플레이 규칙에 대한 사용자 지정 서버 런타임 코드를 작성합니다(즉, 몇 명의 플레이어가 가입할 수 있는지, 진행 중인 대결에 가입할 수 있는지 여부 등).
다른 것보다 중계 또는 권한 보유 접근 방식을 필요로 하는 강력한 결정 요소는 없으며 원하는 게임 플레이를 기반으로 한 디자인에 따른 결정입니다. 권한 보유 멀티플레이어는 게임 서버에서 관리하는 중앙 상태에 의존하는 게임 플레이, 대결당 플레이어 수가 더 많은 게임 플레이, 게임 클라이언트를 신뢰하지 않고 대신 게임 플레이 규칙을 더 엄격하게 제어하여 부정 행위를 최소화하려는 경우 등에 더 적합합니다.
서버에서 유지 관리되는 상태를 변경하기 위해 데이터 메시지를 필요로 하는 멀티플레이어 게임 디자인을 지원하기 위해 권한 보유 멀티플레이어 엔진을 사용하면 고정된 틱 속도로 사용자 지정 대결 로직을 실행할 수 있습니다. 메시지의 유효성이 검사되고 상태 변경 사항이 연결된 피어로 브로드캐스트될 수 있습니다. 이를 통해 다음을 만들 수 있음:
비동기식 실시간 권위 보유 멀티플레이어: 빠르게 진행되는 실시간 멀티플레이어. 메시지는 서버로 전송되고, 서버는 환경과 플레이어의 변경 사항을 계산하고, 데이터는 관련 피어에게 브로드캐스트됩니다. 일반적으로 게임 플레이가 응답성을 느끼려면 높은 틱 속도가 필요합니다.
능동 차례 기반 멀티플레이어: 두 명 이상의 플레이어가 연결되어 빠른 차례 기반 대결을 수행하는 Stormbound 또는 Clash Royale을 예시로 들 수 있습니다. 플레이어는 즉시 차례에 응답해야 합니다. 서버는 입력을 수신하여 유효성을 검사하고 플레이어에게 브로드캐스트합니다. 주고 받는 메시지의 속도가 낮기 때문에 예상되는 틱 속도는 상당히 낮습니다.
수동 차례 기반 멀티플레이어: 게임 플레이가 몇 시간에서 몇 주에 걸쳐 진행될 수 있는 모바일에서 수행하는 Words With Friends를 예시로 들 수 있습니다. 서버는 입력을 수신하고 입력을 검증하고 입력을 데이터베이스에 저장하고 다음 게임 플레이 시퀀스까지 서버 루프를 종료하기 전에 연결된 모든 피어에 변경 사항을 브로드캐스트합니다.
세션 기반 멀티플레이어: 실제 수행을 서버 측에서 실행하려는 복잡한 게임플레이용(예: Unity 헤드리스 인스턴스). Nakama는 오케스트레이션 계층을 통해 이러한 헤드리스 인스턴스를 관리할 수 있으며 매치 메이킹, 대결 완료 시 플레이어 이동, 대결 결과 보고에 사용할 수 있습니다.
서버에서 권한 보유 멀티플레이어 게임을 구축할 때 즉시 사용 가능한 일반적인 시나리오는 없으므로 주의합니다. 사용자 지정 런타임 코드를 작성하여 게임 플레이(대결 당 플레이어 수, 진행 중 가입 허용 여부, 대결 종료 방법 등)를 정의해야 합니다.
권한 보유 멀티플레이어 기능을 구현하기로 결정할 때 주의해야 할 몇 가지 개념이 있습니다.
대결 핸들러는 게임 입력을 처리하고 이를 작동하기 위해 함께 그룹화된 모든 서버 측 기능을 나타냅니다. 이를 대결이 인스턴스화되는 “청사진"으로 생각하십시오. 대결 핸들러는 대결에 대한 게임 플레이 규칙을 설정하며, 게임에는 여러 플레이 모드(예: 깃발 뺏기, 데스매치, 모두 무료 등)가 있을 수 있으므로 각 게임 모드에 하나씩 대결 핸들러가 여러 개 필요할 수 있습니다.
모든 대결 핸들러에는 7가지 기능이 필요합니다. 이러한 기능은 Nakama에서만 호출되며 클라이언트 또는 다른 런타임 코드에서 직접 호출할 수 없습니다.
이러한 함수들에 의해 하드웨어 및 플레이어 수에 따라 수천 개의 대결을 실행할 수 있는 단일 Nakama 노드로 주어진 대결의 상태와 수명 주기가 정의됩니다. 대결 핸들러와 주어진 대결의 상태는 특정 Nakama 인스턴스에 저장되며 해당 인스턴스는 해당 대결의 _호스트_가 됩니다.
단일 노드이므로 최고 수준의 상태 액세스 및 업데이트 일관성이 보장되고 분산 상태 조정의 잠재적 지연이 방지됩니다.
Nakama 회사만
대결 현재 상태는 복제되므로 클러스터의 모든 노드는 대결 목록과 대결 참가자에 대한 세부 정보 모두에 즉시 액세스할 수 있습니다. 노드 간의 균형 조정은 자동으로 수행되며 가장 적절한 노드에서 새로운 대결이 생성되고 종료된 노드에서는 생성되지 않습니다.
Nakama Open-Source에서 Nakama Enterprise로 원활하게 마이그레이션할 수 있으며 클라이언트 또는 서버 측 코드의 변경은 필요하지 않습니다. 현재 상태 복제, 클러스터 간 데이터 교환 및 메시지 라우팅은 모두 단일 인스턴스 클러스터에서 작동하는 것처럼 대결 핸들러에 투명하게 발생합니다.
실행 중인 모든 대결은 자체 포함되며 다른 대결과 통신하거나 다른 대결에 영향을 미칠 수 없습니다. 대결과의 통신은 대결 데이터를 보내는 클라이언트를 통해서만 이루어집니다. Nakama는 각 대결에 대한 CPU 일정 및 메모리 할당을 내부적으로 관리하여 단일 인스턴스 또는 클러스터의 모든 인스턴스에 대해 공정하고 균형 잡힌 부하 분산을 보장합니다.
대결 신호 기능을 사용하여 제한된 방식으로 이를 수행할 수 있습니다: 지정된 플레이어를 위해 대결에서 한 자리를 예약하거나 플레이어/데이터를 다른 대결로 넘겨줍니다. 이는 표준 사례가 아닌 드문 예외로만 사용해야 합니다.
대결 핸들러는 연결되거나 활성화된 현재 상태가 없는 경우에도 실행됩니다. 대결 런타임 로직에서 유휴 또는 빈 대결 처리를 고려해야 합니다.
typeLobbyMatchstruct{}typeLobbyMatchStatestruct{presencesmap[string]runtime.PresenceemptyTicksint}func(m*LobbyMatch)MatchInit(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,paramsmap[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:=""returnstate,tickRate,label}func(m*LobbyMatch)MatchJoin(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,dispatcherruntime.MatchDispatcher,tickint64,stateinterface{},presences[]runtime.Presence)interface{}{lobbyState,ok:=state.(*LobbyMatchState)if!ok{logger.Error("state not a valid lobby state object")returnnil}fori:=0;i<len(presences);i++{lobbyState.presences[presences[i].GetSessionId()]=presences[i]}returnlobbyState}func(m*LobbyMatch)MatchLeave(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,dispatcherruntime.MatchDispatcher,tickint64,stateinterface{},presences[]runtime.Presence)interface{}{lobbyState,ok:=state.(*LobbyMatchState)if!ok{logger.Error("state not a valid lobby state object")returnnil}fori:=0;i<len(presences);i++{delete(lobbyState.presences,presences[i].GetSessionId())}returnlobbyState}func(m*LobbyMatch)MatchLoop(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,dispatcherruntime.MatchDispatcher,tickint64,stateinterface{},messages[]runtime.MatchData)interface{}{lobbyState,ok:=state.(*LobbyMatchState)if!ok{logger.Error("state not a valid lobby state object")returnnil}// If we have no presences in the match according to the match state, increment the empty ticks count
iflen(lobbyState.presences)==0{lobbyState.emptyTicks++}// If the match has been empty for more than 100 ticks, end the match by returning nil
iflobbyState.emptyTicks>100{returnnil}returnlobbyState}
constmatchInit=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:''};};constmatchJoin=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};}constmatchLeave=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};}constmatchLoop=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){returnnull;}return{state};}
localM={}functionM.match_init(context,initial_state)localstate={presences={},empty_ticks=0}localtick_rate=1-- 1 tick per second = 1 MatchLoop func invocations per secondlocallabel=""returnstate,tick_rate,labelendfunctionM.match_join(context,dispatcher,tick,state,presences)for_,presenceinipairs(presences)dostate.presences[presence.session_id]=presenceendreturnstateendfunctionM.match_leave(context,dispatcher,tick,state,presences)for_,presenceinipairs(presences)dostate.presences[presence.session_id]=nilendreturnstateendfunctionM.match_loop(context,dispatcher,tick,state,messages)-- Get the count of presences in the matchlocaltotalPresences=0fork,vinpairs(state.presences)dototalPresences=totalPresences+1end-- If we have no presences in the match according to the match state, increment the empty ticks countiftotalPresences==0thenstate.empty_ticks=state.empty_ticks+1end-- If the match has been empty for more than 100 ticks, end the match by returning nilifstate.empty_ticks>100thenreturnnilendreturnstateend
대결은 외부에서 중지할 수 없으며 수명 주기 함수 중 하나가 nil 상태를 반환할 때만 종료됩니다.
대결 핸들러를 사용하려면 등록해야 합니다.
Server
1
2
3
4
5
6
7
8
funcInitModule(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,initializerruntime.Initializer)error{iferr:=initializer.RegisterMatch("lobby",func(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule)(runtime.Match,error){return&LobbyMatch{},nil});err!=nil{logger.Error("unable to register: %v",err)returnerr}}
-- the name must be the same as the match handler file (e.g. lobby.lua)nk.register_matchmaker_matched(function(context,matched_users)localmatch_id,err=nk.match_create("lobby",{invited=matched_users})returnmatch_idend)
대부분의 대결 핸들러 함수는 사용자의 행동이나 내부 서버 프로세스로 인해 호출되지만, 처리를 기다리는 입력이 없는 경우에도 서버는 주기적으로 대결 루프 함수를 호출합니다. 이 로직은 필요에 따라 게임 상태를 진전시킬 수 있으며 들어오는 입력을 검증하고 비활성 플레이어를 추방할 수도 있습니다.
틱 속도는 서버가 대결 루프 함수를 호출하는 원하는 빈도(초당)를 나타냅니다. 즉, 대결이 업데이트되어야 하는 빈도입니다. 예를 들어 10 속도는 초당 대결 루프에 대한 10틱을 나타냅니다.
서버는 항상 _시작_점 간격을 균일하게 유지하려고 합니다. 10 예시의 틱 속도를 사용하여 각 루프는 마지막 루프가 시작된 후 100ms을(를) 시작합니다. 가장 좋은 방법은 루프 사이에 가능한 한 많은 시간을 두어 불규칙하게 호출된 비루프 함수가 루프 사이에서 실행되도록 만드는 것입니다.
게임 루프 로직과 구성된 틱 속도로 인해 서버가 뒤처지지 않도록 하는 것이 중요합니다. 즉, 다음 루프가 예약되기 전에 각 루프를 완료할 수 있어야 합니다(예시에서 100ms보다 작음). 대결 루프가 뒤처지면 서버는 먼저 가능한 한 빨리 다음 루프를 시작하여 “따라잡기"를 시도합니다. 너무 많은 루프가 뒤쳐지는 경우(일반적으로 루프 로직 설계가 불량한 결과) 서버는 대결을 종료합니다.
틱 속도는 구성 가능하며 일반적인 빈도 범위는 차례 기반 게임의 경우 초당 한 번에서 빠르게 진행되는 게임 플레이의 경우 초당 수십 번입니다. 틱 속도를 선택할 때 염두에 두어야 할 몇 가지 고려 사항:
허용 가능한 플레이어 경험을 제공하는 가능한 가장 낮은 틱 속도를 선택합니다(지연 없음 등).
틱 속도가 높을수록 대결 루프 사이의 간격이 줄어들고 플레이어의 반응성 “느낌"이 많아집니다.
Nakama는 권한 보유 대결이 대결 중 상태를 저장하는 데 사용할 메모리 내 영역을 노출합니다. 이 영역에는 대결 중 게임 데이터 및 클라이언트 동작을 계속 추적하는 데 필요한 모든 정보가 포함될 수 있습니다.
각 대결은 고유한 개별 격리 상태가 유지됩니다. 이 상태는 유효성 검사 후 사용자 입력 루프에 따라 초기 상태에 적용된 연속 변환의 결과입니다. 이러한 상태 변경 사항은 연결된 클라이언트에 자동으로 전송되지 않습니다. 적절한 작업 코드와 데이터를 브로드캐스트하여 대결 핸들러 로직 내에서 수동으로 이 작업을 수행해야 합니다.
constMATCH_START_OPCODE=7matchStartData:=&map[string]interface{}{"started":true,"roundTimer":100,}data,err:=json.Marshal(matchStartData)iferr!=nil{logger.Error("error marshaling match start data",err)returnnil}reliable:=truedispatcher.BroadcastMessage(MATCH_START_OPCODE,data,nil,nil,reliable)
서버는 클라이언트에서 보내는 데이터 메시지를 처리하는 순서대로 데이터를 전달합니다. 클라이언트는 수신되는 대결 데이터 메시지에 대한 콜백을 추가할 수 있습니다. 이것은 대결을 가입하고 종료하기 전에 수행되어야 합니다.
Client
1
2
3
4
5
6
7
8
9
10
11
socket.onmatchdata=(result)=>{varcontent=result.data;switch(result.op_code){case101:console.log("A custom opcode.");break;default:console.log("User %o sent %o",result.presence.user_id,content);}};
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Use whatever decoder for your message contents.varenc=System.Text.Encoding.UTF8;socket.ReceivedMatchState+=newState=>{varcontent=enc.GetString(newState.State);switch(newState.OpCode){case101:Console.WriteLine("A custom opcode.");break;default:Console.WriteLine("User '{0}'' sent '{1}'",newState.UserPresence.Username,content);}};
Client
1
2
3
4
5
6
7
8
9
10
11
12
rtListener->setMatchDataCallback([](constNMatchData&data){switch(data.opCode){case101:std::cout<<"A custom opcode."<<std::endl;break;default:std::cout<<"User "<<data.presence.userId<<" sent "<<data.data<<std::endl;break;}});
Client
1
2
3
4
5
6
SocketListenerlistener=newAbstractSocketListener(){@OverridepublicvoidonMatchData(finalMatchDatamatchData){System.out.format("Received match data %s with opcode %d",matchData.getData(),matchData.getOpCode());}};
Client
1
2
3
4
5
6
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)])
funcCreateMatchRPC(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,payloadstring)(string,error){params:=make(map[string]interface{})iferr:=json.Unmarshal([]byte(payload),¶ms);err!=nil{return"",err}modulename:="pingpong"// Name with which match handler was registered in InitModule, see example above.
ifmatchId,err:=nk.MatchCreate(ctx,modulename,params);err!=nil{return"",err}else{returnmatchId,nil}}// Register as RPC function, this call should be in InitModule.
iferr:=initializer.RegisterRpc("create_match_rpc",CreateMatchRPC);err!=nil{logger.Error("Unable to register: %v",err)returnerr}
매치메이커를 사용하여 상대방을 찾고 서버에서 매치메이커 대결 콜백을 사용하여 권한 보유 대결을 생성하고 대결 ID를 반환합니다. 여기에서는 클라이언트에서 표준 매치메이커 API를 사용합니다.
클라이언트는 대결 ID가 있는 매치메이커 콜백을 정상적으로 수신합니다.
Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
localnk=require("nakama")localfunctionmakematch(context,matched_users)-- print matched usersfor_,userinipairs(matched_users)dolocalpresence=user.presencenk.logger_info(("Matched user '%s' named '%s'"):format(presence.user_id,presence.username))fork,vinpairs(user.properties)donk.logger_info(("Matched on '%s' value '%s'"):format(k,v))endendlocalmodulename="pingpong"localsetupstate={invited=matched_users}localmatchid=nk.match_create(modulename,setupstate)returnmatchidendnk.register_matchmaker_matched(makematch)
funcMakeMatch(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,entries[]runtime.MatchmakerEntry)(string,error){for_,e:=rangeentries{logger.Info("Matched user '%s' named '%s'",e.GetPresence().GetUserId(),e.GetPresence().GetUsername())fork,v:=rangee.GetProperties(){logger.Info("Matched on '%s' value '%v'",k,v)}}matchId,err:=nk.MatchCreate(ctx,"pingpong",map[string]interface{}{"invited":entries})iferr!=nil{return"",err}returnmatchId,nil}// Register as matchmaker matched hook, this call should be in InitModule.
iferr:=initializer.RegisterMatchmakerMatched(MakeMatch);err!=nil{logger.Error("Unable to register: %v",err)returnerr}
functionmatchmakerMatched(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{constmatchId=nk.matchCreate("pingpong",{invited: matches});returnmatchId;}catch(err){logger.error(err);throw(err);}}// ...
initializer.registerMatchmakerMatched(matchmakerMatched);
매치메이커 대결 후크는 대결 ID 또는 nil을(를) 반환해야 하거나 대결이 중계된 멀티플레이어로 진행되어야 하는지 여부를 반환해야 합니다.
match create 함수에 전달된 문자열은 사용된 서버 런타임 언어에 따라 다릅니다:
_Lua_의 경우 모듈 이름이어야 합니다. 이 예제에서는 이름이 pingpong.lua인 파일이므로 대결 모듈은 pingpong입니다.
Go 및 _TypeScript_의 경우 대결 핸들러 함수의 등록된 이름이어야 합니다. 위의 예에서 InitModule 함수에서 initializer.RegisterMatch을(를) 호출할 때 pingpong(으)로 등록했습니다.
Nakama 인스턴스의 정상적인 종료 시작으로 인해 대결이 종료되면 이 유예 기간을 사용하여 플레이어를 새 대결로 마이그레이션할 수 있습니다.
먼저 새로운 대결을 만들거나 대결 목록을 통해 참여할 수 있는 기존 대결을 찾습니다. 그런 다음 이 새로운 대결 정보가 포함된 발송자 브로드캐스트를 영향을 받는 클라이언트에 보냅니다. 마지막으로 현재 대결에서 해당 플레이어가 나가기를 기다리거나 필요한 경우 강제로 추방할 수 있습니다.
// Define an op code for sending a new match id to remaining presences
constnewMatchOpCode=999func(m*LobbyMatch)MatchTerminate(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,dispatcherruntime.MatchDispatcher,tickint64,stateinterface{},graceSecondsint)interface{}{logger.Debug("match will terminate in %d seconds",graceSeconds)varmatchIdstring// Find an existing match for the remaining connected presences to join
limit:=1authoritative:=truelabel:=""minSize:=2maxSize:=4query:="*"availableMatches,err:=nk.MatchList(ctx,limit,authoritative,label,minSize,maxSize,query)iferr!=nil{logger.Error("error listing matches",err)returnnil}iflen(availableMatches)>0{matchId=availableMatches[0].MatchId}else{// No available matches, create a new match instead
matchId,err=nk.MatchCreate(ctx,"match",nil)iferr!=nil{logger.Error("error creating match",err)returnnil}}// Broadcast the new match id to all remaining connected presences
data:=map[string]string{matchId:matchId,}dataJson,err:=json.Marshal(data)iferr!=nil{logger.Error("error marshaling new match message")returnnil}dispatcher.BroadcastMessage(newMatchOpCode,dataJson,nil,nil,true)returnstate}
// Define an op code for sending a new match id to remaining presences
constnewMatchOpCode=999;constmatchTerminate=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.`);letmatchId=null;// Find an existing match for the remaining connected presences to join
constlimit=1;constauthoritative=true;constlabel="";constminSize=2;constmaxSize=4;constquery="*";constavailableMatches=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};}
-- Define an op code for sending a new match id to remaining presenceslocalnew_match_op_code=999functionM.match_terminate(context,dispatcher,tick,state,grace_seconds)localmessage="Server shutting down in "..grace_seconds.." seconds"localmatch_id-- Find an existing match for the remaining connected presences to joinlocallimit=1;localauthoritative=true;locallabel="";localmin_size=2;localmax_size=4;localquery="*";localavailable_matches=nk.match_list(limit,authoritative,label,min_size,max_size,query);if#available_matches>0thenmatch_id=available_matches[0].match_id;else-- No available matches, create a new match insteadmatch_id=nk.match_create("match",{invited=state.presences});end-- Broadcast the new match id to all remaining connected presencesdispatcher.broadcast_message(new_match_op_code,nk.json_encode({["matchId"]=match_id}))returnstateend
typeLobbyMatchStatestruct{presencesmap[string]runtime.Presencestartedbool}func(m*LobbyMatch)MatchInit(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,paramsmap[string]interface{})(interface{},int,string){state:=&LobbyMatchState{presences:map[string]runtime.Presence{},started:false,}tickRate:=1label:=""returnstate,tickRate,label}func(m*LobbyMatch)MatchLoop(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,dispatcherruntime.MatchDispatcher,tickint64,stateinterface{},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;}returnlobbyState}
localM={}functionM.match_init(context,initial_state)localstate={presences={},started=false}localtick_rate=1locallabel=""returnstate,tick_rate,labelendfunctionM.match_loop(context,dispatcher,tick,state,messages)-- Get the count of presences in the matchlocaltotalPresences=0fork,vinpairs(state.presences)dototalPresences=totalPresences+1endiftotalPresences>2thenstate.started=trueendreturnstateend