【联机对战】微信小程序联机游戏开发流程详解

news2025/1/18 7:20:40

现有一个微信小程序叫中国象棋项目,棋盘类的单机游戏看着有缺少了什么,现在给补上了,加个联机对战的功能,增加了可玩性,对新手来说,实现联机游戏还是有难度的,那要怎么实现的呢,接下来给大家讲一下。

考虑到搭建联机游戏的服务器成本不小,第一个想法是用小程序的蓝牙功能实现游戏联机的,但是其API接口提供的蓝牙硬件支持兼容问题不少,暂时不去折腾了,现在采用UDP通信就很容易实现,可以在WIFI局域网内让两个以上小程序实现通信。

UDP通信

先来了解一下 UDP通信 的工作原理,这是一个面向无连接的传输协议,是UDP通信,与之对应的是 面向可连接的传输协议,是 TCP通信

client客户端 server服务端 发送报文 收到 client客户端 server服务端

从上图看出来,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()就是发送信号的意思,可以这样认为,在网络上冒个泡,可以让对方知道你在线,主动找你沟通,如果超过时间还不吐泡泡,就认为你潜水了(隐身)

请添加图片描述

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
中央路由器
客户端B
客户端C
其余客户端...

来打个比喻:

客户端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是255.255.255.255,就是发给局域网内的路由器,需要注意的是,不是所有的路由器都是支持广播IP的,要确保支持它,需要登录路由器的控制页面,找看有没有其中的隔离AP项,取消勾选即可

发送的广播消息是{ intent: 'scan' },需要从另一个小程序的服务端负责接收那方法onReceive()中处理这个广播消息,返回响应数据

游戏联机

实现游戏的联机方式分两种,上面开始讲过,用其中的一个角色:服务端或者客户端

主动加入

一种是主动加入游戏,就用客户端发送问候消息方法sendMessage(e)去请求服务端,服务端会处理响应请求

Hello World
...
客户端
服务端

加入前,客户端需要先知道对方的IP地址和端口,从上面讲得发送广播方法sendBroadcast(e)来扫描一下,找到对方后,然后发送加入请求
在对方的服务端返回同意消息时,附带了游戏入口消息,告诉客户端加入游戏的途径

主动等待

另一种,是主动创建好游戏地盘,创建好服务端,用一个接收数据的绑定的端口,去负责监听客户端发来的请求,然后处理响应数据

快到碗里来
...
服务端
客户端

这里注意不要搞错,当另一个小程序客户端发来加入游戏的请求时,要留点心,别把客户端的端口号当作服务端的端口号(接收数据)

关于项目

好了,UDP通信的方法就讲到这里,这样有了大致的方向,

如需要看项目源码的请在 下载列表点这里 找联机游戏相关的项目,那些联机游戏项目里都有应用,有对应的介绍,请放心下载,多谢支持,愿学有所获!

打开项目源码,如果遇到微信开发工具提示Error: 登录用户不是该小程序的开发者 ,需要替换项目的测试号替换为自己的,点击开发工具右边的详情,里面有AppID,可修改替换,

这里有一个中国象棋-单机游戏开发流程详解文章,需要的同学可以先看看,

这里是在原单机游戏项目的基础上增加了联机功能,联机游戏运行的动图效果如下
请添加图片描述

联机测试

项目还没有自己的测试号,就前往申请一个测试号,申请成功后,登录如下图,其中AppID的就是
在这里插入图片描述

  • 申请测试号 🔗传送门

开发工具上扫码预览出来的小程序是开发版,只能在自己的微信上体验,想测试联机游戏就这样做,选择里面的真机调试项,这样就可以模拟两个用户来体验了,一个在开发工具上模拟器上,另一个就是自己的真机微信上

如果有两个手机微信就这样试试,用两个手机微信分别登录开发工具弄一个开发版小程序来,这样两个手机微信上就能测试游戏联机,

上面操作有点麻烦,就用自己申请好的一个小程序来测试,发布一个体验版小程序就可以让很多人参与测试了。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/378631.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL表的增删查改(基础)

gitee:博客中的所有操作整合新增语法:insert [into] table_name values(value_list)[案例] 创建一个学生表进行数据插入1.1单行数据全列插入[提示]我们可以想在记事本上写下命令,让后复制到数据库客户端,这样可以在出错的时候进行快速修改.同时为了美观和明了,我们可以进行适当…

15个Spring扩展点,一般人知道的不超过5个!

Spring的核心思想就是容器&#xff0c;当容器refresh的时候&#xff0c;外部看上去风平浪静&#xff0c;其实内部则是一片惊涛骇浪&#xff0c;汪洋一片。Spring Boot更是封装了Spring&#xff0c;遵循约定大于配置&#xff0c;加上自动装配的机制。很多时候我们只要引用了一个…

苹果认证MFI学习

MFi是"Made for iPhone/iPod/iPad“的缩写。 MFi包含一系列技术或标准&#xff0c;你所开发的配件&#xff0c;要符合这个标准&#xff0c;才能够苹果设备如手机所认可&#xff0c;才能得到授权在市场销售。 比如蓝牙数据传输&#xff0c;就需要符合iAP2协议&#xff0c…

统计学习方法学习笔记——概论(一)

# 文章内容来自学习李航老师的《统计学习方法》后的总结&#xff0c;相当于学习笔记&#xff0c;若有不正确的地方还请大家指正。# 所需先验知识&#xff1a;概率论与数理统计、线性代数一、 统计学习概述可能有些小伙伴没有听过统计学习&#xff0c;但是我相信机器学习大家一定…

智慧物业管理系统【源码好优多】

简介 《智慧物业》一个免费开源的基于java的物业管理系统。未来将涵盖停车、安保、客服、工单、收费、财务、办公自动化等模块&#xff0c;构建一个软硬件一体的智慧物业解决方案。 功能 系统内置功能&#xff1a; 资产管理 商业区管理、楼栋管理、商铺管理、商铺租售 缴费…

【数电基础】——组合逻辑电路

目录 1.大纲 2.组合逻辑电路的特点 3.组合逻辑电路分析步骤 4.组合逻辑电路设计 5.组合逻辑电路中的竞争和冒险&#xff08;重要&#xff09; 6.怎样消除竞争冒险现象 7.常用的MSI组合逻辑器件 1.加法器 1.一位半加器 2.一位全加器 2.数据选择器 1.二选一数据选择器&#x…

spring容器,@Bean 与 @Component 用在同一个类上,会怎么样?

疑虑背景疑虑描述最近&#xff0c;在进行开发的过程中&#xff0c;发现之前的一个写法&#xff0c;类似如下以我的理解&#xff0c;Configuration 加 Bean 会创建一个 userName 不为 null 的 UserManager 对象&#xff0c;而 Component 也会创建一个 userName 为 null 的 UserM…

第七章 分词器:Text Analysis

1、分词器认知基础 1.1 基本概念 分词器官方称之为文本分析器,顾名思义,是对文本进行分析处理的一种手段,基本处理逻辑为按照预先制定的分词规则,把原始文档分割成若干更小粒度的词项,粒度大小取决于分词器规则。 1.2 分词发生时期 分词器的处理过程发生在 Index Tim…

【牛客刷题专栏】0x0D:JZ5 替换空格(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…

ICV光子盒:2023全球量子通信与安全产业发展展望

近日&#xff0c;全球著名的前沿科技咨询机构ICV与国内专注量子领域的行业研究机构光子盒&#xff0c;联合发布了2023全球量子通信与安全产业发展展望。报告主要从技术进展、产业生态、公司分析、网络建设、投资概况、政策发布、产业预测、展望观点的几方面对2023全球量子通信与…

【Java】Java进阶学习笔记(三)—— 面向对象(多态)

【Java】Java进阶学习笔记&#xff08;三&#xff09;—— 面向对象&#xff08;多态&#xff09;一、多态的概念1、多态的优点2、多态存在的三个必要条件3、多态中的成员特点4、重写方法的快捷键二、多态的转型1、向上转型2、向下转型3、代码示例4、转型的异常类型转换异常ins…

无线蓝牙耳机哪个品牌延迟低?玩游戏延迟低的蓝牙耳机推荐

无线蓝牙耳机因为摆脱了线的束缚&#xff0c;在使用上会更便捷&#xff0c;不少人喜欢戴蓝牙耳机玩游戏&#xff0c;但又怕蓝牙耳机有延迟。正因为蓝牙耳机摆脱了线的束缚&#xff0c;在信号传输的过程中难免产生延迟。那么&#xff0c;无线蓝牙耳机哪个品牌延迟低&#xff1f;…

【Linux】程序员的易筋经——冯诺依曼体系结构

文章目录&#x1f449;冯诺伊曼体系结构&#x1f448;概念内存的重要性&#x1f449;操作系统&#xff08;Operating System&#xff09;&#x1f448;概念目的定位特征发展和分类运行机制体系结构&#x1f449;冯诺伊曼体系结构&#x1f448; 概念 下图是描述冯诺依曼体系结…

6天重建一遍中国台湾省,三维模型还可以这样做!

说起三维模型&#xff0c;大家脑海中显现的大多是一个可通过电子屏幕进行全方位展示的立体物体。一般来说&#xff0c;所显示的物体既可以是现实世界的实体&#xff0c;也可以是通过想象所创作的虚构物体。 而实景三维正是镜像作用于现实世界真实化表达的新兴技术&#xff0c;是…

Java中的过滤器和拦截器

Java中的过滤器和拦截器 一.应用场景 拦截器应用场景 拦截器本质上是面向切面编程&#xff08;AOP&#xff09;&#xff0c;符合横切关注点的功能都可以放在拦截器中来实现&#xff0c;主要的应用场景包括&#xff1a; 登录验证&#xff0c;判断用户是否登录。权限验证&…

JSP的分页

分页在读取数据库里的数据需要用&#xff0c;在以后数据库肯定还会有很多数据&#xff0c;一个页面装不下&#xff0c;所以需要分页功能。数据库查询的分页语句是“SELECT * FROM emp LIMIT 0, 5;”这里0是指起始行&#xff0c;5是查询5行&#xff0c;第二页起始行就是5&#x…

QT 实现右键菜单

有时我们希望在窗口中右键弹出菜单&#xff0c;这里来介绍一下QT中怎么实现. .h 中添加事件相应函数声明和变量定义&#xff1a; private:// 菜单事件void contextMenuEvent(QContextMenuEvent* event) override;void initMenu();private:QMenu* m_pMenu nullptr;在构造函数中…

聊聊如何避免多个jar通过maven打包成一个jar,多个同名配置文件发生覆盖问题

前言 不知道大家在开发的过程中&#xff0c;有没有遇到这种场景&#xff0c;外部的项目想访问内部nexus私仓的jar&#xff0c;因为私仓不对外开放&#xff0c;导致外部的项目没法下载到私仓的jar&#xff0c;导致项目因缺少jar而无法运行。 通常遇到这种场景&#xff0c;常用…

HUN工训中心:开关电路和按键信号抖动

工训中心的牛马实验 1.实验目的&#xff1a; 1) 认识开关电路&#xff0c;掌握按键状态判别、开关电路中逻辑电平测量、逻辑值和逻辑函数电路。 2) 掌握按键信号抖动简单处理方法。 3) 实现按键计数电路。 2.实验资源&#xff1a; HBE硬件基础电路实验箱、示波器、万用表…

Java学习--网络编程

1. 网络编程入门 1.1 网络编程概述 计算机网络 是指将地理位置不同的具有独立功能的多台计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络操作系统&#xff0c;网络管理软件及网络通信协议的管理和协调下&#xff0c;实现资源共享和信息传递的计算机系统…