最近工作之余看了一个俄罗斯方块小游戏实现的视频教程,觉得这个小游戏的算法挺有意思,打算整理出来学习参考一下。
虽然标题标着canvas,其实这个小游戏用到的canvas知识点甚少,其核心还是js算法问题。教程会比较枯燥,可能对实际工作中也没太大用处(不过可以用俄罗斯方块做一个营销小活动),写出来目的主要是为了学习一下这款小游戏的算法思想,以及锻炼解决问题的能力。
前言
本篇文章将分为几个模块逐步讲解:
- 游戏玩法介绍
- 算法思想
- 创建游戏地图
- 方块的创建
- 生成不同形状方块
- 方块下落
- 方块碰撞
- 方块左右移动
- 方块变形
- 方块向下加速
- 消除整行
- 游戏结束
游戏虽小,功能还挺复杂,需要的就是思维和耐心。下面直接进入正题。
游戏玩法介绍
俄罗斯方块想必大家都玩过,大体玩法简单说一下,在一个格子地图上随机下落不同形状的方块,通过←→方向键去控制方块的移动,以及↑键改变方块的形状,↓键加速其下落,当方块落下后满一行时消除该行。
来看一下我们最终实现的效果图:
算法思想
我们可以把俄罗斯方块一分为二的看,地图+方块。我们把地图看成一个二维的数组:
[
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
[0,0,0,0,0,0...],
...
]
方块也看作一个二维数组
如Z形状的方块可看作为:
[
[1,1,0],
[0,1,1]
]
倒T形状的方块可看作为:
[
[0,1,0],
[1,1,1]
]
等等 然后两个数组结合起来:
[
[0,0,1,1,0,0,0,0],
[0,0,0,1,1,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0],
]
上面是一个Z字形方块在第一行。
从上面数组可以一目了然的看到,我们渲染canvas的时候将数组值为0的元素渲染成地图的颜色,而元素值为1的渲染成方块的颜色即可。 那么方块的移动又是怎么做呢,很简单就是去改变方块左右的元素从0改为1,再将本来的位置重置为0即可,在视觉上造成方块的移动。
如下图一个Z字方块的移动:
知道其核心思想之后,可以码代码开发了。
创建游戏地图
首先我们先准备一个canvas
画布:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>canvas</title>
<style>
#myCanvas {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
background: #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" height="500" width="500"></canvas>
<button id="stop">停止</button>
<button id="score">得分: 0</button>
<script src="/js/tetris/index.js"></script>
</body>
</html>
首先我们需要了解一下canvas
的简单操作,在画布上画一个小方块:
index.js
let Tetris = {
init(){
//初始化
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
this.render();
},
//渲染画布
render(){
//在坐标为100,100的位置画一个40*40的红色方块
this.gc.fillStyle = 'red';
this.gc.fillRect(100,100,40,40);
}
};
Tetris.init();
如图:
做俄罗斯方块所需的canvas
知识点你只需要知道fillStyle
,fillRect
即可,没有那么复杂。
接下来,我们要画出N*N的格子地图了。
先生成一个N*N的二维数组,值都为0:
function map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行长度为 r
data[i].length = r;
//所有元素默认值 0
data[i].fill(0);
}
return data;
}
console.table(map(20,10));
根据上面的方块的渲染方式和地图数组的生成结合,来渲染出一个20*20的地图:
完整代码如下:
index.js
let Tetris = {
init(){
//初始化
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
//20*20的格子
let data = this.map(20,20);
this.render(data);
},
map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行长度为 r
data[i].length = r;
//所有元素默认值 0
data[i].fill(0);
}
return data;
},
render(data){
//计算每个方块的宽高 方块之间间隔为4
let w = 500/20 - 4;
let h = 500/20 - 4;
//计算方块的行列
let r = data.length;
let c = data[0].length;
for(let i = 0; i < r; i ++){
for (let j = 0; j < c; j++){
//判断数组里的值 若为1 则渲染为红色 0 则渲染为白色
this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
this.gc.fillRect(
(w+4)*j+2,
(h+4)*i+2,
w,
h
);
}
}
}
};
Tetris.init();
分析一下上面稍微难以理解的是每个方块x,y轴值的计算。
详细分析如下:
画布宽高500 * 500 要平均分配20*20个方块,
那么方块的最大宽度为 500/20 = 25
但是这样的话方块会挤在一块 因此我们再给每个方块之间增加4个单位的间距
于是 单个方块的 w/h = 500/20 - 4 = 21;
坐标的算法:
20个方块之间有19个间隙 剩下一个4单位的距离 分配到小方块距离左右两侧的间隙
于是可以列出:
n列 x坐标
0 w*0 + 2
1 w*1 + 2 + 4*1
2 w*2 + 2 + 4*2
3 w*3 + 2 + 4*3
...
n w*n + 2 + 4*n
所以第j列的x坐标可以归纳为 (w+4)*j + 2
y坐标亦然
执行完代码看效果如下:
长征已经成功走出第一步,接下来就是方块的创建了。
方块的创建
方块有多种类型,如一字型,L字型,Z字型,倒T字型,田字型等等,根据上面算法中提到的,每种类型可以用一个二维数组描述出来:
blockType: [
[[1,1,1,1]],
[[1,1],[1,1]],
[[1,1,0],[0,1,1]],
[[0,1,1],[1,1,0]],
[[0,1,0],[1,1,1]],
[[1,0,0],[1,1,1]],
[[0,0,1],[1,1,1]]
]
那怎么把一个方块再地图上显示出来呢?
也很简单,只需要把方块的二维数组插入到地图的数组中就可以了。
简单实现如下:
//更新data数组
draw(block){
/*
* 假如block为Z字型的方块 [[1,1,0],[0,1,1]]
*/
for (let i = 0; i < block.length; i++){
for (let j = 0; j < block[0].length; j++){
this.data[i][j + this.y] = block[i][j];
}
}
console.table(this.data);
//再次调用render方法更新画布
this.render(this.data);
}
生成不同形状方块
要随机生成一个不同形状的方块,调用Math.random()
即可。
贴完整的代码:
index.js
let Tetris = {
//初始化
init(){
const canvas = document.getElementById('myCanvas');
this.gc = canvas.getContext('2d');
//20*20的格子
this.data = this.map(20,20);
//X轴的偏移量 之所以保存为变量 是以后我们做左右移动的是需要通过改变这个值来实现
this.x = 7;
//随机生成一个方块
this._block = this.block();
this.draw(this._block);
},
//地图数据
map(r,c){
let data = [];
for(let i = 0; i < c; i++){
data.push([]);
//每行长度为 r
data[i].length = r;
//所有元素默认值 0
data[i].fill(0);
}
return data;
},
//随机生成一个类型的方块
block(){
let index = Math.floor(Math.random()*7);
return this.blockType[index];
},
//方块的类型
blockType: [
[[1,1,1,1]],
[[1,1],[1,1]],
[[1,1,0],[0,1,1]],
[[0,1,1],[1,1,0]],
[[0,1,0],[1,1,1]],
[[1,0,0],[1,1,1]],
[[0,0,1],[1,1,1]]
],
//重绘画布
draw(block){
for (let i = 0; i < block.length; i++){
for (let j = 0; j < block[0].length; j++){
//要向x轴偏移 需要为j加一个偏移量即可
this.data[i][j + this.x] = block[i][j];
}
}
console.table(this.data);
this.render(this.data);
},
//渲染
render(data){
//计算每个方块的宽高 方块之间间隔为4
let w = 500/20 - 4;
let h = 500/20 - 4;
//计算方块的行列
let r = data.length;
let c = data[0].length;
for(let i = 0; i < r; i ++){
for (let j = 0; j < c; j++){
//判断数组里的值 若为1 则渲染为红色 0 则渲染为白色
this.gc.fillStyle = data[i][j] === 0 ? 'white' : 'red';
/*
* 坐标算法
* 画布宽度500 小方格宽度21 个数20 则 留下的空隙宽度为 500 - 21*20 = 80 其中 20个小方块可分4单位的间隙
* 20个方块之间有19个间隙 剩下一个4单位的距离 分配到小方块距离左右两侧的间隙
* 总结一下规律
* n行 x坐标
* 0 w*0 + 2
* 1 w*1 + 2 + 4
* 2 w*2 + 2 + 4*2
* 3 w*3 + 2 + 4*3
* ...
* n w*n + 2 + 4*n
* 所以第j列的x坐标可以归纳为 (w+4)*j + 2
* y坐标亦然
*/
this.gc.fillRect(
(w+4)*j+2,
(h+4)*i+2,
w,
h
);
}
}
}
};
Tetris.init();
刷新页面会出现随机的方块如下图:
方块下落
方块下落,其实就是要改变方块在画布上的Y轴位置,怎么持续下落呢,很显然需要开一个定时器。
[未完待续。。。]
方块碰撞
[未完待续。。。]
方块左右移动
[未完待续。。。]
方块变形
[未完待续。。。]
方块向下加速
[未完待续。。。]
消除整行
[未完待续。。。]
游戏结束
[未完待续。。。]
后言
最近时间比较紧,剩下模块慢慢更新。大体思想了解之后,其他问题都可以迎刃而解,无非就是需要耐心和不断的调试。
喜欢就赞一波,给点更新的动力😆
参考文献
人人贷大前端技术博客中心
最后广而告之。 欢迎访问人人贷大前端技术博客中心
里面有关nodejs
react
reactNative
小程序 前端工程化等相关的技术文章陆续更新中,欢迎访问和吐槽~