如何使用原生JS,写出一个俄罗斯方块小游戏

语言: CN / TW / HK

1655261810331.gif

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

```

得分
一次消1行得1分
一次消2行得4分
一次消3行得10分
一次消4行得20分


键盘上下左右控制,Enter键暂停

```

01.jpg

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() ```

02.jpg

3.控制移动

图形渲染数来了,就要控制移动了。移动的话逻辑很简单,向下就是原点的Y坐标+1,向左就是X坐标-1,向右就是X坐标+1。移动的同时,要清除图形之前的样式,同时渲染一个新坐标上的图形。

要注意的是图形到达边界时要加一个条件使其不能继续移动,否则就报错了。因为每个图形的宽不同,所以不同图形内X可达到的最小值和最大值时不同的,我这里用了try catch来写条件。如果报错,就说明图形走出界了,就执行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() }

03.jpg

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() } } } }) } }

04.jpg

6.消除与得分功能

消除功能的逻辑就是,判断这一行的灰色格子的数量,如果等于9,就说明这一行的格子都是灰色了,那么就将这一行的格子里的num值清空,重新渲染。

但是仅仅把num值清空还不行,因为下面的格子虽然空了,但是上面的格子又不会自动掉下来,如图所示

05.jpg 这个时候就需要代码来把上面的格子给移动下来了。原理就是清空的同时,从清空的那一行开始向上面的行遍历,把所有灰色的格子挑出来向下移一行。清空几行,这个代码就会执行几次,这样方块就不会卡在空中了。

计算得分就很好写了,清除了几行,就加上对应的分数。

//得分函数 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()一定要在图形变灰色之后调用,不调用的话,那就写了个寂寞。

06.jpg

1655270751466.gif

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() } }

07.jpg

总结

写到这,俄罗斯方块的基本功能就都写完了。文章最开头的那个GIF是我写的完整版,其中那个最高分的功能,用本地存储的方法写一个就行了,我这里就不做过多的叙述了。完整版因为代码太多了,而且当时写的时候很随意,没怎么注意排版和注释,我就不贴出来了。

这个游戏算是小游戏中比较复杂的,为了写这篇文章,我重写了个简易版的,代码不算多,看起来就不会那么复杂,希望能给到读者一些帮助。

10.jpg

11.jpg