HTML+CSS+JS实现的贪吃蛇游戏<二>(完)

  通过上一部分的分析,我们已经完成了游戏的地图和基本的游戏元素,其主要的思想就是:通过js的DOM编程来动态添加和修改html元素和其属性,然后通过css来控制其界面的显示。现在我们需要进一步的深入,来解释后面几个逻辑的具体实现方法。

第三步和第四步

  这里我们把三、四两步放在一起来考虑。仔细想一想,蛇动起来后肯定会碰到各种东西,比如我们设置的食物,还有各个边界,所以我们在运动的过程中就要来考虑判定的机制。大致的方向我们有了,我们就来考虑细部的解决方法。
  第一个要做的让蛇动起来,也就是让蛇在非身体的三个方向上移动一格,单纯考虑在空地上移动的话,我们可以这样来做:我们去掉蛇身体的最后一截,然后把下一格放入蛇的头部。我们都知道蛇吃到食物是要变长的,所以如果我们判定到下一步格子里有食物,那我们就不用去掉最后一截了,只要直接在头部加一截就好了。基本方法有了,那我们怎么知道玩家控制的蛇下一步是去哪?大家肯定说:按哪个方向键就去哪呗!是的,玩过的都知道,那么我们怎么才能实现?这个过程可以理解为对键盘按键的响应,我们捕获按键,并获得其值,然后判断并反应到程序中。

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
this.step = function (){
//判断现在是否在暂停状态
if (this.pause) {
this.talk("暂停啦,休息一下~");
window.clearInterval(this.snakeTimer);
for (var i = 0; i < this.brakeTimers.length; i++) window.clearTimeout(this.brakeTimers[i]);
for (var i = 0; i < this.skateTimers.length; i++) window.clearTimeout(this.skateTimers[i]);
return;
}
//取得现在的头节点
var headX = this.snake_pos[0][0];
var headY = this.snake_pos[0][1];
//加入一些随机元素
if (this.len >= 10 && this.brakeTimers.length <= Math.floor(this.len /2)) {
this.brakeTimers.push(window.setTimeout(function() {
this.addObject("brake").bind(this)
}, randNum(5000, 50000)));
}
if (this.skateTimers.length <= 5 || this.skateTimers.length >= 35) {
this.skateTimers.push(window.setTimeout(function() {
this.addObject("skate").bind(this)
}, randNum(5000, 50000)));
}
//根据方向取得下一步头节点坐标
switch (this.directionkey){
case 37://左下右上
headY -= 1;
break;
case 38:
headX -= 1;
break;
case 39:
headY += 1;
break;
case 40:
headX += 1;
break;
}
//针对头节点撞墙和撞自己
if(headX < 0 || headX >= this.HEIGHT || headY < 0 || headY >= this.WIDTH || this.carrier[headX][headY] == "cover" || this.carrier[headX][headY] == "block" ){
//撞墙后则进入非正常状态,为暂停和死亡的区分
this.status = false;
this.talk("你撞到东西啦~游戏结束!");
//设置最高分
if (this.score >= parseInt($("best_score").innerHTML)) {
$("best_score").innerHTML = this.score;
}
$("btnStart").removeAttribute("disabled");
$("btnStart").style.color = "#000";
//清除定时器
window.clearInterval(this.snakeTimer);
for (var i = 0; i < this.brakeTimers.length; i++) window.clearTimeout(this.brakeTimers[i]);
for (var i = 0; i < this.skateTimers.length; i++) window.clearTimeout(this.skateTimers[i]);
return;
}
//捡到刹车
if (this.carrier[headX][headY] == "brake" ) {
if (this.speed >= 20) {
this.speed -= 10;
this.move();
this.talk("慢一点,步子太快会扯着蛋~");
}else{
this.talk("已经很慢啦,不能再减速了~");
}
}
//遭遇滑板
if (this.carrier[headX][headY] == "skate" ) {
if (this.speed <= 50) {
this.speed += 10;
this.move();
this.talk("带你飞~");
} else{
this.talk("不能再快啦~");
}
}
//如果吃到食物
if(this.carrier[headX][headY] == "food"){
this.talk("吃到食物啦~");
this.score += 1;
$("score").innerHTML = this.score;
//清除旧食物,添加新食物,加分
this.carrier[headX][headY] = '';
this.gridElems[headX][headY].className = "";
this.addObject("food");
//满足一定条件则加速,并重置定时器
if (this.len % 4 == 0 && this.speed < 60) {
this.speed += 2.5;
this.move();
this.talk("加速,加速~");
}
if (this.len % 6 == 0 && this.len < 60) {
this.addObject("block");
this.talk("增加难度啦~");
}
this.len += 1;
}else{//没有吃食物
var tail = this.snake_pos.pop();
this.carrier[tail[0]][tail[1]] = "";
this.gridElems[tail[0]][tail[1]].className = false;
}
//最后向前动一步,上面已经对是否吃食处理
this.snake_pos.unshift([headX, headY]);
this.carrier[headX][headY] = "cover";
this.gridElems[headX][headY].className = "cover";
this.direction_changed = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.onkeydown = function(e){
//对空格的暂停/开始动作的响应
if (snake_obj.snake_pos && snake_obj.status) {
if (e.keyCode == 32) {
if (snake_obj.pause) {
snake_obj.pause = false;
snake_obj.talk("游戏重新开始啦~");
snake_obj.move();
} else{
snake_obj.pause = true;
}
}
}
//捕获下一步的方向
var bool = (e.keyCode > 36 && e.keyCode < 41 && Math.abs(e.keyCode - snake_obj.directionkey) != 2);
snake_obj.directionkey = (bool && !snake_obj.direction_changed) ? e.keyCode:snake_obj.directionkey;
//保证方向键在被执行后才能修改
if(bool && !snake_obj.direction_changed){
snake_obj.direction_changed = true;
}
return;
}

  上面的第一块代码就是步进程序,里面的每一小块代码前都有一行注释说明功能,有些我没有提到的功能是最后我加入的,现在可以不关注,你只要理解我上面提到的逻辑。后一块是按键响应代码,基本的是捕获下一步的方向,其他的是我做的优化,包括暂停功能。此处还修改了一个小bug,这个bug是在蛇长度只有3格的时候,你快速的按键会使游戏结束,原因是撞到了自己(见下图)。这个结果是由捕获程序造成的,虽然它禁止了蛇身的方向,但是它的判断机制是与现在的记录方向相比,所以你快速切换两次方向就可以换到错误的方向,而修正的方法也很简单,那就是在没有响应前一个准确按键前不能响应下一个按键。
        原有bug展示 
  上面的想法已经可以让蛇平稳的走出第一步了,但是,我们的游戏需要不断的继续下去,所以光走一步是不行的,我们需要让它不断的走下去,直到游戏结束,也就是撞边界或者自己的身体。这个我们就需要定时的调用让它走一步的方法,来实现不断的前进的效果。

1
2
3
4
5
6
this.move = function(){
if (this.snakeTimer) {
window.clearInterval(this.snakeTimer);
}
this.snakeTimer = window.setInterval(self.step, Math.floor(3000 / self.speed));
}

  该段程序很简单,就是给步进程序设置一个定时器,不断的执行。细心的同学肯定注意到了我们在这段代码了使用了bind,这是什么?简单来说这是为了在window.setInterval()中使用类内的方法而不出错,细节我会另写一篇博客记录。好了,到这里我们的游戏的基础逻辑都完整了。

第五步

  终于到最后一步了,但是也不能掉以轻心。其实这一段的内容已经写在上一部分了,你可以回去看步进的程序代码,里面有一段加入一些随机元素的代码,其实也就是增加一些娱乐性,基本的原理就是在游戏过程中判断并修改游戏的参数,增加游戏乐趣,由于我也只是初学者,只是模仿加了一些简单的元素,在此也就不赘述了,大家看一下就可以。

写在最后

  文章结尾我把整个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
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
//common
function $(id){
return document.getElementById(id);
}
//global
var len = 3;
var speed = 10;
var directionkey = 39; //37-40左上右下
var WIDTH = 50;
var HEIGHT = 20;
window.onload = function(){
var size = document.getElementsByName("size");
for (var i = 0; i < size.length; i++) {
if (size[i].checked) {
// window.alert(size[i].value);
WIDTH = parseInt(size[i].value);
}
}
var snake_obj = new snake(len, speed, directionkey, WIDTH, HEIGHT, "say");//生成一个对象
snake_obj.initGrid("snakeWrap");//生成游戏区域
var btnStart = $("btnStart");
btnStart.onclick = function(){
btnStart.blur(); //firefox中必须释放焦点
snake_obj.clearAll();//重置数据
//重新生成区域,获取页面设置值
var size = document.getElementsByName("size");
for (var i = 0; i < size.length; i++) {
if (size[i].checked) {
// window.alert(size[i].value);
snake_obj.WIDTH = parseInt(size[i].value);
}
}
snake_obj.initGrid("snakeWrap");
startgame(snake_obj);
btnStart.setAttribute("disabled", true);
btnStart.style.color = "#aaa";
}
window.onkeydown = function(e){
if (snake_obj.snake_pos && snake_obj.status) {
if (e.keyCode == 32) {
if (snake_obj.pause) {
snake_obj.pause = false;
snake_obj.talk("游戏重新开始啦~");
snake_obj.move();
} else{
snake_obj.pause = true;
}
}
}
var bool = (e.keyCode > 36 && e.keyCode < 41 && Math.abs(e.keyCode - snake_obj.directionkey) != 2);
snake_obj.directionkey = (bool && !snake_obj.direction_changed) ? e.keyCode:snake_obj.directionkey;
//保证方向键在被执行后才能修改,否则可能会有bug,比如身体长度为3时可能撞到自己
if(bool && !snake_obj.direction_changed){
snake_obj.direction_changed = true;
}
return;
}
}
function startgame(obj){
obj.initSnake();
obj.addObject("food");
obj.move();
}
//定义类
function snake(l, s, d, W, H, t){
this.len = l;
this.speed = s;
this.directionkey = d;
this.WIDTH = W;
this.HEIGHT = H;
this.gridElems = multiArray(H, W); //关联html中要控制的表格对象
this.carrier = multiArray(H, W); //用来记录表格的格点使用情况
this.snake_pos;
this.snakeTimer;
this.score = 0;
this.talk_id = t; //绑定界面上用来显示对话的窗口的id
this.brakeTimers = [];
this.skateTimers = [];
this.direction_changed = false; //当前按钮是否被执行
this.pause = false; //当前是否在停止状态
this.status = true; //当前是否在正常游戏状态,撞墙后则设置为false
this.table;
this.initGrid = function(target){
if (this.table) {
$(target).removeChild(this.table);
}
this.table = document.createElement("table");
var tbody = document.createElement("tbody");
for (var i = 0; i < this.HEIGHT; i++) {
var row = document.createElement("tr");
for (var j = 0; j < this.WIDTH; j++) {
var col = document.createElement("td");
row.appendChild(col);
this.gridElems[i][j] = col;
}
tbody.appendChild(row);
}
this.table.appendChild(tbody);
$(target).appendChild(this.table);
}
this.initSnake = function (){
this.talk("贪吃蛇游戏开始啦~");
$("score").innerHTML = this.score;
this.snake_pos = new Array();
var snake_head_dot = randdot(this.carrier, 0, this.len - 1, this.HEIGHT, Math.floor(WIDTH/2));
for (var i = 0; i < this.len; i++) {
var x = snake_head_dot[0];
var y = snake_head_dot[1] - i;
this.snake_pos.push([x, y]);
this.carrier[x][y] = "cover";
this.gridElems[x][y].className = "cover";
}
}
this.addObject = function(type){
var pos = randdot(this.carrier);
this.carrier[pos[0]][pos[1]] = type;
this.gridElems[pos[0]][pos[1]].className = type;
}
this.step = function (){
//判断现在是否在暂停状态
if (this.pause) {
this.talk("暂停啦,休息一下~");
window.clearInterval(this.snakeTimer);
for (var i = 0; i < this.brakeTimers.length; i++) window.clearTimeout(this.brakeTimers[i]);
for (var i = 0; i < this.skateTimers.length; i++) window.clearTimeout(this.skateTimers[i]);
return;
}
//取得现在的头节点
var headX = this.snake_pos[0][0];
var headY = this.snake_pos[0][1];
//加入一些随机元素
if (this.len >= 10 && this.brakeTimers.length <= Math.floor(this.len /2)) {
this.brakeTimers.push(window.setTimeout(function() {
this.addObject("brake").bind(this)
}, randNum(5000, 50000)));
}
if (this.skateTimers.length <= 5 || this.skateTimers.length >= 35) {
this.skateTimers.push(window.setTimeout(function() {
this.addObject("skate").bind(this)
}, randNum(5000, 50000)));
}
//根据方向取得下一步头节点坐标
switch (this.directionkey){
case 37://左下右上
headY -= 1;
break;
case 38:
headX -= 1;
break;
case 39:
headY += 1;
break;
case 40:
headX += 1;
break;
}
//针对头节点撞墙和撞自己
if(headX < 0 || headX >= this.HEIGHT || headY < 0 || headY >= this.WIDTH || this.carrier[headX][headY] == "cover" || this.carrier[headX][headY] == "block" ){
//撞墙后则进入非正常状态,为暂停和死亡的区分
this.status = false;
this.talk("你撞到东西啦~游戏结束!");
//设置最高分
if (this.score >= parseInt($("best_score").innerHTML)) {
$("best_score").innerHTML = this.score;
}
$("btnStart").removeAttribute("disabled");
$("btnStart").style.color = "#000";
//清除定时器
window.clearInterval(this.snakeTimer);
for (var i = 0; i < this.brakeTimers.length; i++) window.clearTimeout(this.brakeTimers[i]);
for (var i = 0; i < this.skateTimers.length; i++) window.clearTimeout(this.skateTimers[i]);
return;
}
//捡到刹车
if (this.carrier[headX][headY] == "brake" ) {
if (this.speed >= 20) {
this.speed -= 10;
this.move();
this.talk("慢一点,步子太快会扯着蛋~");
}else{
this.talk("已经很慢啦,不能再减速了~");
}
}
//遭遇滑板
if (this.carrier[headX][headY] == "skate" ) {
if (this.speed <= 50) {
this.speed += 10;
this.move();
this.talk("带你飞~");
} else{
this.talk("不能再快啦~");
}
}
//如果吃到食物
if(this.carrier[headX][headY] == "food"){
this.talk("吃到食物啦~");
this.score += 1;
$("score").innerHTML = this.score;
//清除旧食物,添加新食物,加分
this.carrier[headX][headY] = '';
this.gridElems[headX][headY].className = "";
this.addObject("food");
//满足一定条件则加速,并重置定时器
if (this.len % 4 == 0 && this.speed < 60) {
this.speed += 2.5;
this.move();
this.talk("加速,加速~");
}
if (this.len % 6 == 0 && this.len < 60) {
this.addObject("block");
this.talk("增加难度啦~");
}
this.len += 1;
}else{//没有吃食物
var tail = this.snake_pos.pop();
this.carrier[tail[0]][tail[1]] = "";
this.gridElems[tail[0]][tail[1]].className = false;
}
//最后向前动一步,上面已经对是否吃食处理
this.snake_pos.unshift([headX, headY]);
this.carrier[headX][headY] = "cover";
this.gridElems[headX][headY].className = "cover";
this.direction_changed = false;
}
this.move = function(){
if (this.snakeTimer) {
window.clearInterval(this.snakeTimer);
}
this.snakeTimer = window.setInterval(this.step.bind(this), Math.floor(3000 / this.speed));
}
this.talk = function(sth){
$(this.talk_id).innerHTML = sth;
}
this.clearAll = function(){//把记录清空,把图像恢复,参数重置
this.len = 3;
this.speed = 10;
this.directionkey = 39;
this.score = 0;
this.status = true;
this.pause = false;
for (var i = 0; i < this.HEIGHT; i++) {
for (var j = 0; j < this.WIDTH; j++) {
this.carrier[i][j] = false;
this.gridElems[i][j].className = "";
}
}
}
function randNum(start, end){
return (Math.floor(Math.random() * (end - start)) + start);
}
function randdot(carrier, startX, startY, endX, endY){
startX = startX || 0;
startY = startY || 0;
endX = endX || this.HEIGHT;
endY = endY || this.WIDTH;
var x = Math.floor(Math.random() * (endX - startX)) + startX;
var y = Math.floor(Math.random() * (endY - startY)) + startY;
var dot = [x, y];
if (carrier[x][y]) {
return randdot(carrier, startX, startY, endX, endY);
}
return dot;
}
function multiArray(m, n) { //创建m*n的二维数组,即矩阵
var arr = new Array(m);
for (var i = 0; i < m; i++) {
arr[i] = new Array(n);
}
return arr;
}
}

  上面的就是所有的代码了,比起前面的游戏逻辑,其中还有一些基本的操作,比如:在页面加载完成后对一些按钮绑定一些操作等等,这些都不是本文的重点。大家可以看到,用类封装后,基本的操作就只需要调用类的方法了,而所有的属性也都存储在类之中,获取都很有针对性,而作为主函数的startgame非常的简单,根本不用考虑具体实现,优势立现。

版权:本文采用以下协议进行授权,自由转载 - 非商用 - 非衍生 - 保持署名 | Creative Commons BY-NC-ND 3.0,转载请注明作者及出处。