目錄
前言
有興趣的同學先掃碼體驗一下小程序
繼我的個人小程序(“你劃我猜出題器”)上線第二版本(自建詞庫)後,又有新的想法湧現出來,做一個“誰是臥底”在線隨機發牌吧(有時間再寫一下第一個版本跟第二個版本的博文)。既然如此,就要思考一下“誰是臥底”的技術實現點。
詞的來源,可利用現有詞庫管理系統。接下來的難點就是如何實現在線隨機發牌。很明顯,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,房間已被銷燬')
}
}
走到這裏,全部流程已經走通~~~~
後記
服務端代碼可以優化一波~~~~有待優化,等下波優化再更新