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

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

<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>
复制代码

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

猜你喜欢

转载自juejin.im/post/7109343705760284708