微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

Vue+WebSocket+ES6+Canvas 制作「你画我猜」小游戏

2016-07-11 20:28 前端大全

(点击上方公众号,可快速关注)

来源:jrainlau

链接:https://segmentfault.com/a/1190000005804860


项目地址:https://github.com/jrainlau/draw-something


下载 & 运行


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

cd draw-something && npm install

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真的非常小清新。



【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!


 
前端大全 更多文章 50W+年薪,用友友金所招大牛啦! 也谈前端面试常见问题之「数组乱序」 用 JavaScript 构建一个3D引擎 浅谈浏览器http的缓存机制 Web开发的发展史
猜您喜欢 敏捷破冰之旅(五) 实验楼11.11 || 全年仅此一天的特惠!! 创业电商产品四大要点【创业+产品】 Connector中的SSL属性如何实现的 超强通用的React Native Tab控制器使用详解-入门篇