使用 STM32 制作 Chrome 恐龙小游戏
最初的想法
相信大家都玩过 Chrome 断网后的恐龙小游戏吧? 谷歌隐藏的这个小彩蛋,是为了缓解用户断网后的无聊,刚刚出现时也是小小的风靡全球!这款游戏的玩法非常简单,只需按键盘的方向键,躲避迎面而来的障碍物。
游戏模式的简单易操作,是这款小游戏能够通过 STM32 配合非常小巧的 OLED 屏幕制作出来的要素之一。游戏画面是单色调的黑色,也非常完美的迎合 OLED 单色屏幕。不论是恐龙还是障碍物,形状简单,可通过绘制像素点制作成 BMP 格式,便于存储。这些因素促成了将恐龙游戏编写到STM32上,并通过OLED屏幕显示出来的可能。
在开始动手前,在想法变成现实前,除了要分析可能性外,还要做充足的准备。
编写代码的过程,就是将自己的思想,自己脑海中的整体流程梳理成计算机认识的 0 和 1 ,让计算机重复的按你所思一步步的执行你的所想。所以在开始编码前,一定要确保你的脑海中有了一个坚固的流程。如果混乱不堪,请不要打开电脑。你自己如果都没有一个完整的整体流程,那编写出来的代码一定是无法直视的。所以编码前,请认真缕清你的想法和逻辑。
硬件资源
- 开发板使用的是 野火 指南者,采用 STM32F103VET6 微处理器。
- 按键使用指南者板载的两个按压按键,一个电容按键。
- 使用一个指南者板载的有源蜂鸣器。
- OLED 使用的是中景园 0.96寸 SSD1306 蓝色。
视频展示游戏效果
开发环境
- 使用keil uvision5 开发环境。
- 使用PCtoLCD2002 取字模软件。
- 使用Adobe Photoshop CS6 绘制图片。
游戏画面截图
一些需要解决的问题
0.96寸 OLED 屏幕 分辨率是 128 * 64 像素,屏幕并不是按像素点刷新,而是将 64 的长度分成了 8 个页,每页由 128 个十六进制操作的,十六进制 的八个位控制这当前页的一个列八位。这种方式绘制图片个人感觉比较繁琐,不便于操作。经过指导老师的提示,可以自己实现一个绘点函数,仅通过给定 X,Y 坐标就可以绘点。
- 第一个问题就是要完成通过 X,Y 参数绘制像素点的函数。
// 绘制像素点函数
void OLED_spot(unsigned char x, unsigned char y)
{
unsigned char x_quyu, x_chushu;
unsigned char y_quyu, y_chushu;
unsigned int i;
if (x >= 2 && x <= 128)
x = x - 2;
else if (x == 1)
x = 127;
y = y - 1;
x_quyu = x % 8;
x_chushu = x / 8;
y_quyu = y % 8;
y_chushu = y / 8;
for (i = 0; i < 1024; i++)
{
if ( i == (128 * y_chushu) + x)
{
switch(y_quyu)
{
case 0 : buffer_spot[i] = hex[7]; break;
case 1 : buffer_spot[i] = hex[6]; break;
case 2 : buffer_spot[i] = hex[5]; break;
case 3 : buffer_spot[i] = hex[4]; break;
case 4 : buffer_spot[i] = hex[3]; break;
case 5 : buffer_spot[i] = hex[2]; break;
case 6 : buffer_spot[i] = hex[1]; break;
case 7 : buffer_spot[i] = hex[0]; break;
}
Buffer_Zero[i] = Buffer_Zero[i] | buffer_spot[i];
break;
}
}
}
STM32F103VET6 属于大容量微控制器,即便这样,flash 容量也仅为 512K ,一张位图是1024Bite,也就是1KB,最多能够存储500张左右的位图图片,所以使用图片存储游戏画面是不现实的,且游戏过程中的游戏画面随机性大,不能准确定位图片。这就需要通过计算来获取游戏的画面,计算完成后将图片刷新到屏幕。由于恐龙的运动方向是朝上的,而障碍物的运动方向是从右到左的,在物体运动方向不同并最终要显示在一张图片上的问题困扰了我很长时间。
最终灵感来于早期猫和老鼠动画片的制作方法,早期动画片都是人工手画出来的每一帧图片,当时动画画面非常复杂,需要分别进行绘画,比如先画上猫的动作在一张图片上,然后再绘制老鼠的动作在另一张图片上了,最后绘制动画的背景在第三张图片上,最终输出时,需要将三张图片合成为一张,就可以得到一动画帧的图片了。
问题二如何分别绘制游戏画面
下面是恐龙站立时的绘制代码,找到恐龙的每一个像素点的坐标,通过上面介绍的绘点函数将所有点信息保存在 Buffer_Zero 中。其他恐龙的状态和障碍物等图片都是通过这种方法绘制出来,将点信息保存在 BUFFER 中。
void Dinosaur_01(unsigned char d) // 恐龙站立
{
unsigned char i, j;
memset(Buffer_Zero ,0x00,BUFFER_SIZE);
for (i = 15; i <= 60; i++)
{
for (j = 9; j <= 30; j++)
{
if ((i == 40 - d && j >= 22 &&j <= 29) || (i == 41 - d && j >= 21 &&j <= 30))
OLED_spot(j, i);
if ((i == 42 - d && j >= 21 &&j <= 23 )|| (i == 42 - d && j >= 25 &&j <= 30 ))
OLED_spot(j, i);
if ((i == 43 - d && j >= 21 &&j <= 30) || (i == 44 - d && j >= 21 &&j <= 30) || (i == 45 - d && j >= 21 &&j <= 25))
OLED_spot(j, i);
if ((i == 46 - d && j >= 21 &&j <= 29) || (i == 46 - d && j == 9))
OLED_spot(j, i);
if ((i == 47 - d && j >= 20 &&j <= 24) || (i == 47 - d && j == 9))
OLED_spot(j, i);
if ((i == 48 - d && j >= 18 &&j <= 24) || (i == 48 - d && j >= 9 && j <= 10))
OLED_spot(j, i);
if ((i == 49 - d && j >= 17 &&j <= 27) || (i == 49 - d && j >= 9 && j <= 11))
OLED_spot(j, i);
if ((i == 50 - d && j >= 14 &&j <= 24) || (i == 50 - d && j == 27) || (i == 50 - d && j >= 9 && j <= 12))
OLED_spot(j, i);
if ((i == 51 - d && j >= 10 &&j <= 24) || (i == 52 - d && j >= 11 &&j <= 24))
OLED_spot(j, i);
if ((i == 53 - d && j >= 12 &&j <= 23) || (i == 54 - d && j >= 13 &&j <= 23))
OLED_spot(j, i);
if ((i == 55 - d && j >= 14 &&j <= 22) || (i == 56 - d && j >= 15 &&j <= 21))
OLED_spot(j, i);
//-----------------------------------------
if ((i == 57 - d && j >= 15 &&j <= 17) || (i == 57 - d && j >= 20 && j <= 21))
OLED_spot(j, i);
if ((i == 58 - d && j >= 15 &&j <= 16) || (i == 58 - d && j == 21))
OLED_spot(j, i);
if ((i == 59 - d && j == 15) || (i == 59 - d && j == 21))
OLED_spot(j, i);
if ((i == 60 - d && j >= 15 &&j <= 17) || (i == 60 - d && j >= 21 && j <= 23))
OLED_spot(j, i);
}
}
}
问题三 如何将绘制好的图片合成为最终输出到屏幕的图片。
由于所有游戏图片都是通过十六进制的方法记录像素点信息,可以通过位操作来实现合成。
Buffer_Zero_Two[ui_i] = Buffer_Zero_Two[ui_i] | Buffer_Zero[ui_i];
这样就可以一边计算图片,一边将计算好的图片或运算到最终的 BUFFER 中。
int Dinosaur_h_l(unsigned char c) // 恐龙后腿
{
unsigned int ui_i = 0, ui_j = 0;
unsigned int col_break;
memset(Buffer_Zero_Two,0x00,BUFFER_SIZE);
Dinosaur_03(0);
for (ui_i = 0;ui_i <= 1023; ui_i++)
{
Buffer_Zero_Two[ui_i] = Buffer_Zero_Two[ui_i] | Buffer_Zero[ui_i];
}
switch(sum_sum)
{
case 1 : Cactus_01(c); break;
case 2 : Cactus_02(c); break;
case 3 : Cactus_03(c); break;
case 4 : number_01(c); break;
case 5 : number_02(c); break;
case 6 : number_03(c); break;
case 7 : number_04(c); break;
}
for (ui_i = 0;ui_i <= 1023; ui_i++)
{
Buffer_Zero_Two[ui_i] = Buffer_Zero_Two[ui_i] | Buffer_Zero[ui_i];
}
col_break = Game_collision(3);
if (col_break == -1)
{
return -1;
}
road_01(0);
for (ui_i = 0;ui_i <= 1023; ui_i++)
{
Buffer_Zero_Two[ui_i] = Buffer_Zero_Two[ui_i] | Buffer_Zero[ui_i];
}
for (ui_i = 0;ui_i <= 256; ui_i++)
{
Buffer_Zero_Two[ui_i] = Buffer_Zero_Two[ui_i] | BMP03[ui_i]| Buffer_Zero_score[ui_i];
}
OLED_DrawBMP(0,0,128,8,Buffer_Zero_Two);
}
取模软件可以取汉字或单词,或是绘制简单的图片,但是想要绘制精细的图片就要使用 Photoshop 。
问题四 如何通过 PS 制作位图图片。
PS 教程网上非常多,在这里不做描述
游戏碰撞检测
根据游戏规则,恐龙要躲避障碍物,如果恐龙没有及时躲避障碍物,视为游戏结束。
所以我们要做的就是要实时检测恐龙的所有像素点是否与障碍物的像素点有重叠,这是恐龙游戏制作的关键。
同样,根据图片是由十六进制保存像素点信息,那同样可以通过位运算中的 & 来逐字节的检测是否有两个位重叠。
int Game_collision(unsigned char col) // 游戏碰撞检测
{
unsigned int ui_i = 0;
for (ui_i = 0;ui_i <= 1023; ui_i++)
{
if ((buffer_Zero_col_1[ui_i] & buffer_Zero_col_2[ui_i]) != 0x00)
{
return -1;
}
}
}
不足与存在的问题
- 由于游戏画面是通过大量计算获得的图片,所以必然会影响性能,在游戏进行时,能感觉到明显的卡顿,整体的运行和物体的移动都十分缓慢,需要解决这个办法,需要使用更加好用的算法计算图片,但是代码需要整体推翻重构。等下次有机会再使用更好的算法吧。
- 当前游戏的最高分,应该存储在外部 flash ,即使关机重启,最高分记录不会消失。
- 游戏难易程度没有实现,由于时间关系,这个功能并没有去制作。希望有时间可以后续加上。
- 游戏模式单一。
信息
/*
使用 STM32F103VET6 微控制器
通过 SPI 控制 OLED 屏幕
版本 V1.0 完成时间 :2017年6月3日
实现功能: 游戏基本功能实现
增加三个障碍物
新增大量神奇的 BUG
版本 V2.0 完成时间 :2017年6月11日21:16:36
实现功能: 修复大量神奇的 BUG
增加菜单选项等
美化游戏界面
吴佳轶
w.45@qq.com
www.wujiayi.vip
*/
小萌新这学期刚学stm32开发,很想学习参考一下前辈的这个代码!有没有源码呢?