純前端如何利用幀同步做一款聯機遊戲?

遊資網發表於2020-12-03
一、遊戲幀同步

1.簡介

·現代多人遊戲中,多個客戶端之間的通訊大多以同步多方狀態為主要目標,為了實現這一目標,主要有兩個技術方向:狀態同步、幀同步。

·狀態同步的思想中不同玩家螢幕上的一致性的表現並不是重要指標,只要每次操作的結果相同即可。所以狀態同步對網路延遲的要求並不高。

·幀同步主要依賴客戶端的能力,伺服器僅僅是做一個轉發,甚至客戶端可以無需伺服器,通過P2P方式來轉發資料。由於只是轉發遊戲的行為,所以廣播的資料量比狀態同步要小很多。

本文將以幀同步技術為主來介紹如何實現一款聯機遊戲。

2.小遊戲案例

·本次我們在《街霸小遊戲》中利用騰訊雲的遊戲聯機對戰引擎實現了玩家之間的PVP玩法。

純前端如何利用幀同步做一款聯機遊戲?

感興趣的同學可以掃碼體驗:

純前端如何利用幀同步做一款聯機遊戲?

二、遊戲聯機對戰引擎:Mgobe

1.引擎簡介

·Mgobe是由騰訊雲提供的遊戲聯機對戰引擎,可以為遊戲提供房間管理、線上匹配、幀同步、狀態同步等網路通訊服務,幫助開發者快速搭建多人互動遊戲。

·Mgobe可以讓我們在沒有後臺開發人力的情況下也能實現遊戲的幀同步。

Unity Editor也嵌入了MGOBE,在Unity Editor 2019.1.9及以上版本,各位開發者可以通過服務皮膚,一鍵開通騰訊雲服務MGOBE。

純前端如何利用幀同步做一款聯機遊戲?

純前端如何利用幀同步做一款聯機遊戲?

Cocos Creator嵌入了MGOBE,在v2.3.4及以上版本,各位開發者可以通過Cocos Service服務皮膚,一鍵開通騰訊雲服務MGOBE。

純前端如何利用幀同步做一款聯機遊戲?

·官網:https://cloud.tencent.com/product/mgobe

2.開發語言

·Mgobe支援使用 JavaScript 或 TypeScript 來進行前端開發。

3.支援平臺

·Mgobe目前支援:微信小遊戲、QQ小遊戲、百度小遊戲、OPPO小遊戲、vivo小遊戲、位元組小遊戲;H5小遊戲和手遊。

三、純前端打造幀同步實現聯機對戰

·接下來會從前端的角度來一步一步講解使用Mgobe的方法,藉助Mgobe我們可以不用知曉後臺和運維知識,就可以構建起一套效能優越的幀同步遊戲。

1.控制檯配置

·首先我們需要在Mgobe的控制檯中建立遊戲例項,以獲取遊戲ID、遊戲Key和域名等資訊,我們會在初始化SDK時使用到遊戲ID和遊戲Key。

純前端如何利用幀同步做一款聯機遊戲?

·出於安全考慮,微信小遊戲會限制請求域名,所有的 HTTPS、WebSocket、上傳、下載請求域名都需要在微信公眾平臺進行配置。因此,在正式接入遊戲聯機對戰引擎 SDK 前,還需要開發者在微信公眾平臺配置合法域名。 ·需要配置的域名包含一條 request 域名和兩條 socket 域名記錄,配置如下:

// request 域名

report.wxlagame.com

// socket 域名

xxx.wxlagame.com

xxx.wxlagame.com:5443

2.SDK

2.1.下載

·SDK下載地址:https://cloud.tencent.com/document/product/1038/33406

2.2.引入SDK

·SDK檔案包含 MGOBE.js 和 MGOBE.d.ts,即原始碼檔案和定義檔案。在 MGOBE.js 中,SDK介面被全域性注入到 window 物件下。因此,只需要在使用SDK介面之前執行 MGOBE.js 檔案即可。

·以微信為例,只需將 MGOBE.js 放到專案下任意位置,在 game.js 中 import SDK 檔案後即可使用 MGOBE 的方法。當然也可以使用 import/from、require 語法顯式匯入 MGOBE 模組。

2.3.直接使用金鑰進行初始化

·用這種方式可以快速初始化SDK,可以最快的速度使用引擎的幀同步功能,但這種方式會在前端暴露遊戲Key。

  1. <p>var gameInfo = {</p><p>openId: 'xxxxxx', //玩家的openID</p><p>gameId: "xxxxxx", //遊戲id,在控制檯中的“遊戲ID”中獲取</p><p>secretKey: 'xxxxxx' //遊戲金鑰,在控制檯中的“遊戲key”獲取</p><p>};</p><p>var config = {</p><p>url: 'xxx.wxlagame.com',//遊戲域名,在控制檯中的“域名”獲取</p><p>reconnectMaxTimes: 5, //重連線次數</p><p>reconnectInterval: 1000, //重連線時間間隔</p><p>resendInterval: 1000, //訊息重發時間間隔</p><p>resendTimeout: 10000 //訊息重發超時時間</p><p>};</p><p>Listener.init(gameInfo, config, function() {</p><p>if (event.code === 0) {</p><p>// 初始化成功</p><p>}</p><p>});</p>
複製程式碼

·Listener 物件為 MGOBE 的子屬性,該物件方法全為靜態方法,不需要例項化。Listener物件主要用於給 Room 物件的例項繫結廣播事件監聽。

·初始化 Listener 成功後才能繼續呼叫 Mgobe 引擎的其他介面。

2.4.利用簽名來進行初始化(在前端隱藏遊戲Key)

·用2.3的方法初始化 SDK 時,會在前端暴露遊戲的金鑰,為了避免在客戶端洩露遊戲的金鑰,我們也可以使用簽名的方式來初始化 SDK。

·在開發者伺服器通過遊戲 ID、遊戲 Key、玩家 openId 等資訊計算出遊戲簽名,然後再下發給客戶端。客戶端在初始化 SDK 時,需要實現一個 createSignature 簽名函式,從服務端獲取簽名資訊然後回撥給 SDK。也就是在 gameInfo 中,將2.3中 的 secretKey 欄位改為 createSignature 欄位。

  1. <p>//這裡僅列出與2.3不同的gameInfo, config和Listener.init與2.3一致,不再贅述。 var gameInfo = {</p><p>gameId: "xxxxx", //遊戲id,在控制檯中的“遊戲ID”中獲取</p><p>openId: "xxxxxx", //玩家的openID</p><p>// 實現簽名函式</p><p>createSignature: callback => { //假設https://example.com/sign就是我們後臺計算簽名的介面</p><p>fetch("https://example.com/sign").then(rsp => rsp.json()).then(json => {</p><p>const sign = json.sign;</p><p>const nonce = json.nonce;</p><p>const timestamp = json.timestamp;</p><p>return callback({ sign, nonce, timestamp });</p><p>});</p><p>},</p><p>};</p>
複製程式碼


·簽名過程詳見:https://cloud.tencent.com/document/product/1038/38863

3.房間

·在開發遊戲的過程中,大部分介面都位於 Room 物件中。由於每個玩家只能加入一個房間,在遊戲生命週期中可以只例項化一個 Room 物件來進行介面的呼叫。

3.1.例項化Room

  1. var roomInfo = { id: "xxx" //房間ID }; var room = new MGOBE.Room(roomInfo);
複製程式碼

建立房間、加入房間、匹配等介面呼叫直接使用 room 例項即可。但有3個介面例外:getMyRoom、getRoomList、getRoomByRoomId 介面是 Room 物件的靜態方法,需要使用 Room.getMyRoom、Room.getRoomList、Room.getRoomByRoomId 來呼叫。

3.2.幾個常用屬性

3.2.1.roomInfo 屬性

·roomInfo 為 Room 例項的屬性,儲存房間的相關資訊,呼叫 Room 相關的介面會導致該屬性發生變化。可以從 roomInfo 中獲得房間的id、名稱和玩家列表等。

3.2.2.networkState 屬性

·用於獲取客戶端本地 SDK 的網路狀態。注意 networkState 的網路狀態與玩家資訊 Player 中的網路狀態概念不同,room.networkState 表示本地 socket 的狀態,而 Player.commonNetworkState 和 Player.relayNetworkState 表示玩家在 Mgobe 後臺中的狀態。 ·networkState 網路狀態發生變化時,room.onUpdate 將被觸發。

  1. <p>room.onUpdate = function() {</p><p>console.log("房間資訊更新:", room.roomInfo);</p><p>};</p>
複製程式碼

3.3.初始化Room

  1. room.initRoom();
複製程式碼

·通過 room.initRoom 方法可以初始化一個房間,同時更新房間資訊 roomInfo 。初始化可以更新 WebSocket 連線,這樣才能及時收到房間的廣播。此外,如果要加入指定ID的房間,也需要先對房間進行初始化,否則將無法使用 room.joinRoom 加入指定ID的房間。

3.4.為Room新增廣播偵聽

  1. MGOBE.Listener.add(room);
複製程式碼

·一個房間物件會有很多廣播事件與其相關,例如該房間有新成員加入、房間屬性變化、房間開始對戰等廣播。Room 例項需要在 Listener 中註冊廣播監聽,之後可以通過 room.xxx 回撥函式的形式來使用廣播偵聽,詳見下文。

3.5.建立房間

·通過使用 room 例項的 createRoom 可以建立一個房間,建立成功後建立者會自動進入該房間。

  1. <p>var playerData = {</p><p>name:nickname, //玩家暱稱</p><p>customPlayerStatus:playerStatus, //自定義玩家狀態</p><p>customProfile:figureURL //自定義玩家資訊</p><p>};//玩家資訊</p><p>var createRoomData = {</p><p>roomName:"roomName", //房間名稱</p><p>roomType:"1v1", //房間型別</p><p>maxPlayers:2, //房間最大玩家數量</p><p>isPrivate:true, //是否為私有房間,屬性為 true 表示該房間為私有房間,不能被 matchRoom 介面匹配到</p><p>customProperties:roomStatus, //自定義房間屬性</p><p>playerInfo:playerData //房主資訊</p><p>};//房間資訊</p><p>room.createRoom(createRoomData, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>//建立房間成功</p><p>}</p><p>});</p>
複製程式碼

·注意:建立房間的結果是通過回撥非同步返回的,而非派發事件。

3.6.加入房間

·通過使用 room 例項的 joinRoom 可以加入一個已經存在的房間。

  1. <p>var playerData =</p><p>{</p><p>name:nickname, //玩家暱稱</p><p>customPlayerStatus:playerStatus, //自定義玩家狀態</p><p>customProfile:figureURL //自定義玩家資訊</p><p>};//玩家資訊</p><p>var joinRoomInfo =</p><p>{</p><p>playerInfo:playerData</p><p>};//加入房間的資訊</p><p>room.initRoom({ id: "xxx" });//加入房間前需要先初始化room例項</p><p>room.joinRoom(joinRoomInfo, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>console.log("加入房間成功");</p><p>}</p><p>});</p>
複製程式碼

·注意:加入房間的結果也是通過回撥非同步返回的,而非派發事件。加入房間前必須先初始化房間例項。

·對於已經存在於房間中的其他人,可以通過 room.onJoinRoom 來偵聽新玩家的加入。

  1. <p>room.onJoinRoom = function(e)</p><p>{</p><p>console.log("新玩家加入,ID為:", e.data.joinPlayerId);</p><p>};</p>
複製程式碼

3.7.離開房間

·使用 room.leaveRoom 就可以退出房間。

  1. <p>room.leaveRoom({}, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>console.log("離開房間成功");</p><p>}</p><p>});</p>
複製程式碼

·對於房間中的其他人,可以通過 room.onLeaveRoom 來偵聽玩家的離開。

  1. <p>room.onLeaveRoom = function(e)</p><p>{</p><p>console.log("離開房間的玩家的ID:", e.data.leavePlayerId);</p><p>};</p>
複製程式碼

4.匹配

4.1.匹配規則

·要進行房間匹配,需要先在控制檯建立匹配規則,匹配規則既可以滿足按人數匹配、按隊伍匹配,也可以按段位等特殊方式來匹配。成功建立規則後會獲得一個匹配code,匹配code將會用於匹配的相關介面,表示用這個規則來匹配符合條件的玩家。

純前端如何利用幀同步做一款聯機遊戲?

規則建立之後還需要將規則繫結到伺服器中才能生效,在“新建匹配”中選擇上一步建立的匹配集即可。

純前端如何利用幀同步做一款聯機遊戲?

4.2.匹配玩家

·有了匹配code後我們就可以在前端進行玩家匹配了,只要是符合規則中定義的條件的玩家,就會被匹配進同一個房間中。

  1. <p>var matchPlayersData = {</p><p>playerInfo:playerData, //發起匹配的玩家的資訊,playerData在上文已多次出現,這裡不再贅述</p><p>matchCode:matchCode //匹配code,在4.1中獲得</p><p>};//玩家匹配資訊</p><p>room.matchPlayers(matchPlayersData, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>console.log("匹配請求成功");</p><p>}</p><p>});</p>
複製程式碼

4.3.匹配房間

·matchPlayers 配合匹配code可以用來匹配玩家,那麼通過使用 room.matchRoom 則可以進行房間的匹配。房間匹配是指按照傳入的引數搜尋現存的房間,如果存在,則將玩家加入該房間,如果不存在,則為玩家建立並加入一個新房間。

·matchRoom 不需要使用匹配code。

  1. <p>var playerInfo = {</p><p>name: "Tom",</p><p>customPlayerStatus: 1,</p><p>customProfile: "https://xxx.com/icon.png",</p><p>};//發起匹配者的資訊</p><p>const matchRoomPara = {</p><p>playerInfo,</p><p>maxPlayers: 5,</p><p>roomType: "1",</p><p>};//房間匹配資訊</p><p>room.matchRoom(matchRoomPara, function(e) {</p><p>if (event.code === 0) {</p><p>console.log("匹配成功");</p><p>}</p><p>});</p>
複製程式碼

·matchRoom 與 matchPlayers 最大的不同就是:matchRoom 一定會讓匹配發起人進入一個房間,但 matchPlayers 則不一定,如果當前沒有符合匹配規則的玩家,則 matchPlayers 會返回失敗。

5.幀同步

·終於來到這一步了,如果玩家已經成功加入房間,就可以通過幀同步功能進行遊戲對戰。

5.1.開啟幀同步

·使用 room.startFrameSync 介面就可以開啟幀廣播。房間內任意一個玩家成功呼叫該介面都將導致全部玩家開始接收幀廣播。

  1. <p>room.startFrameSync({}, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>console.log("開始幀同步成功");</p><p>}</p><p>});</p>
複製程式碼

·呼叫成功後房間內全部成員都將收到 onStartFrameSync 廣播。該介面會修改房間幀同步狀態為“已開始幀同步”。

  1. <p>room.onStartFrameSync = function()</p><p>{</p><p>//收到此廣播後將持續收到 onRecvFrame 廣播 //注意,這裡還不是玩家之間相互進行幀同步的資訊內容,onRecvFrame 中才是我們拿到幀同步內容的地方,見下文</p><p>};</p>
複製程式碼

5.2.傳送幀訊息

·玩家收到幀同步開始廣播後,才可以傳送幀訊息,後臺會將每個玩家的幀訊息組合後再廣播給每個玩家。

·幀資料內容 data 型別為普通 Object,由開發者自定義,目前支援最大長度不超過1k。後臺將集合全部玩家的幀資料,並以一定時間間隔(由房間幀率定義,可以在控制檯配置)通過 onRecvFrame 廣播給各客戶端。呼叫結果將在 callback 中非同步返回。

  1. <p>var frame = { cmd: "xxxxxxxx", id: "xxxxxxxx" };//一幀的內容,由開發者自定義</p><p>var sendFramePara = { data: frame };//傳送給Mgobe的幀內容</p><p>room.sendFrame(sendFramePara, function(e) { console.log("傳送幀同步資料"); });</p>
複製程式碼

5.3.接收幀廣播

·開發者可設定 room.onRecvFrame 廣播回撥函式來獲得幀廣播資料。onRecvFrame 廣播表示收到一個幀 frame,frame 的內容由多個 MGOBE.types.FrameItem 組成,即一幀時間內房間內所有玩家向伺服器傳送幀訊息的集合。

  1. <p>room.onRecvFrame = function()</p><p>{</p><p>console.log("收到幀同步訊息=", e.data.frame); //我們就是從 e.data.frame.items 這個陣列的每個元素的 data 屬性來拿到我們在5.2中傳送給Mgobe的幀內容的。 //5.2的幀內容:var frame = {cmd: "xxxxxxxx", id:"xxxxxxxx"}</p><p>};</p>
複製程式碼

5.4.停止幀同步

·使用 room.stopFrameSync 介面可以停止幀廣播。房間內任意一個玩家成功呼叫該介面將導致全部玩家停止接收幀廣播。

  1. <p>room.stopFrameSync({}, function(e)</p><p>{</p><p>if(e.code === 0)</p><p>{</p><p>console.log("停止幀同步成功");</p><p>}</p><p>});</p>
複製程式碼

·呼叫成功後房間內全部成員將收到 onStopFrameSync 廣播。該介面會修改房間幀同步狀態為“已停止幀同步”。

  1. <p>room.onStopFrameSync = function()</p><p>{</p><p>//收到該廣播後將不再收到 onRecvFrame 廣播</p><p>};</p>
複製程式碼

·至此,利用Mgobe來進行幀同步開發的相關主要介面就介紹完畢了。下面將講一些關於玩家資訊的內容。

6.玩家資訊

6.1.玩家ID

·玩家資訊 Player 物件為 MGOBE 的子屬性,用於訪問玩家的基本資訊,例如玩家 ID、openId 等。該物件記錄了玩家的基本資訊,預設全部為空。成功初始化 Listener 後,ID、openId 屬性才生效。

·Player 中的 玩家 ID 是 MGOBE 後臺生成的 ID,而 openId 是開發者初始化時候使用的 ID。需要注意,openId 只有初始化 Listener 的時候才使用,後續其它介面提到的“玩家 ID”均指後臺生成的 ID,也就是 Player.id 屬性,它不是 openId,切記!

·玩家進入房間後,Player 物件中的屬性與 roomInfo.playerList 中的玩家資訊是一致,通過兩者任何一個都可以獲得正確的玩家資訊。

6.2.幾個常用事件

·這裡提兩個經常用到的玩家事件:網路狀態變化、玩家狀態變化。

·在Mgobe中,玩家的網路狀態分以下4種,但玩家的網路狀態發生變化時均會觸發。

  1. <p>room.onChangePlayerNetworkState = function(e)</p><p>{</p><p>if(e.data.networkState === MGOBE.ENUM.NetworkState.COMMON_OFFLINE)</p><p>{</p><p>console.log("房間中玩家掉線");</p><p>}</p><p>else if(e.data.networkState === MGOBE.ENUM.NetworkState.COMMON_ONLINE)</p><p>{</p><p>console.log("房間中玩家線上");</p><p>}</p><p>else if(e.data.networkState === MGOBE.ENUM.NetworkState.RELAY_OFFLINE)</p><p>{</p><p>console.log("幀同步中玩家掉線");</p><p>}</p><p>else if(e.data.networkState === MGOBE.ENUM.NetworkState.RELAY_ONLINE)</p><p>{</p><p>console.log("幀同步中玩家線上");</p><p>} //通過 e.data.changePlayerId 可以知道是哪個玩家的網路狀態發生了變化</p><p>};</p>
複製程式碼

·如果修改了玩家的自定義資訊(由開發者自定義的,也即上文多次提到的 playerInfo 中的 customPlayerStatus),則以下事件會被觸發:

  1. <p>room.onChangeCustomPlayerStatus = function()</p><p>{ //房間內 ID 為 changePlayerId 的玩家狀態發生變化。玩家狀態由開發者自定義。</p><p>console.log("玩家自定義狀態變化=", e.data.changePlayerId); console.log("自定義資料=", e.data.customPlayerStatus);</p><p>};</p>
複製程式碼

7.錯誤處理

·最後,如果在使用Mgobe的過程中如果發生客戶端錯誤、系統邏輯錯誤、使用者資訊錯誤、房間錯誤、匹配錯誤、幀同步錯誤、引數錯誤、隊伍團隊錯誤時,均會發出錯誤碼,可以通過以下文件查閱相關錯誤碼對應的描述資訊,以便排除和解決錯誤。

·錯誤碼說明文件詳見:https://cloud.tencent.com/document/product/1038/33317

四、結尾

· 本文僅從前端角度出發,介紹了利用 Mgobe 進行純前端的幀同步開發,但 Mgobe 的功能遠不止這些,Mgobe 也支援在後臺編寫自定義匹配邏輯來實現更加豐富的幀同步,感興趣的同學可自行查閱官方文件。也可關注公眾號,關注持續技術分享。

來源:騰訊遊戲雲
原文:https://cloud.tencent.com/developer/article/1747879

相關文章