第二篇,主要实现俄罗斯方块中的主体部分,包括容器的数据结构以及容器的相关操作,特别是大方块和容器之间的交互逻辑,包括碰撞检测,消除检测等等。

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);
}
}
}
}

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

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

如果你也很想学编程,可以来我的C语言/C++编程学习基地【点击进入】!

还有(源码,零基础教程,项目实战教学视频)!带你入个门还是简简单单啦~

涉及:游戏开发、课程设计、常用软件开发、编程基础知识、黑客等等...

 

 

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

  1. java客房管理小项目,适合java小白练手的项目!

    java客房管理小项目 这个客房管理小项目,适合java初学者练手.功能虽然不多,但是内容很齐全! 喜欢这样文章的可以关注我,我会持续更新,你们的关注是我更新的动力!需要更多java学习资料的也可以私 ...

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

    俄罗斯方块的核心玩法非常简单,所以制作起来并不是很复杂,我准备先用2篇文字的篇幅详细讲解一下俄罗斯方块的制作方法. 今天咱们算是第一篇,主要讲解俄罗斯方块中如何定义方块,以及如何实现方块的移动.旋转. ...

  3. C#,JavaScript两种语言 2048小游戏

    <html> <head> <style type="text/css"> .haha { border-width: 2; font-size ...

  4. 《C语言程序设计》学习笔记(二)

    第八章 函数 函数的基本概念 定义:函数由函数名.参数和函数体组成. 函数定义的一般形式: 类型说明符 函数名(形式参数声明) { [说明与定义部分] 语句: } 说明: 1.类型说明符用来说明函数的 ...

  5. android 小游戏 ---- 数独(二)

    > 首先创建一个自己的View类   -->继承SurfaceView并实现SurfaceHolder.Callback接口    --> SurfaceView.getHolder ...

  6. 【沙龙报名中】与微信&云开发官方团队零距离互动,揭秘爆款微信小游戏背后的技术!

    有人说 微信小程序游戏的百花齐放 活像十几年前的4399小游戏称霸互联网的景象 " 歪,斗地主吗,三缺二, 不用下app,小程序就能玩,我保证不抢地主让你抢!" ...... &q ...

  7. 原生javascript开发仿微信打飞机小游戏

    今天闲来无事,于是就打算教一个初学javascript的女童鞋写点东西,因此为了兼顾趣味性与简易程度,果断想到了微信的打飞机小游戏.. 本来想用html5做的,但是毕竟人家才初学,连jquery都还不 ...

  8. C#俄罗斯方块小游戏程序设计与简单实现

    C#俄罗斯方块小游戏程序设计与简单实现 相信90后或者80后都玩过这款小游戏,一直想干一票,琢磨一下,但又不太懂,于是网上搜集修改就有了以下效果!bug较多,多多包涵! 1.效果展示 2.实现方法 参 ...

  9. 【C语言程序设计】—最近超火的小游戏—【数字炸弹】!

    ✍  准备工作和建议 一.程序的原理 在动手编程之前,得先跟大家说一下这个程序是干什么的. 我们可以称呼这个游戏为<数字炸弹>. 游戏的原理是这样: 每一轮电脑从 1 到 100 中随机抽 ...

随机推荐

  1. jenkins iOS自动打包

    1.Jenkins配置 采用命令行下载配置Jenkins,防止产生权限问题 1)先安装brew,打开命令行,输入:/usr/bin/ruby -e "$(curl -fsSL https:/ ...

  2. [算法]体积不小于V的情况下的最小价值(0-1背包)

    题目 0-1背包问题,问要求体积不小于V的情况下的最小价值是多少. 相关 转移方程很容易想,初始化的处理还不够熟练,可能还可以更简明. 使用一维dp数组. 代码 import java.util.Sc ...

  3. 软件工程与UML作业2

    博客班级 https://edu.cnblogs.com/campus/fzzcxy/2018SE1 作业要求 https://edu.cnblogs.com/campus/fzzcxy/2018SE ...

  4. rest_framework五大模块

    面向对象封装 面向对象封装导入 # 1.将子类共有的方法抽离形成父类方法 # 2.子类使用共有方法,使用的是父类方法 # 3.共有方法中的资源,在子类使用方法时,获取的是子类资源 class MyCl ...

  5. ThreadLocal 和神奇的数字 0x61c88647

    这篇文章会详细阐述ThreadLocal的内部结构及其原理,以及神奇的0x61c88647 在Java 1.4之前,ThreadLocals会产生线程间的竞争,无法写出高性能的代码. Java 1.5 ...

  6. 谈谈Netty内存管理

    前言 正是Netty的易用性和高性能成就了Netty,让其能够如此流行. 而作为一款通信框架,首当其冲的便是对IO性能的高要求. 不少读者都知道Netty底层通过使用Direct Memory,减少了 ...

  7. 关于struts1与struts2

    1.Action方面 Action是整个Struts框架的核心内容,Struts1.x与Struts2的Action模型很大的区别.Struts2是一个 pull-MVC架构 ,从开发者角度看,就是说 ...

  8. react-router 路由切换动画

    路由切换动画 因为项目的需求,需要在路由切换的时候,加入一些比较 zb 的视觉效果,所以研究了一下.把这些学习的过程记录下来,以便以后回顾.同时也希望这些内容能够帮助一些跟我一样的菜鸟,让他们少走些坑 ...

  9. day61:Linux:权限管理&rpm软件包管理&yum工具

    目录 1.权限管理 2.rpm软件包管理 3.yum工具(联网) 权限管理 1.什么是权限? 权限主要用来约束用户能对系统所做的操作 2.为什么要使用权限? 因为系统中不可能只存在一个root用户,一 ...

  10. 商品现货数据不好拿?商品季节性难跟踪?一键解决没烦恼的Python爬虫分享

    更多精彩内容,欢迎关注公众号:数量技术宅.探讨数据分析.量化投资问题,请加技术宅微信:sljsz01 季节性在大宗商品的交易中至关重要 大宗商品交易中,特别是在期货市场,由于商品价格周期的存在,季节性 ...