canvas+websocket+vue做一個完整的你畫我猜小遊戲

buppt發表於2019-03-03

第一次在掘金髮文章,瑟瑟發抖。

這個主要是為了學習使用一下canvas和websocket,專案地址。求star~

你畫我猜大家應該都玩過,一個人畫,其他人猜。目前實現了最基本的功能,慢慢修改。

專案截圖

canvas+websocket+vue做一個完整的你畫我猜小遊戲
canvas+websocket+vue做一個完整的你畫我猜小遊戲

完成進度

  • 登入,登入後username儲存到了sessionStorage中。
  • 座位,登入後可以選擇座位,並通過ws告訴所有人你的座位。
  • 傳送內容,登入後可以通過ws將輸入內容釋出給所有人。
  • 聊天記錄,可以接收所有人的聊天打字內容,可以清屏。
  • 開始遊戲,一樓可以點選開始遊戲按鈕,開始遊戲後不可再調整座位。
  • 按座位順序輪流進行畫圖與看圖猜詞,畫圖玩家不可以輸入內容與傳送內容。
  • 隨機在詞表中選擇詞語。用於練習只寫了12個詞。
  • 傳送內容為正確詞時特殊顯示(遊戲開始後有效)。
  • 記分
  • 多房間遊戲
  • 移動端

客戶端

前端從前面的截圖可以看到,基本分為四部分,畫圖部分,選擇座位部分,聊天顯示部分,傳送聊天部分。四部分當然要使用一個websocket連線,所以使用了vuex,同時在vuex中儲存使用者名稱等資料。

建立ws連線,存入vuex中。

this.ws = new WebSocket(`ws://localhost:3000/`);
this.$store.commit(`wsStore/connect`,ws);
複製程式碼

登入部分

這裡登入不太重要,就直接把使用者名稱放到sessionStorage裡了,同時再存入vuex中供其他部分使用。

saveName(){
    if(!window.sessionStorage){
        alert("瀏覽器不支援sessionStorage!");
    }else{
        let storage = window.sessionStorage;
        storage.setItem("username",this.inputName);
    }
    this.username=this.inputName;
    this.showLogin=0;
    this.$store.commit(`username/setUsername`,this.inputName)
}
複製程式碼

完整程式碼可以檢視github裡的src/components/HomeHeader.vue檔案。

畫圖部分

這部分是最主要的部分,先定義一個canvas

<canvas id="drawBoard" width="700" height="400"></canvas>
複製程式碼

然後是一個class draw,這裡只是使用畫筆的函式,從上面gif可以看出來還可以畫直線,矩形,圓,調整顏色等,最後也都是寫到這個類裡面的。

class canvasDraw{
	constructor(id,ws){
		this.ws= ws;
		this.canvas = document.getElementById(id);
		this.ctx = this.canvas.getContext(`2d`);
		this.stage_info = this.canvas.getBoundingClientRect()
		this.path = {
			beginX: 0,
			beginY: 0,
			endX: 0,
			endY: 0
		}
		this.isDraw=false
	}
	draw(){
		let that = this
		this.canvas.onmousedown = () => {
			that.ctx.beginPath()
			that.path.beginX = event.pageX - that.stage_info.left
			that.path.beginY = event.pageY - that.stage_info.top
			that.ctx.moveTo(
				that.path.beginX,
				that.path.beginY
			)
			that.isDraw=true
		}
		this.canvas.onmousemove = () => {
			if(that.isDraw){
				that.drawing(event)
			}
		}
		this.canvas.onmouseup = () => {
			that.isDraw=false
			that.ws.send(`stop,`)
		}
	}
	drawing(e) {
		this.path.endX = e.pageX - this.stage_info.left
		this.path.endY = e.pageY - this.stage_info.top
		this.ctx.lineTo(
			this.path.endX,
			this.path.endY
		)
		this.ctx.stroke()
		this.ws.send(`draw,${this.path.beginX},${this.path.beginY},${this.path.endX},${this.path.endY}`)

	}
	clearCanvas(){
		this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height); 
		this.ws.send(`clear,`)
	}
}
複製程式碼

可以看到,建構函式主要是儲存起始和終點的座標,雖然畫筆可以隨便畫,但是也是由一小段一小段線段組成的。

然後draw函式,當監聽到mousedown事件後,新建一條路徑,將起始座標儲存下來,隨著mousemove事件,滑鼠每移動一點,就畫一條線段,直到監聽到mouseup事件,完成一次畫線。

畫直線、矩形、圓的原理差不多,都是儲存滑鼠的起始和終點座標,然後算一下矩形的長和寬,圓的圓心和半徑即可。以畫圓的程式碼為例。

drawRound(){
	this.canvas.onmousedown=()=>{
		this.ctx.beginPath()
		this.path.beginX = event.pageX - this.stage_info.left
		this.path.beginY = event.pageY - this.stage_info.top
	}
	this.canvas.onmouseup=()=>{
		this.path.endX = event.pageX - this.stage_info.left
		this.path.endY = event.pageY - this.stage_info.top
		let width = this.path.endX-this.path.beginX;
		let height = this.path.endY-this.path.beginY;
		this.ctx.arc(this.path.beginX+width/2, this.path.beginY+height/2,Math.sqrt(width*width+height*height)/2,0, Math.PI *2);
		this.ctx.fill();
		this.ctx.stroke();
	}
}
複製程式碼

改變線條寬度、顏色等就更簡單了,只要將選擇的數值傳到類中的如下函式裡即可。

changeColor(color){
	this.ctx.strokeStyle = color;
}
複製程式碼

完整程式碼可以檢視github裡的src/components/DrawBoard.vue檔案。

座位部分

這一部分就沒什麼了,就點選button,做一下判斷就好了。例如

seatDown(index){
	if(this.$store.state.wsStore.ws.readyState==3){
		this.$message(`伺服器未連線`);
		return ;
	}
	if(this.username==null||this.username==``){
		this.$message(`請登陸`);
	}else if(this.seats[index]==`空位`){
		this.$store.state.wsStore.ws.send(`seats,${index},${this.username}`);
	}else{
		this.$message(`此位置有人`);
	}
},
複製程式碼

如果是一樓的話,可以開始遊戲,開始遊戲就給後端發一個訊號,然後就是後端的一些邏輯了。

begin(){
	this.$store.state.wsStore.ws.send(`begin,`);
},
複製程式碼

還有,如果遊戲開始之後,不可以再調整座位或者新使用者坐下。

watch:{
	beginGame(newval,oldval){
		let buttonEle = document.querySelectorAll(`button[component="seat"]`);
		if(this.beginGame){
			for(let bt of buttonEle){
				bt.setAttribute(`disabled`,`disabled`);
			}
		}else{
			for(let bt of buttonEle){
				bt.removeAttribute(`disabled`);
			}
		}
	}
},
複製程式碼

完整程式碼可以檢視github裡的src/components/TheSeats.vue檔案。

聊天顯示部分

聊天顯示部分只負責將接受到的伺服器的訊息顯示出來,可以清屏。

<div v-for="(record,index) in chatRecords" :key="index" class="record">
    <span class="blue">{{record.username}}</span>:{{record.content}}
</div>
watch:{
      msg(newval,oldval){
        let record = {
            username: this.msg[1],
            content: this.msg[2]
        };
        this.chatRecords.push(record);
        let div = document.getElementById(`totalBoard`)
        div.scrollTop = div.scrollHeight;
        console.log(div.scrollTop,div.scrollHeight+100)
      }
  },
複製程式碼

完整程式碼可以檢視github裡的src/components/ChatBoard.vue檔案。

聊天輸入部分

聊天輸入部分只負責將資料傳送給伺服器,然後伺服器再傳送給所有使用者,上面的聊天顯示部分接受到之後顯示出來。

submitWord(){
    if(this.$store.state.wsStore.ws.readyState==3){
        this.$message(`伺服器未連線`);
        return ;
    }
    if(this.username==null||this.username==``){
        this.$message(`請登陸`);
        return ;
    }
    if(this.guassWord!=``){
        this.$store.state.wsStore.ws.send(`chat,${this.username},${this.guassWord}`);
        this.guassWord=``;
    }
}
複製程式碼

還有如果輪到自己畫的話,不可以傳送資訊。

watch:{
   drawuser(newval,oldval){
       let inputEle = document.querySelector(`input[component="inputBoard"]`);
       let buttonEle = document.querySelector(`button[component="inputBoard"]`);
       if(this.drawuser==this.username){
           inputEle.setAttribute(`disabled`,`disabled`)
           buttonEle.setAttribute(`disabled`,`disabled`);
       }else{
           inputEle.removeAttribute(`disabled`);
           buttonEle.removeAttribute(`disabled`);
       }
   }
},
複製程式碼

完整程式碼可以檢視github裡的src/components/InputBoard.vue檔案。

服務端

服務端使用了node的ws模組,還在學習中,寫的比較菜,只是滿足了遊戲需要。

主要是將使用者畫圖時的起始和終點座標做一個轉發。

有使用者坐下或調整座位後傳送一下最終的座位資訊。

遊戲開始後,按座位順序,輪流給每個人傳送畫圖人和要畫的詞。

使用者輸入後判斷是否為正確詞,如果不對,就傳送原詞,如果猜對了,就傳送“猜對了”

// 匯入WebSocket模組:
const WebSocket = require(`ws`);

// 引用Server類:
const WebSocketServer = WebSocket.Server;

// 例項化:
const wss = new WebSocketServer({
    port: 3000
});

let seats=[`空位`,`空位`,`空位`,`空位`,`空位`,`空位`]
let dict=[`老鼠`,`牛`,`老虎`,`兔子`,`龍`,`蛇`,`馬`,`羊`,`猴子`,`雞`,`狗`,`豬`]

let gameBegin = false;
let guassWord = ``;
wss.on(`connection`, function (ws) {
    console.log(`[SERVER] connection()`);
    ws.on(`message`, function (message) {
        console.log(`[SERVER] Received: ${message}`);
        let msg = message.split(`,`);
        if(msg[0]=="seats"&&msg.length==3){
            let i = seats.indexOf(msg[2])
            if(i>-1){
                seats[i]=`空位`;
            }
            seats[msg[1]]=msg[2];
            wss.clients.forEach((client) => {
                client.send(`seats,${seats}`)
            })
        }else if(msg[0]==`begin`){
            function settime(username,ms){
                let i = Math.floor(Math.random()*dict.length);
                setTimeout(()=>{
                    guassWord=dict[i];
                    wss.clients.forEach((client) => {
                        client.send(`begin,${username},${dict[i]}`)
                    })
                },ms)
            }
            gameBegin=true;
            let ms = 0;
            for(let username of seats){
                if(username!=`空位`){
                    settime(username,ms)
                    ms+=10000;
                }
            }
            setTimeout(()=>{
                wss.clients.forEach((client) => {
                    client.send(`end,`)
                })
                gameBegin=false;
            },ms)
        }else if(gameBegin&&msg[0]==`chat`&&msg[2]==guassWord){
            wss.clients.forEach((client) => {
                client.send(`chat,${msg[1]},猜對啦!`)
            })
        }else{
            wss.clients.forEach((client) => {
                    client.send(message)
            })
        }
		 
    })
});
複製程式碼

參考

參考了下面這篇文章,在其基礎上完善了小遊戲。

juejin.im/entry/57708…

相關文章