控制台俄罗斯方块

这是一个控制台项目,一切设计都要从字符角度考虑。
这次项目我终于敢放下顾虑,大胆解决问题了,其价值足以我写一篇博客来记录
俄罗斯方块不是什么很难的东西,也没人教过我怎么去构建这个项目。说明这个项目是真的简单。

这个项目构思不是一瞬间完成的,是先有总的思路,然后考虑某个部分的内容,再接着去做其细节、写成代码,常常因为出现前面考虑不到而去修改前面的东西的情况。看来这种修改是稀松的,不用担心去修改前面的代码,
这种修改是一种进步,不用担心不用怕。
那咱就挑点重点写写吧
Console Tetris.png

阅前提示

建议先编译看看效果然后再来读博客。代码链接
(还有很多需要优化的地方……)

代码实现

总框架

void gameStart()

1
2
3
4
5
6
7
初始化
无脑死循环:
处理键盘事件
更新游戏内的各种数据
unblockedSleep()
写入渲染器的缓冲区
渲染器渲染

然后想到这个死循环有停下俩的时候,那就是:游戏失败或者是玩家退出,所以又加上了gameOvergameExit这俩控制变量,然后在循环之后加上了例如分数记录之类的东西

贴图实现

实际上我以前都觉得贴图应该存在一个二维数组里面,但这次我却明白贴图还有多种存储方式。比如我能把一个”山“样的下落物存储在一个一维数组里,把它的贴图当作一个矩形,记录他的宽度,然后一排一排地解析。

1
2
3
4
5
降维打击前:
#
###
降维打击之后:
# ### width:3 height:2

大概是我第一次在显示这个问题上”活脑筋“,其价值足以我写一篇博客来记录

双缓冲渲染

我把双缓冲渲染改成了三个部分:绘制(输出画面)、写入Buffer、Buffer更新
以前我会顾虑:到底在哪里使用双缓冲?
我突然就敢放下这个顾虑:”只在游戏部分使用不就行了,其他地方根本就不需要双缓冲。“

绘制(输出画面)

void render()

比较用于绘制的Buffer(renderBuffer[DRAWING])和显示在屏幕上的信息(renderBuffer[SHOWING]),然后到相应的位置去输出不同的部分。用gotoxy()可以解决调用system('cls')带来的闪屏问题。

写入Buffer

void renderDraw()

这次把所有的绘制都封装到了一个函数renderDraw()里,以前的时候我总是会顾虑”逻辑运算完成了,在哪里绘制啊??????“。现在我通过gameStart()函数里的总流程,实现了贴图绘制的一个封装,就不用担心”这里是一个绘制,那里是一个绘制“导致混乱的问题、避免在这个问题上继续纠结了。
renderDraw()会在(dropX,dropY)解析下落物的贴图,并写入renderBuffer[DRAWING],然后把那些已经固定下来的方块一一对应绘制进buffer

Buffer更新

void renderFresh()

比较两个buffer[],然后把旧的推进新的里去。

选择选项

int makeChoice(char**, int[, char*])函数

如果直接把做选择这个事情封闭起来,那么其它部分代码就不好实现,所以需要让makeChoice()有一个返回值,便于信息传递,而不是简简单单的去显示个动画。
首先,肯定要有个变量choice来记录选择……
做选择时会有多个选项,每个选项前都要有其被选中的标志,按下回车键确定,因此采用了分行输出每个选项,每个选项前留出空白,在空白区域用->表示这是当前选中的选项,以->的左右移动为其动画效果美化纯文字的界面。
所以首先要显示每个选项并留出空白

1
2
for (int i = 0; i < _amo; ++i)
printf(" %s\n", _choice[i]);

然后就是考虑动效了。既然->要上下移动和左右移动——尽管不会同时进行——那么我们就得去确定它的移动坐标变化范围。显然该用WindowsAPI获取选项输出后的光标位置。
1
2
3
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO pos;
GetConsoleScreenBufferInfo(h, &pos);

简单把->输出到相应位置十分简单,这里就不讲了。我们直接考虑动效的几个步骤:

  1. 清空旧的绘制图案
  2. 绘制图案

清空旧的绘制图案,有如下情况

  • 用户改变了选中,旧的->要清除
  • ->移动了,原来位置上的不能再显示了

考虑到控制台覆盖先前字符的特性,->自动移动可以由新的图案直接输出覆盖,而用户改变选中,则需要我们把光标放到相应位置并输出足够的空格。当前动画播放到哪一帧,也需要去记录,就用ani吧。
而且,不管用户是否按键,我们都要去更新->的动画。所以我们不能阻塞进程并获取按键信息,conio.hkbhit()以及我写的伪非阻塞SleepunblockedSleep()就派上用场了。

1
2
3
4
5
6
7
8
9
10
11
if kbhit() == 1:
光标移动
覆盖旧->(无论按什么键什么都去覆盖一下,是为了代neng码tou简ge便lan
判断按键
按w:改变choice
按s:改变choice
按Enter:return choice
光标移动
在旧->上直接覆盖当前的帧的图案
unblockedSleep(); // 这样能保证延时时间或对用户按键”随叫随到“的相应
ani++

后来在Esc暂停菜单里我用makeChoice()来实现菜单选项,就又在里面加了个选择后”隐藏“菜单的功能,于是又在按下Enter键后的代码里加上了,让makeChoice自己清除自己的的代码
一开始考虑的设计是如>Start!的样式,但是测试之后发现不如->Start!好康,于是就改成了这个

作者

勇敢梧桐树

发布于

2021-08-15

更新于

2023-01-04

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×