今天制作方块掌机的另一个游戏 —— 赛车。这个游戏本身没有什么难度,就是操作由几个方块组成的车辆,在屏幕下方左右移动躲避下落的敌方车辆。游戏的需求非常明确,整个游戏包含几个东西:
- 玩家操作的赛车。
- 移动的公路。
- 玩家需要躲避的其它赛车。
事实上通过分析,我们可以将后两者合并,只需要制作一个移动的公路即可,让敌方赛车属于公路的一部分。这个方法和俄罗斯方块中的容器差不多,而玩家操作的赛车和容器进行碰撞检测,如果赛车和容器中的方块发生碰撞,则直接 GameOver。
移动的容器
首先我们先定义公路这个容器。因为游戏一共 10 列,所以用 10 个比特就是表示,每一行代码中用一个 unsigned long
,一共可以定义 25 行。其中包括上方隐藏的 5 行用来放置新生成的敌方车辆。
具体代码如下:
//赛车游戏
typedef struct Race
{
unsigned long roadContainer [RACE_CONTAINER_HEIGHT]; //公路容器
unsigned int frontIndex; //环形队列队首索引
//...
} Race;
有了容器,接下来我们需要制作公路移动的效果。最简单的方法是直接将数组的所有项向下移,这样做的效率虽然有些低但是比较容易理解。代码中用了另一种方法,直接将容器看作一个环形队列,只需要附带一个指向队首的索引标记,以后每一次公路的移动则可以从移动数组所有元素变为只移动队首的索引位置。
下面是容器移动效果的代码:
//处理游戏逻辑
while (race->tick >= race->speed) {
//移动一格, 并清空首行
race->frontIndex = (race->frontIndex - 1 + RACE_CONTAINER_HEIGHT) % RACE_CONTAINER_HEIGHT;
race->roadContainer[race->frontIndex] &= 0x0201;
race->nextCarCountdown--;
//......
race->tick -= race->speed;
}
因为容器变为环形,每一次的索引在操作后可能存在越界的问题,所以代码中在读取索引的时候,会让当前索引取模:
race->frontIndex = (race->frontIndex - 1 + RACE_CONTAINER_HEIGHT) % RACE_CONTAINER_HEIGHT;
这可以确保索引不越界。
解决完容器的移动,不要忘记研究敌方赛车的生成策略。因为赛车的高度为 4 ,所以理论上你必须要控制敌方赛车在生成的时候保持一定的缝隙,否则你无法操作赛车进行躲避,这个缝隙的最小值是赛车的高度,下面这两种情况玩家肯定是必死无疑:
为了确保玩家在游戏的过程中有一定的躲避时间,这里我将缝隙的大小设定为 [5, 10]:
#define CAR_WAIT_COUNTDOWN (rand()%6 + 5)
每一次公路向下移动一格,都可能生成一个新的敌方车辆,但是为了确保缝隙的范围,代码中定义了两个参数:
//赛车游戏
typedef struct Race
{
CarPos prevCarPos; //敌方车辆上一次的位置
int nextCarCountdown; //敌方下一次车出现倒计数[5,10]
//......
} Race;
其中 prevCarPos 记录上一次生成敌方车辆的赛道,nextCarCountdown 则记录准备生成车辆的倒计时。具体代码如下:
//重新生成一辆车
if (race->nextCarCountdown <= 0) { //下一辆车等待时间 race->nextCarCountdown = CAR_WAIT_COUNTDOWN;
//下一辆车的位置
CarPos nextPos = rand() % CP_MAX;
//上一次为空 或者 同测
if (race->prevCarPos == CP_EMPTY || nextPos == race->prevCarPos) {
initCarBlock(race->roadContainer, race->frontIndex, nextPos);
//记录最后一次车辆位置
race->prevCarPos = nextPos;
} else {
race->prevCarPos = CP_EMPTY;
}
}
如果倒计时为 0,则生成一辆新车,新车的位置有三种:
//赛车位置
typedef enum CarPos
{
CP_EMPTY = 0,
CP_LEFT, //左侧
CP_RIGHT, //右侧
CP_MAX
} CarPos;
空的、左侧和右侧,为了避免生成上图左侧必死无疑的情况,代码中还做了是否同侧的判断,如果同侧则跳过至少 5 行,具体跳过的行数由这一次生成的 nextCarCountdown 控制。
玩家的车辆
车辆的形状根据原始的方块赛车的样式:
因为赛车的移动单一,不是在左侧就是在右侧,所以我们用一个变量就可以表示车辆的当前位置:
//赛车游戏
typedef struct Race
{
CarPos currCarPos; //我方当前赛车位置
//......
} Race;
和俄罗斯方块类似,知道了赛车的位置不代表知道赛车的样式形状,仍然用打表法,事先存好赛车左侧和右侧的样式表, 用 4 行数据表示赛车,其中分别包括左侧和右侧的情况:
static void initCarBlock(unsigned long* roadContainer, int startIndex, CarPos pos)
{
//清除
for (int i = 0; i < ROAD_MOVE_PERIOD; i++) {
roadContainer[(i + startIndex)%RACE_CONTAINER_HEIGHT] &= 0x0201;
}
//左侧车
if (pos == CP_LEFT)
{
roadContainer[(1 + startIndex) % RACE_CONTAINER_HEIGHT] |= 0x0008;
roadContainer[(2 + startIndex) % RACE_CONTAINER_HEIGHT] |= 0x001C;
roadContainer[(3 + startIndex) % RACE_CONTAINER_HEIGHT] |= 0x0008;
roadContainer[(4 + startIndex) % RACE_CONTAINER_HEIGHT] |= 0x0014;
}
else if (pos == CP_RIGHT)
{
roadContainer[(1 + startIndex) % RACE_CONTAINER_HEIGHT] |= (0x0008 << 3);
roadContainer[(2 + startIndex) % RACE_CONTAINER_HEIGHT] |= (0x001C << 3);
roadContainer[(3 + startIndex) % RACE_CONTAINER_HEIGHT] |= (0x0008 << 3);
roadContainer[(4 + startIndex) % RACE_CONTAINER_HEIGHT] |= (0x0014 << 3);
}
}
这个函数用来初始化赛车到公路容器的指定位置。其中 roadContainer 表示公路容器,而 startIndex 表示要放置的位置,pos 表示车辆放置的赛道。
有了车辆的状态和样式,剩下的就是控制,直接获取键盘消息即可:
if (evt->type == SDL_KEYDOWN)
{
if (evt->key.keysym.sym == SDLK_LEFT) {
MoveLeft(&gRaceGame);
} else if (evt->key.keysym.sym == SDLK_RIGHT) {
MoveRight(&gRaceGame);
}
}
碰撞检测
公路和赛车全部讲解完毕,只剩最后一个内容,就是碰撞检测:
//检测是否发生碰撞
unsigned long carContainer[ROAD_MOVE_PERIOD] = {0};
//获取车的样式
initCarBlock(carContainer, 0, race->currCarPos);
//枚举所有行
for (int i = 0; i < ROAD_MOVE_PERIOD; i++)
{
int row = (race->frontIndex + RACE_CONTAINER_HEIGHT - ROAD_MOVE_PERIOD + i) % RACE_CONTAINER_HEIGHT;
for (int j = 2; j < RACE_CONTAINER_WIDTH - 2; j++)
{
if (((carContainer[i] & (1 << j)) != 0) && ((race->roadContainer[row] & (1 << j)) != 0))
{
//发生碰撞......
return 0;
}
}
}
车辆的碰撞检测和俄罗斯方块的碰撞检测原理是一样的,就是枚举所有位置,查看玩家控制的车辆和赛道的敌方车辆是否有重合,如果重合,则发生碰撞。
总结
这个游戏制作比较简单,不过仿制的不是很完美。其一是网上很少有完整的游玩过程,我自己的掌上游戏机也不知道扔到哪里去了,没有用来参考的东西。其二就是没有音乐资源,这个赛车我记得是有像噪音一样的背景音乐播放的,我搜了好久没有找到,所以暂时放弃了。其三是游戏中死亡,我记得是有三条命的,不过我找不到证据和跳转流程,所以也没有仿制。最后想说的是这个游戏真正花费我无数精力的不是本身的逻辑,而是死亡的过场动画……