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

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. 单应用模式 - Layuiadmin单页版放入TP6.0的部署方案

    thinkphp6.0.3单应用模式.layuiadmin1.4.0单页版,不需要tp的视图驱动 1. 复制 src.start 两个文件夹 2. 粘贴到 thinkphp 的 public 目录下 ...

  2. Vue 事件的高级使用方法

    Vue 事件的高级使用方法 事件方法 在Vue中提供了4中事件监听方法,分别是: $on(event: string | Array, fn) $emit(event: string) $once(e ...

  3. pytest allure 生成html测试报告

    前提:需要 java 1.8 以上.python3环境 一.下载pytest pip install pytest 二.下载Allure Pytest Adaptor插件 pip install py ...

  4. 动手编写—动态数组(Java实现)

    目录 数组基础回顾 自定义动态数组 动态数组的设计 抽象父类接口设计 抽象父类设计 动态数组之DynamicArray 补充数组缩容 全局的关系图 声明 数组基础回顾 1.数组是一种常见的数据结构,用 ...

  5. CentOS6.10下安装MongoDB和Redis

    安装mongodb 首先考虑离线安装,但是安装过程中在启动服务的时候出现了问题,centOS出于稳定原因考虑,系统自带的glibc版本过低, 而编译需要使用较高版本,这个问题我查询了一下,需要升级gl ...

  6. apache-apollo启动报错

    启动Apollo后,进入网页版管理中心后报错:500: Server Error https://127.0.0.1:61681/console/index.html 网页抓包 报错:{"c ...

  7. 利用Node实现HTML5离线存储

    前言 支持离线Web应用开发是HTML5的一个重点.离线Web应用就是在设备不能上网的时候仍然可以运行的应用.开发离线Web应用需要几个步骤,其中一个就是离线下必须能访问一定的资源(图像 JS css ...

  8. PostGreSQL不同索引类型(btree & hash)的性能问题

    在关系型数据库调优中,查询语句涉及到的索引类型是不得不考虑的一个问题.不同的类型的索引可能会适用不同类型的业务场景.这里我们所说的索引类型指的是访问方法(Access Method),至于从其他维度区 ...

  9. web自动化(python)——selenium工具基本使用

    WebDriver基本操作 生成driver--启动浏览器 #启动谷歌浏览器,预先安装chromedrvier插件 from selenium import webdriver driver = we ...

  10. python类中的__init__和__new__方法

    Python中类: Python中在创建类的过程中最先调用的不是__init__方法而是__new__方法,__new__方法是一个静态方法,在创建一个类对象时其实是通过__new__方法首先创建出一 ...