持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
1.绘制游戏区域
建立一个二维数组,对这个数组进行遍历。第一层遍历的时候创建tr,第二层遍历的时候创建td。然后添加一些CSS样式,游戏区域就写好了。
let arr = [ [{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
[{}, {}, {}, {}, {}, {}, {}, {}, {}],
]
//渲染游戏区域
const renderTable = () => {
document.querySelector('table').innerHTML = ''
arr.forEach((item, index) => {
//第一层遍历创建tr
let tr = document.createElement('tr')
tr.dataset.y = index
item.forEach((item2, index2) => {
//第二层遍历创建td
let td = document.createElement('td')
td.dataset.x = index2
tr.appendChild(td)
})
document.querySelector('table').appendChild(tr)
})
}
renderTable()
CSS&HTML
<style>
td {
width: 50px;
height: 50px;
border: 1px solid black;
}
.bgc1 {
background-color: black;
}
.bgc2 {
background-color: rgb(107, 101, 101);
}
.defen {
font-size: 30px;
position: absolute;
top: 40%;
left: 600px;
}
.guize {
font-size: 20px;
position: absolute;
top: 50%;
left: 600px;
}
</style>
<body>
<table></table>
<div class="defen">得分</div>
<div class="guize">
一次消1行得1分<br>
一次消2行得4分<br>
一次消3行得10分<br>
一次消4行得20分<br>
<br>
<br>
<div>键盘上下左右控制,Enter键暂停</div>
</div>
<script src="./俄罗斯方块.js"></script>
</body>
2.写方块图形的构造函数,以及图形的渲染函数
每一个不同的方块类型都是由4个格子组成,将其中的一个格子视为原点,其余3个格子相对它来定位。把这个形状放到构造函数的实例方法里面,这样只需要控制原点的坐标,图形就会跟随变化了。
为了方便,这里我暂时只写了2个方块类型,起名就用A B来区分。
//创建构造函数
//第一种类型形状1
function A1(x, y) {
this.x = x
this.y = y
this.shape = function (a) {
arr[this.y][this.x].num = a
arr[this.y][this.x - 1].num = a
arr[this.y][this.x + 1].num = a
arr[this.y + 1][this.x + 1].num = a
}
}
//第一种类型形状2
function A2(x, y) {
this.x = x
this.y = y
this.shape = function (a) {
arr[this.y][this.x + 1].num = a
arr[this.y - 1][this.x + 1].num = a
arr[this.y + 1][this.x + 1].num = a
arr[this.y + 1][this.x].num = a
}
}
//第一种类型形状3
function A3(x, y) {
this.x = x
this.y = y
this.shape = function (a) {
arr[this.y + 1][this.x].num = a
arr[this.y][this.x - 1].num = a
arr[this.y + 1][this.x + 1].num = a
arr[this.y + 1][this.x - 1].num = a
}
}
//第一种类型形状4
function A4(x, y) {
this.x = x
this.y = y
this.shape = function (a) {
arr[this.y][this.x - 1].num = a
arr[this.y][this.x].num = a
arr[this.y + 1][this.x - 1].num = a
arr[this.y + 2][this.x - 1].num = a
}
}
//第二种类型,正方形
function B(x, y) {
this.x = x
this.y = y
this.shape = function (a) {
arr[this.y][this.x].num = a
arr[this.y][this.x + 1].num = a
arr[this.y + 1][this.x].num = a
arr[this.y + 1][this.x + 1].num = a
}
}
接着是图形的渲染函数了。实例方法里的num值为1就渲染成黑色,num值为2就渲染成灰色,num值为0就不渲染。
//渲染方块函数
const renderColor = () => {
arr.forEach((item, index) => {
const trArr = document.querySelectorAll('tr')
item.forEach((item2, index2) => {
//num为1,这个格子渲染黑色
if (item2.num === 1) {
trArr[index].querySelectorAll('td')[index2].classList.add('bgc1')
}
//num为1,这个格子渲染灰色
else if (item2.num === 2) {
trArr[index].querySelectorAll('td')[index2].classList.remove('bgc1')
trArr[index].querySelectorAll('td')[index2].classList.add('bgc2')
}
else {
trArr[index].querySelectorAll('td')[index2].className = ''
}
})
})
}
//设置原点坐标
let a = new A1(5, 0)
//渲染默认图形
a.shape(1)
renderColor()
3.控制移动
图形渲染数来了,就要控制移动了。移动的话逻辑很简单,向下就是原点的Y坐标+1,向左就是X坐标-1,向右就是X坐标+1。移动的同时,要清除图形之前的样式,同时渲染一个新坐标上的图形。
要注意的是图形到达边界时要加一个条件使其不能继续移动,否则就报错了。因为每个图形的宽不同,所以不同图形内X可达到的最小值和最大值时不同的,我这里用了try catch来写条件。X如果报错,说明图形走出界了,就执行catch里的代码。
图形到底后,就要渲染成灰色,同时生成新的图形。生成新图形的时候,可以写一个随机数来控制形状的类型。
//键盘控制事件
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown') {
down()
} else if (e.key === 'ArrowRight') {
right()
} else if (e.key === 'ArrowLeft') {
left()
}
else if (e.key === 'ArrowUp') {
change()
}
})
//下降函数
const down = () => {
//清除之前的图形
a.shape(0)
a.y += 1
//渲染移动后的图形
a.shape(1)
renderColor()
//图形到底,渲染成灰色,同时生成新图形
if (a.y == 10) {
a.shape(2)
nums()
}
}
//右移动函数
const right = () => {
if (a.x < 7) {
a.shape(0)
a.x += 1
a.shape(1)
renderColor()
}
}
//左移动函数
const left = () => {
//左移动涉及到方块类型和A类型最左边格子的X坐标不同
//这里用try catch方法来写左移动,报错就说明图形走出界了,执行catch的代码
try {
//方块类型,它的X坐标最小值可以为0
if (a.x > 0) {
a.shape(0)
a.x -= 1
a.shape(1)
renderColor()
}
} catch {
//A类型的X最小值只能为1
a.x = 1
a.shape(1)
renderColor()
}
}
// 随机图形函数
let num1 = 0
function nums() {
num1 = Math.floor(Math.random() * 100)
if (num1 <= 50) {
//不同类型就生成不同实例
a = new A1(5, 0)
a.shape(1)
} else if (num1 > 50 && num1 <= 100) {
a = new B(5, 0)
a.shape(1)
}
renderColor()
}
4.按上键变形状
变形状的逻辑就是清空当前形状,渲染新的形状。每一个类型的4个形状构造函数里面都写好了,直接调用即可。
//变形状函数
const change = () => {
//先清除当前形状
a.shape(0)
//判断这个形状的类型,然后生成新的形状
if (a.constructor == A1) {
a = new A2(a.x, a.y)
} else if (a.constructor == A2) {
a = new A3(a.x, a.y)
} else if (a.constructor == A3) {
a = new A4(a.x, a.y)
} else if (a.constructor == A4) {
a = new A1(a.x, a.y)
}
//渲染新形状
a.shape(1)
renderColor()
}
5.图形堆叠
现在移动和变形写完了,接下来就要写如何让图形向上叠起来,否则图形和图形之间会重合。
不仅仅是碰到底部的时候图形会变灰色,底部如果是其他的灰色格子,这个图形也应该变成灰色。那么在下降函数里面要加个判断条件了,如果图形下面一个的那个格子里面的num值是2,也就是说那个格子是灰色,这个时候图形就应该变灰色了。
将写好的代码放到下降函数里面就可以了。
const down = () => {
//清除之前的图形
a.shape(0)
a.y += 1
//渲染移动后的图形
a.shape(1)
renderColor()
//图形到底,渲染成灰色,同时生成新图形
if (a.y == 10) {
a.shape(2)
nums()
}
//判断是否碰到灰色格子,碰到图形就渲染成灰色
//先把图形的四个格子给找出来
for (let i = a.y; i < a.y + 2; i++) {
arr[i].forEach((item, index) => {
if (item.num == 1) {
//判断如果这个格子的下面一个格子是灰色
if (arr[i + 1][index].num == 2) {
a.shape(2)
//判断如果第一排没有灰色格子,才会出来新图形
if (!arr[0].some(item => item.num == 2)) {
nums()
}
}
}
})
}
}
6.消除与得分功能
消除功能的逻辑就是,判断这一行的灰色格子的数量,如果等于9,就说明这一行的格子都是灰色了,那么就将这一行的格子里的num值清空,重新渲染。
但是仅仅把num值清空还不行,因为下面的格子虽然空了,但是上面的格子又不会自动掉下来,如图所示
这个时候就需要代码来把上面的格子给移动下来了。原理就是清空的同时,从清空的那一行开始向上面的行遍历,把所有灰色的格子挑出来向下移一行。清空几行,这个代码就会执行几次,这样方块就不会卡在空中了。
计算得分就很好写了,清除了几行,就加上对应的分数。
//得分函数
function get() {
//用来计算得分的数组
let getArr = []
arr.forEach((item, index) => {
//筛选颜色为灰色的格子
const arr0 = item.filter(function (item02) {
return item02.num == 2
})
//如果arr0这个数组长度为9时,说明这一排都是灰色,就清除
if (arr0.length === 9) {
//同时将数据放入getArr中,最后会根据数组的长度来判断得分
getArr.push(arr0)
for (let i = 0; i < arr0.length; i++) {
//将这一排的格子num值都清空
arr0[i].num = 0
}
//从下往上遍历,目的是把所有Num为2的格子往下移动一格,遍历选中Num为2的格子,将其清空,然后把其下一排对应的格子赋值
for (let i = index - 1; i > 0; i--) {
arr[i].forEach((item, index1) => {
if (item.num === 2) {
item.num = 0
arr[i + 1][index1].num = 2
}
})
}
renderColor()
}
})
//得分判断
if (getArr.length == 1) {
Defen += 1
} else if (getArr.length == 2) {
Defen += 4
} else if (getArr.length == 3) {
Defen += 10
} else if (getArr.length == 4) {
Defen += 20
}
//渲染分数
defen.innerHTML = `得分${Defen}`
}
- 得分函数get()一定要在图形变灰色之后调用,不调用的话,那就写了个寂寞。
7.自动下降和暂停功能
自动下降功能写个间歇函数,每秒钟执行一次down操作就可以了。暂停功能就是把定时器关掉,还有就是暂停后不能再对页面进行其他的移动操作,这个时候就需要一个全局变量flag来操控了。这个flag要加在键盘事件里面去。
//定时器
let timer = setInterval(function () {
down()
}, 500)
//暂停功能
let flag = true
document.addEventListener('keydown', function (e) {
if (flag) {
if (e.key === 'Enter') {
clearInterval(timer)
flag = !flag
}
} else {
if (e.key === 'Enter') {
timer = setInterval(function () {
down()
}, 500)
flag = true
}
}
})
8.游戏结束
游戏结束很好写,直接判断第一排的格子里面有没有灰色,有灰色就直接寄。同样要记得在下降函数的最后面调用这个游戏结束的函数。
//游戏结束判定
function end() {
if (arr[0].some(item => item.num == 2)) {
clearInterval(timer)
alert('游戏结束!')
location.reload()
}
}
总结
写到这,俄罗斯方块的基本功能就都写完了。文章最开头的那个GIF是我写的完整版,其中那个最高分的功能,用本地存储的方法写一个就行了,我这里就不做过多的叙述了。完整版因为代码太多了,而且当时写的时候很随意,没怎么注意排版和注释,我就不贴出来了。想要其他的形状,写新的构造函数加进去就行。
这个游戏算是小游戏中比较复杂的,为了写这篇文章,我重写了个简易版的,代码不算多,看起来就不会那么复杂,希望能给到读者一些帮助。