Canvas+Socket搞出一個多人遊玩的“我畫你猜”

chenhongdong發表於2019-01-28

各位看官,好久不見,這篇文章本應該是在2018年的時候與你們見面的,結果因為種種原因拖到了此時此刻(我的鍋啊我的鍋),再過一週就要春節了,在這裡和大家說一聲過年好啊!!!

今天是個好日子,我們來一起利用socket.io和canvas這兩樣利器,搞出個簡單的《我畫你猜》遊戲吧

快過年了,大家放鬆一下忙碌了一年的緊張神經,玩玩自己搞出來的小遊戲吧!

PS: 如果對接下來說的即時通訊的實現沒有太搞懂的,可以再看看這裡

好吧,天下武功為快不破,趕緊先上個目錄結構

Canvas+Socket搞出一個多人遊玩的“我畫你猜”

實現訊息的即時通訊

由於之前專門寫過文章介紹了socket.io實現即時通訊的內容,那麼我這邊就儘量快速寫起了

首先通過express來啟動一個服務並且來建立socket.io的連線

啟動服務並建立連線

服務端

// app.js檔案
const express = require('express');
const app = express();
// 設定靜態資料夾
// 這樣設定會自動識別當前資料夾下的index.html檔案
app.use(express.static(__dirname));

const server = require('http').createServer(app);
const io = require('socket.io')(server);

server.listen(8888);
複製程式碼

啟動服務,然後訪問localhost:8888,就可以訪問到index.html檔案上的內容了

小貼士

Canvas+Socket搞出一個多人遊玩的“我畫你猜”
在客戶端訪問io的時候有時候會報io is not defined的錯誤,就是需要先啟動服務後才會自動生成一個 socket.io/socket.io.js檔案,如上圖所示再去引用就OK了

客戶端

// index.js檔案

// 用來處理遊戲物件資料
let gameObj = {};
// socket例項
let socket = io();
// 監聽connect事件
socket.on('connect', () => {
    console.log('客戶端連線成功'); 
});
複製程式碼

到這一步重新整理頁面在控制檯裡就可以看到客戶端連線成功這7個字了

傳送/接收/展示訊息

寫到這裡,只是邁出了小小的一步罷了,接下來,我們快馬加鞭繼續寫下處理訊息的邏輯吧

服務端

// app.js

...省略

+++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 區分是聊天還是在繪圖
const LINE = 0;
const MESSAGE = 1;
const userList = ['皮卡丘', '巴大蝴', '比比鳥',  '妙蛙種子', '小火龍', '傑尼龜'];
+++++++++++++++++++++++++++++++++++++++++++++++++++++++

io.on('connection', socket => {
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++
    // 隨機分配使用者名稱併傳送給所有人
    const user = userList[Math.floor(Math.random() * userList.length)];
    const message = `歡迎${user}加入遊戲!!!`;

    // 將資料封裝成json物件
    let data = {};
    // 通過type來區分
    data.type = MESSAGE;
    data.sender = '系統';
    data.message = message;
    // 將訊息分發出去
    // 訊息資料必須是字串型別,so需要轉換一下
    io.emit('message', JSON.stringify(data));

    socket.on('message', msg => {
        // 傳過來的訊息也是json字串格式的,需要JSON.parse轉成json
        let data = JSON.parse(msg);
        // 如果是聊天型別,就給sender賦值為當前使用者名稱
        if (data.type === MESSAGE) {
            data.sender = user;
        }
        io.emit('message', JSON.stringify(data));
    });
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++
});

server.listen(8888);
複製程式碼

上面程式碼裡把訊息資料都打包成json的格式是為了方便處理,畢竟訊息資料只能接收字串的格式

然後在傳送的時候再通過JSON.stringify給轉成json字串,這樣就不會導致報錯了

當然解析對應的訊息資料時再通過JSON.parse來轉換成真正的json即可了

服務端傳送和接收訊息都搞完了,接下來就該客戶端出場了,客戶端除了上述兩個功能之外還會展示訊息(聽起來屌屌的)

那麼,不囉嗦了,快開搞吧!!!

客戶端

// index.js檔案

++++++++++++++++++++++++++++++++++++++++++++++++++++++
const LINE = 0;
const MESSAGE = 1;
++++++++++++++++++++++++++++++++++++++++++++++++++++++

let gameObj = {};
let socket = io();

++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 監聽服務端發來的訊息
socket.on('message', msg => {
    // 需要先用JSON.parse轉一下
    let data = JSON.parse(msg);
    console.log(data);  // {type: 1, sender: "系統", message: "歡迎皮卡丘進入遊戲"}
    
    // 如果型別為聊天
    if (data.type === MESSAGE) {
        let li = `<li><span>${data.sender}: </span>${data.message}</li>`;
        $('#history').append(li);
        // 聊天區域滾動到最新聊天內容位置
        $('#history-wrapper').scrollTop($('#history-wrapper')[0].scrollHeight);
    }
});

// 點選傳送按鈕發訊息
$('#btn').click(sendMsg);
// 按Enter鍵傳送訊息
$('#input').keyup(e => {
    let keyCode = e.keyCode;
    if (keyCode === 13) {
        sendMsg();
    }
});

// 傳送訊息函式
function sendMsg() {
    let value = $.trim($('#input').val());
    if (value !== '') {
        let data = {};
        data.type = MESSAGE;
        data.message = value;
        gameObj.socket.send(JSON.stringify(data));
        $('#input').val('');
    }
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++
複製程式碼

客戶端上述程式碼都做了哪些事情?

  1. 常量區分聊天與繪畫
    const LINE = 0;
    const MESSAGE = 1;
    複製程式碼
  2. 監聽服務端發來的訊息
    • 監聽訊息
    socket.on('message', msg => {});
    複製程式碼
    • 轉換訊息為json格式
    let data = JSON.parse(msg);
    複製程式碼
    • 訊息型別為聊天
    if (data.type === MESSAGE) {
        // 新增內容
        ...省略
        // 滾動到最新訊息位置
        ...省略
    }
    複製程式碼
  3. 傳送訊息
    • 傳送訊息方法
    function sendMsg() { ...省略 }
    複製程式碼
    • 點選or回車傳送

以上就是關於訊息通訊的基本實現了,下面我們要進入下一環節,canvas登場了,繼續看下去

Canvas來繪製畫板

canvas這個元素已經等候多時了,終於輪到它大展身手了,用過canvas的都知道,我們常見的都是在2d上進行繪圖操作,所以在此之前先來獲取一下

// index.js檔案

const LINE = 0;
const MESSAGE = 1;

++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 用原生來獲取,jq物件中並沒有我們需要的2d
let cvs = document.getElementById('canvas');
let ctx = cvs.getContext('2d');

let gameObj = {
    // 當前使用者是否在繪圖
    isDrawing: false,
    // 下一條線的起始點
    startX: 0,
    startY: 0
};
++++++++++++++++++++++++++++++++++++++++++++++++++++++

...省略

socket.on('message', msg => {
    let data = JSON.parse(msg);

    if (data.type === MESSAGE) {
        ...省略
    }
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++
    else if (data.type === LINE) {
        // 這是畫線函式,專門繪製所用
        drawLine(ctx, data.startX, data.startY, data.endX, data.endY, 1);
    }
    ++++++++++++++++++++++++++++++++++++++++++++++++++++++
});


// 傳送訊息函式
...省略

++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 開始在畫板上畫畫了
// 滑鼠按下時的操作
$('#canvas').on('mousedown', function(e) {
    let cvsPos = $(this).offset(),
        mouseX = e.pageX - cvsPos.left || 0,
        mouseY = e.pageY - cvsPos.top || 0;

    // 更新一下startX和startY
    gameObj.startX = mouseX;
    gameObj.startY = mouseY;
    // 更新為繪圖狀態
    gameObj.isDrawing = true;
});
// 滑鼠移動時的操作
$('#canvas').on('mousemove', function(e) {
    // 當繪圖狀態為true的時候才可以繪製
    if (gameObj.isDrawing) {
        let cvsPos = $(this).offset(),
            mouseX = e.pageX - cvsPos.left || 0,
            mouseY = e.pageY - cvsPos.top || 0;

        if (gameObj.startX !== mouseX && gameObj.startY !== mouseY) {
            // 開始繪製線段,drawLine為畫線函式
            drawLine(ctx, gameObj.startX, gameObj.startY, mouseX, mouseY, 1, $('#color').val());

            // 既然畫線了,那就把畫的線段資料也打包成json傳給服務端
            let data = {};
            data.startX = gameObj.startX;
            data.startY = gameObj.startY;
            data.endX = mouseX;
            data.endY = mouseY;
            data.type = LINE;
            // 別猶豫,直接通過socket發給服務端
            socket.send(JSON.stringify(data));

            // 這裡還要更新一下startX和startY
            gameObj.startX = mouseX;
            gameObj.startY = mouseY;
        }
    }
});

// 滑鼠抬起時的操作
$('#canvas').on('mouseup', function() {
    gameObj.isDrawing = false;
});

// 畫線函式
function drawLine(ctx, x1, y1, x2, y2, thick) {
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.lineWidth = thick;
    ctx.strokeStyle = '#00a1f4';
    ctx.stroke();
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++
複製程式碼

別看上面程式碼突然多了好多好多,但實際上也無非下面幾點,莫慌,我們來梳理一下

  1. 區分訊息為畫線的資料
    • 在監聽訊息的函式裡,通過data.type === LINE來區分出來訊息型別為畫線的資料
  2. 滑鼠在canvas上的事件
    • mousedown按下事件
      • 記錄點選時的位置
      • 將按下的位置賦給gameObj.startX和gameObj.startY
      • 修改gameObj.isDrawing狀態為true
    • mousemove移動事件
      • 記錄移動的位置
      • 繪製線段
      • 傳送畫線資料到服務端
      • 把移動的最新位置更新到gameObj上
    • mouseup抬起事件
      • 修改gameObj.isDrawing狀態為初始值false
  3. 實現畫線方法
    • ctx.beginPath() - 繪製初始路徑
    • ctx.moveTo(x1, y1) - 線段下筆點
    • ctx.lineTo(x2, y2) - 線段移動路徑
    • ctx.lineWidth - 線段寬度
    • ctx.storkeStyle - 線段顏色
    • ctx.stroke() - 繪製線段

說難不難,說簡單不簡單。不過視熟練程度不同,會有不同的感受罷了,多寫多練習,知識自然會收錄到自己的手中

寫到這裡基本可以說是socket.io和canvas的要領都掌握了,那麼最後的最後,我們不能忘記文章的標題主旨,是多人遊玩,不僅僅是簡單的自娛自樂,也要眾人同樂

那麼讓我們再加把勁,實現多人遊戲邏輯

構建多人遊戲

遊戲邏輯

玩遊戲嘛,自然有個遊戲的邏輯和規則

遊戲邏輯在客戶端方面的實現相對來說還是比較簡單的,更多的操作還是要靠服務端那裡了

那麼就先由簡到難的實現一下吧

客戶端

// index.js檔案

const LINE = 0;
const MESSAGE = 1;

// 新增個遊戲常量
const GAME = 2;

let gameObj = {
    ...省略

    // 遊戲狀態
    WAITTING: 0,
    START: 1,
    OVER: 2,
    RESTART: 3,
    // 當前輪到誰來繪圖
    isPlayer: false
};

...省略

socket.on('message', msg => {
    let data = JSON.parse(msg);

    if (data.type === MESSAGE) {
        ...省略
    } else if (data.type === LINE) {
        ...省略
    } else if (data.type === GAME) { // 如果進行遊戲,傳過來的type值必須是GAME
        // 通過data.state來判斷遊戲當前的進度

        // 遊戲開始的邏輯
        if (data.state === gameObj.START) {
            // 遊戲要是開始了就需要清空畫布
            ctx.clearRect(0, 0, cvs.width, cvs.height);

            // 清空聊天記錄和隱藏重新開始
            $('#restart').hide();
            $('#history').html('');

            // 區分一下是當前畫圖的玩家還是猜圖的玩家
            if (data.isPlayer) {
                gameObj.isPlayer = true;
                $('#history').append(`<li>輪到你了,請你畫出<span class="answer">${data.answer}</span></li>`);
            } else {
                $('#history').append(`<li>遊戲即將開始,請準備,你們有一分鐘的時間去猜答案哦</li>`);
            }
        }

        // 遊戲結束的邏輯
        if (data.state === gameObj.OVER) {
            gameObj.isPlayer = false;
            $('#restart').show();
            $('#history').append(`<li>本輪遊戲的獲勝者是<span class="winner">${data.winner}</span>,正確答案是: ${data.answer}</li>`);
        }

        if (data.state === gameObj.RESTART) {
            $('#restart').hide();
            ctx.clearRect(0, 0, cvs.width, cvs.height);
        }
    }
});

...省略

// 畫線函式
...省略


// 重玩
$('#restart').on('click', function() {
    let data = {};
    data.type = GAME;
    data.state = gameObj.RESTART;
    socket.send(JSON.stringify(data));
});
複製程式碼

繼續來梳理一下以上程式碼都做了什麼?

  1. 遊戲常量區分訊息型別及遊戲狀態
    const GAME = 2;
    
    let gameObj = {
        ...省略
        
        // 遊戲狀態
        WAITTING: 0,
        START: 1,
        OVER: 2,
        RESTART: 3,
        // 當前輪到誰來繪圖
        isPlayer: false
    };
    複製程式碼
  2. data.type為GAME表示進行遊戲
  3. data.state來判斷遊戲當前的進度
    • 遊戲開始
      • 清空畫布
      • 清空聊天記錄和隱藏重新開始按鈕
      • 通過data.isPlayer來區分是繪圖者還是猜圖者以展示不同文案
    • 遊戲結束
      • 顯示重新開始按鈕
      • 顯示獲勝者和答案
    • 重新開始
      • 清空畫布、隱藏按鈕

上述三點就是客戶端實現多人遊戲的程式碼了,不要停歇,馬上就要到頭了,繼續寫服務端的邏輯吧

服務端

// app.js檔案

...省略

const LINE = 0;
const MESSAGE = 1;

// 新增遊戲常量
const GAME = 2;

// 遊戲狀態和遊戲邏輯
const WAITTING = 0;
const START = 1;
const OVER = 2;
const RESTART = 3;

let player = 0;
let wordsList = ['蘋果', '運動鞋', '火箭', '足球', '小黃人', '汽車', '小鳥'];
let currentAnswer;
let currentState = WAITTING;
let timer;
// 連線的客戶端數量
let len = 0;

io.on('connection', socket => {
    ...省略
    
    // 將資料封裝成json物件
    ...省略
    
    
    // 把遊戲的訊息通知所有人
    let game = {};
    game.type = GAME;
    game.state = WAITTING;
    io.emit('message', JSON.stringify(game));

    // 遍歷客戶端的連線
    io.clients((err, client) => {
        if (err) throw err;
        len = client.length;
    });

    // 當前狀態為等待並且連線數超過兩個的時候才開始遊戲
    if (currentState === WAITTING && len > 2) {
        startGame(socket);
    }
    
    
    socket.on('message', msg => {
        ...省略
        
        // 判斷是不是有玩家答對了
        if (currentState === START && data.message === currentAnswer) {
            let game = {};
            game.type = GAME;
            game.answer = currentAnswer;
            game.winner = user;
            game.state = OVER;
            io.emit('message', JSON.stringify(game));

            currentState = WAITTING;
            
            clearTimeout(timer);
        }

        // 重新開始
        if (data.state === RESTART && data.type === GAME) {
            startGame(socket);
        }
    });
});

// 開始遊戲方法
function startGame(socket) {
    // 分配一個玩家來畫畫
    player = (player + 1) % len;
    // 隨機分配個圖案
    let random = Math.floor(Math.random() * wordsList.length);
    currentAnswer = wordsList[random];

    // 通知所有玩家遊戲開始
    let data = {};
    data.type = GAME;
    data.isPlayer = false;
    data.state = START;
    io.emit('message', JSON.stringify(data));

    // 遍歷客戶端,然後找到畫畫的那個使用者告訴他相關data
    let count = 0;
    io.clients((err, client) => {
        client.forEach(item => {
            if (count === player) {
                let game = {};
                game.type = GAME;
                game.state = START;
                game.isPlayer = true;
                game.answer = currentAnswer;
                // 這條訊息只有繪圖的玩家才能看到
                socket.send(JSON.stringify(game));
            }
            count++;
        });
    });

    // 1分鐘後遊戲結束
    timer = setTimeout(() => {
        let obj = {};
        obj.type = GAME;
        obj.state = OVER;
        obj.winner = '沒有人啊!';
        obj.answer = currentAnswer;
        io.emit('message', JSON.stringify(obj));
    }, 60 * 1000);
    
    // 當前狀態修改為START
    currentState = START;
}

server.listen(8888);
複製程式碼

服務端,我的朋友,你剛才都做了什麼?

  1. 新增遊戲常量以及遊戲的狀態和邏輯
    // 遊戲常量
    const GAME = 2;
    // 遊戲狀態
    const WAITTING = 0;
    ...省略
    const RESTART = 3;
    // 遊戲邏輯
    let player = 0;
    ...省略
    let len = 0;
    複製程式碼
  2. 初始化遊戲和遊戲人數
    // 把遊戲的訊息通知所有人
    let game = {};
    ...省略
    io.emit('message', JSON.stringify(game));
    
    // 遍歷客戶端的連線
    io.clients((err, client) => {
        if (err) throw err;
        len = client.length;
    });
    
    // 當前狀態為等待並且連線數超過兩個的時候才開始遊戲
    if (currentState === WAITTING && len > 2) {
        startGame(socket);
    }
    複製程式碼
  3. 開始遊戲 - startGame
    • 分配玩家畫畫
    player = (player + 1) % len;
    複製程式碼
    • 隨機分配個圖案
    let random = Math.floor(Math.random() * wordsList.length);
    currentAnswer = wordsList[random];
    複製程式碼
    • 通知所有玩家遊戲開始
    let data = {};
    ...省略
    io.emit('message', JSON.stringify(data));
    複製程式碼
    • 遍歷客戶端,然後找到畫畫的那個使用者告訴他相關data
    let count = 0;
    io.clients((err, client) => {
        client.forEach(item => {
            // 匹配為分配的玩家才可以繪製答案
            if (count === player) {
                let game = {};
                ...省略
                // 這條訊息只有繪圖的玩家才能看到
                socket.send(JSON.stringify(game));
            }
            count++;
        });
    });
    複製程式碼
    • 1分鐘遊戲結束並改變當前狀態
    timer = setTimeout(() => {
        ...省略
    }, 60 * 1000);
    
    currentState = START;
    複製程式碼
  4. 監聽訊息判斷是否有人回答正確
    // 判斷是不是有玩家答對了
    if (currentState === START && data.message === currentAnswer) {
        let game = {};
        game.type = GAME;
        game.answer = currentAnswer;
        game.winner = user;
        game.state = OVER;  // 狀態修改為OVER
        // 把該訊息資料傳遞給所有玩家
        io.emit('message', JSON.stringify(game));
        // 恢復當前狀態初始值
        currentState = WAITTING;
        // 清空1分鐘計時器
        clearTimeout(timer);
    }
    複製程式碼

好了,到這裡就都結束了,小夥伴們可以跑起來試一試了,當然動手敲起來才是王道了

Canvas+Socket搞出一個多人遊玩的“我畫你猜”
就寫到這裡吧,感謝大家一直以來的關照,祝大家2019年豬事順利了,哈哈,886

最後的最最後: 必須把地址發給大家以作參考

相關文章