实现效果

推箱子是一款风靡已久的益智小游戏,如何在网页实现推箱子功能?这就不得不说我们html5新增的canvas功能,让我们能对图片等资源可以更好的绘制。

设计思路

素材

首先我们应该拥有设计素材,博主木有嗷~所以在网上找了一些合适的素材。分别代表墙、草地、应该被推入的位置标志、箱子和我们的人物,如下图。

实现思路

  1. 生成背景图

对于页面的显示,我们使用像素化的表示方式。如上图,一个正方块为假设为一个单元,我们的实现图就是一个10 * 7 的地图,我们可以用二维数组来表示它:0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置。

1
2
3
4
5
6
7
8
// 我们的初始地图可以表示为:
[[0,0,0,1,1,1,1,1,1,0],
[0,1,1,1,2,2,2,2,1,0],
[1,1,3,2,4,1,1,2,1,1],
[1,3,3,4,2,4,2,2,5,1],
[1,3,3,2,4,2,4,2,1,1],
[1,1,1,1,1,1,2,2,1,0],
[0,0,0,0,0,1,1,1,1,0]];

我们生成一个Sokoban对象,用来记录我们的推箱子一个地图的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Sokoban = function(boxContext,width,height) {
this.context = boxContext; //存储canvas的 ctx 对象
this.width = width;
this.height = height;
this.curLoc = {x:8,y:3}; //是箱子初始的位置
this.targetLoc = [{x:1,y:3},{x:1,y:4},{x:2,y:2},{x:2,y:3},{x:2,y:4}]; //是应该把箱子推入的位置
this.imgArray = [new Image(),new Image(),new Image(),new Image(),new Image()]; //我们填充地图的图片资源,现在没有给src
//0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
this.cellArray = [[0,0,0,1,1,1,1,1,1,0],
[0,1,1,1,2,2,2,2,1,0],
[1,1,3,2,4,1,1,2,1,1],
[1,3,3,4,2,4,2,2,5,1],
[1,3,3,2,4,2,4,2,1,1],
[1,1,1,1,1,1,2,2,1,0],
[0,0,0,0,0,1,1,1,1,0]];
}

  1. 页面打开时,根据地图把二维数组用图片进行背景的填充:
    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
    //给sokoban对象附加原型方法
    paintCells : function(){
    //0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
    for(var i=0,w=this.cellArray[0].length,h=this.cellArray.length;i<h;i++){
    for(var j=0;j<w;j++){
    this.fillCell(this.cellArray[i][j],j,i);
    }
    }
    },
    fillCell : function(type,x,y){
    var cellWidth = this.width/this.cellArray[0].length; //一个单元的宽度
    var cellHeight = this.height/this.cellArray.length; //一个单元的高度
    switch(type){
    case 0: this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.fillStyle = '#000';
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    case 1:
    this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    var ptn = this.context.createPattern(this.imgArray[0],"repeat");
    this.context.fillStyle = ptn;
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    case 2: this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    case 3: this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.drawImage(this.imgArray[2],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    case 4: this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.drawImage(this.imgArray[3],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    case 5: this.context.save();
    this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
    this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.drawImage(this.imgArray[4],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
    this.context.restore();
    break;
    }
    },

3、监听按键事件window.onkeydown ,其中我们的重点是上下左右按键,分别对应的unicode编码为:
←:37
↑:38
→:39
↓:40

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
window.onkeydown = function(e){
e.preventDefault();
that.action(e.keyCode); //处理按键值
};
//处理上下左右的逻辑
action : function(direction){
var nextLoc = {};
switch(direction){
case 38:
nextLoc.type = this.cellArray[this.curLoc.y-1][this.curLoc.x];
nextLoc.x = this.curLoc.x;
nextLoc.y = this.curLoc.y-1;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc); //更新移动后的位置
this.paintCells(); //重新绘制地图
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y-2][this.curLoc.x] == 2 || this.cellArray[this.curLoc.y-2][this.curLoc.x] == 3){
this.pushBox(nextLoc.x,nextLoc.y-1);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 40:
nextLoc.type = this.cellArray[this.curLoc.y+1][this.curLoc.x];
nextLoc.x = this.curLoc.x;
nextLoc.y = this.curLoc.y+1;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y+2][this.curLoc.x] == 2 || this.cellArray[this.curLoc.y+2][this.curLoc.x] == 3){
this.pushBox(nextLoc.x,nextLoc.y+1);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 37:
nextLoc.type = this.cellArray[this.curLoc.y][this.curLoc.x-1];
nextLoc.x = this.curLoc.x-1;
nextLoc.y = this.curLoc.y;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y][this.curLoc.x-2] == 2 || this.cellArray[this.curLoc.y][this.curLoc.x-2] == 3){
this.pushBox(nextLoc.x-1,nextLoc.y);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 39:
nextLoc.type = this.cellArray[this.curLoc.y][this.curLoc.x+1];
nextLoc.x = this.curLoc.x+1;
nextLoc.y = this.curLoc.y;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y][this.curLoc.x+2] == 2 || this.cellArray[this.curLoc.y][this.curLoc.x+2] == 3){
this.pushBox(nextLoc.x+1,nextLoc.y);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
default: break;
}
},
pushBox : function(boxToX,boxToY){
this.cellArray[boxToY][boxToX] = 4;
},
move : function(nextLoc){
this.cellArray[nextLoc.y][nextLoc.x] = 5;
this.cellArray[this.curLoc.y][this.curLoc.x] = 2;
for(var i = this.targetLoc.length -1; i>=0; i--){
if(this.targetLoc[i].x == this.curLoc.x && this.targetLoc[i].y == this.curLoc.y){
this.cellArray[this.curLoc.y][this.curLoc.x] = 3;
}
}
this.curLoc.y = nextLoc.y;
this.curLoc.x = nextLoc.x;
},

  1. 检测我们的通关条件,提示“通关”!
    所有的应该被添位置,都有箱子了,就通关啦

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    checkResult : function(){
    var done = 0, total = this.targetLoc.length;
    for(var i = total -1; i>=0; i--){
    if(this.cellArray[this.targetLoc[i].y][this.targetLoc[i].x] == 4){
    done++;
    }
    }
    if( done == total ){
    window.onkeydown = null;
    this.context.save();
    this.context.font="40px Verdana";
    this.context.textAlign = "center";
    // 创建渐变
    var gradient=this.context.createLinearGradient(0,0,this.width,this.height);
    gradient.addColorStop("0.3","red");
    gradient.addColorStop("0.5","blue");
    gradient.addColorStop("0.7","yellow");
    // 用渐变填色
    this.context.fillStyle = gradient;
    this.context.fillText("Congratulations!!",this.width/2,this.height/2);
    this.context.restore();
    }
    },
  2. 重置地图
    如果用户发现走到了死胡同,这时候需要重置地图。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    restart : function(){
    this.curLoc = {x:8,y:3};
    //0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
    this.cellArray = [[0,0,0,1,1,1,1,1,1,0],
    [0,1,1,1,2,2,2,2,1,0],
    [1,1,3,2,4,1,1,2,1,1],
    [1,3,3,4,2,4,2,2,5,1],
    [1,3,3,2,4,2,4,2,1,1],
    [1,1,1,1,1,1,2,2,1,0],
    [0,0,0,0,0,1,1,1,1,0]];
    sokoban.init();
    }

整体实现

以下,献上完整代码:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>推箱子</title>
</head>
<body>
<canvas id="boxCanvas" width="500px" height="350px"></canvas>
<div style="width:500px;text-align:center;margin-top:10px">
<button style="width:90px;height:32px;" onclick="restartGame();">Restart</button>
</div>
</body>
<script type="text/javascript">
function restartGame(){
sokoban.restart();
}

var Sokoban = function(boxContext,width,height) {
this.context = boxContext;
this.width = width;
this.height = height;
this.curLoc = {x:8,y:3};
this.targetLoc = [{x:1,y:3},{x:1,y:4},{x:2,y:2},{x:2,y:3},{x:2,y:4}];
this.imgArray = [new Image(),new Image(),new Image(),new Image(),new Image()];
//0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
this.cellArray = [[0,0,0,1,1,1,1,1,1,0],
[0,1,1,1,2,2,2,2,1,0],
[1,1,3,2,4,1,1,2,1,1],
[1,3,3,4,2,4,2,2,5,1],
[1,3,3,2,4,2,4,2,1,1],
[1,1,1,1,1,1,2,2,1,0],
[0,0,0,0,0,1,1,1,1,0]];
}

Sokoban.prototype = {
fillCell : function(type,x,y){
var cellWidth = this.width/this.cellArray[0].length;
var cellHeight = this.height/this.cellArray.length;
switch(type){
case 0: this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.fillStyle = '#000';
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
case 1:
this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
var ptn = this.context.createPattern(this.imgArray[0],"repeat");
this.context.fillStyle = ptn;
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
case 2: this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
case 3: this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.drawImage(this.imgArray[2],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
case 4: this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.drawImage(this.imgArray[3],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
case 5: this.context.save();
this.context.clearRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.fillStyle = this.context.createPattern(this.imgArray[1],"repeat");
this.context.fillRect(cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.drawImage(this.imgArray[4],cellWidth*x,cellHeight*y,cellWidth,cellHeight);
this.context.restore();
break;
}
},
prepareImage : function(callback){
var imgSrcArray = ['images/1.png','images/2.png','images/3.png','images/4.png','images/5.png'];
var that = this;
for(var i=0,len=this.imgArray.length,loading=len-1;i<5;i++){
this.imgArray[i].src = imgSrcArray[i];
this.imgArray[i].onload = function(){
len--;
if(!len){
callback.call(that);
window.onkeydown = function(e){
e.preventDefault();
that.action(e.keyCode);
};
}
};
}
},
paintCells : function(){
//0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
for(var i=0,w=this.cellArray[0].length,h=this.cellArray.length;i<h;i++){
for(var j=0;j<w;j++){
this.fillCell(this.cellArray[i][j],j,i);
}
}
},
init : function(){
this.prepareImage(this.paintCells);
},
action : function(direction){
var nextLoc = {};
switch(direction){
case 38:
nextLoc.type = this.cellArray[this.curLoc.y-1][this.curLoc.x];
nextLoc.x = this.curLoc.x;
nextLoc.y = this.curLoc.y-1;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y-2][this.curLoc.x] == 2 || this.cellArray[this.curLoc.y-2][this.curLoc.x] == 3){
this.pushBox(nextLoc.x,nextLoc.y-1);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 40:
nextLoc.type = this.cellArray[this.curLoc.y+1][this.curLoc.x];
nextLoc.x = this.curLoc.x;
nextLoc.y = this.curLoc.y+1;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y+2][this.curLoc.x] == 2 || this.cellArray[this.curLoc.y+2][this.curLoc.x] == 3){
this.pushBox(nextLoc.x,nextLoc.y+1);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 37:
nextLoc.type = this.cellArray[this.curLoc.y][this.curLoc.x-1];
nextLoc.x = this.curLoc.x-1;
nextLoc.y = this.curLoc.y;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y][this.curLoc.x-2] == 2 || this.cellArray[this.curLoc.y][this.curLoc.x-2] == 3){
this.pushBox(nextLoc.x-1,nextLoc.y);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
case 39:
nextLoc.type = this.cellArray[this.curLoc.y][this.curLoc.x+1];
nextLoc.x = this.curLoc.x+1;
nextLoc.y = this.curLoc.y;
if( nextLoc.type == 2 || nextLoc.type == 3){
this.move(nextLoc);
this.paintCells();
}else if( nextLoc.type == 4 && this.cellArray[this.curLoc.y][this.curLoc.x+2] == 2 || this.cellArray[this.curLoc.y][this.curLoc.x+2] == 3){
this.pushBox(nextLoc.x+1,nextLoc.y);
this.move(nextLoc);
this.paintCells();
}
this.checkResult();
break;
default: break;
}
},
pushBox : function(boxToX,boxToY){
this.cellArray[boxToY][boxToX] = 4;
},
move : function(nextLoc){
this.cellArray[nextLoc.y][nextLoc.x] = 5;
this.cellArray[this.curLoc.y][this.curLoc.x] = 2;
for(var i = this.targetLoc.length -1; i>=0; i--){
if(this.targetLoc[i].x == this.curLoc.x && this.targetLoc[i].y == this.curLoc.y){
this.cellArray[this.curLoc.y][this.curLoc.x] = 3;
}
}
this.curLoc.y = nextLoc.y;
this.curLoc.x = nextLoc.x;
},
checkResult : function(){
var done = 0, total = this.targetLoc.length;
for(var i = total -1; i>=0; i--){
if(this.cellArray[this.targetLoc[i].y][this.targetLoc[i].x] == 4){
done++;
}
}
if( done == total ){
window.onkeydown = null;
this.context.save();
this.context.font="40px Verdana";
this.context.textAlign = "center";
// 创建渐变
var gradient=this.context.createLinearGradient(0,0,this.width,this.height);
gradient.addColorStop("0.3","red");
gradient.addColorStop("0.5","blue");
gradient.addColorStop("0.7","yellow");
// 用渐变填色
this.context.fillStyle = gradient;
this.context.fillText("Congratulations!!",this.width/2,this.height/2);
this.context.restore();
}
},
restart : function(){
this.curLoc = {x:8,y:3};
//0表示无,1表示隔离,2表示空地,3表示目标,4表示箱子,5表示当前位置
this.cellArray = [[0,0,0,1,1,1,1,1,1,0],
[0,1,1,1,2,2,2,2,1,0],
[1,1,3,2,4,1,1,2,1,1],
[1,3,3,4,2,4,2,2,5,1],
[1,3,3,2,4,2,4,2,1,1],
[1,1,1,1,1,1,2,2,1,0],
[0,0,0,0,0,1,1,1,1,0]];
sokoban.init();
}
};

var boxCanvas = document.getElementById("boxCanvas");
var sokoban = new Sokoban(boxCanvas.getContext('2d'),boxCanvas.width,boxCanvas.height);
sokoban.init();


</script>
</html>

你学会了吗?