首发:裸机思维
作者: FledgingSu 支离苏
今天我们用Arm-2D制作一款时下流行的小游戏flappy bird,由于版权问题,没有用原版素材,它的玩法也很简单,只需要一个按键控制拍翅膀就行。
先上最终游戏成品的视频演示:
(注:现有硬件不包含发声设备,背景音乐为后期添加)
软硬件配置如下:
【游戏简介】
这款floppy bird小游戏使用了Arm-2D图形引擎,使得制作很简单。为了祝愿玩Arm-2D的人越来越多——如星星之火燎原之势,势不可挡——我们游戏的主人公“小火星”就出来了,如下图所示:
- 外围是Arm-2D小火星,中间是一个虫字,也预祝嵌入式小书虫公众号能够更好的写下去,实现破茧成蝶的蜕变。
是的,这个小游戏刚开始是把下面的一个个蜡烛点燃,走着走着就会变成一只蝴蝶,蝴蝶飞过,下面的玫瑰花会开放。背景是一个大满月,也是花好月圆,美好圆满的寓意。
- 有了美好的寓意,那这款游戏的玩法也是很简单,小火星会自动前进,并在前进过程中自动下落,当按下按键后,会控制小火星向上移动,实现从障碍物中间的缝隙中穿过。
【界面设计】
首先设计障碍物,如下图:
从概念设计图可知,已知屏幕的宽度和高度都是240像素,障碍物的宽度为30,间距为140,因此只要确定了上边障碍物的(x,y)坐标和上下障碍物的间隙h就可以在屏幕中绘制障碍物了。
障碍物的位置确定了,可是随着y和h的不同,障碍物也会有长有短,这个怎么弄呢,不会都是图片吧?显然不是的,对障碍物的拆解如下图所示:
看到了吧,障碍物是由两张小图片和绘制一个矩形得到的。
这个是下边的障碍物,那上边的障碍物怎么绘制呢?是要用rotation功能把图片旋转180度吗?
这样做倒是也可以, 不过Arm-2D还提供了更简单的方法。
什么, 还有简单的方法?
是的,就是用“Y轴镜像拷贝”(Y-Mirroring)图片,效果和旋转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 & 0x03) < 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程序举例,如下:
- 定义一个mask Tile,iWidth为200,iHeight 为1
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为透明度渐变
void init_line_mask(){
//! generate line-fading template
do {
memset(s_bmpFadeMask, 0, sizeof(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);
}
- 绘制透明度渐变的圆,这个就是上面我们看到的透明度渐变的满月了。
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[]的值),百叶窗效果的程序如下:
memset(s_bmpFadeMask2, 0, sizeof(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
使用了随机数,其他变量也可以是随机数,但要注意变量的范围。那小火星碰到障碍物怎么检测呢?其实也简单,就是判断两个矩形是否有重合,原理如下图:
程序如下:
bool hit_against_inspect(void)
{
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 true;
}
//判断右下角的y是否在矩形区域
else if((iY+tBird.tSize.iHeight) > ((play_obstacle[i].iHight + play_obstacle[i].iGap))){
return true;
}
}
}
return false;
}
那现在就剩下小火星的上下移动了,相信大家也已经想到了,其实就是 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 & 0x01 ){
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(void)
{
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也可以用链表的形式进行动态添加和删除操作哦!
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。