码云笔记前端博客
Home > HTML/CSS > HTML5游戏之Websocket俄罗斯方块终极版(三)

HTML5游戏之Websocket俄罗斯方块终极版(三)

2019-01-18 分类:HTML/CSS 作者:码云 阅读(7014)

本文共计11876个字,预计阅读时长需要30分钟。

  本文是HTML5小游戏俄罗斯方块案例的最后一篇,在这篇文章我们会实现这个案例的所有效果,在前两篇《HTML5游戏之Websocket俄罗斯方块基础版(一)》和《HTML5游戏之Websocket俄罗斯方块进阶版(二)》我们分别介绍了基础和进阶版知识,在基础中主要介绍了Websocket的基础知识和一些使用技巧,在进阶版我们主要是对游戏的一些实现逻辑进行一个详细的介绍。在本章将会增加服务器端逻辑,还有Websocket一些通讯机制,将一个单机版的俄罗斯方块游戏升级成一个真正的双人版火拼游戏,如果说你没有看过之前的两篇文章内容直接来看本篇可能会有点吃力,所以建议先去看之前的两篇文章,打好基础再过来。但是如果你看了之前的基础版和进阶版内容,再看本章内容,将会很轻松的学习到,所以强烈建议大家看之前的两篇文章,好了,废话不多说直接奔主题吧。

HTML5游戏之Websocket俄罗斯方块终极版(三)

  通过上图我们看到,在我的游戏区域内已经可以正常运行一个俄罗斯方块游戏了,在对方游戏区域内我们可以根据按钮对方块进行一个简单操作,现在哦我们要做的就是不再需要页面按钮去控制对方游戏区域俄罗斯方块的运作,而是通过websocket发送过来的数据来驱动对方游戏俄罗斯方块的运动。

案例及代码回顾

  看一下我们之前写的代码,在js文件夹中共有6个文件,首先是square.js和aquareFactory.js这两个文件主要是封装方块的逻辑,然后是我们的game.js文件,这个文件是我们整个俄罗斯方块核心,再代码最下方导出了很多API;接着是我们的local.js,这个主要是控制我的游戏区域的逻辑,还有remote.js,这个是控制对方游戏区域逻辑,当然之前是通过按钮控制的。之后是我们的script.js,这个主要是调用local和remote去实现整个案例的逻辑。由于欧文们接下来要使用websocket,我们将使用socketio来实现对websocket通讯,关于socketio在基础版里介绍过了,它需要在服务端和客户端同时引入socketio模块,如果你之前看了第一节基础版内容,并且安装过socketio,那么你可以直接复制一份即可,不需要重复安装。

waiting消息的逻辑及处理

  首先我们来实现一下服务器端js文件,在根目录下新建一个wsServer.js,里面的diamante可以从之前的基础版复制过来,这里我带大家重写一遍。当一个客户端连接进来的时候,因为我们这个双人的俄罗斯游戏呢需要客户端两两配对,所以需要我们再定义一个变量clientCount,初始值为0,用来表示客户端计数;在定义一个socketMap,用来存储客户端socket。

1
2
3
4
//客户端计数
var clientCount = 0;
//用来存储客户端socket
var socketMap = {};

当我们socket连接出来的时候,首先clientCount加1

1
clientCount = clientCount + 1;

接着把clientCount存入socket里面

1
socket.clientNum = clientCount;

然后我们需要把socket存入到socketMap里,将clientCount作为key,socket作为value把它存进来

1
socketMap[clientCount] = socket;

  这里我们需要判断一下,假如clientCount是基数,也就是第一个进来的,他需要等待第二个进来和他进行一个配对,这个时候我们就给他发送一个waiting消息,调用socket.emit输出一段话,其他的情况,当第二个进来和第一个进行了配对,他们两个就可以都开始进行游戏,首先我们要给这个socket发送一个start消息,同时给他配对的也发送一个start消息,和他配对的这个消息我们去socketMap里面取,他的key就是(clientCount-1),然后给他emit发送start消息,这样waiting和start消息就写完了。

1
2
3
4
5
6
if (clientCount % 2 == 1) {
        socket.emit('waiting', 'waiting for another person');
    } else {
        socket.emit('start');
        socketMap[(clientCount - 1)]
    }

完整的wsServer.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var app = require('http').createServer();
var io = require('socket.io')(app);

var PORT = 3000;
//客户端计数
var clientCount = 0;
//用来存储客户端socket
var socketMap = {};
app.listen(PORT);

io.on('connection', function (socket) {
    clientCount = clientCount + 1;
    socket.clientNum = clientCount;
    socketMap[clientCount] = socket;

    if (clientCount % 2 == 1) {
        socket.emit('waiting', 'waiting for another person');
    } else {
        socket.emit('start');
        socketMap[(clientCount - 1)].emit('start');
    }

    socket.on('disconnect', function () {

    });
});

console.log('websocket listening on port ' + PORT);

接着我们在index里面新添加一个id为waiting的div,表示等待状态。记得引入sockit.io.js

1
2
3
4
5
6
7
8
9
10
11
12
<div>请用方向键和空格键进行操作:上->旋转,左->左移,右->右移,下->下移,空格->坠落</div>
<div id="waiting"></div>
<div class="square" id="local">
    <div class="title">我的游戏区域</div>
    <div class="game" id="local_game"></div>
    <div class="next" id="local_next"></div>
    <div class="info">
        <div>已用时:<span id="local_time">0</span>s</div>
        <div>已得分:<span id="local_score">0</span>分</div>
        <div id="local_gameover"></div>
    </div>
</div>

接下来我们在script.js里面创建socket,然后传递到local和remote对象里面,到时候会用到,这样在它们的构造函数中也需要将socket作为参数传入

1
2
3
var socket = io('ws://localhost:3000');
var local = new Local(socket);
var remote = new Remote(socket);

然后在script.js中,当我们收到waiting消息的时候做一个处理

1
2
3
socket.on('waiting', function (str) {
    document.getElementById('waiting').innerHTML = str;
});

通过命令行node wsServer.js运行

通过命令行node wsServer.js运行

页面上显示效果:

页面显示waiting

通过上图可以看到,“waiting for another person”已经显示出来了,说明我们收到了一个waiting这样的一个消息。但是在打开一个标签的话,就不会有waiting在显示了,因为它是第二个进来的,所以它收到一个start消息,代码如下:

waiting for another person

游戏开始的处理

  接着我们实现start消息背后的逻辑,我们先找到local.js,最后的导出API就不需要了,我们就直接删除即可。我们通过socket.on去监听start消息,拿到waiting元素,设置它的innerHTML为空,把它的状态清理一下,然后调用start方法。调用start方法之后,这个游戏就开始了

1
2
3
4
socket.on('start', function () {
        document.getElementById('waiting').innerHTML = '';
        start();
    })

但是对方的游戏区域未运行,看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//开始
var start = function () {
    var doms = {
        gameDiv: document.getElementById('local_game'),
        nextDiv: document.getElementById('local_next'),
        timeDiv: document.getElementById('local_time'),
        scoreDiv: document.getElementById('local_score'),
        resultDiv: document.getElementById('local_gameover')
    }
    game = new Game();
    game.init(doms, generateType(), generateDir());
    bindKeyEvent();
    game.performNext(generateType(), generateDir());
    timer = setInterval(move, INTERVAL);
}

  start之后我们看一下有哪些消息要发送出去,首先是gime.init,里面有三个参数,第一个是dome元素,这个我们不用管它,后面的generateType参数是表示随机生成的种类,generateDir参数表示随机生成方块的方向,这两个参数我们需要通过websocket传送给相对应的另外一个客户端,随意这两个参数我们先把他缓存下来。我们先定义一个变量type等于generateType(),变量dir等于generateDir(),init之后我们发送一个消息告诉我们的wsServer,这里面传入的参数就是我们刚刚定义的两个变量需要传递的数据变成一个对象传过去。改造后的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//开始
var start = function () {
    var doms = {
        gameDiv: document.getElementById('local_game'),
        nextDiv: document.getElementById('local_next'),
        timeDiv: document.getElementById('local_time'),
        scoreDiv: document.getElementById('local_score'),
        resultDiv: document.getElementById('local_gameover')
    }
    game = new Game();
    var type = generateType();
    var dir = generateDir();
    game.init(doms, type, dir);
    socket.emit('init', {type: type, dir: dir});
    bindKeyEvent();
    game.performNext(generateType(), generateDir());
    timer = setInterval(move, INTERVAL);
}

  然后在wsServer.js中,我们就可以接收到这个消息,发送给与这个socket相匹配的另外一个socket,所以需要我们判断一下,如果(socket.clientNum%2==0)也就是是偶数的时候,比如说它是2,就需要发送给clientNum为1的socket。我们通过socketMap把对应的socket取出来socketMap[socket.clientNum-1],那么让它如给他的客户端发送一个消息;else的话就是一个基数,就与另外一个匹配的socket加1即可

1
2
3
4
5
6
7
socket.on('init', function (data) {
        if (socket.clientNum % 2 == 0) {
            socketMap[socket.clientNum - 1].emit('init', data);
        } else {
            socketMap[socket.clientNum + 1].emit('init', data);
        }
});

那么他的客户端怎么接收消息呢?其实就是在remote.js中,在bindEvents事件中接收这个init,然后调用一下这个start方法,这个start方法里面的参数就是通过data传递过来的

1
2
3
4
5
6
//绑定按钮事件
var bindEvents = function () {
    socket.on('init', function (data) {
        start(data.type, data.dir);
    });
}

在local.js中的start方法内还调用了game.performNext,那么这个消息同样需要传给另外一个客户端,然后调用socket发送next消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//开始
var start = function () {
    var doms = {
        gameDiv: document.getElementById('local_game'),
        nextDiv: document.getElementById('local_next'),
        timeDiv: document.getElementById('local_time'),
        scoreDiv: document.getElementById('local_score'),
        resultDiv: document.getElementById('local_gameover')
    }
    game = new Game();
    var type = generateType();
    var dir = generateDir();
    game.init(doms, type, dir);
    socket.emit('init', {type: type, dir: dir});
    bindKeyEvent();
    var t = generateType();
    var d = generateDir();
    game.performNext(t, d);
    socket.emit('next',{type: t, dir: d});
    timer = setInterval(move, INTERVAL);
}

然后在wsServer.js中进行接收next消息,收到消息后,发送到对应的客户端

1
2
3
4
5
6
7
socket.on('next', function (data) {
    if (socket.clientNum % 2 == 0) {
        socketMap[socket.clientNum - 1].emit('next', data);
    } else {
        socketMap[socket.clientNum + 1].emit('next', data);
    }
});

那么在对方的游戏区域中,在它接收到next消息后调用game.performNext,这样就相当于驱动对方游戏区域中也去调用game.performNext,这样一来,当游戏开始的时候,对方游戏区域也会有一些东西,我们来看一下

1
2
3
4
5
6
7
8
9
10
//绑定按钮事件
var bindEvents = function () {
    socket.on('init', function (data) {
        start(data.type, data.dir);
    });

    socket.on('next', function (data) {
        game.performNext(data.type, data.dir)
    });
}
游戏开始的处理

基本操作处理

在wsServer.js有两段代码它的冗余度是非常高的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
socket.on('init', function (data) {
    if (socket.clientNum % 2 == 0) {
        socketMap[socket.clientNum - 1].emit('init', data);
    } else {
        socketMap[socket.clientNum + 1].emit('init', data);
    }
});

socket.on('next', function (data) {
    if (socket.clientNum % 2 == 0) {
        socketMap[socket.clientNum - 1].emit('next', data);
    } else {
        socketMap[socket.clientNum + 1].emit('next', data);
    }
});

所以我们可以抽出来写一个公共的函数,然后在io.on里面直接调用函数方法,这个函数改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var bindListener = function (socket, event) {
    socket.on(event, function (data) {
        if (socket.clientNum % 2 == 0) {
            if (socketMap[socket.clientNum - 1]) {
                socketMap[socket.clientNum - 1].emit(event, data);
            }
        } else {
            if (socketMap[socket.clientNum + 1]) {
                socketMap[socket.clientNum + 1].emit(event, data);
            }
        }
    });
}

在io.on调用方式

1
2
bindListener(socket, 'init');
bindListener(socket, 'next');

然后在local.js中看一下那些消息需要转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//绑定键盘事件
var bindKeyEvent = function () {
    document.onkeydown = function (e) {
        if (e.keyCode == 38) {
            //up
            game.rotate();
            socket.emit('rotate');
        } else if (e.keyCode == 39) {
            //right
            game.right();
            socket.emit('right');
        } else if (e.keyCode == 40) {
            //down
            game.down();
            socket.emit('down');
        } else if (e.keyCode == 37) {
            //left
            game.left();
            socket.emit('left');
        } else if (e.keyCode == 32) {
            //space
            game.fall();
            socket.emit('fall');
        }
    }
}

接着在move方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//移动
var move= function() {
    timeFunc();
    if (!game.down()) {
        game.fixed();
        socket.emit('fixed');
        var line = game.checkClear();
        if (line) {
            game.addScore(line);
            socket.emit('line', line);
        }
        var gameOver = game.ckeckGameOver();
        if (gameOver) {
            game.gameover(false);
            stop();
        }else{
            var t = generateType();
            var d = generateDir();
            game.performNext(t, d);
            socket.emit('next',{type: t, dir: d});
        }
    } else{
        socket.emit('down');
    }
}

然后在wsServer.js中将这些消息传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
io.on('connection', function (socket) {
    clientCount = clientCount + 1;
    socket.clientNum = clientCount;
    socketMap[clientCount] = socket;

    if (clientCount % 2 == 1) {
        socket.emit('waiting', 'waiting for another person');
    } else {
        if (socketMap[(clientCount - 1)]) {
            socket.emit('start');
            socketMap[(clientCount - 1)].emit('start');
        } else {
            socket.emit('leave');
        }
    }

    bindListener(socket, 'init');
    bindListener(socket, 'next');
    bindListener(socket, 'rotate');
    bindListener(socket, 'right');
    bindListener(socket, 'down');
    bindListener(socket, 'left');
    bindListener(socket, 'fall');
    bindListener(socket, 'fixed');
    bindListener(socket, 'line');
    bindListener(socket, 'time');
    bindListener(socket, 'lose');
});

然后在remote.js接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//绑定按钮事件
var bindEvents = function () {
    socket.on('init', function (data) {
        start(data.type, data.dir);
    });

    socket.on('next', function (data) {
        game.performNext(data.type, data.dir)
    });

    socket.on('rotate', function (data) {
        game.rotate();
    });
    socket.on('right', function (data) {
        game.right();
    });
    socket.on('down', function (data) {
        game.down();
    });
    socket.on('left', function (data) {
        game.left();
    });
    socket.on('fall', function (data) {
        game.fall();
    });
    socket.on('fixed', function (data) {
        game.fixed();
    });
    socket.on('line', function (data) {
        game.checkClear();
        game.addScore(line);
    });
}

细节处理一

接下来我们看看还有什么消息需要转发,在local.js中timeFunc函数中去掉之前写的在底部添加一些干扰行,然后在发送一个消息time

1
2
3
4
5
6
7
8
9
10
//计时函数
var timeFunc = function () {
    timeCount = timeCount + 1;
    if (timeCount == 5) {
        timeCount = 0;
        time = time + 1;
        game.setTime(time);
        socket.emit('time', rime)
    }
}

然后在wsServer端进行转换。

1
bindListener(socket, 'time');

在remote.js中我们接收到time

1
2
3
socket.on('time', function (data) {
    game.setTime(data);
});

这样我们的时间就同步过来了。接下来处理一下输赢的问题,我们在local.js中找到move函数的game.gameover(),这里我们发送一个“lose”

1
2
3
4
5
6
7
8
9
10
11
var gameOver = game.ckeckGameOver();
if (gameOver) {
    game.gameover(false);
    socket.emit('lose');
    stop();
}else{
    var t = generateType();
    var d = generateDir();
    game.performNext(t, d);
    socket.emit('next',{type: t, dir: d});
}

同样,在wsServer.js中进行转换

1
bindListener(socket, 'lose');

然后在remote.js中接收lose时,我们调用gameover

1
2
3
socket.on('lose', function (data) {
    game.gameover(false);
});

最后,在local.js中监听lose,这里要注意,我们收到的lose一定是对方发过来的

1
2
3
4
socket.on('lose', function () {
    game.gameover(true);
    stop();
})

讲到这儿,我们在测试的时候发现。不管输赢最后提示消息只在我方显示,而对方没有,我们接着对对方区域消息的改造,首先去掉index里面按钮,因为这些已经没有用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>请用方向键和空格键进行操作:上->旋转,左->左移,右->右移,下->下移,空格->坠落</div>
<div id="waiting"></div>
<div class="square" id="local">
    <div class="title">我的游戏区域</div>
    <div class="game" id="local_game"></div>
    <div class="next" id="local_next"></div>
    <div class="info">
        <div>已用时:<span id="local_time">0</span>s</div>
        <div>已得分:<span id="local_score">0</span>分</div>
        <div id="local_gameover"></div>
    </div>
</div>
<div class="square" id="remote">
    <div class="title">对方的游戏区域</div>
    <div class="game" id="remote_game"></div>
    <div class="next" id="remote_next"></div>
    <div class="info">
        <div>已用时:<span id="remote_time">0</span>s</div>
        <div>已得分:<span id="remote_score">0</span>分</div>
        <div id="remote_gameover"></div>
    </div>
</div>

然后在local.js中找到lose的地方(move函数方法中)获取显示消息的ID

1
document.getElementById('remote_gameover').innerHTML = '你赢了';

这样我们的输赢就处理完了。

细节处理二

接着我们处理一下disconnect,这个逻辑和我们的bindListener方法逻辑是一样的,所以我法如法炮制,然后我们将删除socketMap相应的socket

1
2
3
4
5
6
7
8
9
10
11
12
socket.on('disconnect', function () {
    if (socket.clientNum % 2 == 0) {
        if (socketMap[socket.clientNum - 1]) {
            socketMap[socket.clientNum - 1].emit('leave');
        }
    } else {
        if (socketMap[socket.clientNum + 1]) {
            socketMap[socket.clientNum + 1].emit('leave');
        }
    }
    delete(socketMap[socket.clientNum]);
});

然后在local.js中,让我们收到leave消息的时候,注意这个消息一定是对方发过来的,说明对方掉线了

1
2
3
4
5
socket.on('leave', function () {
    document.getElementById('local_gameover').inneiHTML = '对方掉线';
    document.getElementById('remote_gameover').innerHTML = '已掉线';
    stop();
});

这个掉线出来就完毕了。

细节处理三

接下来我们处理一下消行,找到loacl.js中的move方法判断一下,如果line大于1的话,那说明他一次最少消了两行,这个时候我们要给对方增加一个底部干扰,调用generataBottomLine方法,产生的干扰我们用bottomLines接收一下,产生了消息后给对方发送一个消息到server

1
2
3
4
5
6
7
8
if (line) {
    game.addScore(line);
    socket.emit('line', line);
    if (line &gt; 1) {
        var bottomLines = generataBottomLine(line);
        socket.emit('bottomLines', bottomLines);
    }
}

在server这边进行一个转发

1
bindListener(socket, 'bottomLines');

转发之后在loacl.js进行监听,调用addTailLines方法增加干扰,传入数据data,然后给对方同步一下

1
2
3
4
socket.on('bottomLines', function (data) {
    game.addTailLines(data);
    socket.emit('addTailLines', data);
});

然后在server端进行转化

1
bindListener(socket, 'addTailLines');

转化了之后在remote.js进行监听

1
2
3
socket.on('addTailLines', function (data) {
    game.addTailLines(data);
});

完!

「除特别注明外,本站所有文章均为码云笔记原创,转载请保留出处!」

赞(5) 打赏

觉得文章有用就打赏一下文章作者

支付宝
微信
5

觉得文章有用就打赏一下文章作者

支付宝
微信

上一篇:

下一篇:

你可能感兴趣

共有 2 条评论 - HTML5游戏之Websocket俄罗斯方块终极版(三)

  1. Sixheudh Linux Chrome 62.0.3202.84

    干货满满,支持一下

    1. 码云 Windows 7 Chrome 69.0.3497.100

      @Sixheudh感谢认可,多多交流

博客简介

码云笔记网 mybj123.com,一个专注Web前端开发技术的博客,主要记录和总结博主在前端开发工作中常用的实战技能及前端资源分享,分享各种科普知识和实用优秀的代码,以及分享些热门的互联网资讯和福利!码云笔记网有你更精彩!
更多博客详情请看关于博客

精彩评论