当前位置:网站首页>大道至简 html + js 实现最朴实的小游戏俄罗斯方块

大道至简 html + js 实现最朴实的小游戏俄罗斯方块

2020-11-06 21:04:13 kingapple

前言

老实说这其实这是自己写的第二个俄罗斯方块。
本次的重写,除了复习一下以前自己写的代码的同时,也加有了一些新的思考。
其中最重要的一个目的就是渲染层与逻辑层在代码层面的分离(重新设计代码)。
——最终效果图如下。
在这里插入图片描述


正题

来看下一下俄罗斯方块的一些概念与逻辑

一、方块

最传统的俄罗斯方块,只有7个方块。
通常由T、I、Z、S、L、J、O这7个字母代替。

转换成代码如下。

shaps = {
    'I': [
      [1, 1, 1, 1],
    ],
    'L': [
      [1, 1, 1],
      [1, 0, 0],
    ],
    'J': [
      [1, 1, 1],
      [0, 0, 1],
    ],
    'Z': [
      [1, 1, 0],
      [0, 1, 1],
    ],
    'S': [
      [0, 1, 1],
      [1, 1, 0],
    ],
    'T': [
      [1, 1, 1],
      [0, 1, 0],
    ],
    'O': [
      [1, 1],
      [1, 1],
    ],
  };
  
  enumShaps = {
    'I': [
      [[0,0],[1,0],[2,0],[3,0]],
      [[1,-1],[1,0],[1,1],[1,2]],
    ],
    'L':[
      [[0,0],[1,0],[2,0],[0,1]],
      [[0,0],[1,0],[1,1],[1,2]],
      [[2,0],[0,1],[1,1],[2,1]],
      [[0,0],[0,1],[0,2],[1,2]],
    ],
    'J':[
      [[0,0],[1,0],[2,0],[2,1]],
      [[1,0],[1,1],[1,2],[0,2]],
      [[0,0],[0,1],[1,1],[2,1]],
      [[0,0],[1,0],[0,1],[0,2]],
    ],
    'Z':[
      [[0,0],[1,0],[1,1],[2,1]],
      [[1,0],[1,1],[0,1],[0,2]],
    ],
    'S':[
      [[1,0],[2,0],[0,1],[1,1]],
      [[0,0],[0,1],[1,1],[1,2]],
    ],
    'T':[
      [[0,0],[1,0],[2,0],[1,1]],
      [[1,-1],[1,0],[1,1],[0,0]],
      [[0,0],[1,0],[2,0],[1,-1]],
      [[1,-1],[1,0],[1,1],[2,0]],
    ],
    'O':[
      [[1,0],[2,0],[1,1],[2,1]],
    ],
  };

上面代码有两组方块。(矩阵型,没举型)
两者没有直接联系,是旋转方块衍生的两种思路。
两者最大的区别在于,第一组矩阵型方块的旋转需要相关算法进行换算,相对复杂。
而第二组,已经将美中方块的旋转结果枚举出来,更佳便于理解。
第一种方块选装

let shapData = shaps['T']
  
  // 用算法旋转矩阵
  shapData = matirx2dRotation(shapData)
  const shapRender = (vector) => {
    // 与枚举型略有不同
  }

第二种方块旋转

// 比如下面代码接可以直接去除T方块的其中一个旋转数据
  // 这个数据中的各个矢量再加上方块的场景中的方块偏移量就是方块的渲染数据了
  const shapData = enumShaps['T'][1]

  // vector 偏移量(就是方块在场景中的位置信息)
  const shapRender = (vector) => {
    return shapData.map(([x, y]) => [x + X, y + Y]);
  }

以上完整代码在底部仓库地址中


二、碰撞与逻辑

标准的情况,场景中只有唯一一个方块收到玩家操作。
玩家操作的方块在游戏开始后便生成并交与玩家控制权。

玩家控制的方块,每当玩家操作反馈后进行碰撞逻辑的检测。
这里的操作反馈主要是方块左、右、下移动与旋转。

  • 方块是否溢出场景(场景左右与底部边界碰撞)
  • 方块是否碰撞场景中静止的方块

每次游戏心跳间隔,还需要将当前玩家控制的方块下降一格。

其中不论是玩家主动方块下降或是游戏心跳间隔中自动将方块下降。都需要检测是否与场景底部或其它静止方块发生碰撞与否。
如果碰撞成立,则将当前方块加入静止方块序列。
这时候就可以执行小方块的逻辑。
消玩方块后需要将空缺出来的位置补上。
最后生成一个新的方块加入到场景中。
新的方块进入场景的同时,需要立即检测与静止方块的碰撞。如果为真则游戏是否game over。

// 方块静止逻辑
  const isShapDead = vector => {
    // ...如果为真便加入到静止方块的序列
  }

  // 是否溢出场景
  // 不需要检测顶部
  const isOverFlow = vectors => {
    const [width, height] = screenSize;
    return vectors.some(([x, y]) => {
      return x < 0 || x >= width || y >= height;
    });
  }

  // 是否发生碰撞
  const isShapHit = vectors => {
    // ...
  }

  // 游戏是否失败
  const isGameOver = data => {
  	// 方块初始位置矢量
    const [, y] = vector;

    if (y <= 0 && isShapHit(data)) {
      return true;
    }
    return false;
  }

以上完整代码在底部仓库地址中


三、渲染层与逻辑层分离

每一次游戏心跳,与每一次用户操作反馈会触发渲染

或许是做前后端分离有些时日了,多少受了虚拟dom的一点影响。
重新设计的代码,将游戏的整个过程虚拟化。
只在每一个心跳(类似游戏中帧的概念)或者用户每次主动操作后才会推送游戏虚拟数据到渲染层,由渲染层实现渲染逻辑。

// TetrisJS 整个游戏的逻辑代码
    const TJS = new TetrisJS({
      screenSize,
      intervals: 1000,
    });
    // update 的回调函中接受渲染请求的推送
    TJS.update(() => {
      // TJS.map就是游戏的虚拟数据
      renderGame(TJS.map)
    });

	const renderGame = data => {
	  // render 渲染逻辑
	}

最后上仓库代码(以上代码基于此)https://github.com/applelee/tetris-js.git
老代码(慎点)https://github.com/applelee/tetris-js-old.git

版权声明
本文为[kingapple]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/1243524/blog/4540920