来一场秋名山的对决吧(基于socket.io的双人竞速赛车游戏)

前几天看了关于websocket的相关教程,觉的在做多人联网游戏方面,socket还是很有用的。所以这两天稍微研究了一些。做了一个简单的火箭车游戏的demo。用了加深对soecket应用的了解。

环境

这里我使用了node来作为服务器端。主要用到的库是express 和 socket.io.两者都可以通过npm install来安装。因为我们要做游戏,所以我这里用了自己封装的一个简单web canvas引擎:Hamster。客户端主要使用jqeury和Hamster。

搭建开发目录

整个项目的目录结构如下图所示:

  • app
  • client
    • public
      • resource
      • js
    • index.html
  • server.js

根目录下的server.js是我们的服务器脚本。客户端的逻辑写在client/public/下面的js文件夹中。resource为所有的图片资源。client下的index.html为游戏入口文件。

搭建服务

ok,安装相关的依赖之后我们就可以开始写服务端的逻辑了。

打开server.js,代码如下:

1
2
3
4
5
6
7
8
9
var express = require("express");
var app = express();
var server = require("http").createServer(app);
var io = require("socket.io").listen(server);

app.use("/", express.static(__dirname + "/client"));
server.listen(3000, function () {
console.log("server is running now.");
});

记得在client下的index.html写点什么。然后命令行通过node server.js来启动服务。浏览器打开127.0.0.1:3000这个时候应该可以看见你写下的那什么了。其实这段代码也挺好理解的。首先启动一个express的app服务,再用socket来监听这个服务。然后使用app.use()函数来确定服务端的路由。最后是设置服务启动监听的端口。

客户端

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Road Fighter</title>
</head>
<style>
.wrapper {
width: 800px;
height: 300px;
margin: 0 auto;
background: #333;
padding-top: 100px;
margin-top: 50px;
}

.join {
width: 400px;
height: 40px;
margin-left: 300px;
}

.build {
width: 400px;
height: 40px;
margin-left: 300px;
}

.result {
color: #fff;
text-align: center;
}
</style>

<body>
<h1 style="text-align: center;">Road Fighter</h1>

<canvas id="main" style="display: none;"></canvas>
<div class="wrapper">
<div class="join">
<input type="text" placeholder="please input you room code" id="join-text">
<input type="button" value="join room" id="join-button">
</div>

<div class="build">
<input type="text" placeholder="pleace input you nickname" id="build-text">
<input type="button" value="bulid room" id="build-button">
</div>

<h1 class="result">
<span class="notice">error</span>
<span class="text">connect faild</span>
</h1>

</div>
<script src="/socket.io/socket.io.js"></script>
<script src="public/js/jquery.1.11.3.min.js"></script>
<!--配置文件-->
<script src="public/js/config.js"></script>
<!--引擎库-->
<script src="public/js/Hamster.js"></script>
<!--用户代码-->
<script src="public/js/main.js"></script>
</body>

</html>

这里我先把整个的html文件贴了出来。主要是一些html的标签的设置与样式的设置。这里我们稍微注意下js文件加入的顺序。socket.io.js和jqeury我们放在最前面,因为后面很多的操作都要基于这两者,config和Hamster是引擎的配置文件和库文件。这里要放在我们用户代码之前。最后只要注意下。id 为main的canvas为我们的游戏主场景。

登陆以及获取PassCode

还记得我们以前玩那种局域网游戏,在你创建完了服务器主机之后,小伙伴们都会想你询问你的ip地址,或者设置的密码以便加入游戏。这里我们也采用类似的方法。只是不是通过ip地址来加入。我们在玩家创建房间之后,服务端来随机生成一个PassCode,然后玩家通过输入passcode来进入游戏。只有两个输入相同的passcode的玩家才能进入游戏场景。

image

然后我们先开始写客户端生成passcode的代码。

打开main.js,这里我们创建游戏房间以及其他的一些dom操作用到了jqeury库。首先我们创建一个Login的类,用它来管理整个玩家的创建房间与加入游戏房间的逻辑。

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
$(function(){
var login = new Login();
login.init();
})

function Login() {
var self = this;
self.socket = io.connect();
}

Login.prototype.init = function () {
var self = this;
self.socket.on("connect", function () {
$(".result .notice").empty();
$(".result .text").text("Connect Sucess!");
});

// 创建游戏房间
$("#build-button").click(function () {
if ($("#build-text").val().length <= 0) {
$(".result .notice").text("ERROR");
$(".result .text").text("Please Input Your Nickname");
} else {
var nickName = $("#build-text").val();
self.socket.emit("login", nickName);
}
});

// 创建房间成功
self.socket.on("loginSuccess", function (passCode) {
$(".result .text").text(passCode);
});
}

首先我们先写一个链接成功的反馈。我们的服务在前一部分已经成功启动了。现在我们需要看我们的客户端的socket链接是否成功。这里我们用socket.on(“connect”,callback)来做检测。如果链接是成功的。则反馈一些相对应的dom文本。接下来是关于建立游戏房间的逻辑。我们需要在输入框中输入一个id,然后点击build来创建房间。如果通过验证,会像服务端发送一个login的消息。这个消息还传送了一个叫做nickName的参数。其实这个游戏中,并没有使用nickname这个参数。因为我们的passcode仅仅只是自动返回随机数。如果你像做一个验证更加完美,或者安全度更高的多人游戏,可以把这个nickname和passcode的生成做一下算法的关联。这里我就不说太多了。

正如你所看到的。socket.emit()这个函数是向服务端发送消息。而socket.on是接受来自服务端的消息。socket通信的一个很大的特点是客户度啊与服务端是一直链接着的。不管是服务端还是客户端,都可以即时的相应到来自对方发送过来的消息。也正是这个特点,才使即时联网的游戏更加流畅。

既然发送了消息,那么我们服务端就要响应来自客户端的消息。

这里我们打开server.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
34
35

var express = require("express");
var app = express();
var server = require("http").createServer(app);
var io = require("socket.io").listen(server);

var passCodeList = [];
var gamePlayer = 0;
app.use("/", express.static(__dirname + "/client"));
server.listen(3000, function () {
console.log("server is running now.");
});

io.on("connection", function (socket) {
if (passCodeList.length > 2) {
return;
}

socket.on("login", function (nickname) {

var randomCode = 0;
randomCode = Math.floor(Math.random() * 10000);
socket.emit("loginSuccess", randomCode);
passCodeList.push(randomCode);

});

// 离线事件广播
socket.on("disconnect", function () {
passCodeList = [];
gamePlayer = 0;
socket.broadcast.emit("refresh");
console.log("玩家离线");
});
}

这里的io.on是整个socket的启动,当有新的客户端链接进来的,就开始执行其中的逻辑。参数socket代表的是当前链接的信息,有点像js中的this。

然后我们开始通过socket.on(“login”)来接收来自客户端的login消息。如果有这养的消息传进来。我们就开始执行下部分的逻辑。这里你可以尝试着打印下参数中的nickname,看和你所输入的nickname是不是吻合的。接着我们把生成的passcode来push到passcodelist中去做统一的管理,因为我们后面还得通过验证passcode来加入游戏。

后面我们发现了一个不同的消息传送的方式。socket.broadcast.emit()这个是socket广播消息的函数。用来向所有的已经链接到了服务端的所有客户端发布消息。这里当我们的服务器收到来自客户端的disconnect消息之后,他便向所有的客户端广播。refresh消息来告诉客户端,有玩家离线了。最前面的if语句是用来做一个小小的游戏人数控制用的。因为目前我们只做双人游戏。所以暂时只允许两名玩家加入游戏。

ok,接下来又得写客户端的逻辑了。当我们受到来自服务端的loginSuccess消息之后,我们得做点什么。

打开main.js,加入

1
2
3
4

self.socket.on("loginSuccess", function (passCode) {
$(".result .text").text(passCode);
});

这样我们在前端的页面中显示这个passcode了。

image

有了创建的逻辑之后,我们开始写用户加入游戏房间的逻辑。

继续在main.js中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 通过passcode加入游戏
$("#join-button").click(function () {
if ($("#join-text").val().length <= 0) {
$(".result .notice").text("ERROR");
$(".result .text").text("Pass Code Wrong");
} else {
self.socket.emit("addRoom", $("#join-text").val());
}
});

self.socket.on("addSuccess", function () {
$(".result .text").text("Wait For Another Player");
});

这个逻辑很简单,我就不多说了,就是输入passcode,然后发送消息到服务端。

我们要在服务端接收这个消息,并且去验证这个passcode是否是正确的。

ok,打开server.js,往下加入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

socket.on("addRoom", function (passCode) {
if (passCodeList.indexOf(parseInt(passCode)) != -1) {
passCodeList.push(passCode);
socket.emit("addSuccess");
gamePlayer++;
if (gamePlayer == 1) {
socket.position = "1p";
} else if (gamePlayer == 2) {
socket.position = "2p";
} else {
socket.position = "wrong";
}
if (gamePlayer == 2) {
console.log("达到了两个玩家哟");
socket.emit("gameStart", socket.position);
socket.broadcast.emit("gameStart", "1p");
}
}
});

我们首先验证passcode是不是正确的。如果正确,就可以开始向客户端发送成功的消息,并且分配1p和2p的游戏位置给两个玩家。然后在控制台作出相应的输出。当玩家数量到可以开始游戏了之后,向客户端发送gameStart消息。

游戏逻辑以及数据交换

回到客户端,我们这里拿到了gameStart消息之后,意味着游戏可以开始了。

打开main.js,我们开始要写游戏逻辑了。

这里首先说明下。因为这个只是基于socket学习的demo,我不打算把这个游戏逻辑设计得有多么的复杂。只是为了简短的实现联机游戏的基本功能。如果想让这个游戏更加具有游戏性,完全可以自己来做一些修改添加。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

// 游戏客户端主逻辑
function GameLogic(socket) {
var self = this;
self.socket = socket;
setTimeout(function () {
self.init(socket);
}, 1000);
}

GameLogic.prototype.init = function (socket) {
Hamster.init("main", 800, 600, null, "#000");

// 闪屏logo
var flash = Hamster.sprite({
"name": "flash",
"imageName": "flash",
"x": "0",
"y": "0"
});
Hamster.add(flash);
flash.setIndex(1);
flash.setSize(178, 100);
flash.scale(6, 6);
flash.x = -120;
flash.y = 0;

var timeout = setTimeout(function () {
Hamster.remove(flash);
}, 3000);

var road = Hamster.sprite({
"name": "road_bg",
"imageName": "road_bg",
"x": "0",
"y": "0"
});

road.x = 200;
road.y = 50;
Hamster.add(road);

var hero1 = Hamster.sprite({
"name": "Hero1",
"imageName": "Hero1",
"x": "0",
"y": "0"
});

hero1.setSize(11, 16);
hero1.scale(2.5, 2.5);

hero1.x = 340;
hero1.y = 520;

var hero2 = Hamster.sprite({
"name": "Hero2",
"imageName": "Hero2",
"x": "0",
"y": "0"
});
hero2.setSize(11, 16);
hero2.scale(2.5, 2.5);

hero2.x = 410;
hero2.y = 520;

Hamster.add(hero1);
Hamster.add(hero2);

// 玩家信息
var playerInfo = Hamster.UI.Text({
"text": "You are " + window.player,
"color": "#ffffff",
"fontSize": "18"
});
playerInfo.x = 20;
playerInfo.y = 50;
Hamster.add(playerInfo);

// 提示
var noticeText = Hamster.UI.Text({
"text": "按键盘'W'让小车前进,率先到达终点获胜",
"color": "#ffffff",
"fontSize": "18"
});
noticeText.x = 20;
noticeText.y = 20;
Hamster.add(noticeText);

socket.on("position1Fresh", function (position1) {
if (window.player == "2p") {
hero1.y = position1;
}
});

socket.on("position2Fresh", function (position2) {
if (window.player == "1p") {
hero2.y = position2;
}
});

Hamster.addEventListener(hero1, "keyUp", function (e) {
if (e.code == "KeyW") {
if (window.player == "1p") {
hero1.y -= 9;

socket.emit("position1", hero1.y);

} else if (window.player == "2p") {
hero2.y -= 9;
socket.emit("position2", hero2.y);
}
}
});

// 玩家断线
socket.on("refresh", function () {
location.reload();
});

socket.on("gameOver", function (result) {
alert(result + "is the Winner");
Hamster.removeAll();
});

}

这里我就不详细一段一段的说明了,前面主要是各种游戏元素的添加,加入键盘的keyUp事件,当按下w松开的时候,会触发小车前进的动作。当玩家player1的小车前进了之后,将自己的坐标通过soket.emit(“position1”)发送到服务端,服务端在得到player1的数据之后会向player2玩家发送消息“position1Fresh”,这样player2拿到这个消息中player1的位置之后,在自己的屏幕上更新player1的位置,这样就像是player1在player2的客户端进行了移动。

最后加上游戏结束时的消息响应,客户端的逻辑基本就结束了。

回到server.js ,加入对player1和player2的消息的响应,以及结束条件的判定。整个游戏基本就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

socket.on("position1", function (position1) {
socket.broadcast.emit("position1Fresh", position1);
if (position1 <= 50) {
var result = "1p";
socket.emit("gameOver", result);
socket.broadcast.emit("gameOver", result);
}

});

socket.on("position2", function (position2) {
socket.broadcast.emit("position2Fresh", position2);
if (position2 <= 50) {
var result = "2p";
socket.broadcast.emit("gameOver", result);
socket.emit("gameOver", result);
}
});

结尾

最后放一张游戏的截图。

注意,这里使用的游戏素材请注意版权,不要做学习之外的用途。

整个项目的源码放在了github上。

image