小游戏之俄罗斯方块

前言

非常经典的一个小游戏,也非常的消磨时间。而这个游戏也是我工作之余消磨时间消磨出来的。俄罗斯方块起源于拼图游戏,后来发展出了很多玩法,其中最为经典的包含了七种方块,L字型、J型、Z字型、S型、T字型、I型、O字形,游戏中需要把方块旋转,堆叠等进行消除获取分数,随着分数增加,难度也会增加。

一、基本布局

此游戏为了方便,我直接使用的是vue2,并且使用CDN方式引入,少了很多配置文件,运行也快。
关于布局,我用了一个背景图作为游戏场景图,让游戏更有氛围,其他还有旋转音效、消除音效。

(1)先看效果图:
在这里插入图片描述
在这里插入图片描述

在效果图中我们可以看到遮罩提示、分数以及下一个方块。

(2)布局

<div id="app">
	<!-- 基本布局 -->
	<div class="tetris">
		<img src="img/bg4.jpg" alt="" class="tetris-bg">
		<!-- 棋盘 -->
		<div class="tetris-box">
			<div class="map">
				<div class="tr" v-for="(item1, index1) in list" :key="index1">
					<div class="td" v-for="(item2, index2) in item1" :key="index2">
						<div class="brick brick-1" v-if="item2 == 1 || item2 == 2"></div>
					</div>
				</div>
			</div>
			<!-- 游戏遮罩 -->
			<div class="mask" v-if="!isStart || (isStart && isStop)"><span>空格</span>开始游戏</div>
		</div>
		<!-- 下一个形状 -->
		<div class="tetris-next">
			<div class="tetris-next-tr" v-for="item1 in list_next">
				<div class="tetris-next-td" v-for="item2 in item1">
					<div class="brick brick-1" v-if="item2 == 1"></div>
				</div>
			</div>
		</div>
		<!-- 分数 -->
		<div class="tetris-score">{{ score }}</div>
	</div>
	
	<!-- 背景音乐 -->
	<audio ref="audioBg" autoplay loop>
		<source src="audio/bg.mp3" />
	</audio>
	<!-- 上下左右音效 -->
	<audio ref="audio1">
		<source src="" />
	</audio>
	<!-- 消除音效 -->
	<audio ref="audio2">
		<source src="" />
	</audio>
</div>

(3)样式

基本样式如下,
这里方块颜色,我统一用的是蓝色,也就是游戏中方块用的同一种色块,这样方便玩家观察。
但是有的俄罗斯方块是多色系的。这里可以直接去我源码的index.css上看,我提供了七种颜色砖块。

#app {
	width: 100vw;
	height: 100vh;
	overflow: hidden;
}
.tetris {
	width: 580px;
	height: 930px;
	overflow: hidden;
	display: flex;
}
.tetris-box {
	position: absolute;
	width: 274px;
	height: 355px;
	top: 302px;
	left: 135px;
	z-index: 2;
	background-color: transparent;
	border-radius: 30px;
	overflow: hidden;
}
.tetris-bg {
	height: 100%;
}

/* 游戏区 */
.map {
	overflow: hidden;
	display: flex;
	flex-direction: column;
	width: 100%;
	height: 100%;
}
.map .tr {
	flex: 1;
	display: flex;
}
.map .tr .td {
	flex: 1;
}

/* 遮罩 */
.mask {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	z-index: 10;
	background-color: rgba(0,0,0,.6);
	border-radius: 30px;
	display: flex;
	align-items: center;
	justify-content: center;
	color: #fff;
	letter-spacing: 4px;
	user-select: none;
	line-height: 1;
}
.mask span {
	font-size: 25px;
	font-weight: bold;
}

/* 下一个 */
.tetris-next {
	width: 60px;
	height: 60px;
	position: absolute;
	z-index: 10;
	left: 484px;
	top: 330px;
	border-radius: 20px;
	display: flex;
	flex-direction: column;
	filter: opacity(.6);
}
.tetris-next .tetris-next-tr {
	flex: 1;
	display: flex;
}
.tetris-next .tetris-next-tr .tetris-next-td {
	flex: 1;
}

/* 分数 */
.tetris-score {
	width: 70px;
	text-align: center;
	position: absolute;
	left: 25px;
	top: 492px;
	z-index: 10;
	color: #b0cfff;
	filter: opacity(.5);
	font-size: 30px;
	font-weight: bold;
	transform: rotate(14deg);
	text-shadow: -4px 4px 4px rgba(0,0,0,.5);
}

/* 定义方块样式 */
.brick {
	width: 100%;
	height: 100%;
	overflow: hidden;
	border: 4px solid;
}
.brick.brick-1 { 
	background: linear-gradient(to bottom, rgb(145,207,249), rgb(55,117,212));
	border-top-color: rgb(214,238,254);
	border-right-color: rgb(27,114,231);
	border-bottom-color: rgb(13,78,210);
	border-left-color: rgb(144,206,248);
}

二、游戏实现

(1)定义我们需要的基本参数
list: [], 					// 地图数组 0=空地 1=移动砖块 2=固定砖块
list_next: [],				// 存放下一个图形的地图
row: 18,					// 行
col: 14,					// 列
speed: 400,					// 下落速度
isStart: false, 			// 是否开始
isOver: false,				// 是否结束
isStop: true,				// 是否暂停
brickList: [],				// 总的形状数组
brickNow: [],				// 当前形状
nowIndex1: null,			// 当前是哪个图形
nowIndex2: null,			// 当前图形是哪个形状
brickNext: [],				// 下一个
nextIndex1: null,			// 下一个是哪个图形
nextIndex2: null,			// 下一个图形是哪个形状
timer: null,				// 定时器
score: 0,					// 分数
scoreList: [				// 消除分数数组
	{ row: 1, score: 10 },
	{ row: 2, score: 30 },
	{ row: 3, score: 60 },
	{ row: 4, score: 100 }
],
(2)定义我们需要用到的方法

比如我们需要用的效果延迟定时器(后面会说到)、音效播放。

// 延迟加载特效
sleep(time) {
	return new Promise((resolve) => {
		setTimeout(() => {
			resolve()
		}, time)
	})
},

// 上下左右音效
playAudioRotate() {
	if(this.$refs.audio1) {
		this.$refs.audio1.src = 'audio/rotate.mp3';
		this.$refs.audio1.play();
	}
},
// 消除音效
playAudioClear() {
	if(this.$refs.audio2) {
		this.$refs.audio2.src = 'audio/clear.mp3';
		this.$refs.audio2.play();
	}
},

(3)监听键盘事件

我们在初始化的时候,需要开启监听键盘按下事件,监听用户按下WASD、上下左右、空格
其中涉及对应的方法,后面会说

// 监听键盘事件
window.addEventListener('keydown', (e) => {
	if(e.keyCode === 32) {
		this.onSpace();
	}
	if([37,38,39,40, 87,65,83,68].includes(e.keyCode)) {
		this.onDirection(e.keyCode)
	}
}, true);
(4)数据初始化

我们需要根据row、col动态生成二维地图
还要生成存放下一块图形的地图

// 生成地图
createMap() {
	this.list = [];
	for (let i = 0; i < this.row; i++) {
		this.list[i] = [];
		for(let j = 0; j < this.col; j++) {
			this.list[i][j] = 0;
		}
	}
	for (let i = 0; i < 4; i++) {
		this.list_next[i] = [];
		for(let j = 0; j < 4; j++) {
			this.list_next[i][j] = 0;
		}
	}
	this.initAnimate()
},
(5)游戏初始动画以及游戏结束时的动画(initAnimate)

在上面,我们可以看到有一个this.initAnimate()方法,
这个方法就是经典俄罗斯方块中的至下而上清屏效果
也就是会从最底一行开始往上运动,到达第一行再运动下去,一整个效果。
可直接复制完整代码查看效果

在这里插入图片描述

// 初始化地图动画
// 因为sleep方法是用promise处理的,所以这里我们需要用async和await
async initAnimate() {
	// 画面整体从下到上扫一遍
	for (let i = this.row - 1; i >= 0; i--) {
		await this.sleep(30)
		for(let j = 0; j < this.col; j++) {
			this.list[i][j] = 1;
		}
		this.$forceUpdate()
	}
	// 画面整体从上到下扫一遍
	for (let i = 0; i < this.row; i++) {
		await this.sleep(30)
		for(let j = 0; j < this.col; j++) {
			this.list[i][j] = 0;
		}
		this.$forceUpdate()
	}
	this.isStart = false;
	this.isOver = false;
},

我们为什么要用sleep方法呢,我当时做这个的时候,直接用的两个for循环,但是效果就是闪一下。没有那种感觉,这时候问题就是,如何让第一行先出现,然后经过某个时间再让第二行也出现?以此类推。
这里我就想到了面试常见的一个题目:

给一个数组arr = [ 1,2,3,4,5 ],如何一秒钟输出一个数字
也就是第一秒输出1,经过一秒输出2…

let arr = [1,2,3,4,5]
for(let i = 0; i < arr.length; i++) {
	await this.sleep(1000)
	console.log(arr[i]);
}

显然,我们这里也需要用到这个触发机制,效果就会出来。

(6)生成所需要的方块图形(createBrick)

上面说了,一共七种,并且包含每种旋转后的图形。

createBrick() {
	this.brickList = [
		[
			[[0,1], [1,1], [2,1], [2,2]], 
			[[1,2],	[1,1], [1,0], [2,0]], 
			[[2,1], [1,1], [0,1], [0,0]], 
			[[1,0], [1,1], [1,2], [0,2]],
		],			// L
		[
			[[0,1], [1,1], [2,1], [2,0]], 
			[[1,2], [1,1], [1,0], [0,0]], 
			[[2,1], [1,1], [0,1], [0,2]], 
			[[1,0],	[1,1], [1,2], [2,2]],
		],			// J
		[
			[[0,0], [0,1], [1,1], [1,2]], 
			[[0,2], [1,2], [1,1], [2,1]], 
		],			// Z
		[
			[[0,2], [0,1], [1,1], [1,0]], 
			[[2,2], [1,2], [1,1], [0,1]], 
		],			// S
		[
			[[0,1], [1,0], [1,1], [1,2]], 
			[[1,2], [0,1], [1,1], [2,1]], 
			[[2,1], [1,2], [1,1], [1,0]], 
			[[1,0], [2,1], [1,1], [0,1]],
		],			// T
		[
			[[0,1], [1,1], [2,1], [3,1]], 
			[[1,0],	[1,1], [1,2], [1,3]], 
		],			// |
		[
			[[0,1], [0,2], [1,1], [1,2]],
		]			// O
	];
	// 生成完方块,需要随机抽取一个作为第一个图形
	this.randomBrick();
},
(7)随机产生一个形状(randomBrick)

我只拿到某个图形的第一个形态
我们还要记录一下当前拿到的放块是哪个图形的哪个形态,为了后面做旋转所用

randomBrick() {
	let randomNum = Math.floor(Math.random() * this.brickList.length);
	this.brickNext = this.brickList[randomNum][0]; // 默认拿出某个图形的第一个形态
	this.nextIndex1 = randomNum; 
	this.nextIndex2 = 0;
	// 每次随机拿一个方块,就要把之前的方块清空掉,避免渲染重复
	for (let i = 0; i < 4; i++) {
		for(let j = 0; j < 4; j++) {
			this.list_next[i][j] = 0;
		}
	}
	// 把下一个渲染在next数组上面
	for (let i = 0; i < this.brickNext.length; i++) {
		let [x, y] = this.brickNext[i];
		this.list_next[x][y] = 1;
	}
	this.$forceUpdate()
},

在这里插入图片描述

(8)空格 —— 游戏开始(onSpace、onPassOn、drawdBrick)

处理空格事件:
空格的作用就是开始游戏、暂停、继续
首先开始游戏,我们需要把下一个方块传递过来,作为当前下落方块使用(onPassOn方法)
其次开始我们要开始方块下落定时器 最后暂停的时候我们需要清楚定时器

onSpace() {
	if(this.isOver) return;
	if(!this.isStart) {
		this.isStart = true;
		this.onPassOn()
	}
	if(this.isStop) {
		this.isStop = false;
		this.dropBrick()
	} else {
		this.isStop = true;
		clearInterval(this.timer)
	}
},

关于onPassOn方法
把下一个形状传递给当前
传递的时候需要注意,我们默认的图形是在边缘创建的,我们传递给当前的话,需要把图形放到中间去
传递完,自己要还要重新生成一个新的图案

onPassOn() {
	let arr = JSON.parse(JSON.stringify(this.brickNext));
	let a = Math.floor(this.col / 2 - 2);
	arr.forEach(item => {
		item[1] += a
	})
	this.brickNow = arr;
	// 记录当前是哪个图形的哪个形状
	this.nowIndex1 = JSON.parse(JSON.stringify(this.nextIndex1));
	this.nowIndex2 = JSON.parse(JSON.stringify(this.nextIndex2));
	this.drawdBrick()
	this.randomBrick()
},

关于 drawdBrick 方法
把当前图形渲染在地图上
每次渲染前,要把除了固定的砖块(=2)外的都清除一下
每次渲染完成,我们都要去判断游戏是否结束了

drawdBrick() {
	for (let i = 0; i < this.list.length; i++) {
		for(let j = 0; j < this.list[i].length; j++) {
			if(this.list[i][j] !== 2) {
				this.list[i][j] = 0;
			}
		}
	}
	for (let i = 0; i < this.brickNow.length; i++) {
		let [x, y] = this.brickNow[i];
		this.list[x][y] = 1;
	}
	this.$forceUpdate();
	this.isEnd();
}

关于isEnd方法
判断游戏是否结束
我们只要判断地图中第一行是否有砖块 = 2就行

isEnd() {
	let bool = this.list[0].some(item => item == 2);
	if(bool) {
		clearInterval(this.timer);
		this.isOver = true;
		this.isStart = false;
		this.isStop = true;
		this.brickNow = [];
		this.score = 0;
		this.initAnimate()
	}
}
(9)方块下落(dropBrick)

利用定时器来让图形每隔一定时间就会往下滑动一格
方法就是让图形的每个砖块row都+1即可

dropBrick() {
	this.timer = setInterval(() => {
		// 如果到底了,则需要把方块固定, 然后进行下一个
		if(this.isToBottom()) {
			let fixedArr = JSON.parse(JSON.stringify(this.brickNow))
			for (let i = 0; i < fixedArr.length; i++) {
				let [x, y] = fixedArr[i];
				this.list[x][y] = 2;
			}
			this.onClean();
			return;
		}
		// 每个砖块的row都加1 = 下落
		for (let i = 0; i < this.brickNow.length; i++) {
			this.brickNow[i][0] += 1;
		}
		this.drawdBrick()
	}, this.speed)
}

判断是否到底,或者下面已有砖块

// 判断是否触底 true到底或者下面已有砖块
isToBottom(arr = this.brickNow){
	let bool = false;
	arr.forEach(item=>{
		if(item[0] == this.row - 1 || this.list[item[0] + 1][item[1]] == 2){
			bool = true;
		}
	})
	return bool;
},
(10)消除行(onClean)

到底了,或者下面有砖块了,这时候也就会固定住当前砖块,固定完,我们还需要知道当前是否有可以消除的情况
this.onClean()方法就是用于消除行的
消除行的时候,我们回想一下经典俄罗斯方块会有个闪动的效果,这里也需要用到之前说的sleep方法
首先我们需要知道有哪些行可以消除,也就是找出一行中全部 = 2的一行
其次我们让这些行进行闪动效果,闪动完整行方块全部设为 = 0
最后消除的行上面的已有固定的砖块要全体进行下移。

async onClean() {
	let arr = JSON.parse(JSON.stringify(this.list));
	let clearList = [];	// 可以消除的行数
	for(let i = arr.length - 1; i >= 0; i--) {
		let a = arr[i].every(item => item == 2);
		if(a) {
			clearList.push(i); // 我们只要拿到行数就可以了
		}
	}
	if(clearList.length > 0) {
		// 播放消除音效
		this.playAudioClear()
		// 这里要做闪动的效果,用了定时器处理,效果开始之前不能让方块往下滑动,要暂停
		clearInterval(this.timer);
		// 这里我做了闪动5次,也就是让 0 和 1之间来回切换,最后以0结尾
		for(let z = 0; z < 5; z++) {
			await this.sleep(100)
			for(let i = 0; i < clearList.length; i++) {
				let a = clearList[i];
				for(let j = 0; j < arr[a].length; j++) {
					this.list[a][j] = (z % 2 == 0 ? 0 : 1);
				}
			}
			this.$forceUpdate();
		}
		// 消除行之后 剩余的要下移	
		// 我们要从上往下进行一行一行处理
		for(let i = clearList.length; i >= 0; i--) {
			for(let j = clearList[i] - 1; j >= 0; j--) {
				for(let k = 0; k < this.col; k++) {
					if(this.list[j][k] == 2) {
						// 把固定方块清空,它下面那一块设为固定=2
						this.list[j][k] = 0; 
						this.list[j + 1][k] = 2; 
					}
				}
			}
		}
		this.$forceUpdate();
		// 消除完要计算分数
		this.onCountScore(clearList.length);
		// 消除完重新开启方块下落
		this.dropBrick()
	}
	// 无论有没有消行,都要把下一个图形拿过来用
	this.onPassOn();
},
(11)左移动、右移动、下移动、上旋转(onDirection)

这里需要说明,我们进行左右下移动的时候,一定需要判断临界值,也就是不能超出地图
并且还需要判断左右下是否有砖块
左右有砖块 怎不能移动
下有砖块则固定到砖块上面

// 上下左右
onDirection(code) {
	if(!this.isStart || this.isOver) return;
	// 音效
	this.playAudioRotate()
	// 上  旋转 (具体在下一个)
	if(code == 38 || code == 87) {
		this.onRotate()
	}
	// 下  加快下落
	// 找出图案最下面,首先触碰的砖块
	// 根据当前图形,一层一层往下找,是否有砖块或者是否到底
	if((code == 40 || code == 83) && !this.isToBottom()) {
		let brickCopy = JSON.parse(JSON.stringify(this.brickNow));
		for(let i = 0; i < this.row; i++) {
			for(let j = 0; j < brickCopy.length; j++) {
				brickCopy[j][0] += 1;
			}
			if(this.isToBottom(brickCopy)) {
				this.brickNow = JSON.parse(JSON.stringify(brickCopy));
				break;
			}
		}
	}
	// 左  左移动
	if((code == 37 || code == 65) && !this.isToLeft()) {
		for (let i = 0; i < this.brickNow.length; i++) {
			this.brickNow[i][1] -= 1;
		}
	}
	// 右  右移动
	if((code == 39 || code == 68) && !this.isToRight()) {
		for (let i = 0; i < this.brickNow.length; i++) {
			this.brickNow[i][1] += 1;
		}
	}
	this.drawdBrick()
},

如上代码中,判断左右临界值方法isToLeft、isToRight 具体如下

// 判断左边是否到边,或者是否有砖块
isToLeft() {
	let bool = false;
	this.brickNow.forEach(item=>{
		let [x, y] = item;
		if(y == 0 || this.list[x][y - 1] == 2){
			bool = true;
		}
	})
	return bool;
},

// 判断右边是否有边,或者是否有砖块
isToRight() {
	let bool = false;
	this.brickNow.forEach(item=>{
		let [x, y] = item;
		if(y == this.col - 1 || this.list[x][y + 1] == 2){
			bool = true;
		}
	})
	return bool;
},
(12)按上 —— 方块旋转(onRotate)

旋转操作其实比较简单

  1. 我们只要计算 原始图形的位置与当前图形位置中每个砖块的row col的差值
  2. 再去获取旋转后的原始图形
  3. 再把旋转后的原始图形的row col都加上差值
  4. 会得到当前旋转后的图形及位置
  5. 具体细节可看代码中注释
onRotate() {
	// 获取当前图形的变化数组
	let arr = JSON.parse(JSON.stringify(this.brickList[this.nowIndex1]));	
	// 如果大于1 说明有旋转后的图形     比如O型就只有一种,则不能旋转
	if(arr.length > 1) {
		// 拿到原始图形,也就是刚开始创建的7个原始图形
		let protoBrick = JSON.parse(JSON.stringify(this.brickList[this.nowIndex1][this.nowIndex2]));
		// 复制一份当前图形
		let brickNowCopy = JSON.parse(JSON.stringify(this.brickNow));
		// 差值列表
		let gapArr = [];	
		// 计算出当前图形与原始图形的差值,也就是移动了多少步了
		for(let a = 0; a < brickNowCopy.length; a++) {
			gapArr[a] = [
				brickNowCopy[a][0] - protoBrick[a][0], 
				brickNowCopy[a][1] - protoBrick[a][1]
			];
		}
		// 这里我们需要拿到旋转后的图形,所以之前需要记录当前图形是哪个图形的哪个形状
		// 如果转到最后一个,则需要重新回到第一个
		this.nowIndex2 += 1;
		if(this.nowIndex2 == arr.length) {
			this.nowIndex2 = 0;
		}
		// 因为整个图形的每个方块是一起移动的,所以我们只要拿到第一个差值数据即可
		let [xGap, yGap] = gapArr[0];
		// 拿到原始图形旋转后的图形
		let protoBrickNextCopy = JSON.parse(JSON.stringify(this.brickList[this.nowIndex1][this.nowIndex2]));
		// 把原始图形旋转后的图形都加上差值,计算出旋转后的图形目前在哪个位置
		protoBrickNextCopy.forEach(item => {
			item[0] += xGap;
			item[1] += yGap;
		})
		// 得到旋转后的图形之后,我们需要判断旋转后的图形的数组中,是否有负数或者大于地图的边界值(也就是出地图了),
		// 或者坐标映射在地图中是否有等于2的情况(也就是跟固定砖块重叠了),这两种情况都不能旋转
		let bool = false;
		for(let a = 0; a < protoBrickNextCopy.length; a++) {
			let [x, y] = protoBrickNextCopy[a];
			if(x < 0 || y < 0 || x > this.row - 1 || y > this.col - 1 || this.list[x][y] == 2) {
				bool = true;
				break;
			}
		}
		// 因为上面我们把nowIndex+1了,则如果不能旋转,则需要同时要把当前图形的下标在变成之前的那个图形
		if(bool) {
			this.nowIndex2 -= 1;
			if(this.nowIndex2 == -1) {
				this.nowIndex2 = arr.length - 1;
			}
			return;
		}
		// 这就是把旋转后的图形坐标赋值给当前的图形,然后重新渲染出来,则完成旋转
		this.brickNow = protoBrickNextCopy;
		this.drawdBrick()
	}
},

注意上面所说的,某些不能旋转的情况,比如下面这种,旋转之后的图形,有一块就会超出地图。
还有如果方块与方块之间的空隙太小,也不能旋转。
在这里插入图片描述

(13)计算分数、提升难度
onCountScore(num) {
	const arr = this.scoreList.filter(item => item.row == num)[0];
	this.score += arr.score;
	// 下落速度加快
	if(this.score >= 200 && this.score < 500) {
		this.speed = 300;
	} else if(this.score >= 500 && this.score < 800) {
		this.speed = 200;
	} else if(this.score >= 800 && this.score < 1000) {
		this.speed = 100;
	} else if(this.score >= 1000) {
		this.speed = 50;
	}
}

三、总结

整体做下来,难度还算可以,其中一些小细节还有比较难啃的,但是慢慢琢磨,慢慢分析,慢慢尝试,总会有办法解决的,享受游戏的乐趣,享受学历的乐趣。

最后附上代码地址:
张大炮 —— 俄罗斯方块

<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>俄罗斯方块</title> [removed] var TETRIS_ROWS = 20; var TETRIS_COLS = 14; var CELL_SIZE = 24; // 没方块是0 var NO_BLOCK = 0; var tetris_canvas; var tetris_ctx; // 记录当前积分 var curScore = 0; // 记录当前速度 var curSpeed = 1; // 记录曾经的最高积分 var maxScore = 0; var curScoreEle , curSpeedEle , maxScoreEle; var curTimer; // 记录当前是否游戏中的旗标 var isPlaying = true; // 记录正在下掉的四个方块 var currentFall; // 该数组用于记录底下已经固定下来的方块。 var tetris_status = []; for (var i = 0; i < TETRIS_ROWS ; i++ ) { tetris_status[i] = []; for (var j = 0; j < TETRIS_COLS ; j++ ) { tetris_status[i][j] = NO_BLOCK; } } // 定义方块的颜色 colors = ["#fff", "#f00" , "#0f0" , "#00f" , "#c60" , "#f0f" , "#0ff" , "#609"]; // 定义几种可能出现的方块组合 var blockArr = [ // 代表第一种可能出现的方块组合:Z [ {x: TETRIS_COLS / 2 - 1 , y:0 , color:1}, {x: TETRIS_COLS / 2 , y:0 ,color:1}, {x: TETRIS_COLS / 2 , y:1 ,color:1}, {x: TETRIS_COLS / 2 + 1 , y:1 , color:1} ], // 代表第二种可能出现的方块组合:反Z [ {x: TETRIS_COLS / 2 + 1 , y:0 , color:2}, {x: TETRIS_COLS / 2 , y:0 , color:2}, {x: TETRIS_COLS / 2 , y:1 , color:2}, {x: TETRIS_COLS / 2 - 1 , y:1 , color:2} ], // 代表第三种可能出现的方块组合: 田 [ {x: TETRIS_COLS / 2 - 1 , y:0 , color:3}, {x: TETRIS_COLS / 2 , y:0 , color:3}, {x: TETRIS_COLS / 2 - 1 , y:1 , color:3}, {x: TETRIS_COLS / 2 , y:1 , color:3} ], // 代表第四种可能出现的方块组合:L [ {x: TETRIS_COLS / 2 - 1 , y:0 , color:4}, {x: TETRIS_COLS / 2 - 1, y:1 , color:4}, {x: TETRIS_COLS / 2 - 1 , y:2 , color:4}, {x: TETRIS_COLS / 2 , y:2 , color:4} ], // 代表第五种可能出现的方块组合:J [ {x: TETRIS_COLS / 2 , y:0 , color:5}, {x: TETRIS_COLS / 2 , y:1, color:5}, {x: TETRIS_COLS / 2 , y:2, color:5}, {x: TETRIS_COLS / 2 - 1, y:2, color:5} ], // 代表第六种可能出现的方块组合 : 条 [ {x: TETRIS_COLS / 2 , y:0 , color:6}, {x: TETRIS_COLS / 2 , y:1 , color:6}, {x: TETRIS_COLS / 2 , y:2 , color:6}, {x: TETRIS_COLS / 2 , y:3 , color:6} ], // 代表第七种可能出现的方块组合 : ┵ [ {x: TETRIS_COLS / 2 , y:0 , color:7}, {x: TETRIS_COLS / 2 - 1 , y:1 , color:7}, {x: TETRIS_COLS / 2 , y:1 , color:7}, {x: TETRIS_COLS / 2 + 1, y:1 , color:7} ] ]; // 定义初始化正在下掉的方块 var initBlock = function() { var rand = Math.floor(Math.random() * blockArr.length); // 随机生成正在下掉的方块 currentFall = [ {x: blockArr[rand][0].x , y: blockArr[rand][0].y , color: blockArr[rand][0].color}, {x: blockArr[rand][1].x , y: blockArr[rand][1].y , color: blockArr[rand][1].color}, {x: blockArr[rand][2].x , y: blockArr[rand][2].y , color: blockArr[rand][2].color}, {x: blockArr[rand][3].x , y: blockArr[rand][3].y , color: blockArr[rand][3].color} ]; }; // 定义一个创建canvas组件的函数 var createCanvas = function(rows , cols , cellWidth, cellHeight) { tetris_canvas = document.createElement("canvas"); // 设置canvas组件的高度、宽度 tetris_canvas.width = cols * cellWidth; tetris_canvas.height = rows * cellHeight; // 设置canvas组件的边框 tetris_canvas.style.border = "1px solid black"; // 获取canvas上的绘图API tetris_ctx = tetris_canvas.getContext('2d'); // 开始创建路径 tetris_ctx.beginPath(); // 绘制横向网络对应的路径 for (var i = 1 ; i < TETRIS_ROWS ; i++) { tetris_ctx.moveTo(0 , i * CELL_SIZE); tetris_ctx.lineTo(TETRIS_COLS * CELL_SIZE , i * CELL_SIZE); } // 绘制竖向网络对应的路径 for (var i = 1 ; i < TETRIS_COLS ; i++) { tetris_ctx.moveTo(i * CELL_SIZE , 0); tetris_ctx.lineTo(i * CELL_SIZE , TETRIS_ROWS * CELL_SIZE); } tetris_ctx.closePath(); // 设置笔触颜色 tetris_ctx.strokeStyle = "#aaa"; // 设置线条粗细 tetris_ctx.lineWidth = 0.3; // 绘制线条 tetris_ctx.stroke(); } // 绘制俄罗斯方块的状态 var drawBlock = function() { for (var i = 0; i < TETRIS_ROWS ; i++ ) { for (var j = 0; j < TETRIS_COLS ; j++ ) { // 有方块的地方绘制颜色 if(tetris_status[i][j] != NO_BLOCK) { // 设置填充颜色 tetris_ctx.fillStyle = colors[tetris_status[i][j]]; // 绘制矩形 tetris_ctx.fillRect(j * CELL_SIZE + 1 , i * CELL_SIZE + 1, CELL_SIZE - 2 , CELL_SIZE - 2); } // 没有方块的地方绘制白色 else { // 设置填充颜色 tetris_ctx.fillStyle = 'white'; // 绘制矩形 tetris_ctx.fillRect(j * CELL_SIZE + 1 , i * CELL_SIZE + 1 , CELL_SIZE - 2 , CELL_SIZE - 2); } } } } // 当页面加载完成时,执行该函数里的代码。 window. { // 创建canvas组件 createCanvas(TETRIS_ROWS , TETRIS_COLS , CELL_SIZE , CELL_SIZE); document.body.appendChild(tetris_canvas); curScoreEle = document.getElementById("curScoreEle"); curSpeedEle = document.getElementById("curSpeedEle"); maxScoreEle = document.getElementById("maxScoreEle"); // 读取Local Storage里的tetris_status记录 var tmpStatus = localStorage.getItem("tetris_status"); tetris_status = tmpStatus == null ? tetris_status : JSON.parse(tmpStatus); // 把方块状态绘制出来 drawBlock(); // 读取Local Storage里的curScore记录 curScore = localStorage.getItem("curScore"); curScore = curScore == null ? 0 : parseInt(curScore); curScoreEle[removed] = curScore; // 读取Local Storage里的maxScore记录 maxScore = localStorage.getItem("maxScore"); maxScore = maxScore == null ? 0 : parseInt(maxScore); maxScoreEle[removed] = maxScore; // 读取Local Storage里的curSpeed记录 curSpeed = localStorage.getItem("curSpeed"); curSpeed = curSpeed == null ? 1 : parseInt(curSpeed); curSpeedEle[removed] = curSpeed; // 初始化正在下掉的方块 initBlock(); // 控制每隔固定时间执行一次向下”掉“ curTimer = setInterval("moveDown();" , 500 / curSpeed); } // 判断是否有一行已满 var lineFull = function() { // 依次遍历每一行 for (var i = 0; i < TETRIS_ROWS ; i++ ) { var flag = true; // 遍历当前行的每个单元格 for (var j = 0 ; j < TETRIS_COLS ; j++ ) { if(tetris_status[i][j] == NO_BLOCK) { flag = false; break; } } // 如果当前行已全部有方块了 if(flag) { // 将当前积分增加100 curScoreEle[removed] = curScore+= 100; // 记录当前积分 localStorage.setItem("curScore" , curScore); // 如果当前积分达到升级极限。 if( curScore >= curSpeed * curSpeed * 500) { curSpeedEle[removed] = curSpeed += 1; // 使用Local Storage记录curSpeed。 localStorage.setItem("curSpeed" , curSpeed); clearInterval(curTimer); curTimer = setInterval("moveDown();" , 500 / curSpeed); } // 把当前行的所有方块下移一行。 for (var k = i ; k > 0 ; k--) { for (var l = 0; l < TETRIS_COLS ; l++ ) { tetris_status[k][l] =tetris_status[k-1][l]; } } // 消除方块后,重新绘制一遍方块 drawBlock(); //② } } } // 控制方块向下掉。 var moveDown = function() { // 定义能否下掉的旗标 var canDown = true; //① // 遍历每个方块,判断是否能向下掉 for (var i = 0 ; i < currentFall.length ; i++) { // 判断是否已经到“最底下” if(currentFall[i].y >= TETRIS_ROWS - 1) { canDown = false; break; } // 判断下一格是否“有方块”, 如果下一格有方块,不能向下掉 if(tetris_status[currentFall[i].y + 1][currentFall[i].x] != NO_BLOCK) { canDown = false; break; } } // 如果能向下“掉” if(canDown) { // 将下移前的每个方块的背景色涂成白色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = 'white'; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2 , CELL_SIZE - 2); } // 遍历每个方块, 控制每个方块的y坐标加1。 // 也就是控制方块都下掉一格 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; cur.y ++; } // 将下移后的每个方块的背景色涂成该方块的颜色值 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = colors[cur.color]; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2 , CELL_SIZE - 2); } } // 不能向下掉 else { // 遍历每个方块, 把每个方块的值记录到tetris_status数组中 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 如果有方块已经到最上面了,表明输了 if(cur.y < 2) { // 清空Local Storage中的当前积分值、游戏状态、当前速度 localStorage.removeItem("curScore"); localStorage.removeItem("tetris_status"); localStorage.removeItem("curSpeed"); if(confirm("您已经输了!是否参数排名?")) { // 读取Local Storage里的maxScore记录 maxScore = localStorage.getItem("maxScore"); maxScore = maxScore == null ? 0 : maxScore ; // 如果当前积分大于localStorage中记录的最高积分 if(curScore >= maxScore) { // 记录最高积分 localStorage.setItem("maxScore" , curScore); } } // 游戏结束 isPlaying = false; // 清除计时器 clearInterval(curTimer); return; } // 把每个方块当前所在位置赋为当前方块的颜色值 tetris_status[cur.y][cur.x] = cur.color; } // 判断是否有“可消除”的行 lineFull(); // 使用Local Storage记录俄罗斯方块游戏状态 localStorage.setItem("tetris_status" , JSON.stringify(tetris_status)); // 开始一组新的方块。 initBlock(); } } // 定义左移方块的函数 var moveLeft = function() { // 定义能否左移的旗标 var canLeft = true; for (var i = 0 ; i < currentFall.length ; i++) { // 如果已经到了最左边,不能左移 if(currentFall[i].x <= 0) { canLeft = false; break; } // 或左边的位置已有方块,不能左移 if (tetris_status[currentFall[i].y][currentFall[i].x - 1] != NO_BLOCK) { canLeft = false; break; } } // 如果能左移 if(canLeft) { // 将左移前的每个方块的背景色涂成白色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = 'white'; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE +1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2, CELL_SIZE - 2); } // 左移所有正在下掉的方块 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; cur.x --; } // 将左移后的每个方块的背景色涂成方块对应的颜色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = colors[cur.color]; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1, CELL_SIZE - 2 , CELL_SIZE - 2); } } } // 定义右移方块的函数 var moveRight = function() { // 定义能否右移的旗标 var canRight = true; for (var i = 0 ; i < currentFall.length ; i++) { // 如果已到了最右边,不能右移 if(currentFall[i].x >= TETRIS_COLS - 1) { canRight = false; break; } // 如果右边的位置已有方块,不能右移 if (tetris_status[currentFall[i].y][currentFall[i].x + 1] != NO_BLOCK) { canRight = false; break; } } // 如果能右移 if(canRight) { // 将右移前的每个方块的背景色涂成白色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = 'white'; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2 , CELL_SIZE - 2); } // 右移所有正在下掉的方块 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; cur.x ++; } // 将右移后的每个方块的背景色涂成各方块对应的颜色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = colors[cur.color]; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2, CELL_SIZE -2); } } } // 定义旋转方块的函数 var rotate = function() { // 定义记录能否旋转的旗标 var canRotate = true; for (var i = 0 ; i < currentFall.length ; i++) { var preX = currentFall[i].x; var preY = currentFall[i].y; // 始终以第三个方块作为旋转的中心, // i == 2时,说明是旋转的中心 if(i != 2) { // 计算方块旋转后的x、y坐标 var afterRotateX = currentFall[2].x + preY - currentFall[2].y; var afterRotateY = currentFall[2].y + currentFall[2].x - preX; // 如果旋转后所在位置已有方块,表明不能旋转 if(tetris_status[afterRotateY][afterRotateX + 1] != NO_BLOCK) { canRotate = false; break; } // 如果旋转后的坐标已经超出了最左边边界 if(afterRotateX < 0 || tetris_status[afterRotateY - 1][afterRotateX] != NO_BLOCK) { moveRight(); afterRotateX = currentFall[2].x + preY - currentFall[2].y; afterRotateY = currentFall[2].y + currentFall[2].x - preX; break; } if(afterRotateX < 0 || tetris_status[afterRotateY-1][afterRotateX] != NO_BLOCK) { moveRight(); break; } // 如果旋转后的坐标已经超出了最右边边界 if(afterRotateX >= TETRIS_COLS - 1 || tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK) { moveLeft(); afterRotateX = currentFall[2].x + preY - currentFall[2].y; afterRotateY = currentFall[2].y + currentFall[2].x - preX; break; } if(afterRotateX >= TETRIS_COLS - 1 || tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK) { moveLeft(); break; } } } // 如果能旋转 if(canRotate) { // 将旋转移前的每个方块的背景色涂成白色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = 'white'; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2, CELL_SIZE - 2); } for (var i = 0 ; i < currentFall.length ; i++) { var preX = currentFall[i].x; var preY = currentFall[i].y; // 始终以第三个方块作为旋转的中心, // i == 2时,说明是旋转的中心 if(i != 2) { currentFall[i].x = currentFall[2].x + preY - currentFall[2].y; currentFall[i].y = currentFall[2].y + currentFall[2].x - preX; } } // 将旋转后的每个方块的背景色涂成各方块对应的颜色 for (var i = 0 ; i < currentFall.length ; i++) { var cur = currentFall[i]; // 设置填充颜色 tetris_ctx.fillStyle = colors[cur.color]; // 绘制矩形 tetris_ctx.fillRect(cur.x * CELL_SIZE + 1 , cur.y * CELL_SIZE + 1 , CELL_SIZE - 2, CELL_SIZE - 2); } } } window.focus(); // 为窗口的按键事件绑定事件监听器 window. { switch(evt.keyCode) { // 按下了“向下”箭头 case 40: if(!isPlaying) return; moveDown(); break; // 按下了“向左”箭头 case 37: if(!isPlaying) return; moveLeft(); break; // 按下了“向右”箭头 case 39: if(!isPlaying) return; moveRight(); break; // 按下了“向上”箭头 case 38: if(!isPlaying) return; rotate(); break; } } [removed] <style type="text/css"> body>div { font-size: 13pt; padding-bottom: 8px; } span { font-size: 20pt; color: red; } </style> </head> <body> <h2>俄罗斯方块</h2> <div <div id="curSpeedEle"></span> 当前积分:<span id="curScoreEle"></span></div> <div id="maxScoreEle"></span></div> </div> </body> </html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张_大_炮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值