【小程序】websocket实现“谁是卧底”在线随机发牌

目录

前言

1. 逻辑分析

2. websocket

3. 小程序端代码实现

4.服务端代码实现

后记


前言

有兴趣的同学先扫码体验一下小程序

继我的个人小程序(“你划我猜出题器”)上线第二版本(自建词库)后,又有新的想法涌现出来,做一个“谁是卧底”在线随机发牌吧(有时间再写一下第一个版本跟第二个版本的博文)。既然如此,就要思考一下“谁是卧底”的技术实现点。

词的来源,可利用现有词库管理系统。接下来的难点就是如何实现在线随机发牌。很明显,http请求无法实现“谁是卧底”的发牌,应该是服务端主动推送词语给各个客户端。

websocket能够很好的实现这个功能。

小程序websocket传送门:https://developers.weixin.qq.com/miniprogram/dev/api/network/websocket/wx.sendSocketMessage.html

 

1. 逻辑分析

 开发惯例,整理基本逻辑。“谁是卧底”的发牌逻辑相对简单。参考微信的“面对面建群”概念,我化用成“面对面建房”。

(1)房的概念用来隔离发牌的环境,每一个房就是一个单独的游戏环境,用过的词不再出现。

(2)房内的所有用户,第一个进入房的称为房主,拥有控制发牌权,其他用户无。

(3)客户端与服务端的交互组成:①建立websocket连接;②用户进入房间;③发牌;④用户离开房间;⑤断开websocket连接。

 

2. websocket

 客户端需要处理三个触发事件,三个监听事件。

(1)用户进入房间,通知服务端;

(2)用户离开房间,通知服务端;

(3)房主点击“开始发牌”,通知服务端。

对应的,客户端同样需要监听三个事件。

(1)服务端通知其他用户进入房间;

(2)服务端通知其他用户离开房间;

(3)服务端发牌。

看了websocket的API,它不似socket.io那般可以拆分事件去监听,去触发,而是统一接收服务端数据,统一发送数据给服务端,而且也不能传送对象数据,websocket只能传送字符串和二进制数据。

所以,监听和触发用onSocketMessage和sendSocketMessage统一处理的话,需要自定义对象参数来区分每个事件。这里用到JSON.stringify和JSON.parse,将对象转化为字符串,将字符串转化为对象。

    wx.sendSocketMessage({  // 发送消息给服务端
      data: JSON.stringify(obj), // 特别注意!!!websocket只接收string或ArrayBuffer
      success(res) {
        console.log('sendSocketMessage发送消息至服务器成功', res)
      },
      fail(err) {
        console.error('sendSocketMessage发送消息至服务器报错', err)
      }
    })
    wx.onSocketMessage(function (res) { // 接收服务端下发的消息
      let obj = JSON.parse(res.data)  // 特别注意!!res只能是string/ArrayBuffer
      console.log('这是来自服务器的消息')
    })

“谁是卧底”websocket传送数据的结构

    let obj = {
      event: eventName, // 事件
      roomKey: roomKey, // 房间号
      data: data, // 数据,用户信息或者词牌信息
    }

/** @event
* 'createRoom' 进入房间-客户端
* 'leaveRoom' 离开房间-客户端
* 'deliverPocker' 开始发牌-客户端
* 'intoRoom' 进入房间-服务端
* 'leaveRoom' 离开房间-服务端
* 'revivePocker' 发牌-服务端
*/

 

3. 小程序端代码实现

3.1 准备roomKey和用户信息

点击"面对面建房"按钮申请用户授权,获取用户头像和昵称,将信息保存在客户端storage中。输入四位数字房号,将房号保存在客户端storage中,跳转到游戏页面,websocket处理均在游戏页面中处理。

3.2 建立websocket链接

进入游戏页面,在onReady生命周期函数中建立websocket连接,并且通知服务端“用户进入房间”

  onReady() {
    console.log('onReady---------')
    let that = this
    that.setData({
      roomKey: wx.getStorageSync('roomKey'),
      user: wx.getStorageSync('userInfo')
    })
    that.connect()  // 建立websocket连接
    wx.setNavigationBarTitle({
      title: '发牌房间' + that.data.roomKey
    })
  },

  connect() {
    let that = this
    wx.connectSocket({     // 建立websocket连接
      url: 'wss://www.*****.****/'   // wss地址
    })
    wx.onSocketOpen(function (res) {  // 建立连接成功
      that.setData({
        connectStatus: 1
      })
      console.log('websocket 已经连接服务器', res)
      that.send('createRoom', that.data.roomKey, that.data.user)  // 通知服务端用户进入房间
    })
  },

3.3 封装send函数,其作用——发送数据给服务端

  send(eventName, roomKey, data) {
    let obj = {
      event: eventName, // 事件
      roomKey: roomKey, // 房间号
      data: data, // 传送数据
    }
    wx.sendSocketMessage({
      data: JSON.stringify(obj),
      success(res) {
        console.log('sendSocketMessage发送消息至服务器成功', res)
      },
      fail(err) {
        console.error('sendSocketMessage发送消息至服务器报错', err)
      }
    })
  },

3.4 关闭websocket连接

在onUnload生命周期函数里处理“离开房间”事件,并且关闭websocket连接

  onUnload() {
    console.log('onUnload---------')
    this.close()
  },
  close() {
    this.send('leaveRoom', this.data.roomKey, this.data.user)
    let that = this
    wx.closeSocket()  // 关闭websocket连接
    wx.onSocketClose(function (res) {   // 关闭成功
      that.setData({
        connectStatus: 0
      })
      console.log('websocket服务器已经断开', res)
    })
  },

3.5 在onLoad生命周期函数里监听服务端发送的消息

  onLoad() {
    console.log('onLoad---------')
    let that = this
    let key = wx.getStorageSync('roomKey')
    wx.onSocketMessage(function (res) {  // 接收服务端下发的消息
      let obj = JSON.parse(res.data)  // 将字符串转化为对象
      console.log('这是来自服务器的消息', obj.event, obj.roomKey, obj.data)
      if (obj.event == 'intoRoom' && key == obj.roomKey) {
        that.resetUsers(obj.data)  // 更新当前房间里的用户列表视图
      }
      if (obj.event == 'leaveRoom' && key == obj.roomKey) {
        that.resetUsers(obj.data) // 更新当前房间里的用户列表视图
      }
      if (obj.event == 'revivePocker' && key == obj.roomKey) {
        that.deliverPocker(obj.data) // 更新牌面词语
      }
    })
  },
  resetUsers(users) {
    this.setData({
      users: users,
      isOwner: false
    })
    for (let i = 0; i < users.length; i++) {
      if (users[i].isOwner && (users[i].nickName == this.data.user.nickName)) { // 是否是房主
        this.setData({
          isOwner: true
        })
      }
    }
  },
  deliverPocker(users) {
    let myuser = wx.getStorageSync('userInfo')
    for (let i = 0; i < users.length; i++) {
      if (users[i].nickName == myuser.nickName) {
        this.setData({
          pocker: users[i].pocker
        })
        this.showWord()
        break;
      }
    }
  },

 

4.服务端代码实现

服务端主要用node的ws实现

const ws = require('ws');

(1)每个用户建立websocket连接,需要保存该连接

// https服务
const serve = https.createServer(options, app.callback()).listen(config.port, (err) => {
  if (err) {
    console.log('服务启动出错', err);
  } else {
    db.connect();  // 数据库连接
    console.log('guessWord-server运行在' + config.port + '端口');
  }
});

// wss服务
let clients = []  // 客户端websocket连接队列
let userIndex = 0  // 客户数
let undercoverWords = null  // 谁是卧底词库
const wss = new ws.Server({ server: serve })
wss.on('connection', function (wxConnect) {
  clients.push({
    "ws": wxConnect,
    "nickname": 'userIndex' + (userIndex++) 
  });
  console.log('wss connection wxConnect ------ ')
  WordAPI.undercover().then(res => {  // 获取“谁是卧底”词库
    if (res.code == 200) {
      let arr = res.data
      arr.sort(function () { return 0.5 - Math.random() }) // 打乱题库
      undercoverWords = arr
    } else {
      console.error(res.message)
    }
  })
  wxConnect.on('message', function (msg) { // 监听客户端发送的消息
    let obj = JSON.parse(msg)
    // console.log('接收来至客户端的信息', obj.event, obj.roomKey, obj.data.nickName)
    if (obj.event == 'createRoom') { // 进入房间
      createRoom(obj.roomKey, obj.data)
    }
    if (obj.event == 'leaveRoom') { // 离开房间
      leaveRoom(obj.roomKey, obj.data)
    }
    if (obj.event == 'deliverPocker') { // 房主发牌
      deliverPocker(obj.roomKey)
    }
  })
})

(2)封装服务端的广播函数(通知所有客户端)

// 广播所有客户端消息
function broadcastSend(event, roomKey, data) {
  clients.forEach(function (v, i) {
    if (v.ws.readyState === ws.OPEN) {
      v.ws.send(JSON.stringify({
        event: event,
        roomKey: roomKey,
        data: data
      }));
    }
  })
}

(3)每个用户进入房内,服务端需要往该房的用户列表里增加数据,并通知客户端

function createRoom(roomKey, user) {
  if (rooms[roomKey]) { // 房间已存在,加入房间
    rooms[roomKey].users.push(user)
  } else { // 房间不存在,创建房间
    user['isOwner'] = true
    rooms[roomKey] = {
      users: [user],
      games: []
    }
  }
  broadcastSend('intoRoom', roomKey, rooms[roomKey].users)
  console.log(user.nickName + '进入房间' + roomKey + ',当前房间人数' + rooms[roomKey].users.length)
}

(4)房主每点击一次“开始发牌”,服务端需要进行三个随机处理:

①随机抽取一组词语作为本轮游戏词

②随机决定哪个词作为卧底词

③从房内用户随机分配卧底身份

分配完毕,每个用户拥有自己的身份词,就可以将词下发给客户端了。

function deliverPocker(roomKey) { // 发牌
  // 排除已发过的牌
  let g = rooms[roomKey].games
  let current = []
  for (let i = 0; i < undercoverWords.length; i++) {
    if (g.indexOf(undercoverWords[i].title) == -1) {
      current.push(undercoverWords[i].title)
    }
  }
  // 打乱题库
  current.sort(function () { return 0.5 - Math.random() })
  if (current.length == 0) { // 牌已发完
    broadcastSend('revivePocker', roomKey, '')
  } else {
    // 随机取出牌,并记录
    let pocker = current[0]
    rooms[roomKey].games.push(pocker)
    // 决定哪个词是卧底牌
    let arr = pocker.split(',')
    let random = Math.random() // 随机数
    let wodi = arr[0] // 卧底牌
    let pingm = arr[1] // 平民牌
    if (random > 0.5) {
      wodi = arr[1]
      pingm = arr[0]
    }
    // 决定哪几个人是卧底牌
    let ul = rooms[roomKey].users.length // 当前房间参与游戏的人数
    // 3-5人则1个卧底,6-8则2个卧底,9-11人则3个卧底,12人以上4个卧底
    let wodiIndex = []
    if (ul >= 3 && ul <= 5) {
      wodiIndex.push(randomNum(0, ul - 1))
    }
    if (ul >= 6 && ul <= 8) {
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
    }
    if (ul >= 9 && ul <= 11) {
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
    }
    if (ul >= 12) {
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
      wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))
    }
    // 分发牌面
    let u = rooms[roomKey].users
    for (let i = 0; i < ul; i++) {
      if (wodiIndex.indexOf(i) != -1) {
        u[i]['pocker'] = wodi
      } else {
        u[i]['pocker'] = pingm
      }
    }
    broadcastSend('revivePocker', roomKey, u)
  }
}
// 生成从minNum到maxNum的随机数
function randomNum(minNum, maxNum) {
  switch (arguments.length) {
    case 1:
      return parseInt(Math.random() * minNum + 1, 10);
    case 2:
      return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
    default:
      return 0;
  }
}
// 生成从minNum到maxNum的随机数,但不含已有的数组
function getRandomNoRepeat(minNum, maxNum, had) {
  let i = randomNum(minNum, maxNum);
  if (had.indexOf(i) === -1) {
    return i;
  }
  return getRandomNoRepeat(minNum, maxNum, had);
}

(5)每个用户离开房,需要删除房内该用户,并通知客户端,当房内人数为0,需要销毁房

function leaveRoom(roomKey, user) {
  let u = rooms[roomKey].users
  let index = -1
  for (let i = 0; i < u.length; i++) {
    if (u.nickName == user.nickName) {
      index = i
      break
    }
  }
  rooms[roomKey].users.splice(index, 1)
  if (rooms[roomKey].users.length == 0) {
    delete rooms[roomKey]
    broadcastSend('leaveRoom', roomKey, rooms[roomKey].users)
  } else {
    if (index == 0) {
      rooms[roomKey].users[0]['isOwner'] = true
    }
    broadcastSend('leaveRoom', roomKey, rooms[roomKey].users)
  }
  if (rooms[roomKey]) {
    console.log(user.nickName + '离开房间' + roomKey + ',当前房间人数' + rooms[roomKey].users.length)
  } else {
    console.log(user.nickName + '离开房间' + roomKey + ',当前房间人数为0,房间已被销毁')
  }
}

走到这里,全部流程已经走通~~~~

后记

服务端代码可以优化一波~~~~有待优化,等下波优化再更新

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
小程序中使用 WebSocket 实现实时聊天的步骤如下: 1. 在小程序 app.json 文件中添加网络权限: ```json { "permission": { "scope.userLocation": { "desc": "获取用户地理位置信息" }, "scope.record": { "desc": "录音功能,用于发送语音消息" }, "scope.writePhotosAlbum": { "desc": "保存到相册功能,用于保存聊天图片" }, "scope.userInfo": { "desc": "获取用户信息,用于显示聊天用户头像和昵称" }, "scope.websocket": { "desc": "使用 WebSocket,用于实现实时聊天" } } } ``` 2. 在小程序页面中创建 WebSocket 连接: ```javascript // 建立连接 let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url', success() { console.log('WebSocket 连接成功') }, fail() { console.log('WebSocket 连接失败') } }) // 监听 WebSocket 连接状态变化 socketTask.onOpen(() => { console.log('WebSocket 已连接') }) socketTask.onError(() => { console.log('WebSocket 连接错误') }) socketTask.onClose(() => { console.log('WebSocket 已关闭') }) ``` 3. 发送和接收消息: ```javascript // 发送消息 socketTask.send({ data: 'Hello, WebSocket!', success() { console.log('消息发送成功') }, fail() { console.log('消息发送失败') } }) // 接收消息 socketTask.onMessage((res) => { console.log('接收到消息:', res.data) }) ``` 完整的实时聊天代码示例: ```javascript Page({ data: { messages: [] // 聊天消息列表 }, onLoad: function() { // 建立连接 let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url', success() { console.log('WebSocket 连接成功') }, fail() { console.log('WebSocket 连接失败') } }) // 监听 WebSocket 连接状态变化 socketTask.onOpen(() => { console.log('WebSocket 已连接') }) socketTask.onError(() => { console.log('WebSocket 连接错误') }) socketTask.onClose(() => { console.log('WebSocket 已关闭') }) // 接收消息 socketTask.onMessage((res) => { console.log('接收到消息:', res.data) let message = JSON.parse(res.data) let messages = this.data.messages messages.push(message) this.setData({ messages: messages }) }) }, // 发送消息 sendMessage: function(event) { let message = event.detail.value if (message) { let socketTask = wx.connectSocket({ url: 'wss://your.websocket.url' }) socketTask.send({ data: message, success() { console.log('消息发送成功') }, fail() { console.log('消息发送失败') } }) this.setData({ inputMessage: '' }) } } }) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值