目录
前言
1. 逻辑分析
2. websocket
3. 小程序端代码实现
4.服务端代码实现
后记
有兴趣的同学先扫码体验一下小程序
继我的个人小程序(“你划我猜出题器”)上线第二版本(自建词库)后,又有新的想法涌现出来,做一个“谁是卧底”在线随机发牌吧(有时间再写一下第一个版本跟第二个版本的博文)。既然如此,就要思考一下“谁是卧底”的技术实现点。
词的来源,可利用现有词库管理系统。接下来的难点就是如何实现在线随机发牌。很明显,http请求无法实现“谁是卧底”的发牌,应该是服务端主动推送词语给各个客户端。
websocket能够很好的实现这个功能。
小程序websocket传送门:https://developers.weixin.qq.com/miniprogram/dev/api/network/websocket/wx.sendSocketMessage.html
开发惯例,整理基本逻辑。“谁是卧底”的发牌逻辑相对简单。参考微信的“面对面建群”概念,我化用成“面对面建房”。
(1)房的概念用来隔离发牌的环境,每一个房就是一个单独的游戏环境,用过的词不再出现。
(2)房内的所有用户,第一个进入房的称为房主,拥有控制发牌权,其他用户无。
(3)客户端与服务端的交互组成:①建立websocket连接;②用户进入房间;③发牌;④用户离开房间;⑤断开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.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;
}
}
},
服务端主要用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,房间已被销毁')
}
}
走到这里,全部流程已经走通~~~~
服务端代码可以优化一波~~~~有待优化,等下波优化再更新