【小程序】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,房間已被銷燬')
  }
}

走到這裏,全部流程已經走通~~~~

後記

服務端代碼可以優化一波~~~~有待優化,等下波優化再更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章