这篇文章主要是代码分享,俄罗斯方块的游戏功能代码部分以及简单解析。
对代码哪里不理解的,欢迎找我讨论哈。
和上篇代码结构不一样的地方是多文件和分模块,使得工程更加有条理。
本意是直接抛代码的,没想到啰里啰唆搞了一大堆。
目录
俄罗斯方块总体设计
为什么第三篇和其他不一样呢?很显然这块是这个小项目的核心嘛,单片机是个专用的计算机系统,在这个项目里就体现在这里了。因为你所有的设计都是围绕着这个目标来的,甚至于可能要推到重来。
一、游戏规则
我们大多数人就算没有玩过,也应该听说过这个游戏,不然你也搜不到这个文章。这里我们以自己观察到的游戏现象,简单的例举几条规则。
1.游戏中,图形自上而下以恒定的速度下落
我觉得,这条规则,进入游戏,不需要任何操作,立马就能观察到,但可能不以为然。3秒之后你就可能认为不重要了,但是你不能忘记这个时候你的角色不是玩家,你是要自己写出来这个游戏的。单片机所有的现象都是出自于你的程序,如果出现了你预料之外的情况,那就是你作为开发者考虑不周,丢掉这些情况的处理,这就要求,你要尽可能多的制定规则,多熟悉规则,当你完整的做完一个小游戏,你就能体会到什么叫无规矩不成方圆,规则之内你是自由的。
那么你是不是应该能想到,他是不是应该有个限制,这就引出了第二条规则。
2.游戏中,图形运动范围是有限的,且是变化的
我觉得,你立马能想到的是速度,是恒定的。接着是下落是由范围的,接触到底边就不动了。然后顶部会出现新的图形,继续下落,当碰到上一个已经不动的图形后,新图形也就不动了,再在顶部出现一个新图形继续开始下落。
3.游戏结束的触发条件
直至最新的图新出现后立马就碰到了上一个图形,游戏就结束了。
以上观察到的规则都是基于你没有任何操作。
如果这个时候你的小手手那么轻轻的一抖,碰到了按键,恭喜你发现新大陆了。
4.游戏中,图形左右运动范围是也有限的
假设你碰到的是左右移按键,你绝对会发现,下落过程中的图形是可以左右移动的,且两边都是有边界的。碰到之后就不能往这个方向移动了。然后你在看看按键,这才两个,另外的可能是什么效果。
5.游戏中,移动的图形是可以旋转的
假设你碰到的是图形变换按键,下落过程中的图形就会旋转,实际上是每次90°,但部分图形,旋转180°还是原来的样子,有的干脆怎么旋转都是原来的样子,没有什么变化。
6.游戏中,移动的图形是可以加速下落的
假设你碰到的是加速键,你就会敏锐的发现,下落过程中的图形,嗖就下来了。说明速度在这个时候变大了,但是是另一个很定值。当你松开加速按键的时候,速度就恢复到原来的速度了。
7.游戏中,每集满一行,这一行是可以消除掉的,并且上边累积的图形可以向下落,补上这一行
正常情况下我们能观察到的游戏规则就多这么多了。
如果从来没有做过复盘的人,是不是被震惊到了,没想到一个简单的小游戏竟然这么多规则。
所以说没有哪个东西是容易的,都是需要花心思的。
二、游戏中的小功能实现
了解了上述规则,浅玩一下这个小游戏肯定是够了,但是离动手做出来还是有点距离的。
到目前为止,我们已经有独立按键的输入检测和屏幕的输出驱动。
但是屏幕输出驱动提供的接口只提供了像素点的读写,总是感觉哪里不对,是滴,你得感觉没有错。
这篇文章到这里位置,从来没有提过每个图形的组成。
1.画小方块的函数实现
你仔细观察就会发现,每个图形都是由小方块组成的。每个小方块都是由像素点组成的。
聪明的你立马就能想到,我需要一个函数能画小方块。
这个时候就会和显示硬件打交道了,我们这次用的显示屏LCD12864,每个像素点该怎么控制显示。这个屏幕呢是个单色屏,也就是一个像素点只能显示两种状态,亮和灭,似乎有一丝丝熟悉的味道,没错就是led灯。那就是说,我只要告诉显示屏你把我需要的像素点亮和关闭就可以了。那问题来了,这里边你需要的哪个像素点怎么描述,我想行你瞬间就能想到坐标这两个字。这个时候画小方块的函数呼之欲出了,有没有。那还有几个问题,每个小方块该画多大,样式是什么样的,还能让人一眼就看出来每个小方块边界。我们的屏幕规格是长边是128个像素点,短边是64个像素点,所以方块不能太大,那就每个方块占4X4个像素点,为了能一眼就分辨出,再留个边,显示部分3X3,中间空一个像素点。
void Game_SquareDraw(Game_PointTypedef P,uint8_t Value)
{
Value &= 0x01;
Game_PointTypedef p = {P.x << 2,P.y << 2};
LCD_Pixel_Write(p.x + 0,p.y + 0,Value);
LCD_Pixel_Write(p.x + 1,p.y + 0,Value);
LCD_Pixel_Write(p.x + 2,p.y + 0,Value);
LCD_Pixel_Write(p.x + 0,p.y + 1,Value);
//LCD_Pixel_Write(p.x + 1,p.y + 1,0);
LCD_Pixel_Write(p.x + 2,p.y + 1,Value);
LCD_Pixel_Write(p.x + 0,p.y + 2,Value);
LCD_Pixel_Write(p.x + 1,p.y + 2,Value);
LCD_Pixel_Write(p.x + 2,p.y + 2,Value);
}
2.屏幕坐标描述
那你就会说,这多简单啊,坐标嘛,不就是x,y吗,我在数学书上学过这个,这有什么值得说道的。没错没错,这个确实简单,数学中常常是这么用x轴向右,y轴向上。x和y就可以确定平面里的所有坐标了,当然屏幕里也是x和有,只是需要注意的是,大家公认的都是左上角为原点(0,0),向右为x的正方向,向下为y的正方向,包括windows图形界面开发中也是这样。
这样的话抛个描述坐标的结构体不过分吧。
typedef struct
{
int8_t x;
int8_t y;
}Game_PointTypedef;
3.图形的组成描述
小方块可以画了,接下来肯定是画图形了嘛,所以我们需要观察这个游戏,你会发现一共7种图形,每个图形由四个小方块组成。那我们应该把这图形组织起来,那就的来个结构体了。每个小方块形状是固定的,那我们就可以用一个坐标代替一个小方块,这样一个坐标就可以由四个坐标组成。
typedef struct
{
Game_PointTypedef p[4];
}Game_PicTypedef;
4.图形的枚举
好了,图形并不是凭空产生的,这种情况下我们就需要把我们所需要的所有图形枚举出来。C语言关键字const,在单片机的世界里一般认为是保存在flash中,使用的时候也是直接从flash中读取,不可在运行过程中修改。为什么说是一般认为呢?我们大多数情况下单片机的程序是直接从flash运行的,和其他soc,通用计算机(eg:你的windows电脑)不一样。
#define GamePicNum 7ul
const Game_PicTypedef PicEg[GamePicNum] = {
{{{0,0},{1,0},{2,0},{3,0}}},
{{{0,0},{1,0},{0,1},{1,1}}},
{{{1,0},{0,1},{1,1},{2,1}}},
{{{0,0},{1,0},{1,1},{2,1}}},
{{{1,0},{2,0},{0,1},{1,1}}},
{{{0,0},{1,0},{2,0},{2,1}}},
{{{2,0},{0,1},{1,1},{2,1}}},
};
5.图形的显示
现在我们已经有了图形,那就成热打铁,直接给他先显示出来。很好很好,马上就产生了一个问题,我要把这个图形显示在哪里?给这个图形一个base坐标呗,我想改变位置的时候,直接更改这个base坐标就好了。
void Game_DisplyaPic(Game_PicTypedef* Pic,Game_PointTypedef Point)
{
for(uint32_t i = 0;i < 4; i ++)//指定位置显示
{
Game_PointTypedef p;
p.x = Pic->p[i].x + Point.x;
p.y = Pic->p[i].y + Point.y;
Game_SquareDraw(p,1);
}
}
6.图形的清除
很好很好,图形显示出来了,欢呼雀跃。但当你换了一个坐标后发现,为什么之前的图形还在?好嘛你仔细一想,单片机他比较听话,你没让他干的他坚决不会自己主动干,没有主观能动性,所以这个事儿还得你自己动手搞一搞。
void Game_DisplyaPicClean(Game_PicTypedef* Pic,Game_PointTypedef Point)
{
for(uint32_t i = 0;i < 4; i ++)//指定位置显示清除
{
Game_PointTypedef p;
p.x = Pic->p[i].x + Point.x;
p.y = Pic->p[i].y + Point.y;
Game_SquareDraw(p,0);
}
}
7.图形的移动(一)
哇偶哇偶,咱继续加快速度干,图形的显示和清除都有了,那不就是可以移动了嘛。确实是这样子的,咱再回忆一下游戏,就会发现图形每次移动,不管是哪个方向都只有小方块的距离。so,我们就有了图形移动的函数雏形了。
static Game_PointTypedef MovePoint = {0,0};
void Game_Move(Game_PicTypedef* Pic,uint8_t Direction)
{
Direction &= 0x03;//1:left 2:down 3:right
if(Direction == 0)return 0;
Game_DisplyaPicClean(Pic,MovePoint);
switch (Direction)
{
case 1:MovePoint .x -= 1;break;
case 2:MovePoint .y += 1;break;
case 3:MovePoint .x += 1;break;
}
Game_DisplyaPic(Pic,MovePoint);
}
8.图形移动的边沿检测
图形移动函数有了,欢天喜地的编译测试,嗯,似乎没有问题,也按照我们预想的情况可以左右下移动了。等等,可千万不要高兴的太早,但凡可以动的东西都没有简单的,这话可不是毫无根据的,一开始我们就说过无规矩不成方圆、规则之内你是自由的。那这里是不是有什么东西我们没有考虑到,想啊想,也没有写错啊,函数也正确执行了,有啥不对的嘛?规矩、规则,似乎在提示着什么,嗯~~,突然灵光一现,那不就是说,这玩意是不是移动有个极限,到某个情况下就不能移动了。然后翻看游戏规则,显然,第二条规则进入了你得眼睛,游戏是有边界的,图形的移动,总不能超出边界吧。对啊,我的游戏边界呢,好像从来没有定义过。
#define GameWidth 10ul
#define GameHight 32ul
游戏边界有了,那就是需要检测图形移动的时候不能超过边界呗,走起。
uint8_t Game_EdgeCheck(Game_PicTypedef* Pic,Game_PointTypedef *pPoint)
{
for(uint32_t i = 0;i < 4;i ++)
{
if((Pic->p[i].x + pPoint->x) >= GameWidth)return 1;
if((Pic->p[i].y + pPoint->y) >= GameHight)return 1;
if((Pic->p[i].x + pPoint->x) <= 0)return 1;
if((Pic->p[i].y + pPoint->y) <= 0)return 1;
}
return 0;
}
9.图形移动的重叠检测
弄完边沿检测,你谨慎了很多,又仔细看了看规则,就发现上边的边沿检测,还是会部分情况漏掉了,哪种情况呢?就是当前一个图形不能动之后,新图新的检测边沿就不再是一个矩形了,是一个不规则图形,没发用有限描述囊括所有的情况。怎么办?这个时候就需要换个角度看问题了,如果我知道每个点是不是空的。。。。
uint8_t Game_SquareRead(Game_PointTypedef P)
{
Game_PointTypedef p = {P.x << 2,P.y << 2};
return LCD_Pixel_Read(p.x,p.y);
}
好了,这次终于可以愉快的检测了。
uint8_t Game_OverlapCheck(Game_PicTypedef* Pic,Game_PointTypedef *pPoint)
{
for(uint32_t i = 0;i < 4;i ++)
{
Game_PointTypedef Point = {Pic->p[i].x + pPoint->x,Pic->p[i].y + pPoint->y};
if(Game_SquareRead(Point))return 1;
}
return 0;
}
10.图形的移动(二)
经过上述折腾,这个图形移动函数终于达到理想状态了,不会出现预料之外的现象了。
我们的想法就是,先预先移动一下,检查一下看看是不是满足规则,如果不满足,就不移动了,满足规则,那就真的移动一下。
uint8_t Game_Move(Game_PicTypedef* Pic,uint8_t Direction)
{
uint8_t temp = 0;
Direction &= 0x03;//1:left 2:down 3:right
if(Direction == 0)return 0;
Game_PointTypedef Point = {MovePoint.x,MovePoint.y};
switch (Direction)
{
case 1:Point.x -= 1;break;
case 2:Point.y += 1;break;
case 3:Point.x += 1;break;
}
Game_DisplyaPicClean(Pic,MovePoint);
if(Game_EdgeCheck(Pic,&Point))temp = 1;
if(Game_OverlapCheck(Pic,&Point))temp = 1;
if(temp == 0)
{
MovePoint.x = Point.x;
MovePoint.y = Point.y;
}
Game_DisplyaPic(Pic,MovePoint);
return temp;
}
11.图形的旋转
到这里,游戏的一个大功能我们已经实现了,接着肯定的旋转了。
还是个移动是一样的思维,先预先旋转一下,判断一下,如果可以就旋转。
void Game_PicCopy(Game_PicTypedef *dest,Game_PicTypedef *src)
{
for(uint32_t i = 0;i < 4; i ++)
{
dest->p[i].x = src->p[i].x;
dest->p[i].y = src->p[i].y;
}
}
void Game_Spin(Game_PicTypedef* Pic)
{
uint8_t temp = 0;
Game_PicTypedef pic;
Game_PicCopy(&pic,Pic);
for(uint32_t i = 0;i < 4; i ++)
{
Game_PointTypedef p;
p.x = pic.p[i].x;
p.y = pic.p[i].y;
pic.p[i].x = p.y;
pic.p[i].y = (p.x * -1) + 3;
}
Game_DisplyaPicClean(Pic,MovePoint);
if(Game_EdgeCheck(&pic,&MovePoint))temp = 1;
if(Game_OverlapCheck(&pic,&MovePoint))temp = 1;
if(temp == 0)Game_PicCopy(Pic,&pic);
Game_DisplyaPic(Pic,MovePoint);
}
有人要问了,你这个代码都能看懂,唯独哪个变换代码看不懂,怎么就变过来了呢?首先你是知道你即将要旋转图形的每个小方块的坐标,再者,可以通过草稿纸画图的方式,确定旋转后图形的每个小方块坐标,然后观察一下就得到这个坐标关系了。
好了,今天的分享就这么多了。
有哪里不动的可以联系我哦,一起讨论学习。

987

被折叠的 条评论
为什么被折叠?



