Vue+WebSocket+ES6+Canvas 製作【你畫我猜】小遊戲

Jrain發表於2018-12-11

寫於 2016.06.26

專案地址:github.com/jrainlau/dr…

下載 & 執行

git clone git@github.com:jrainlau/draw-something.git
cd draw-something

node ws-server.js // 開啟websocket伺服器

npm run dev  // 執行客戶端程式

然後瀏覽器開啟localhost:8080即可
複製程式碼

效果預覽:

Vue+WebSocket+ES6+Canvas 製作【你畫我猜】小遊戲

整體架構

因為閒得慌,一直和朋友在玩你畫我猜之類的小遊戲,突然想到能不能自己也做一個呢,反正閒著也是閒著,同時正好可以學習一下websocket的用法。

首先分析整體架構部分:

Vue+WebSocket+ES6+Canvas 製作【你畫我猜】小遊戲

可以看到,整體架構非常簡單,僅僅是一臺伺服器和兩個客戶端。

  • WebSocket伺服器:提供資料同步,內容分發功能,採用nodejs寫成。
  • 繪圖畫布:進行繪圖的區域,同時能夠獲取關鍵詞,其繪製的內容會同步到猜圖畫布中。
  • 猜圖畫布:同步自繪圖畫布,輸入框能夠提交關鍵詞,檢測答案是否正確。

下面來看具體的程式碼實現。

WebSocket伺服器

伺服器採用node.js進行搭建,使用了ws實現websocket功能。新建一個名為ws-socket.js的檔案,程式碼如下:

/*** ws-socket.js ***/

'use strict'
// 例項化WebSocketServer物件,監聽8090埠
const WebSocketServer = require('ws').Server
  , wss = new WebSocketServer({port: 8090})

// 定義關鍵詞陣列
let wordArr = ['Monkey', 'Dog', 'Bear', 'Flower', 'Girl']

wss.on('connection', (ws) => {
    console.log('connected.')
    
    // 隨機獲取一個關鍵詞
    let keyWord = ((arr) => {
            let num = Math.floor(Math.random()*arr.length)
            return arr[num]
        })(wordArr)
        
    // 當伺服器接收到客戶端傳來的訊息時
    // 判斷訊息內容與關鍵詞是否相等
    // 同時向所有客戶端派發訊息
    ws.on('message', (message) => {
        console.log('received: %s', message)
        if (message == keyWord) {
            console.log('correct')
            wss.clients.forEach((client) => {
                client.send('答對了!!')
            })
        } else {
            console.log('wrong')
            wss.clients.forEach((client) => {
                client.send(message)
            })
        }
    })
    
    // 伺服器初始化時即向客戶端提供一個關鍵詞
    wss.clients.forEach((client) => {
        client.send('keyword:' + keyWord)
    })
})
複製程式碼

使用方法基本按照ws的文件即可。其中ws.on('message', (message) => { .. })方法會在接收到從客戶端傳來訊息時執行,利用這個方法,我們可以從繪圖畫布不斷地向伺服器傳送繪圖位點的座標,再通過.send()方法把座標分發出去,在猜圖畫布中獲取座標,實現繪圖資料的同步。

客戶端結構

作為客戶端,我選擇了vue進行開發,原因是因為vue使用簡單快速。事先說明,本專案僅僅作為日常學習練手的專案而非vue的使用,所以有蠻多地方我是圖方便暴力使用諸如document.getElementById()之類的寫法的,以後有機會再改成符合vue審美的程式碼吧~

客戶端結構如下:

|
|-- script
|       |-- components
|       |        |-- drawing-board.vue
|       |        |-- showing-board.vue
|       |
|       |-- App.vue
|       |
|       |-- index.js
|
|-- index.html
複製程式碼

詳細程式碼請直接瀏覽專案,這裡僅對關鍵部分程式碼進行剖析。

繪圖畫布

位於./script/components/drawing-board.vue檔案即為繪圖畫布元件。首先我們定義一個Draw類,裡面是所有繪圖相關的功能。

/*** drawing-board.vue ***/


'use strict'

class Draw {
    constructor(el) {
        this.el = el
        this.canvas = document.getElementById(this.el)
        this.cxt = this.canvas.getContext('2d')
        this.stage_info = canvas.getBoundingClientRect()
        // 記錄繪圖位點的座標
        this.path = {
            beginX: 0,
            beginY: 0,
            endX: 0,
            endY: 0
        }
    }
    // 初始化
    init(ws, btn) {
        this.canvas.onmousedown = () => {
            this.drawBegin(event, ws)
        }
        this.canvas.onmouseup = () => {
            this.drawEnd()
            ws.send('stop')
        }
        this.clearCanvas(ws, btn)
    }
    
    drawBegin(e, ws) {
        window.getSelection() ? window.getSelection().removeAllRanges() : document.selection.empty()
        this.cxt.strokeStyle = "#000"
        
        // 開始新的路徑(這一句很關鍵,你可以註釋掉看看有什麼不同)
        this.cxt.beginPath()
        this.cxt.moveTo(
            e.clientX - this.stage_info.left,
            e.clientY - this.stage_info.top
        )
        // 記錄起點
        this.path.beginX = e.clientX - this.stage_info.left
        this.path.beginY = e.clientY - this.stage_info.top

        document.onmousemove = () => {
            this.drawing(event, ws)
        }
    }
    
    drawing(e, ws) {
        this.cxt.lineTo(
            e.clientX - this.stage_info.left,
            e.clientY - this.stage_info.top
        )
        // 記錄終點
        this.path.endX = e.clientX - this.stage_info.left
        this.path.endY = e.clientY - this.stage_info.top
        // 把點陣圖座標傳送到伺服器
        ws.send(this.path.beginX + '.' + this.path.beginY + '.' + this.path.endX + '.' + this.path.endY)

        this.cxt.stroke()
    }
    
    drawEnd() {
        document.onmousemove = document.onmouseup = null
    }
    
    clearCanvas(ws, btn) {
        // 點選按鈕清空畫布
        btn.onclick = () => {
            this.cxt.clearRect(0, 0, 500, 500)
            ws.send('clear')
        }
    }
}
複製程式碼

嗯,相信看程式碼很容易就看懂了當中邏輯,關鍵就是在drawing()的時候要不斷地把座標傳送到伺服器。

定義好Draw類以後,在ready階段使用即可:

ready: () => {
        const ws = new WebSocket('ws://localhost:8090')
        let draw = new Draw('canvas')
        // 清空畫布按鈕
        let btn = document.getElementById('btn')
        // 與伺服器建立連線後執行
        ws.onopen = () => {
            draw.init(ws, btn)
        }
        // 判斷來自伺服器的訊息並操作
        ws.onmessage = (msg) => {
            msg.data.split(':')[0] == 'keyword' ?
                document.getElementById('keyword').innerHTML = msg.data.split(':')[1] :
                false
        }
    }
複製程式碼

猜圖畫布

猜圖畫布很簡單,只需要定義一個canvas畫布,然後接收伺服器傳送來的座標並繪製即可。看程式碼:

ready: () => {
            'use strict'
            const ws = new WebSocket('ws://localhost:8090');
            const canvas = document.getElementById('showing')
            const cxt = canvas.getContext('2d')
            // 是否重新設定路徑起點
            // 為了避免把路徑起點重複定義在同一個地方
            let moveToSwitch = 1
            ws.onmessage = (msg) => {
              let pathObj = msg.data.split('.')
              cxt.strokeStyle = "#000"
              
              if (moveToSwitch && msg.data != 'stop' && msg.data != 'clear') {
                  cxt.beginPath()
                  cxt.moveTo(pathObj[0], pathObj[1])
                  moveToSwitch = 0
              } else if (!moveToSwitch && msg.data == 'stop') {
                  cxt.beginPath()
                  cxt.moveTo(pathObj[0], pathObj[1])
                  moveToSwitch = 1
              } else if (moveToSwitch && msg.data == 'clear') {
                  cxt.clearRect(0, 0, 500, 500)
              } else if (msg.data == '答對了!!') {
                  alert('恭喜你答對了!!')
              }

              cxt.lineTo(pathObj[2], pathObj[3])
              cxt.stroke()
            }

            ws.onopen = () => {
                let submitBtn = document.getElementById('submit')
                // 傳送答案到伺服器
                submitBtn.onclick = () => {
                    let keyword = document.getElementById('answer').value
                    ws.send(keyword)
                }
            }
        }
複製程式碼

到這裡,遊戲已經可以玩啦!不過還有很多細節是有待加強和修改的,比如可以給畫筆選擇顏色啊,多個使用者搶答計分啊等等。

後記

大半天時間鼓搗出來的玩意兒,雖然粗糙,但是學到的東西還真不少,尤其是websocket和canvas這兩個我所不熟悉的領域,果然實踐才能出真知。

選擇ES6真的能夠極大地提升工作效率,Class語法的出現簡直不能更贊,作為才學習jQuery原始碼沒多久的我來說,ES6真的非常小清新。

歡迎持續關注我的專欄,會不斷送出乾貨哦,盡請期待!

相關文章