寫於 2016.06.26
下載 & 執行
git clone git@github.com:jrainlau/draw-something.git
cd draw-something
node ws-server.js // 開啟websocket伺服器
npm run dev // 執行客戶端程式
然後瀏覽器開啟localhost:8080即可
複製程式碼
效果預覽:
整體架構
因為閒得慌,一直和朋友在玩你畫我猜之類的小遊戲,突然想到能不能自己也做一個呢,反正閒著也是閒著,同時正好可以學習一下websocket的用法。
首先分析整體架構部分:
可以看到,整體架構非常簡單,僅僅是一臺伺服器和兩個客戶端。
- 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真的非常小清新。
歡迎持續關注我的專欄,會不斷送出乾貨哦,盡請期待!