天天看點

【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試

現有一個微信小程式叫中國象棋項目,棋盤類的單機遊戲看着有缺少了什麼,現在給補上了,加個聯機對戰的功能,增加了可玩性,對新手來說,實作聯機遊戲還是有難度的,那要怎麼實作的呢,接下來給大家講一下。

考慮到搭建聯機遊戲的伺服器成本不小,第一個想法是用小程式的藍牙功能實作遊戲聯機的,但是其API接口提供的藍牙硬體支援相容問題不少,暫時不去折騰了,現在采用UDP通信就很容易實作,可以在WIFI區域網路内讓兩個以上小程式實作通信。

UDP通信

先來了解一下 UDP通信 的工作原理,這是一個面向無連接配接的傳輸協定,是UDP通信,與之對應的是 面向可連接配接的傳輸協定,是 TCP通信

從上圖看出來,UDP通信的方式很簡單,可以想象它們能充當其中一個角色,

  • client用戶端: 隻負責發送封包
  • server服務端:隻負責接收封包
小程式實作UDP通信,要建立client用戶端和服務端server,各占一個

socket

端口,

對初學者來說,第一次接觸不好了解,端口,可比喻成線路一端的接口。

TCP通信的面向連接配接是比UDP通信最可靠的,那為什麼不優先采用TCP通信呢

小程式的TCP通信實作過程中,需要綁定到wifi,這一點擷取wifi資訊的處理有遇到問題,對新手來說是比較麻煩的,暫且避之,能正常擷取到wifi資訊再來考慮

client用戶端

直接在一個子產品檔案中實作,這個在項目中的檔案是

lan.js

,可以了解它為區域網路工具子產品,

負責發送

要向server服務端發送封包(消息),就寫一個方法

sendMessage(e)

來調用,傳入服務端的

remoteInfo

,實作代碼如下

import Util from './util';

function sendMessage(e){
  //需要傳入的參數
  const { message, port, remoteInfo, fail, success, autoClose } = e;
  let udp = wx.createUDPSocket();
  udp.onError(err=>{
  	//...這裡處理初始化udp的錯誤
  });
  udp.onMessage(res=>{
    const { remoteInfo, localInfo } = res;
    if(autoClose) udp.close();//預設自動關閉udp
    //消息res.message是ArrayBuffer對象,要轉換為json object對象才好處理
    let message = Util.arrayBufferToString({data:res.message});
    message = Util.parseJSON(message);
    //傳回服務端響應的資料
    success(message,remoteInfo,localInfo);
  });
  //綁定端口
  udp.bind(port);
  //發送消息message 到服務端 `address(IP)`和`port(端口)`
  udp.send({
    address:remoteInfo.address,
    port:remoteInfo.port,
    message:toStringMesssage(message)
  });
  return udp;
}
           
用戶端向服務端發出消息,沒必要加請求逾時的處理,後面有個邏輯是處理連接配接的,用它代替連接配接逾時處理的判斷

連接配接服務端

用戶端連接配接到服務端方法是

connectServer(remoteInfo,e)

,實作代碼如下,加了定時連接配接請求,如果請求逾時了,就會提示使用者連接配接逾時(連接配接斷開)

const Timeout = 6000;//逾時6s

function connectServer(remoteInfo,e){
  const { config, onReceive, onError, onConnect, onDisconnect } = e;
  let connectInfo;
  let timer,timer2;
  //關閉定時器
  const closeTimer = function(){
    if(timer) {
      clearTimeout(timer);
      timer=undefined;
    }
    if(timer2) {
      clearTimeout(timer2);
      timer2=undefined;
    }
  };
  const clientUdp = wx.createUDPSocket();
  clientUdp.onClose(function(){
    closeTimer();
  });
  clientUdp.onError(function(err){
    closeTimer();
    //...這裡處理udp抛出的錯誤,回調onError
    onError(err);
  });
  //預設不傳port,就綁定一個随機的port(端口号)
  clientUdp.bind();
  let time;
  let sendSign = function(){
	//定時發送
    timer = setTimeout(function(){
      let message = {
        intent:'keep_connect',
        ntime:Date.now(),
      };
      if(!connectInfo){
        message.intent='create_connect';
        message.data=config;
      }
      //定時向服務端發送連接配接資訊
      clientUdp.send({
        address:remoteInfo.address,
        port:remoteInfo.port,
        message:JSON.stringify(message)
      });
      //加個定時器,用于逾時判斷
      timer2 = setTimeout(function(){    
        connectInfo = null;
        onDisconnect({ errMsg:'the request timeout' });
      },Timeout);
    },config.time || 3000);
  };
  clientUdp.onMessage(function(res){
    //防抖處理
    closeTimer();
    const { localInfo,remoteInfo } = res;
    let message = Util.arrayBufferToString({data:res.message});
    message = Util.parseJSON(message);
    if(message.intent=='create_connect'){
      connectInfo = remoteInfo;
      //回調連接配接事件
      onConnect({message:message.data,localInfo,remoteInfo});
    }else{
      if(time && message.otime==undefined) message.otime = Date.now() - time;
      //回調接收事件
      onReceive({message,localInfo,remoteInfo});
      time = Date.now(); 
    }
    sendSign();
  });
  //開始發送
  sendSign();
  return clientUdp;
}
           
方法

sendSign()

就是發送信号的意思,可以這樣認為,在網絡上冒個泡,可以讓對方知道你線上,主動找你溝通,如果超過時間還不吐泡泡,就認為你潛水了(隐身)
【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試

server服務端

負責接收

接下來,寫一個叫服務端server的建立方法

createServer()

,用于監聽用戶端發來的連接配接請求,還要處理其它的請求,稍微複雜一點,實作代碼如下

import Util from './util';

function createServer(e){
  const { config, onReceive, onError, onConnect, onDisconnect } = e;

  let udp = wx.createUDPSocket();
  udp.onError(function(err){
  	//...這裡處理udp抛出錯誤,回調onError
    onError(err);
  });
  udp.onClose(function(){
    closeTimer();
  });
  udp.onMessage(function(res){
    const { localInfo, remoteInfo } = res;
    let message = Util.arrayBufferToString({data:res.message});
    message = Util.parseJSON(message);
    let response;
    switch(message?.intent){
      case 'create_connect':
        //...處理建立連接配接請求
        break;
      case 'keep_connect':
        //...處理保持連接配接請求
        break;
      default:
	    //如果沒處理,就交給回調onReceive處理
	    response = onReceive({ localInfo, remoteInfo, message });
    }
    //如果還沒處理,就不需要響應了(不理睬)
    if(!response) return;
    //服務端響應資料發給用戶端
    udp.send({
      address:remoteInfo.address,
      port:remoteInfo.port,
      message:toStringMesssage(response)
    });
  });
  //綁定一個伺服器端口
  let port = udp.bind(config.port);
  return {
    getPort(){
      return port;
    },
    close(){
      udp.close();
    }
  };
}
           
有沒有覺得,服務端的處理邏輯很像web的伺服器處理請求,處理響應來自用戶端(浏覽器)的請求

管理用戶端連接配接

再具體一點,處理建立和保持連接配接請求的方法,将上面的代碼改一下,添加後的代碼如下

const Timeout = 6000;//逾時6s
//...
 let connectInfo;
 let timer;
 //保持連接配接(定時連接配接檢查)
 const keepConnectInfo = function(){
   timer = setTimeout(function(){
     if(connectInfo){
       let otime = Date.now()-connectInfo.utime;
       //未逾時
       if(otime<Timeout){
         keepConnectInfo();
         onReceive({ 
           //...回調接收事件,傳回連接配接狀态
         });
         return;
       }
     }
     connectInfo=null;
     //若連接配接逾時了,就回調斷開連接配接的方法
     onDisconnect({ errMsg:'wait update is timeout'});
   },Timeout);
 };
 //關閉定時器
 const closeTimer = function(){
   //...
 };

switch(message?.intent){
  case 'create_connect'://建立連接配接請求
     {
       let data = message.data;
       connectInfo = {
       	//...記錄連接配接資訊
       };
       //回調連接配接事件
       onConnect({
         //...傳連接配接資料
       });
       response = {
         intent:message.intent,
         //...傳回連接配接後擷取的初始化資料
       };
       //防抖處理
       closeTimer();
       keepConnectInfo();
       break;
     }
   case 'keep_connect'://保持連接配接請求
     {
       if(connectInfo) {
         connectInfo.utime = Date.now();
         response = {
           intent:message.intent,
           //...
         };
       }else{
         response = {
           intent:'create_connect',
           //...傳回配置資料
           data:config,
         };
       }
       //記錄時間差
       if(message.ntime) response.utime = response.time - message.ntime;
       break;
     }
   default:
  //如果沒處理,就交給回調onReceive處理
  response = onReceive({ localInfo, remoteInfo, message });
 }
 //...
           
可以看出來,這裡連接配接的邏輯是定時檢查用戶端連接配接更新的狀态,如果超過時間不更新,就判斷為連接配接逾時

區域網路廣播

兩個小程式之間是怎麼知道對方的IP和端口呢,這就要借助廣播IP位址了,

發送廣播IP位址,就可以在區域網路發現線上的裝置,然後請求連接配接,廣播過程是這樣的

來打個比喻:

用戶端A(你)發送廣播消息(點餐訂單),交給路由器(平台)處理,路由器會轉發消息,附帶了你的IP(家)位址和端口(門号),到其餘的用戶端(搶單),

如果有用戶端(搶到單的外賣服務員)對方想回應你,就會給你發消息:你的外賣到了…(票據上有寫了對方的IP(店鋪)位址和端口(門牌号))

【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試

發送廣播

用戶端怎樣發送廣播呢,這個方法是

sendBroadcast()

,很容易實作,代碼如下

function sendBroadcast(e){
  const { port, fail, success, showLoading } = e;
  let udp = wx.createUDPSocket();
  let timer;
  let list = [];//記錄接收的清單
  function complete(callback){
    //...
    udp.close();//處理完要關閉
    callback();
  }
  if(fail instanceof Function){
    udp.onError(function(err){
      complete(function(){
        fail(err);
      });
    });
  }
  udp.onMessage(function(res){
  	//将接收的消息轉換成json對象
    res.message = toDataJSON(res.message);
    list.push(res);
  });
  udp.bind();
  //發送廣播消息,其中port是指定小程式的服務端接收端口
  udp.send({
    address:'255.255.255.255',
    port: port,
    message: JSON.stringify({ intent: 'scan' })
  });
  //加上定時
  timer = setTimeout(function(){
  	//到時結束
    complete(function(){
      if(success instanceof Function) success({ list });
    })
  }, e.timeout || 3000);
  //...
}
           
廣播IP是

255.255.255.255

,就是發給區域網路内的路由器,需要注意的是,不是所有的路由器都是支援廣播IP的,要確定支援它,需要登入路由器的控制頁面,找看有沒有其中的

隔離AP

項,取消勾選即可
發送的廣播消息是

{ intent: 'scan' }

,需要從另一個小程式的服務端負責接收那方法

onReceive()

中處理這個廣播消息,傳回響應資料

遊戲聯機

實作遊戲的聯機方式分兩種,上面開始講過,用其中的一個角色:服務端或者用戶端

主動加入

一種是主動加入遊戲,就用用戶端發送問候消息方法

sendMessage(e)

去請求服務端,服務端會處理響應請求

加入前,用戶端需要先知道對方的IP位址和端口,從上面講得發送廣播方法

sendBroadcast(e)

來掃描一下,找到對方後,然後發送加入請求

在對方的服務端傳回同意消息時,附帶了遊戲入口消息,告訴用戶端加入遊戲的途徑

主動等待

另一種,是主動建立好遊戲地盤,建立好服務端,用一個接收資料的綁定的端口,去負責監聽用戶端發來的請求,然後處理響應資料

這裡注意不要搞錯,當另一個小程式用戶端發來加入遊戲的請求時,要留點心,别把用戶端的端口号當作服務端的端口号(接收資料)

關于項目

好了,UDP通信的方法就講到這裡,這樣有了大緻的方向,

如需要看項目源碼的請在 下載下傳清單點這裡 找聯機遊戲相關的項目,那些聯機遊戲項目裡都有應用,有對應的介紹,請放心下載下傳,多謝支援,願學有所獲!

打開項目源碼,如果遇到微信開發工具提示

Error: 登入使用者不是該小程式的開發者

,需要替換項目的測試号替換為自己的,點選開發工具右邊的詳情,裡面有AppID,可修改替換,

這裡有一個中國象棋-單機遊戲開發流程詳解文章,需要的同學可以先看看,

這裡是在原單機遊戲項目的基礎上增加了聯機功能,聯機遊戲運作的動圖效果如下

【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試

聯機測試

項目還沒有自己的測試号,就前往申請一個測試号,申請成功後,登入如下圖,其中AppID的就是

【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試
  • 申請測試号 🔗傳送門
開發工具上掃碼預覽出來的小程式是開發版,隻能在自己的微信上體驗,想測試聯機遊戲就這樣做,選擇裡面的真機調試項,這樣就可以模拟兩個使用者來體驗了,一個在開發工具上模拟器上,另一個就是自己的真機微信上

如果有兩個手機微信就這樣試試,用兩個手機微信分别登入開發工具弄一個開發版小程式來,這樣兩個手機微信上就能測試遊戲聯機,

上面操作有點麻煩,就用自己申請好的一個小程式來測試,釋出一個體驗版小程式就可以讓很多人參與測試了。

【聯機對戰】微信小程式聯機遊戲開發流程詳解UDP通信client用戶端server服務端區域網路廣播遊戲聯機關于項目聯機測試