現(xiàn)有一個(gè)微信小程序叫中國象棋項(xiàng)目,棋盤類的單機(jī)游戲看著有缺少了什么,現(xiàn)在給補(bǔ)上了,加個(gè)聯(lián)機(jī)對(duì)戰(zhàn)的功能,增加了可玩性,對(duì)新手來說,實(shí)現(xiàn)聯(lián)機(jī)游戲還是有難度的,那要怎么實(shí)現(xiàn)的呢,接下來給大家講一下。
考慮到搭建聯(lián)機(jī)游戲的服務(wù)器成本不小,第一個(gè)想法是用小程序的藍(lán)牙功能實(shí)現(xiàn)游戲聯(lián)機(jī)的,但是其API接口提供的藍(lán)牙硬件支持兼容問題不少,暫時(shí)不去折騰了,現(xiàn)在采用UDP通信就很容易實(shí)現(xiàn),可以在WIFI局域網(wǎng)內(nèi)讓兩個(gè)以上小程序?qū)崿F(xiàn)通信。
UDP通信
先來了解一下 UDP通信 的工作原理,這是一個(gè)面向無連接的傳輸協(xié)議,是UDP通信,與之對(duì)應(yīng)的是 面向可連接的傳輸協(xié)議,是 TCP通信
從上圖看出來,UDP通信的方式很簡單,可以想象它們能充當(dāng)其中一個(gè)角色,
- client客戶端: 只負(fù)責(zé)發(fā)送報(bào)文
- server服務(wù)端:只負(fù)責(zé)接收?qǐng)?bào)文
小程序?qū)崿F(xiàn)UDP通信,要?jiǎng)?chuàng)建client客戶端和服務(wù)端server,各占一個(gè)
socket
端口,
對(duì)初學(xué)者來說,第一次接觸不好理解,端口,可比喻成線路一端的接口。
TCP通信的面向連接是比UDP通信最可靠的,那為什么不優(yōu)先采用TCP通信呢
小程序的TCP通信實(shí)現(xiàn)過程中,需要綁定到wifi,這一點(diǎn)獲取wifi信息的處理有遇到問題,對(duì)新手來說是比較麻煩的,暫且避之,能正常獲取到wifi信息再來考慮
client客戶端
直接在一個(gè)模塊文件中實(shí)現(xiàn),這個(gè)在項(xiàng)目中的文件是lan.js
,可以理解它為局域網(wǎng)工具模塊,
負(fù)責(zé)發(fā)送
要向server服務(wù)端發(fā)送報(bào)文(消息),就寫一個(gè)方法sendMessage(e)
來調(diào)用,傳入服務(wù)端的remoteInfo
,實(shí)現(xiàn)代碼如下
import Util from './util';
function sendMessage(e){
//需要傳入的參數(shù)
const { message, port, remoteInfo, fail, success, autoClose } = e;
let udp = wx.createUDPSocket();
udp.onError(err=>{
//...這里處理初始化udp的錯(cuò)誤
});
udp.onMessage(res=>{
const { remoteInfo, localInfo } = res;
if(autoClose) udp.close();//默認(rèn)自動(dòng)關(guān)閉udp
//消息res.message是ArrayBuffer對(duì)象,要轉(zhuǎn)換為json object對(duì)象才好處理
let message = Util.arrayBufferToString({data:res.message});
message = Util.parseJSON(message);
//返回服務(wù)端響應(yīng)的數(shù)據(jù)
success(message,remoteInfo,localInfo);
});
//綁定端口
udp.bind(port);
//發(fā)送消息message 到服務(wù)端 `address(IP)`和`port(端口)`
udp.send({
address:remoteInfo.address,
port:remoteInfo.port,
message:toStringMesssage(message)
});
return udp;
}
客戶端向服務(wù)端發(fā)出消息,沒必要加請(qǐng)求超時(shí)的處理,后面有個(gè)邏輯是處理連接的,用它代替連接超時(shí)處理的判斷
連接服務(wù)端
客戶端連接到服務(wù)端方法是connectServer(remoteInfo,e)
,實(shí)現(xiàn)代碼如下,加了定時(shí)連接請(qǐng)求,如果請(qǐng)求超時(shí)了,就會(huì)提示用戶連接超時(shí)(連接斷開)
const Timeout = 6000;//超時(shí)6s
function connectServer(remoteInfo,e){
const { config, onReceive, onError, onConnect, onDisconnect } = e;
let connectInfo;
let timer,timer2;
//關(guān)閉定時(shí)器
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拋出的錯(cuò)誤,回調(diào)onError
onError(err);
});
//默認(rèn)不傳port,就綁定一個(gè)隨機(jī)的port(端口號(hào))
clientUdp.bind();
let time;
let sendSign = function(){
//定時(shí)發(fā)送
timer = setTimeout(function(){
let message = {
intent:'keep_connect',
ntime:Date.now(),
};
if(!connectInfo){
message.intent='create_connect';
message.data=config;
}
//定時(shí)向服務(wù)端發(fā)送連接信息
clientUdp.send({
address:remoteInfo.address,
port:remoteInfo.port,
message:JSON.stringify(message)
});
//加個(gè)定時(shí)器,用于超時(shí)判斷
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;
//回調(diào)連接事件
onConnect({message:message.data,localInfo,remoteInfo});
}else{
if(time && message.otime==undefined) message.otime = Date.now() - time;
//回調(diào)接收事件
onReceive({message,localInfo,remoteInfo});
time = Date.now();
}
sendSign();
});
//開始發(fā)送
sendSign();
return clientUdp;
}
方法
sendSign()
就是發(fā)送信號(hào)的意思,可以這樣認(rèn)為,在網(wǎng)絡(luò)上冒個(gè)泡,可以讓對(duì)方知道你在線,主動(dòng)找你溝通,如果超過時(shí)間還不吐泡泡,就認(rèn)為你潛水了(隱身)
server服務(wù)端
負(fù)責(zé)接收
接下來,寫一個(gè)叫服務(wù)端server的創(chuàng)建方法createServer()
,用于監(jiān)聽客戶端發(fā)來的連接請(qǐng)求,還要處理其它的請(qǐng)求,稍微復(fù)雜一點(diǎn),實(shí)現(xiàn)代碼如下
import Util from './util';
function createServer(e){
const { config, onReceive, onError, onConnect, onDisconnect } = e;
let udp = wx.createUDPSocket();
udp.onError(function(err){
//...這里處理udp拋出錯(cuò)誤,回調(diào)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':
//...處理創(chuàng)建連接請(qǐng)求
break;
case 'keep_connect':
//...處理保持連接請(qǐng)求
break;
default:
//如果沒處理,就交給回調(diào)onReceive處理
response = onReceive({ localInfo, remoteInfo, message });
}
//如果還沒處理,就不需要響應(yīng)了(不理睬)
if(!response) return;
//服務(wù)端響應(yīng)數(shù)據(jù)發(fā)給客戶端
udp.send({
address:remoteInfo.address,
port:remoteInfo.port,
message:toStringMesssage(response)
});
});
//綁定一個(gè)服務(wù)器端口
let port = udp.bind(config.port);
return {
getPort(){
return port;
},
close(){
udp.close();
}
};
}
有沒有覺得,服務(wù)端的處理邏輯很像web的服務(wù)器處理請(qǐng)求,處理響應(yīng)來自客戶端(瀏覽器)的請(qǐng)求
管理客戶端連接
再具體一點(diǎn),處理創(chuàng)建和保持連接請(qǐng)求的方法,將上面的代碼改一下,添加后的代碼如下
const Timeout = 6000;//超時(shí)6s
//...
let connectInfo;
let timer;
//保持連接(定時(shí)連接檢查)
const keepConnectInfo = function(){
timer = setTimeout(function(){
if(connectInfo){
let otime = Date.now()-connectInfo.utime;
//未超時(shí)
if(otime<Timeout){
keepConnectInfo();
onReceive({
//...回調(diào)接收事件,返回連接狀態(tài)
});
return;
}
}
connectInfo=null;
//若連接超時(shí)了,就回調(diào)斷開連接的方法
onDisconnect({ errMsg:'wait update is timeout'});
},Timeout);
};
//關(guān)閉定時(shí)器
const closeTimer = function(){
//...
};
switch(message?.intent){
case 'create_connect'://創(chuàng)建連接請(qǐng)求
{
let data = message.data;
connectInfo = {
//...記錄連接信息
};
//回調(diào)連接事件
onConnect({
//...傳連接數(shù)據(jù)
});
response = {
intent:message.intent,
//...返回連接后獲取的初始化數(shù)據(jù)
};
//防抖處理
closeTimer();
keepConnectInfo();
break;
}
case 'keep_connect'://保持連接請(qǐng)求
{
if(connectInfo) {
connectInfo.utime = Date.now();
response = {
intent:message.intent,
//...
};
}else{
response = {
intent:'create_connect',
//...返回配置數(shù)據(jù)
data:config,
};
}
//記錄時(shí)間差
if(message.ntime) response.utime = response.time - message.ntime;
break;
}
default:
//如果沒處理,就交給回調(diào)onReceive處理
response = onReceive({ localInfo, remoteInfo, message });
}
//...
可以看出來,這里連接的邏輯是定時(shí)檢查客戶端連接更新的狀態(tài),如果超過時(shí)間不更新,就判斷為連接超時(shí)
局域網(wǎng)廣播
兩個(gè)小程序之間是怎么知道對(duì)方的IP和端口呢,這就要借助廣播IP地址了,
發(fā)送廣播IP地址,就可以在局域網(wǎng)發(fā)現(xiàn)在線的設(shè)備,然后請(qǐng)求連接,廣播過程是這樣的
來打個(gè)比喻:
客戶端A(你)發(fā)送廣播消息(點(diǎn)餐訂單),交給路由器(平臺(tái))處理,路由器會(huì)轉(zhuǎn)發(fā)消息,附帶了你的IP(家)地址和端口(門號(hào)),到其余的客戶端(搶單),
如果有客戶端(搶到單的外賣服務(wù)員)對(duì)方想回應(yīng)你,就會(huì)給你發(fā)消息:你的外賣到了…(票據(jù)上有寫了對(duì)方的IP(店鋪)地址和端口(門牌號(hào)))
發(fā)送廣播
客戶端怎樣發(fā)送廣播呢,這個(gè)方法是sendBroadcast()
,很容易實(shí)現(xiàn),代碼如下
function sendBroadcast(e){
const { port, fail, success, showLoading } = e;
let udp = wx.createUDPSocket();
let timer;
let list = [];//記錄接收的列表
function complete(callback){
//...
udp.close();//處理完要關(guān)閉
callback();
}
if(fail instanceof Function){
udp.onError(function(err){
complete(function(){
fail(err);
});
});
}
udp.onMessage(function(res){
//將接收的消息轉(zhuǎn)換成json對(duì)象
res.message = toDataJSON(res.message);
list.push(res);
});
udp.bind();
//發(fā)送廣播消息,其中port是指定小程序的服務(wù)端接收端口
udp.send({
address:'255.255.255.255',
port: port,
message: JSON.stringify({ intent: 'scan' })
});
//加上定時(shí)
timer = setTimeout(function(){
//到時(shí)結(jié)束
complete(function(){
if(success instanceof Function) success({ list });
})
}, e.timeout || 3000);
//...
}
廣播IP是
255.255.255.255
,就是發(fā)給局域網(wǎng)內(nèi)的路由器,需要注意的是,不是所有的路由器都是支持廣播IP的,要確保支持它,需要登錄路由器的控制頁面,找看有沒有其中的隔離AP
項(xiàng),取消勾選即可
發(fā)送的廣播消息是
{ intent: 'scan' }
,需要從另一個(gè)小程序的服務(wù)端負(fù)責(zé)接收那方法onReceive()
中處理這個(gè)廣播消息,返回響應(yīng)數(shù)據(jù)
游戲聯(lián)機(jī)
實(shí)現(xiàn)游戲的聯(lián)機(jī)方式分兩種,上面開始講過,用其中的一個(gè)角色:服務(wù)端或者客戶端
主動(dòng)加入
一種是主動(dòng)加入游戲,就用客戶端發(fā)送問候消息方法sendMessage(e)
去請(qǐng)求服務(wù)端,服務(wù)端會(huì)處理響應(yīng)請(qǐng)求
加入前,客戶端需要先知道對(duì)方的IP地址和端口,從上面講得發(fā)送廣播方法
sendBroadcast(e)
來掃描一下,找到對(duì)方后,然后發(fā)送加入請(qǐng)求
在對(duì)方的服務(wù)端返回同意消息時(shí),附帶了游戲入口消息,告訴客戶端加入游戲的途徑
主動(dòng)等待
另一種,是主動(dòng)創(chuàng)建好游戲地盤,創(chuàng)建好服務(wù)端,用一個(gè)接收數(shù)據(jù)的綁定的端口,去負(fù)責(zé)監(jiān)聽客戶端發(fā)來的請(qǐng)求,然后處理響應(yīng)數(shù)據(jù)
這里注意不要搞錯(cuò),當(dāng)另一個(gè)小程序客戶端發(fā)來加入游戲的請(qǐng)求時(shí),要留點(diǎn)心,別把客戶端的端口號(hào)當(dāng)作服務(wù)端的端口號(hào)(接收數(shù)據(jù))
關(guān)于項(xiàng)目
好了,UDP通信的方法就講到這里,這樣有了大致的方向,
如需要看項(xiàng)目源碼的請(qǐng)?jiān)?下載列表點(diǎn)這里 找聯(lián)機(jī)游戲相關(guān)的項(xiàng)目,那些聯(lián)機(jī)游戲項(xiàng)目里都有應(yīng)用,有對(duì)應(yīng)的介紹,請(qǐng)放心下載,多謝支持,愿學(xué)有所獲!
打開項(xiàng)目源碼,如果遇到微信開發(fā)工具提示
Error: 登錄用戶不是該小程序的開發(fā)者
,需要替換項(xiàng)目的測試號(hào)替換為自己的,點(diǎn)擊開發(fā)工具右邊的詳情,里面有AppID,可修改替換,
這里有一個(gè)中國象棋-單機(jī)游戲開發(fā)流程詳解文章,需要的同學(xué)可以先看看,
這里是在原單機(jī)游戲項(xiàng)目的基礎(chǔ)上增加了聯(lián)機(jī)功能,聯(lián)機(jī)游戲運(yùn)行的動(dòng)圖效果如下
聯(lián)機(jī)測試
項(xiàng)目還沒有自己的測試號(hào),就前往申請(qǐng)一個(gè)測試號(hào),申請(qǐng)成功后,登錄如下圖,其中AppID的就是
- 申請(qǐng)測試號(hào) ??傳送門
開發(fā)工具上掃碼預(yù)覽出來的小程序是開發(fā)版,只能在自己的微信上體驗(yàn),想測試聯(lián)機(jī)游戲就這樣做,選擇里面的真機(jī)調(diào)試項(xiàng),這樣就可以模擬兩個(gè)用戶來體驗(yàn)了,一個(gè)在開發(fā)工具上模擬器上,另一個(gè)就是自己的真機(jī)微信上
如果有兩個(gè)手機(jī)微信就這樣試試,用兩個(gè)手機(jī)微信分別登錄開發(fā)工具弄一個(gè)開發(fā)版小程序來,這樣兩個(gè)手機(jī)微信上就能測試游戲聯(lián)機(jī),
上面操作有點(diǎn)麻煩,就用自己申請(qǐng)好的一個(gè)小程序來測試,發(fā)布一個(gè)體驗(yàn)版小程序就可以讓很多人參與測試了。文章來源:http://www.zghlxwxcb.cn/news/detail-440896.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-440896.html
到了這里,關(guān)于【聯(lián)機(jī)對(duì)戰(zhàn)】微信小程序聯(lián)機(jī)游戲開發(fā)流程詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!