C语言小游戏之俄罗斯方块(一)

  上次有讲到利用C语言制作贪吃蛇小游戏,这次我就简单介绍一下俄罗斯方块。相信大家都玩过俄罗斯方块吧,其实它的的核心玩法非常简单,所以制作起来并不是很复杂,今天主要讲解俄罗斯方块中如何定义方块,以及如何实现方块的移动、旋转、下落等操作。

  1. 需求分析

  俄罗斯方块中的基本逻辑非常简单,不过在介绍之前,我们先确定一下名词,防止出现词不达意的现象。

  这种方块,是构成容器的基本单位,我们称之为小方块。

  由四个小方块组成,用来填充容器的东西,我们叫做大方块。

  用来存放大方块的地方,我们叫做容器。

  定义了这三种东西之后,我们就可以这样描述俄罗斯方块这个游戏了。俄罗斯方块主要由一个存放正方形小方块的虚拟容器和实时下落的7种大方块构成。容器的宽是 10 列,高是 20 行,行高和列宽是相等的,所以容器可以看作是 200 个正方形小方块平铺的结果。每个实时下落的大方块都是由 4个正方形小方块组成,共 7 种固定样式,每种方块都可以旋转,所以理论上最多有 28 种样式,但其中有一些大方块在旋转的时候样式不会发生变化。

  7 种大方块按照形象可以由 7种字母替代,它们是:S、Z、L、J、I、O、T。这些大方块在下落到容器最底部或碰撞到其它容器中的小方块时会被固定在容器中,然后容器上方会重新产生一个随机的大方块,重复原来的下落流程。当容器中出现满行的时候,整行会被消除,该行上面的所有小方块都会依次掉落。

  2. 大方块的定义

  俄罗斯方块中虽然基本的单位是小方块,但是所有操作的对象都是大方块,所以这里直接定义大方块的结构类型。

  在定义大方块之前,我们需要先定义两个结构,分别是大方块的类型和大方块的旋转状态。

  // 方块类型

  typedef enum BlockType

  {

  BT_S = 0,

  BT_Z,

  BT_J,

  BT_L,

  BT_I,

  BT_T,

  BT_O,

  BT_NUM

  } BlockType;

  // 方块状态

  typedef enum BlockState

  {

  BS_T = 0,

  BS_R,

  BS_B,

  BS_L,

  BS_NUM

  } BlockState;

  使用上面两个类型,就可以确定当前大方块的样式。除了样式之外,因为大方块会按照固定的速度移动,所以这里需要定义大方块的位置信息,加上前面两个成员,大方块可定义如下:

  // 大方块

  typedef struct Block

  {

  int row; // 方块水平位置

  int col; // 方块垂直位置

  BlockType type; // 方块类型

  BlockState state; // 方块状态

  } Block;

  这里之所以没有定义大方块的下落速度是因为所有的大方块下落速度都是一样的,因此不需要在每个大方块内部都定义一个变量,只需要在外部定义一个速度变量即可。

  确认了大方块的结构,我们还有一个问题需要解决,就是如何获取大方块的具体形状。虽然大方块里面已经定义了类型和状态,但是并没有保存当前方块的形状,为了解决这个问题,我们可以定义一个全局的静态数组,数组的每一项代表一种方块的形状,算上重复的,一共可存放 28 种形状。

  static unsigned short gBlockList[BT_NUM][BS_NUM];

  注意这里,数组的每一项用了一个 unsigned short 类型表示。

  因为大方块是由 4 个小方块组成,所以每个大方块都可以用一个 4 * 4 的二维数组来表示。因为二维数组的每一项都只有两种状态,所以可以进一步变形,每一项用一个比特位,这样算下来,16 位比特就可以描述一个大方块的形状,而 16 位比特对应 C语言中的数据类型,正好是 unsigned short。

  虽然确认了方案,但是我们还需要确认一个表达规则,就是 16 位比特串低四位是表示大方块的第一行,还是表示最后一行,又或者表示左边的列,还是右边的列。方法很多,只需要取一个认为方便的就好,这里我是这样定义的:

  [ 0 0 0 0 ] 0x000F

  [ 0 0 0 0 ]? 0x00F0

  [ 0 0 0 0 ]? 0x0F00

  [ 0 0 0 0 ]? 0xF000

  即低四位表示第一行。最终我们可以求出 28 个形状对应的每一个值:

  static unsigned short gBlockList[BT_NUM][BS_NUM] = {

  {0x00C6, 0x04C8, 0x00C6, 0x04C8}, //S

  {0x006C, 0x08C4, 0x006C, 0x08C4}, //Z

  {0x00E8, 0x088C, 0x002E, 0x0C44}, //J

  {0x00E2, 0x044C, 0x008E, 0x0C88}, //L

  {0x000F, 0x8888, 0x000F, 0x8888}, //I

  {0x04C4, 0x00E4, 0x08C8, 0x004E}, //T

  {0x00CC, 0x00CC, 0x00CC, 0x00CC}? //O

  };

  以后如果在程序中如果需要获取当前大方块的形状,只需要使用类似下面的语句:

  gBlockList[i][j]

  3. 大方块的操作

  确定了大方块的定义,接下来就是对大方的操作实现。首先是初始化操作:

  void initRandBlock(Block* block, int r, int c)

  {

  block->row = r;

  block->col = c;

  block->type = rand() % BT_NUM;

  block->state = rand() % BS_NUM;

  }

  接着就是移动操作,因为我们使用两个变量来确定大方块的位置,所以代码逻辑非常简单:

  //左移

  int moveLeft(Block* block)

  {

  if (block) {

  block->col -= 1;

  }

  return 0;

  }

  //右移

  int moveRight(Block* block)

  {

  if (block) {

  block->col += 1;

  }

  return 0;

  }

  //下移

  int moveDown(Block* block)

  {

  if (block) {

  block->row += 1;

  }

  return 0;

  }

  除了移动,大方块还有一个常用旋转操作,因为前面我们已经使用打表法定义了所有方块的旋转状态,这里直接获取即可:

  //旋转

  int rotate(Block* block)

  {

  block->state = (block->state + 1) % BS_NUM;

  return 0;

  }

  除了上面这些操作,其实还有碰撞检测以及渲染等操作。不过这些需要和其它对象交互,所以这里暂时不讲,等讲解完容器那章之后再反过来继续。

  关于俄罗斯方块的制作,我的B站主页也有相关视频课程。对这个感兴趣的同学来这里学学吧!即使是零基础的学习者,都可以一起成长进步。