在Proteus软件仿真STM32F103寄存器玩俄罗斯方块之第三篇

这篇文章主要是代码分享,俄罗斯方块的游戏功能代码部分以及简单解析。
对代码哪里不理解的,欢迎找我讨论哈。
和上篇代码结构不一样的地方是多文件和分模块,使得工程更加有条理。
本意是直接抛代码的,没想到啰里啰唆搞了一大堆。

俄罗斯方块总体设计

为什么第三篇和其他不一样呢?很显然这块是这个小项目的核心嘛,单片机是个专用的计算机系统,在这个项目里就体现在这里了。因为你所有的设计都是围绕着这个目标来的,甚至于可能要推到重来。

一、游戏规则

我们大多数人就算没有玩过,也应该听说过这个游戏,不然你也搜不到这个文章。这里我们以自己观察到的游戏现象,简单的例举几条规则。

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

有人要问了,你这个代码都能看懂,唯独哪个变换代码看不懂,怎么就变过来了呢?首先你是知道你即将要旋转图形的每个小方块的坐标,再者,可以通过草稿纸画图的方式,确定旋转后图形的每个小方块坐标,然后观察一下就得到这个坐标关系了。

好了,今天的分享就这么多了。
有哪里不动的可以联系我哦,一起讨论学习。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值