現有一個微信小程式叫中國象棋項目,棋盤類的單機遊戲看着有缺少了什麼,現在給補上了,加個聯機對戰的功能,增加了可玩性,對新手來說,實作聯機遊戲還是有難度的,那要怎麼實作的呢,接下來給大家講一下。
考慮到搭建聯機遊戲的伺服器成本不小,第一個想法是用小程式的藍牙功能實作遊戲聯機的,但是其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()
就是發送信号的意思,可以這樣認為,在網絡上冒個泡,可以讓對方知道你線上,主動找你溝通,如果超過時間還不吐泡泡,就認為你潛水了(隐身)
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2YfNWawNiZpdmL1MDO3UDN2UDZlhTO0ATY4MTMlRDZ2MzNxQTYzETN2M2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
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(店鋪)位址和端口(門牌号))
發送廣播
用戶端怎樣發送廣播呢,這個方法是
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是,就是發給區域網路内的路由器,需要注意的是,不是所有的路由器都是支援廣播IP的,要確定支援它,需要登入路由器的控制頁面,找看有沒有其中的
255.255.255.255
項,取消勾選即可
隔離AP
發送的廣播消息是,需要從另一個小程式的服務端負責接收那方法
{ intent: 'scan' }
中處理這個廣播消息,傳回響應資料
onReceive()
遊戲聯機
實作遊戲的聯機方式分兩種,上面開始講過,用其中的一個角色:服務端或者用戶端
主動加入
一種是主動加入遊戲,就用用戶端發送問候消息方法
sendMessage(e)
去請求服務端,服務端會處理響應請求
加入前,用戶端需要先知道對方的IP位址和端口,從上面講得發送廣播方法
sendBroadcast(e)
來掃描一下,找到對方後,然後發送加入請求
在對方的服務端傳回同意消息時,附帶了遊戲入口消息,告訴用戶端加入遊戲的途徑
主動等待
另一種,是主動建立好遊戲地盤,建立好服務端,用一個接收資料的綁定的端口,去負責監聽用戶端發來的請求,然後處理響應資料
這裡注意不要搞錯,當另一個小程式用戶端發來加入遊戲的請求時,要留點心,别把用戶端的端口号當作服務端的端口号(接收資料)
關于項目
好了,UDP通信的方法就講到這裡,這樣有了大緻的方向,
如需要看項目源碼的請在 下載下傳清單點這裡 找聯機遊戲相關的項目,那些聯機遊戲項目裡都有應用,有對應的介紹,請放心下載下傳,多謝支援,願學有所獲!
打開項目源碼,如果遇到微信開發工具提示 Error: 登入使用者不是該小程式的開發者
,需要替換項目的測試号替換為自己的,點選開發工具右邊的詳情,裡面有AppID,可修改替換,
這裡有一個中國象棋-單機遊戲開發流程詳解文章,需要的同學可以先看看,
這裡是在原單機遊戲項目的基礎上增加了聯機功能,聯機遊戲運作的動圖效果如下
聯機測試
項目還沒有自己的測試号,就前往申請一個測試号,申請成功後,登入如下圖,其中AppID的就是
- 申請測試号 🔗傳送門
開發工具上掃碼預覽出來的小程式是開發版,隻能在自己的微信上體驗,想測試聯機遊戲就這樣做,選擇裡面的真機調試項,這樣就可以模拟兩個使用者來體驗了,一個在開發工具上模拟器上,另一個就是自己的真機微信上
如果有兩個手機微信就這樣試試,用兩個手機微信分别登入開發工具弄一個開發版小程式來,這樣兩個手機微信上就能測試遊戲聯機,
上面操作有點麻煩,就用自己申請好的一個小程式來測試,釋出一個體驗版小程式就可以讓很多人參與測試了。