【C语言程序设计】小游戏之俄罗斯方块(二)!适合初学者上手、练手!

1. 容器的表示

大方块的实现涉及到位运算,而容器同样如此。容器显示的部分是由 10 * 20 个小方块构成的矩形,如果我们将每个小方块用一个比特来表示,则一行只需要 10 比特,C语言中可以用 unsigned short 表示,不过这里我们为了后期扩展,选用了 unsigned long 类型。

unsigned long blockContainer[TETRIS_CONTAINER_HEIGHT];

blockContainer 变量代表整个容器,TETRIS_CONTAINER_HEIGHT 代表容器的高度。这里需要注意常量 TETRIS_CONTAINER_HEIGHT 并没有定义为 20,而是定义为 25,容器的每行我们用了 12 位比特表示,并没有用 10 位表示,这所以这样,其实是为了碰撞检测带来方便,其中容器的宽高定义如下:

//俄罗斯方块容器宽高

#define TETRIS_CONTAINER_WIDTH (1 + 10 + 1)

#define TETRIS_CONTAINER_HEIGHT (BLOCK_HEIGHT + 20 + 1)

这里我们用容器的示意图表示一下这样定义的好处:

上图是俄罗斯方块真正的容器区域,其中游戏界面显示的仅仅是其中的蓝色显示区域,而绿色隐藏区域用来放置准备下落的大方块,而灰色是用来碰撞检测的隔离区域。

因为 Windows 窗口的纵坐标是从上到下的,所以我们显示的时候也是从上到下,最上边是容器的第 0 行,最下边是容器的 24 行,这一行会用来兜底,防止大方块在下落的过程中越界。


2. 大方块的表示

Windows 窗口的横坐标是从左到右,所以左边是第 0 行,最右边是第 11 行。这里需要注意这和默认大方块表示的二进制顺序并不一样:

事实上,前台显示的画面左边是二进制的低位,右边是二进制的高位,所以大方块真正表示的二进制是和显示的画面水平方向正好是相反的。


3. 结构定义

明白了上面的介绍,接下来我们就可以定义俄罗斯方块的数据结构:

//俄罗斯方块

typedef struct Tetris

{

unsigned long blockContainer[TETRIS_CONTAINER_HEIGHT]; // 容器

int blockIndex; // 当前块索引

Block blocks[TETRIS_BLOCK_NUM]; // 多个块(前后台)

//......

} Tetris;

俄罗斯方块中成员很多,但最重要的就是容器和两个方块的表示,之所以是两个方块是因为一个是当前下落的前台方块,另一个是下轮下落的后台方块,这里用数组表示,然后增加一个方块索引,用来循环使用。


4. 初始化

有了数据结构之后,接下来可以实现俄罗斯方块的基本操作了。首先当然是初始化操作:

//初始化容器

for (int i = 0; i < TETRIS_CONTAINER_HEIGHT; i++) {

tetris->blockContainer[i] = EMPTY_LINE;

}

tetris->blockContainer[TETRIS_CONTAINER_HEIGHT - 1] = 0xFFFF;

//初始化方块

tetris->blockIndex = 0;

for (int i = 0; i < TETRIS_BLOCK_NUM; i++) {

initRandBlock(&(tetris->blocks[i]), BLOCK_INIT_POSY, BLOCK_INIT_POSX);

}

代码中逻辑就是将容器初始化为前面的示意图状态,其中定义了三个常量:

const int BLOCK_INIT_POSX = (TETRIS_CONTAINER_WIDTH - BLOCK_WIDTH) / 2;

const int BLOCK_INIT_POSY = 2;

const unsigned long EMPTY_LINE = 0x0801;

前两个用来表示方块初始化的位置,后面则是值容器中空行的数值。


5. 碰撞合并

初始化完成之后,我们接下来实现大方块和容器的碰撞操作以及大方块和容器发生碰撞后的合并操作。首先是碰撞操作:

//碰撞测试

int hitTest(const Block* block, const Tetris* tetris)

{

    unsigned short blk = gBlockList[block->type][block->state];

    for (int i = 0; i < BLOCK_HEIGHT; i++)

    {

        unsigned short bits = ((blk >> (i * BLOCK_WIDTH)) & 0x000F);

        //block->col 可能为负数

        if (block->col < 0) {

            bits >>= (-block->col);

        } else {

            bits <<= block->col;

        }

        if(tetris->blockContainer[block->row + i] & bits)

        {

            return 1;

        }

    }

    return 0;

}

碰撞测试中首先获取当前大方块,然后根据大方块的位置,查看大方块和容器是否有重合的地方,逻辑上就是检测容器和大方块相同的位置比特位是否同时为 1。这里有个地方需要注意,大方块的水平位置可能为负,例如下面这种情况:

上图是 I 形的大方块,在竖起的状态下可能呈现出上面的效果,当前这个方块的列为 -1。事实上你可以通过规划大方块的形状和位置来避免这类问题,只不过这里没有这样做,而是直接将负数列作为正常的情况之一。

接下来是碰撞后的合并,操作很简单就是直接将大方块的比特位复印到容器内即可,在位运算上可以使用或运算实现。

//合并

void merge(Block* block, Tetris* tetris)

{

unsigned short blk = gBlockList[block->type][block->state];

for (int i = 0; i < BLOCK_HEIGHT; i++)

{

unsigned short bits = ((blk >> (i * BLOCK_WIDTH)) & 0x000F);

//block->col 可能为负数

if (block->col < 0) {

bits >>= (-block->col);

}

else {

bits <<= block->col;

}

        tetris->blockContainer[block->row + i] |= bits;

}

}


6. 操控大方块

接下来实现大方块的操控函数,主要有左移、右移、下移、旋转以及掉落。这些其实以及在上一篇文章讲过了,这次做的是加上碰撞逻辑,例如当左移动的时候:

//左移

int moveLeftBlock(Tetris* tetris)

{

if (!tetris) {

return -1;

}

//当前方块

Block* currBlock = &(tetris->blocks[tetris->blockIndex]);

//移动后的状态

Block next = *currBlock;

moveLeft(&next);

//检测下一状态的方块会发生碰撞,则取消移动

if (hitTest(&next, tetris)) {

return 0;

}

//没发生碰撞,完成移动

moveLeft(currBlock);

return 0;

}

我们首先获取大方块的状态,然后模拟出大方块左移后的效果,用左移后的方块做碰撞检测,如果发生碰撞,则直接返回,否则将当前的方块左移,整个过程有点类似于投石问路的效果。

其它的操作和左移基本类似,除了下移操作需要在发生碰撞的时候进行合并操作并生成新的方块:

//下移

int moveDownBlock(Tetris* tetris)

{

if (!tetris) {

return -1;

}

//当前方块

Block* currBlock = &(tetris->blocks[tetris->blockIndex]);

if (moveDownTest(tetris, currBlock))

{

//如果底部碰撞,则将方块合并到容器中

merge(currBlock, tetris);

//消行

eraseLines(tetris);

//重新生成方块,并切换当前方块

initRandBlock(currBlock, BLOCK_INIT_POSY, BLOCK_INIT_POSX);

tetris->blockIndex = (tetris->blockIndex + 1) % TETRIS_BLOCK_NUM;

return 1;

}

//没发生碰撞,完成移动

moveDown(currBlock);

return 0;

}

这里面还有一个上面没有说的消行函数,消行本身非常简单,只需要检测当前容器行是否满足“满行”即可,如果满足,则删除该行,让容器其它行依次移动到这里:

//消减行

static void eraseLines(Tetris* tetris)

{

//从下到上,逐步扫描

int line = TETRIS_CONTAINER_HEIGHT - 2;

int afterLine = line;

int eraseLine = 0;

while (line >= BLOCK_HEIGHT)

{

//如果当前不满行

if (0x0FFF != (tetris->blockContainer[line] & 0x0FFF))

{

afterLine--;

}

//记录消行数

else

{

eraseLine++;

}

line--;

if (afterLine != line)

{

tetris->blockContainer[afterLine] = tetris->blockContainer[line];

}

}

//剩余设置为空

while (afterLine >= BLOCK_HEIGHT)

{

tetris->blockContainer[--afterLine] = EMPTY_LINE;

}

}

eraseLine 变量代表最终消行数,你可以用这个值计算一些分数等等。


7. 更新与绘制

完成了周边的操作函数,接下来就是让程序自身动起来,这里直接在更新函数中增加一个不断更新的下落操作就能实现:

//处理游戏逻辑

while (tetris->tick >= tetris->speed) {

// 下落

moveDownBlock(tetris);

tetris->tick -= tetris->speed;

}

tick 变量代表游戏运行中的滴答时间,单位是毫秒。而 speed 代表当前的下落速度,这个单位也是毫秒,代表经过多少毫秒下落一次,更新函数每次检测当前等待的时间是否大于下落速度,大于则执行下落操作。

绘制操作很简单,只是单纯的调用 SDL 显示数据结构中的数据而已。下面是绘制容器的操作:

//绘制容器

for (int i = BLOCK_HEIGHT, r = 0; i < TETRIS_CONTAINER_HEIGHT-1; i++, r++)

{

for (int j = 1, c = 0; j < TETRIS_CONTAINER_WIDTH-1; j++, c++)

{

rtDst.x = c * BLOCK_IMAGE_WIDTH;

rtDst.y = r * BLOCK_IMAGE_HEIGHT;

rtDst.w = BLOCK_IMAGE_WIDTH;

rtDst.h = BLOCK_IMAGE_HEIGHT;

SDL_RenderCopy(pModule->pRenderer,

getResource(RES_TEXTURE), getTileRect(TT_BK), &rtDst);

//当前位置有方块(i,j)

if (tetris->blockContainer[i] & (1 << j))

{

SDL_RenderCopy(pModule->pRenderer,

getResource(RES_TEXTURE), getTileRect(TT_FK), &rtDst);

}

}

//绘制右侧竖线

SDL_Rect rtLineSrc = {0, 0, 5, BLOCK_IMAGE_HEIGHT};

SDL_Rect rtLineDst = { (TETRIS_CONTAINER_WIDTH - 2)*BLOCK_IMAGE_WIDTH+3,

r * BLOCK_IMAGE_HEIGHT, 5, BLOCK_IMAGE_HEIGHT};

SDL_RenderCopy(pModule->pRenderer, getResource(RES_TEXTURE), &rtLineSrc, &rtLineDst);

}

下面是绘制大方块的操作:

void renderBlock(Block* block, unsigned char alpha, SystemModule* pModule)

{

    SDL_Rect rt = { 0, 0, BLOCK_IMAGE_WIDTH, BLOCK_IMAGE_HEIGHT };

    for (int i = 0; i < BLOCK_HEIGHT; i++)

    {

        for (int j = 0; j < BLOCK_WIDTH; j++)

        {

            //如果当前位置有方块

            if ((1 << j << (i * BLOCK_WIDTH)) & (gBlockList[block->type][block->state]))

            {

rt.x = (block->col + j - 1) * BLOCK_IMAGE_WIDTH;

rt.y = (block->row + i - BLOCK_HEIGHT) * BLOCK_IMAGE_HEIGHT;

SDL_SetTextureAlphaMod(getResource(RES_TEXTURE), alpha);

SDL_RenderCopy(pModule->pRenderer, getResource(RES_TEXTURE), getTileRect(TT_FK), &rt);

SDL_SetTextureAlphaMod(getResource(RES_TEXTURE), 255);

            }

        }

    }

}

整个俄罗斯方块的基本逻辑就这些,最后再加上一点细节,例如分数,等级、音乐等等。

这样一个联合前面那篇俄罗斯方块的文章,一个完整的程序就这样诞生了,你学会了吗?没学会,又学到了多少。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343