前边忘了说了,官方视频在此:https://occ.t-head.cn/community/course/detail?id=3988680041243414528
这是学习资料,看完整的比较好。现在生活好紧张,没有什么时间:上学的时候连俄国作家的小说都读的进去,现在看个电影还想找个up主精编版的先速读一下。
别说看电影的了,那个拍电影的也会先搞个分镜头剧本试拍一下。先打样再量产没毛病。
所以,我就打了个“样”,或者说,做了个原型出来,验证一下设想是否靠谱。
这次做的应用,有个很重要的特点就是,基于处理器的性能实现了动画。在原型中可以看出小鸟扇动翅膀、水管和地面的移动,都是动画。
动画的原理很简单,就是把连续的动作分成一帧一帧的图画,在1/10秒内连续播放,由于人眼视觉暂留作用,会自动将静态的独立图片转成动态的连续视频。
上次说的小鸟翅膀,用的就是3张翅膀图,用0-1-2-1-0-1-2-1的顺序连续播放产生的,相关代码在game_FlappyBird.c里面:
//小鸟身体
const static uint8_t _game_FlappyBird_bird_body_data[] = {
2,2,2,2,2,2,1,1,1,1,1,1,2,2,2,2,2 ,
2,2,2,2,1,1,0,0,0,1,0,0,1,2,2,2,2 ,
2,2,2,1,0,0,0,0,1,0,0,0,0,1,2,2,2 ,
2,2,2,2,2,2,2,0,1,0,0,0,1,0,1,2,2 ,
2,2,2,2,2,2,2,0,1,0,0,0,1,0,1,2,2 ,
2,2,2,2,2,2,2,0,0,1,0,0,0,0,1,2,2 ,
2,2,2,2,2,2,2,0,0,0,1,1,1,1,1,1,2 ,
2,2,2,2,2,2,2,0,0,1,0,0,0,0,0,0,1 ,
2,2,2,2,2,2,2,0,1,0,1,1,1,1,1,1,2 ,
2,2,2,2,2,2,2,0,0,1,0,0,0,0,0,1,2 ,
2,2,2,2,2,2,2,0,0,0,1,1,1,1,1,2,2 ,
2,2,2,2,2,1,1,1,1,1,2,2,2,2,2,2,2
};
bit_map game_FlappyBird_bird_body_img={ 17 , 12 ,(uint8_t*)_game_FlappyBird_bird_body_data} ;
//小鸟翅膀
const static uint8_t _game_FlappyBird_bird_wing_0_data[] = {
2,1,1,1,1,0,0 ,
1,0,0,0,0,1,0 ,
1,0,0,0,0,0,1 ,
1,0,0,0,0,0,1 ,
0,1,0,0,0,1,0 ,
0,0,1,1,1,0,0 ,
0,0,1,0,0,0,0 ,
2,0,0,1,1,0,0
};
bit_map game_FlappyBird_bird_wing_0_img={ 7 ,8 ,(uint8_t*)_game_FlappyBird_bird_wing_0_data} ;
const static uint8_t _game_FlappyBird_bird_wing_1_data[] = {
2,0,1,0,0,0,0 ,
0,1,0,0,0,0,0 ,
0,1,1,1,1,1,0 ,
1,0,0,0,0,0,1 ,
1,0,0,0,0,0,1 ,
0,1,1,1,1,1,0 ,
0,0,1,0,0,0,0 ,
2,0,0,1,1,0,0
};
bit_map game_FlappyBird_bird_wing_1_img={ 7 ,8 ,(uint8_t*)_game_FlappyBird_bird_wing_1_data} ;
const static uint8_t _game_FlappyBird_bird_wing_2_data[] = {
2,0,1,0,0,0,0 ,
0,1,0,0,0,0,0 ,
0,1,0,0,0,0,0 ,
0,1,1,1,1,1,0 ,
1,0,0,0,0,0,1 ,
1,0,0,0,0,1,0 ,
1,0,0,0,1,0,0 ,
2,1,1,1,1,0,0
};
bit_map game_FlappyBird_bird_wing_2_img={ 7 ,8 ,(uint8_t*)_game_FlappyBird_bird_wing_2_data} ;
bit_map* game_FlappyBird_bird_wing_frame_array[]={
(bit_map*)&game_FlappyBird_bird_wing_0_img ,
(bit_map*)&game_FlappyBird_bird_wing_1_img ,
(bit_map*)&game_FlappyBird_bird_wing_2_img ,
(bit_map*)&game_FlappyBird_bird_wing_1_img
};
//画小鸟的代码
static int game_FlappyBird_bird_pos_x = 0 ;
static int game_FlappyBird_bird_pos_y = 0 ;
static int game_FlappyBird_bird_wing_frame_index = 0 ;
void game_FlappyBird_set_position_flying_bird_function(int x0,int y0){
game_FlappyBird_bird_pos_x = x0 ;
game_FlappyBird_bird_pos_y = y0 ;
}
void game_FlappyBird_draw_flying_bird_function(){
//screen_set_auto_refresh_flag_function(SCREEN_FALSE);
screen_draw_bitmap_function(
(bit_map*)&game_FlappyBird_bird_body_img ,
game_FlappyBird_bird_pos_x ,
game_FlappyBird_bird_pos_y
);
game_FlappyBird_bird_wing_frame_index = (game_FlappyBird_bird_wing_frame_index+1)%4;
bit_map* b = game_FlappyBird_bird_wing_frame_array[game_FlappyBird_bird_wing_frame_index];
screen_draw_bitmap_function(
(bit_map*)b ,
game_FlappyBird_bird_pos_x + 0 ,
game_FlappyBird_bird_pos_y + 3
);
#ifdef _APP_game_FlappyBird_DEBUG_
screen_draw_rectangular_function(
BLACK,
HYALINE,
game_FlappyBird_bird_pos_x,
game_FlappyBird_bird_pos_y,
game_FlappyBird_bird_body_img.width,
game_FlappyBird_bird_body_img.height
);
#endif
//screen_set_auto_refresh_flag_function(SCREEN_TRUE);
}
做的时候考虑了以下几点:
1】有三张翅膀的图,但是一个扑翼的周期分4阶段,所以计算game_FlappyBird_bird_wing_frame_index时是加1模4.
2】_APP_game_FlappyBird_DEBUG_ 是用来控制在小鸟边缘画框的,打开DEBUG模式是为了方便观察小鸟与水管撞击的判断是否准确。
下面说水管,开始的时候,准备用水管头(4x8的位图)和多个水管体(1x8的位图)来实现,后来觉得直接画上去会更快,于是水管头和水管体分别用一个矩形轮廓加一条竖向直线的阴影来绘制。随机生成上下水管留出的缺口位置。开始的时候,缺口高度为20,,大家看那个鸟的高度是17,不大容易过。后来把缺口高度加到22,就容易多了。为啥捏?因为每按一次左键,小鸟上升3像素,但是每过100毫秒,小鸟掉下去1像素。水管宽8像素,每100毫秒左移2像素,所以在通过缺口时小鸟会掉下去4像素,但是如果你按早了,它向上一窜,就会撞到上边的水管,所以,20px的缺口纯粹是虐自己……
地面的图形比较复杂,用位图实现,从左边一直画到最右边,也是每100毫秒左移2像素,这样看起来水管是在地面上同步运动的。
除了小鸟、柱子和地面,游戏画面中还有一个元素,就是左上角的分数。得分左边画一个小旗子,然后是得分。这里我没有用取位数后逐位生成字符的方法,而是用snprintf函数的%d做的转换。
细心地童鞋一定发现了,前边有一个问题没有交代:screen_draw_bitmap_function 在哪里定义的?这个问题引出了本项目工程量最大的API——Screen。
在这个项目里,Screen的主要作用是管理每一帧的图像缓存,为了能够支持透明、反色,我将画图、反色输出的支持拿出来,抽出“屏幕”这一个逻辑层。Screen.h的定义如下:
#ifndef _APP_SCREEN_H_
#define _APP_SCREEN_H_
#define SCREEN_TRUE (1)
#define SCREEN_FALSE (0)
#define SCREEN_COLUMN (128)
#define SCREEN_ROW (64)
#include "Image.h"
#include "Font.h"
void screen_init_function(int b_flag_inversen);
void screen_set_dirty_flag_function(int b_flag);
void screen_set_auto_refresh_flag_function(int b_flag);
void screen_refresh_function();
void screen_clean_function();
void screen_draw_bitmap_function(bit_map* img,int x0,int y0);
void screen_draw_string_function(char* str_in,int length,int x0, int y0);
void screen_draw_Int32_function(uint32_t n,int x0, int y0);
void screen_draw_horizontal_line_function(color c,int x0,int x1,int y0);
void screen_draw_vertical_line_function(color c,int x0,int y0,int y1);
void screen_draw_rectangular_function(color edge_color,color content_color,int x0,int y0,int w,int h);
void screen_draw_dot_function(color c,int x0,int y0);
#endif
其中:
screen_init_function(int b_flag_inversen);用于初始化屏幕,并设置屏幕是否为反色输出。就是这个反色输出功能,使我能用习惯的黑白来画图。如果需要,也可以在OLEd和LCD间方便地切换。
screen_set_dirty_flag_function(int b_flag) 和 screen_set_auto_refresh_flag_function(int b_flag) 用于设置是否发生更改和是否自动向设备层刷新。这两个函数是联合使用,我一般先关掉自动刷新,然后绘图的时候,绘图函数自动根据颜色来设置是否有更改。所有的画图都完成后,再打开自动刷新,这时就会根据之前的更改标记来决定是否调用OLED驱动函数进行刷新。
void screen_refresh_function();如名字所示,执行它会进行一次强制刷新。
void screen_clean_function();如名字所示,执行它会清理缓存。
另外的几个功能函数,分别是screen_draw_bitmap_function(画位图)、screen_draw_string_function(调用字库画字符串)、screen_draw_Int32_function(数字转字符串画出)、screen_draw_horizontal_line_function(画水平线)、screen_draw_vertical_line_function(画垂直线)、screen_draw_rectangular_function(用边框色和填充色画矩形)、screen_draw_dot_function(画一个点)。
Screen层的根据dirty_flag决定是否更新,只能解决整张图没有更新的情况下不必重复更新的问题。为了提高刷新速度,还要对只有部分像素变化的情况进行刷新。这个永华工作,在oled.c中实现。
首先增加一级缓存,用于记录上一次更新后的屏幕数据:
uint8_t g_oled_ram_cache[8][128]={0};
然后,修改oled_reflesh(),增加输出前判断是否一致,注意由于硬件特性,连续修改不需要设置列的位置:
void oled_reflesh()
{
unsigned char i, j;
/* 此处注释掉的为原代码 */
/*
for (i = 0; i < 8; i++) {
Set_Start_Page(i);
Set_Start_Column(0x00);
for (j = 0; j < 128; j++) {
Write_Data(g_oled_ram[i][j]);
}
}
*/
for (i = 0; i < 8; i++) {
Set_Start_Page(i);
unsigned char last_col;
last_col = 200 ;
for (j = 0; j < 128; j++) {
if(g_oled_ram_cache[i][j] != g_oled_ram[i][j] ){
if(last_col+1!=j){
Set_Start_Column(j);
}
last_col = j;
Write_Data(g_oled_ram[i][j]);
g_oled_ram_cache[i][j] = g_oled_ram[i][j] ;
}
}
}
}
本项目的周围API基本介绍完了。下一次,我会介绍核心的游戏逻辑控制部分。敬请期待。
本帖最后由 nemon 于 2022-6-5 20:38 编辑