嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 【玩转Arm-2D】五、小游戏制作:不到4K RAM的floppy bird 快到我追不上

【玩转Arm-2D】五、小游戏制作:不到4K RAM的floppy bird 快到我追不上

今天我们用Arm-2D制作一款时下流行的小游戏floppy bird,由于版权问题,没有用原版素材,他的玩法也很简单,只需要一个按键就可以了。

视频演示如下:

软硬件配置如下:

硬件配置:树莓派PCIO单片机
屏幕:240*240 SPI接口的显示屏
图形引擎
Arm-2D
PFB缓存(RAM)
120*33(3960)


【游戏简介】

这款floppy bird小游戏使用了Arm-2D图形引擎,使得制作很简单,也预祝玩Arm-2D的人越来越多,像星星之火可以燎原,燎原之势,势不可挡。


有了这个寓意,我们游戏的主人公小火星就出来了,如下图:

  • 外围是Arm-2D小火星,中间是一个虫字,也预祝嵌入式小书虫公众号能够更好的写下去,实现破茧成蝶的蜕变。

    是的,这个小游戏刚开始是把下面的一个个蜡烛点燃,走着走着就会变成一只蝴蝶,蝴蝶飞过,下面的玫瑰花会开放。背景是一个大满月,也是花好月圆,美好圆满的寓意。

  • 有了美好的寓意,那这款游戏的玩法也是很简单,小火星会自动前进,并在前进过程中自动下落,当按下按键后,会控制小火星向上移动,实现从障碍物中间的缝隙中穿过。


【界面设计】

首先设计障碍物,如下图:

  • 我们只要知道上边障碍物的(x,y)坐标和上下障碍物的间隙h就可以在屏幕中绘制障碍物了,因为屏幕的宽度是已知的240像素。

  • 障碍物的宽度为30,间距为140。


障碍物的位置确定了,可是随着y和h的不同,障碍物也会有长有短,这个怎么弄呢,不会都是图片吧,

显然不是的,对障碍物的拆解如下图所示:

  • 看到了吧,障碍物是由两张小图片和绘制一个矩形得到的。


这个是下边的障碍物,那上边的障碍物怎么绘制呢?

是要用rotation功能把图片旋转180度吗,

这样做倒是也可以, 不过Arm-2D还提供了更简单的方法。

什么, 还有简单的方法?

是的,就是用Y镜像拷贝图片,效果和旋转180度是一样的,如下:

arm_2d_rgb16_tile_copy_with_colour_masking( &c_tileP,  ptTile, &tBox, GLCD_COLOR_BLACK, ARM_2D_CP_MODE_Y_MIRROR);
  • 最后一个参数ARM_2D_CP_MODE_Y_MIRROR就是Y镜像拷贝。

  • 同样的,Arm-2D还提供了X镜像拷贝和XY镜像拷贝。


接着就是绘制小火星了,这个很简单,就是一个图片拷贝,如下

arm_2d_rgb16_tile_copy_with_colour_masking( &c_tileStar, ptTile, &tBox, GLCD_COLOR_BLACK, ARM_2D_CP_MODE_COPY);


那那只蝴蝶煽动翅膀是怎么实现的呢?

其实这个也简单,就是两张图片交替显示就可以了,如下:

static uint8_t num = 0;if((num % 4) < 2){         arm_2d_rgb16_tile_copy_with_colour_masking( &c_tileButterfly, ptTile, &tBox, GLCD_COLOR_BLACK,        ARM_2D_CP_MODE_COPY);}else{    arm_2d_rgb16_tile_copy_with_colour_masking( &c_tileButterfly2,  ptTile, &tBox, GLCD_COLOR_BLACK, ARM_2D_CP_MODE_COPY);}if(bIsNewFrame){    num++;}


背景的制作我们要着重讲一下,因为他使用了一个Arm-2D的高级功能,horizontal-line-mask 来实现一个渐变效果。

如下图

  • 是不是很酷,其实用Arm-2D实现也很简单,只需要给 target 这一端提供一个 mask就行了。

  • 需要注意的是,target的这个mask 它的 height 和 width 必须能完整覆盖 target(或者height 为 1时,width 能覆盖整个 target的宽度)

下面我以height 为 1时的target mask程序举例,如下:

ARM_NOINIT static uint8_t s_bmpFadeMask[200];const arm_2d_tile_t c_tileFadeMask = { .tRegion = { .tSize = { .iWidth = 200, .iHeight = 1, }, }, .tInfo = { .bIsRoot = true, .bHasEnforcedColour = true, .tColourInfo = { .chScheme = ARM_2D_COLOUR_8BIT, }, },    .pchBuffer = (uint8_t *)s_bmpFadeMask,};
  • 定义一个mask Tile,iWidth为200,iHeight 为1


void init_line_mask(){ //! generate line-fading template    do {        memset(s_bmpFadeMask, 0sizeof(s_bmpFadeMask)); float fRatio = 255.0f / 200.f; for (int32_t n = 0; n < 200; n++) {            //s_bmpFadeMask[n] = 255 - ((float)n * fRatio);            s_bmpFadeMask[n] = ((float)n * fRatio);             } } while(0);}
  • 初始化mask为透明度渐变


void test_line_mask(const arm_2d_tile_t *ptFrameBuffer){ arm_2d_region_t tRegion= { .tLocation = {30,10}, .tSize = {200, 200}, };        arm_2d_tile_t tTempPanel; //!< generate a child tile for this screen arm_2d_tile_generate_child( ptFrameBuffer, &tRegion, &tTempPanel, false);     //!< set background colour arm_2d_rgb16_fill_colour( &tTempPanel, NULL, GLCD_COLOR_NAVY);     //绘制透明度渐变的圆      tRegion.tLocation.iX = 0;    tRegion.tLocation.iY = 0; arm_2d_rgb565_tile_copy_with_masks( &c_tilecircle, &c_tilecircleMask, &tTempPanel,          &c_tileFadeMask,          &tRegion,             ARM_2D_CP_MODE_COPY);              }
  • 绘制透明度渐变的圆,这个就是上面我们看到的透明度渐变的圆了。



有了这个target mask,我们很容易就做出百叶窗效果,啥都不用改,只需要改mask(即s_bmpFadeMask[200]的值),百叶窗效果的程序如下:

memset(s_bmpFadeMask2, 0sizeof(s_bmpFadeMask)); for (int32_t n = 0; n < 200; n++) {             if(n%4 < 2){        s_bmpFadeMask[n] = 255; }else{        s_bmpFadeMask[n] = 0; }}

实现效果如下图:


简单吧,其实有了这个利器,我们还可以做出各种千奇百怪的擦除效果,比如渐渐出现,渐渐消失的效果,只要动态修改mask,图还是那个图,但是mask一直在变,就可以做出一些非常绚丽的效果。


【小游戏之数据结构】

下面我们讲一下障碍物的数据结构,如下:

typedef struct{ uint32_t iX; uint8_t iHight; uint8_t iGap; uint8_t char_arm_2d;    uint8_t game_level;}play_obstacle_t;
  • 前3个是用来确定障碍物的位置坐标,char_arm_2d是用来保存要显示的字符,如下图

  • char_arm_2d = 'A',就显示字符A,会循环显示Arm-2D。

  • game_level是用来区分第几关,(即第一关为小火星,第二关为蝴蝶)。

由于我们的屏幕只能显示两个障碍物,所以只需要 定义一个长度为2 的数组,如下

play_obstacle_t play_obstacle[2] = { {30,40,80,'A',.game_level=1}, {170,60,100,'r',.game_level=1},};



接下来就是游戏中物体的移动,需要一个坐标,结构也很简单,如下并定义了一个静态全局变量

typedef struct location_t { uint32_t iX; int32_t iY;} location_t;
static location_t tplay_X_Y = {.iX = 0, .iY = 0};


【程序实现】

有了上面的数据类型,我们就来看看他是怎么前进的。

其实前进很简单,因为我们的小火星是横向向右移动,所以只需要把横向坐标累加就可以了,如下

if((!stop_flag) && play_flag){ if(bIsNewFrame){        //tplay_X_Y.iX++; tplay_X_Y.iX += MOVING_SPEED_iX;    }   }
  • MOVING_SPEED_iX为移动速度,可以自己设定。

  • play_flag为游戏开始标志,初始化为False,当按下按键置为True,游戏开始。

  • stop_flag为小火星撞到障碍物置为True,小火星停止前进。


tplay_X_Y.iX增加之后,小火星是怎么移动的呢?其实小火星向右移动就相当于障碍物向左移动(即减去tplay_X_Y.iX的值),如下以绘制红色矩形为例:

||绘制红色矩形tBox.tLocation.iX = play_obstacle[i].iX - tplay_X_Y.iX;tBox.tLocation.iY = 10;tBox.tSize.iWidth = 30;tBox.tSize.iHeight= play_obstacle[i].iHight-27; arm_2d_rgb16_fill_colour(ptTile, &tBox, GLCD_COLOR_RED);
  • 提示:我们也可以根据tplay_X_Y.iX的值在小火星走过一定的距离后,在背景出现一个动画效果,也可以在走过一段距离后速度提升增加难度。


那障碍物移出屏幕之外怎么办呢?

移出屏幕就给他重新赋值新的play_obstacle[i].iX,准备下一次出现,如下图所示:

  • 当判断障碍物的iX < -30时,就可以修改play_obstacle[i].iX的值,然后重新出现,这样就可以一直循环下去,如下:

tBox.tLocation.iX = play_obstacle[i].iX - tplay_X_Y.iX; ...if(tBox.tLocation.iX < -30){ //新的障碍 new_obstacle(i);}
void new_obstacle(uint8_t i){ static uint8_t char_Arm2D_num; play_obstacle[i].iX= play_obstacle[i].iX + 140*2; play_obstacle[i].iGap = 80 + myrand()%21;//80~100 play_obstacle[i].game_level = play_game_mode; play_obstacle[i].char_arm_2d = char_Arm2D[char_Arm2D_num%6]; char_Arm2D_num++;}
  • play_obstacle[i].iGap使用了随机数,其他变量也可以是随机数,但要注意变量的范围


那小火星碰到障碍物怎么检测呢?

其实也简单,就是判断两个矩形是否有重合,原理如下图:

程序如下:

uint8_t hit_against_inspect(){ int32_t iY,iX ; for(uint8_t i= 0; i < 2; i++){ iX = play_obstacle[i].iX - tplay_X_Y.iX; ||先判断X是否在矩形区域 if((iX < (tBird.tLocation.iX+tBird.tSize.iWidth)) && (iX+30) > (tBird.tLocation.iX+tBird.tSize.iWidth)){ iY = tBird.tLocation.iY - tplay_X_Y.iY;            ||判断右上角的y是否在矩形区域 if(iY < play_obstacle[i].iHight){ return 1; } ||判断右下角的y是否在矩形区域 else if((iY+tBird.tSize.iHeight) > ((play_obstacle[i].iHight + play_obstacle[i].iGap))){ return 1; } }    }     return 0;}


那现在就剩下小火星的上下移动了,相信大家也已经想到了,其实就是 tplay_X_Y.iY增加或减小。有按键按下就向上移动,没有就自动向下移动。程序如下:

 void fly(uint8_t key){ static enum{ FLY_INIT = 0, FLY_DOWN, FLY_UP, }e_fly_mod = FLY_INIT; static uint8_t number = 0; switch(e_fly_mod){ case FLY_INIT: number = 0; e_fly_mod = FLY_DOWN; //break; case FLY_DOWN: number++; if((number % 5) == 0){ tplay_X_Y.iY -= 3 * MOVING_SPEED_iX; if(tplay_X_Y.iY < -150){ tplay_X_Y.iY = -150; } }else if((number % 5) == 2){ tplay_X_Y.iY += MOVING_SPEED_iX; } if(key){ e_fly_mod = FLY_UP; number = 10; } break; case FLY_UP: if(number % 2){ tplay_X_Y.iY += 4 * MOVING_SPEED_iX; if(tplay_X_Y.iY > 50){ tplay_X_Y.iY = 50; } }else{ tplay_X_Y.iY -= MOVING_SPEED_iX; } number--; if(number == 0){ e_fly_mod = FLY_INIT; } break; }}


最后就是我们的play_game()函数,如下:

void play_game(){       uint8_t key = 0;   ||获取按键值 key = get_key(); if(play_flag){ fly(key); ||检测是否撞到障碍物 stop_flag = hit_against_inspect(); if(stop_flag == 1){ ||撞到障碍物,游戏停止 play_flag = 0; }    }else        ||有按键按下,游戏开始        if(key){ play_flag = 1; } }}
  • 还是很简单的,是吧。按键驱动函数我就不贴了。(* ̄︶ ̄)

  • 其中还有一些不足之处希望大家能够完善。

  • 期待大家实现自己的小游戏并添加更多好玩的功能。

  • 下一篇继续讲小游戏制作之景深。


补充                                                 


【再谈小游戏之速度控制】


上面我们讲了控制速度用了一个变量MOVING_SPEED_iX,设置不同的值移动速度就会不同,这样也可以实现速度的控制了。

不过

Arm-2D还有一种特有的提速方法,

那就是dirty List脏矩阵(即局部刷新),这也是我们可以使用小PFB的奥秘所在。

dirty List我们前面提到过,不知道大家还有印象没,他的使用就是几个宏,如下:


<1>  IMPL_ARM_2D_REGION_LIST    

#define __IMPL_ARM_2D_REGION_LIST(__NAME, ...) \ enum { \ __NAME##_offset = __COUNTER__, \ }; \ __VA_ARGS__ \ arm_2d_region_list_item_t __NAME[] = { #define IMPL_ARM_2D_REGION_LIST(__NAME, ...) \ __IMPL_ARM_2D_REGION_LIST(__NAME,##__VA_ARGS__)
  • IMPL_ARM_2D_REGION_LIST就是定义一个arm_2d_region_list_item_t的数组

  • IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions,  static
    宏展开为
    static arm_2d_region_list_item_t s_tDirtyRegions[] = {

<2> ADD_REGION_TO_LIST           

#define __ADD_REGION_TO_LIST(__NAME, ...) \ { \ .ptNext = (arm_2d_region_list_item_t *) \ &(__NAME[__COUNTER__ - __NAME##_offset]), \ .tRegion = { \ __VA_ARGS__ \ }, \ } #define ADD_REGION_TO_LIST(__NAME, ...) \ __ADD_REGION_TO_LIST(__NAME, ##__VA_ARGS__)
  • ADD_REGION_TO_LIST就是初始化数组元素

  • ADD_REGION_TO_LIST(s_tDirtyRegions,  .tLocation = {  .iX = 30,   .iY = 0,  }, ),  宏展开为

    {  .ptNext = (arm_2d_region_list_item_t *)    &(s_tDirtyRegions[__COUNTER__ - s_tDirtyRegions_offset]),  .tRegion = {    .tLocation = {      .iX = 30,       .iY = 0,     },   },},

<3>ADD_LAST_REGION_TO_LIST      

#define __ADD_LAST_REGION_TO_LIST(__NAME, ...) \ { \ .ptNext = NULL, \ .tRegion = { \ __VA_ARGS__ \ }, \ } #define ADD_LAST_REGION_TO_LIST(__NAME, ...) \ __ADD_LAST_REGION_TO_LIST(__NAME, ##__VA_ARGS__)
  • ADD_LAST_REGION_TO_LIST就是初始化最后一个元素

  • ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,  .tLocation = {23,30},  .tSize = {.iWidth = 140,.iHeight = 60,}, ),
    宏展开为
    {    .ptNext = NULL,  .tRegion = {        .tLocation = {23,30},    .tSize = {.iWidth = 140,.iHeight = 60,},   },},


<4> END_IMPL_ARM_2D_REGION_LIST  

#define END_IMPL_ARM_2D_REGION_LIST(...) \ };
  • END_IMPL_ARM_2D_REGION_LIST()就是数组初始化结束


<5>Dirty List脏矩阵                         

IMPL_ARM_2D_REGION_LIST(s_tDirtyRegions, static) ADD_REGION_TO_LIST(s_tDirtyRegions,.tLocation = {.iX = 30, .iY = 0,},), ADD_REGION_TO_LIST(s_tDirtyRegions,.tSize = {.iWidth = 30,.iHeight = 40,}, ), ADD_LAST_REGION_TO_LIST(s_tDirtyRegions,.tLocation = {23,30},.tSize = {.iWidth = 140,.iHeight = 60,},),END_IMPL_ARM_2D_REGION_LIST()
  • 这几个宏就是定义一个arm_2d_region_list_item_t类型的数组

  • arm_2d_region_list_item_t类型如下:

typedef struct arm_2d_region_list_item_t { struct arm_2d_region_list_item_t *ptNext; arm_2d_region_t tRegion;}arm_2d_region_list_item_t;
  • 看到这里我们就清楚了,dirty List就是用数组封装了一个链表。


好,知道了这个,下面才是我们要讲的重点:动态修改dirty List的刷新区域。

dirty List虽然是一个链表,但他也是一个数组,我们就用数组的形式对他进行修改,如下:

for(uint8_t i = 0; i < 2; i++){    s_tDirtyRegions[i_num].tRegion.tLocation.iX = play_obstacle[i].iX - tplay_X_Y.iX;    s_tDirtyRegions[i_num].tRegion.tLocation.iY = 0;    ||刷新区域在屏幕外,宽和高设置为0    if((s_tDirtyRegions[i_num].tRegion.tLocation.iX < -31) || (s_tDirtyRegions[i_num].tRegion.tLocation.iX > 240)){        s_tDirtyRegions[i_num].tRegion.tSize.iWidth = 0;        s_tDirtyRegions[i_num].tRegion.tSize.iHeight= 0;     }else{        s_tDirtyRegions[i_num].tRegion.tSize.iWidth = 30+MOVING_SPEED_iX;        s_tDirtyRegions[i_num].tRegion.tSize.iHeight= 240;   i_num++; } }

这段代码的刷新区域如下图白色框所示:

  • 由于我们移动了MOVING_SPEED_iX的距离,所以刷新宽度要加上。

  • 小火星的刷新区域也是一样的,我就不写了。

  • 对了,dirty List也可以用链表的形式进行动态添加和删除操作哦!

精彩继续(* ̄︶ ̄)


原创不易,如果你喜欢我的公众号、觉得我 文章对你有所启发,

请务必“点赞、收藏、转发”,这对我很重要,谢谢!

欢迎订阅    嵌入式小书虫