项目介绍:
本项目来源于小游戏"Dino Game Arduino Edition",将"Dino Game for Arduino"移植到了AVR64DD32 Nano开发板上运行,并适配好DFRobot的0.96寸高清分辨拍照TFT彩屏。
为了方便操作,还增加了巡线传感器来检查手指滑过动作。同时,增加了背景音乐。
"Dino Game for Arduino"是一个运行在Arduino UNO上的小游戏。Google有一个该小游戏的网页版本:Chrome Dino Game Online (dino-chrome.com)。其基本玩法就是在控制屏幕上的小恐龙角色在适当的时候跳动,以免撞上迎面而来的树木,大家可以通过在线版本体验。
设计思路:
原始的Dino Game for Arduino运行于Arduino UNO+ST7735(1.8 Inch TFT Display 128x160),移植到AVR64DD32 Nano开发板运行,需要确保程序正常运行,适配本项目所使用的显示屏。
游戏的基本逻辑,是先绘制小恐龙,然后绘制树木,并在刷新过程中,移动树木,一旦树木撞上小恐龙,则游戏失败。游戏过程中,可以通过控制小恐龙跳跃来躲避树木。
本项目中使用到的显示屏为 DFRobot_0.96,使用SPI接口,可以直接连接到AVR64DD32 Nano开发板的SPI近引脚。因为所使用的显示屏分辨率与原项目不同,故实际使用中,需要调教坐标参数。
原项目包含一个按键操作,在本项目,同样包含一个按键操作,同时增加一个巡线传感器,用于手指滑过检查。
本项目还新增了一个蜂鸣器用于播放背景音乐。受限于AVR64DD32自身的运行频率限制,在无操作无更新的时候,通过蜂鸣器播放背景音乐很流畅,但一旦进行操作时,则其实际播放会收到影响。
驱动蜂鸣器播放音乐通常分为两种方式,一种是输出指定频率的信号并延时,另外一种是在loop过程中,通过millis()检测是否经过了一个音符的播放时间然后播放下一个音符。前者能够完整的播放一个音符,但是程序会在播放时停止。后者播放时不影响主程序的运行,但是会受实际运行程序的影响,实际程序运行处理流程延时小,则播放流畅,否则可能播放不流畅。
本项目中,对蜂鸣器的使用,使用了以上的两种方式,方式一不播放背景音乐,在动作时播放音效;方式二播放背景音乐,都得游戏过程中,声音会有所卡顿。
同时,AVR64DD32还提供了ZCD功能,可以使用中断进行计数,达到一定计数则进行音乐的播放。但同样受限于AVR64DD32自身的运行频率限制,在该方式下,依然会卡顿。在本项目中,也提供了该方式的背景音乐播放。
硬件介绍:
在本项目中,具体使用到的硬件如下:
- AVR64DD32 Nano开发板:【Funpack活动板卡】AVR64DD32 Curiosity Nano开发板 (eetree.cn)
- 显示屏:DFRobot_0.96高清分辨率显示屏
- 无源蜂鸣器:通用版本
- 普通按键:通用版本
- 一路巡线传感器:通用版本
- 导线若干
根据设计,线路原理图如下:
实际硬件如下:
实现功能:
经过反复调教,本项目实现了如下的功能:
- 驱动DFRobot 0.96 高清分辨率 TFT显示屏显示
- 完整的Dino Game游戏逻辑
- 通过按键控制小恐龙的跳跃
- 通过巡线传感器检测手指滑过,控制小恐龙的跳跃
- 可通过代码配置,是播放动作音效,还是播放背景音乐
- 尝试了ZCD控制背景音乐,可在代码中配置尝试,虽然效果不太好。
代码说明:
主代码如下:
#include <SPI.h> // SPI库
#include <SoftwareSerial.h> // 软串口库
#include <Adafruit_GFX.h> // 核心图形库Core graphics library
#include <Adafruit_ST7735.h> // ST7735库
#include <Adafruit_ST7789.h> // ST7789库
// 音乐播放类型
// #define PLAY_TYPE 0 // 仅使用蜂鸣器播放动作音效
#define PLAY_TYPE 1 // 在loop()中使用蜂鸣器播放背景音乐
// #define PLAY_TYPE 2 // 使用ZCD频率计时播放背景音乐
#if PLAY_TYPE == 1
#include <TonePlayer.h>
#include "song.h"
#elif PLAY_TYPE == 2
#include <ZCD.h>
#include <TonePlayer.h>
#include "song.h"
#endif
// 角色定义
#include "sprite.c"
// SPI控制功能引脚定义
#define TFT_CS PIN_PF2
#define TFT_RST PIN_PF3 // Or set to -1 and connect to Arduino RESET pin
#define TFT_DC PIN_PF4
// 按键输入端口定义
#define PIN_BUTTON PIN_PC2
#define PIN_TRACK PIN_PD7
// 蜂鸣器输出端口定义
#define PIN_BUZZER PIN_PD3
// 游戏TICKS定义
const int TICKS_PER_SECOND = 50;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 40;
const int s_w = 320;
const int s_h = 172;
// 基础坐标定义
const int x_base = 0;
const int y_base = 34;
// 角色躯体大小
const int trunk_w = 29;
const int trunk_l = 25;
// 角色腿大小
const int leg_w = 29;
const int leg_l = 6;
// 角色位置
int dino_y = 97 + 20;
int dino_x = 35;
int y;
int y_old;
// 树的位置
int tree_pos_x = s_w - 20;
int tree_pos_y = 0 + 20;
// 树的类型
int type = 2;
int type_old;
// 颜色变量
int color;
// 云的位置和大小
float cloud_pos;
int cloud_height;
int cloud_type;
float cloud_pos1;
int cloud_height1;
int cloud_type1;
// 按键处理变量
double diff;
float u;
// 按键状态
bool button_press = false;
// 游戏状态
bool game_running = false;
bool game_end = false;
// 主菜单
bool main_menu = true;
// 游戏变量
unsigned long next_game_tick;
unsigned long game_F;
unsigned long t1;
unsigned long t2;
unsigned long t3;
unsigned long score;
unsigned long score_prev;
unsigned int t = 0;
unsigned int loops;
float interpolation; // 插值
// 显示屏定义
// Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
// 软串口->USB串口
SoftwareSerial SerialX(PIN_PD5, PIN_PD4); // RX, TX
#if PLAY_TYPE == 1 || PLAY_TYPE == 2
// 实例化播放对象
TonePlayer top(PIN_BUZZER);
#endif
#if PLAY_TYPE == 2
// ZCD频率计次变量
uint32_t counter = 0;
#endif
// 重启调用
void (*resetFunc)(void) = 0; //declare reset function @ address 0
// 按键检测
void button() {
if (game_end == true) {
delay(3000);
tft.fillScreen(ST77XX_WHITE); // 白色背景
SerialX.println("resetting");
resetFunc(); //call reset
} else if (game_running == true) {
t2 = millis();
u = 90.0;
button_press = true;
#if PLAY_TYPE == 0
tone(PIN_BUZZER, 523, 30);
#endif
SerialX.println("Pressed");
} else {
randomSeed(millis());
main_menu = false;
game_running = true;
t = 0;
tft.fillScreen(ST77XX_WHITE);
score = millis();
}
}
// 游戏结束
void game_over() {
game_running = false;
game_end = true;
tft.setCursor(100, y_base+50);
tft.invertDisplay(true);
tft.setTextColor(ST77XX_RED);
tft.invertDisplay(false);
tft.setTextSize(2);
tft.invertDisplay(true);
tft.print("Game Over");
tft.setCursor(100, y_base+80);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(2);
tft.print("SCORE:");
tft.setCursor(180, y_base+80);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(2);
tft.print(score_prev);
tft.invertDisplay(false);
#if PLAY_TYPE == 0
tone(PIN_BUZZER, 523, 60);
#endif
tft.invertDisplay(true);
#if PLAY_TYPE == 0
tone(PIN_BUZZER, 230, 30);
#endif
tft.invertDisplay(false);
#if PLAY_TYPE == 0
tone(PIN_BUZZER, 523, 60);
#endif
tft.invertDisplay(true);
#if PLAY_TYPE == 0
tone(PIN_BUZZER, 230, 30);
#endif
tft.invertDisplay(false);
//add some music + Game over classic display text
}
// 游戏更新
void update_game() {
t1 = millis();
if (button_press) {
diff = (abs(t1 - t2)) / 1000.00;
y = u * diff - 35 * diff * diff;
SerialX.println(y, 8);
if (y <= 0 && diff > 1) { //detect touch down
u = 0;
y = 0;
t2 = 0;
button_press = false;
SerialX.println("Down");
}
}
if (t3 < t1) {
t3 = t1 + 3000;
type = random(1, 3);
if (type_old != type) {
if (type_old == 1) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 99, 4, 29, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113, 20, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 113 - 10, 4, 12, 1.5, ST77XX_WHITE);
} else if (type_old == 2) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 108, 4, 20, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 8, tree_pos_y + 115, 18, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 9, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 25, tree_pos_y + 108, 4, 20, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 8 + 25, tree_pos_y + 115, 18, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 9 + 25, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
//tft.fillRoundRect(tree_pos_x+8+25,tree_pos_y+115-8,4,10,1.5,ST77XX_WHITE);
}
type_old = type;
tree_pos_x = s_w - random(100);
}
}
if (type == 1 && ((dino_y - y + trunk_l + leg_l) > (tree_pos_y + 128 - 29)) && (dino_x > (tree_pos_x - 10)) && (dino_x < (tree_pos_x + 8))) {
game_over();
} else if (type == 2 && ((dino_y - y + trunk_l + leg_l) > (tree_pos_y + 128 - 20)) && (dino_x > (tree_pos_x - 9)) && (dino_x < (tree_pos_x + 35))) {
game_over();
}
}
// 显示游戏
void display_game() {
//check if the dino pose changed, if yes rewrite the old one with background and write the new one else only do leg animation
//use game_F to switch between leg frames (odd/even)
//检查恐龙姿势是否改变,如果是,用背景重写旧的并写新的,否则只做腿部动画
//使用game_F在腿帧之间切换(奇数/偶数)
if (y_old != y) {
drawBitmap(dino_x, dino_y - y, body, trunk_w, trunk_l, ST77XX_BLACK);
drawBitmap(dino_x, dino_y - y + trunk_l, leg1, leg_w, leg_l, ST77XX_BLACK);
tft.fillRect(dino_x, dino_y - y - 5, trunk_w + 5, trunk_l + 5, ST77XX_WHITE); //clearing extra bits
//drawBitmap(dino_x, dino_y - y, body, 40, 35, ST77XX_WHITE);
tft.fillRect(dino_x, dino_y - y + trunk_l - 5, leg_w + 5, leg_l + 10, ST77XX_WHITE);
//drawBitmap(dino_x, dino_y - y+ 35, leg1, 40, 8, ST77XX_WHITE);
y_old = y;
delay(2);
} else {
drawBitmap(dino_x, dino_y - y, body, trunk_w, trunk_l, ST77XX_BLACK);
SerialX.println(game_F);
if (game_F % 20 == 0) {
//drawBitmap(dino_x, dino_y - y+ 35, leg2, 40, 8, ST77XX_WHITE);
tft.fillRect(dino_x, dino_y - y + trunk_l, leg_w, leg_l, ST77XX_WHITE);
drawBitmap(dino_x, dino_y - y + trunk_l, leg1, leg_w, leg_l, ST77XX_BLACK);
//delay(250);
} else if(game_F % 10 == 0) {
tft.fillRect(dino_x, dino_y - y + trunk_l, leg_w, leg_l, ST77XX_WHITE);
drawBitmap(dino_x, dino_y - y + trunk_l, leg2, leg_w, leg_l, ST77XX_BLACK);
//delay(250);
}
game_F++;
}
if (type == 1) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 99, 4, 29, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113, 20, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 113 - 10, 4, 12, 1.5, ST77XX_WHITE);
} else if (type == 2) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 108, 4, 20, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 8, tree_pos_y + 115, 18, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 9, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x + 25, tree_pos_y + 108, 4, 20, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 8 + 25, tree_pos_y + 115, 18, 4, 1.5, ST77XX_WHITE);
tft.fillRoundRect(tree_pos_x - 9 + 25, tree_pos_y + 115 - 8, 4, 10, 1.5, ST77XX_WHITE);
//tft.fillRoundRect(tree_pos_x+8+25,115-8,4,10,1.5,ST77XX_WHITE);
}
if (tree_pos_x > 0) {
tree_pos_x = tree_pos_x - 5;
color = ST77XX_GREEN;
} else {
color = ST77XX_WHITE; // to avoid stray pixels due to border effects
}
if (type == 1) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 99, 4, 29, 1.5, color);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113, 20, 4, 1.5, color);
tft.fillRoundRect(tree_pos_x - 10, tree_pos_y + 113 - 8, 4, 10, 1.5, color);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 113 - 10, 4, 12, 1.5, color);
} else if (type == 2) {
tft.fillRoundRect(tree_pos_x, tree_pos_y + 108, 4, 20, 1.5, color);
tft.fillRoundRect(tree_pos_x - 8, tree_pos_y + 115, 18, 4, 1.5, color);
tft.fillRoundRect(tree_pos_x - 9, tree_pos_y + 115 - 8, 4, 10, 1.5, color);
tft.fillRoundRect(tree_pos_x + 8, tree_pos_y + 115 - 8, 4, 10, 1.5, color);
tft.fillRoundRect(tree_pos_x + 25, tree_pos_y + 108, 4, 20, 1.5, color);
tft.fillRoundRect(tree_pos_x - 8 + 25, tree_pos_y + 115, 18, 4, 1.5, color);
tft.fillRoundRect(tree_pos_x - 9 + 25, tree_pos_y + 115 - 8, 4, 10, 1.5, color);
//tft.fillRoundRect(tree_pos_x+8+25,tree_pos_y+115-8,4,10,1.5,ST77XX_GREEN);
}
if (cloud_type == 1) {
//drawBitmap(cloud_pos, cloud_height, cloud_big, 18, 8, ST77XX_WHITE);
tft.fillRoundRect(cloud_pos, cloud_height, 18, 8, 1.5, ST77XX_YELLOW);
} else if (cloud_type == 2) {
//drawBitmap(cloud_pos, cloud_height, cloud_small, 11, 8, ST77XX_WHITE);
tft.fillRoundRect(cloud_pos, cloud_height, 11, 8, 1.5, ST77XX_YELLOW);
}
if (cloud_pos > 0) {
cloud_pos = cloud_pos - 0.5;
} else {
if (cloud_type == 1) {
tft.fillRoundRect(cloud_pos, cloud_height, 18, 8, 1.5, ST77XX_YELLOW);
} else if (cloud_type == 2) {
tft.fillRoundRect(cloud_pos, cloud_height, 11, 8, 1.5, ST77XX_YELLOW);
}
cloud_pos = random(s_w-100, s_w);
cloud_type = random(1, 3);
cloud_height = random(y_base+20, y_base+30);
}
if (cloud_type == 1) {
tft.fillRoundRect(cloud_pos, cloud_height, 18, 8, 1.5, ST77XX_ORANGE);
//drawBitmap(cloud_pos, cloud_height, cloud_big, 18, 8, tft.color565( 0x00, 0x00, 0x00));
} else if (cloud_type == 2) {
tft.fillRoundRect(cloud_pos, cloud_height, 11, 8, 1.5, ST77XX_ORANGE);
//drawBitmap(cloud_pos, cloud_height, cloud_small, 11, 8, tft.color565( 0x00, 0x00, 0x00));
}
if (cloud_type1 == 1) {
//drawBitmap(cloud_pos, cloud_height, cloud_big, 18, 8, ST77XX_WHITE);
tft.fillRoundRect(cloud_pos1, cloud_height1, 18, 8, 1.5, ST77XX_WHITE);
} else if (cloud_type1 == 2) {
//drawBitmap(cloud_pos, cloud_height, cloud_small, 11, 8, ST77XX_WHITE);
tft.fillRoundRect(cloud_pos1, cloud_height1, 11, 8, 1.5, ST77XX_WHITE);
}
if (cloud_pos1 > 0) {
cloud_pos1 = cloud_pos1 - 0.5;
} else {
if (cloud_type1 == 1) {
tft.fillRoundRect(cloud_pos1, cloud_height1, 18, 8, 1.5, ST77XX_WHITE);
} else if (cloud_type1 == 2) {
tft.fillRoundRect(cloud_pos1, cloud_height1, 11, 8, 1.5, ST77XX_WHITE);
}
cloud_pos1 = random(s_w-100, s_w);
cloud_type1 = random(1, 3);
cloud_height1 = random(y_base+20, y_base+40);
}
if (cloud_type1 == 1) {
tft.fillRoundRect(cloud_pos1, cloud_height1, 18, 8, 1.5, ST77XX_ORANGE);
//drawBitmap(cloud_pos, cloud_height, cloud_big, 18, 8, tft.color565( 0x00, 0x00, 0x00));
} else if (cloud_type1 == 2) {
tft.fillRoundRect(cloud_pos1, cloud_height1, 11, 8, 1.5, ST77XX_ORANGE);
//drawBitmap(cloud_pos, cloud_height, cloud_small, 11, 8, tft.color565( 0x00, 0x00, 0x00));
}
}
// 显示位图:x, y, 位图, 宽, 高, 颜色
void drawBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, uint16_t color) {
int16_t i, j, byteWidth = (w + 7) / 8;
uint8_t byte;
for (j = 0; j < h; j++) {
for (i = 0; i < w; i++) {
if (i & 7) byte <<= 1;
else byte = pgm_read_byte(bitmap + j * byteWidth + i / 8);
if (byte & 0x80) tft.drawPixel(x + i, y + j, color);
}
}
}
// 设置
void setup() {
randomSeed(millis());
tree_pos_x = s_w - random(100);
next_game_tick = millis();
// 串口初始化
SerialX.begin(9600);
// 按键中断设置
pinMode(PIN_BUTTON, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), button, FALLING);
// 循迹传感器
pinMode(PIN_TRACK, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_TRACK), button, FALLING);
// 显示屏初始化
// tft.initR(INITR_BLACKTAB);
tft.init(240, 320); // Init ST7789 320x240
tft.setRotation(3); // 设置旋转方向
tft.fillScreen(ST77XX_WHITE); // 白色背景
tft.drawRoundRect(1, y_base+1, s_w-2, s_h-2, 25, ST77XX_RED);
// 显示游戏信息
tft.setCursor(25, y_base+40);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(2);
tft.print("Dino Game");
tft.setCursor(20, y_base+60);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(1);
tft.print("Press ENTER to start");
tft.setCursor(50, y_base+75);
tft.setTextColor(ST77XX_BLUE);
tft.setTextSize(1);
// tft.print("By: TextZip");
// 显示角色身体
drawBitmap(10, dino_y, body, trunk_w, trunk_l, ST77XX_BLACK);
//game_over();
//drawBitmap(20, 20,cloud_big,31,16,tft.color565( 0x00, 0x00, 0x00));
//drawBitmap(20, 40,cloud_small,21,16,tft.color565( 0x00, 0x00, 0x00));
#if PLAY_TYPE == 1 || PLAY_TYPE == 2
top.setSong(SONG, sizeof(SONG), BPM);
top.play();
#endif
#if PLAY_TYPE == 2
// 禁止ZCD输出
zcd.output = out::disable;
// 初始化ZCD
zcd.init();
// 设置中断回调s (支持 RISING, FALLING , CHANGE)
zcd.attachInterrupt(interruptFunction, RISING);
// 启动ZCD
zcd.start();
#endif
}
void loop() {
#if PLAY_TYPE == 1
top.loop();
#endif
// 检查当前状态
if (main_menu) {
// 显示角色腿部动作
if(t==0) {
drawBitmap(10, dino_y + 25, leg1, leg_w, leg_l, ST77XX_BLACK);
}
// delay(250);
if(t==5) {
drawBitmap(10, dino_y + 25, leg1, leg_w, leg_l, ST77XX_WHITE);
drawBitmap(10, dino_y + 25, leg2, leg_w, leg_l, ST77XX_BLACK);
}
// delay(250);
if(t==10) {
drawBitmap(10, dino_y + 25, leg2, leg_w, leg_l, ST77XX_WHITE);
}
t++;
if(t>10) {
t = 0;
}
delay(50);
}
while (game_running) {
// 游戏运行中状态
// tft.fillRect(80, y_base+2, 80, 15, ST77XX_BLUE);
if(t == 0) {
score_prev = 0;
tft.drawRoundRect(20, y_base, s_w-40, 20, 25, ST77XX_BLUE);
tft.setCursor(50, y_base+2);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(2);
tft.print("SCORE: ");
}
if(t%100==0) {
for(int i=0;i<s_w;i+=10) {
if(t%200==0) {
if(i%20==0) {
tft.drawPixel(x_base + i , dino_y + 35, ST77XX_WHITE);
tft.drawPixel(x_base + i , dino_y + 40, ST77XX_BLACK);
} else {
tft.drawPixel(x_base + i , dino_y + 40, ST77XX_WHITE);
tft.drawPixel(x_base + i , dino_y + 35, ST77XX_BLACK);
}
} else {
if(i%20==0) {
tft.drawPixel(x_base + i , dino_y + 40, ST77XX_WHITE);
tft.drawPixel(x_base + i , dino_y + 35, ST77XX_BLACK);
} else {
tft.drawPixel(x_base + i , dino_y + 35, ST77XX_WHITE);
tft.drawPixel(x_base + i , dino_y + 40, ST77XX_BLACK);
}
}
}
}
t++;
if((millis() - score) / 1000 > score_prev) {
score_prev = (millis() - score) / 1000;
tft.fillRect(130, y_base+2, 64, 16, ST77XX_GREEN);
tft.setCursor(135, y_base+2);
tft.setTextColor(ST77XX_RED);
tft.setTextSize(2);
tft.print((millis() - score) / 1000);
}
//update_game();
//display_game();
loops = 0;
while (millis() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();
#if PLAY_TYPE == 1
top.loop();
#endif
next_game_tick += SKIP_TICKS;
loops++;
}
display_game();
#if PLAY_TYPE == 1
top.loop();
#endif
}
}
#if PLAY_TYPE == 2
// 频率计时中断
void interruptFunction() {
counter++;
if(counter==1000) {
counter = 0;
top.loop();
}
}
#endif
在上述代码中,使用到了Arduino自身的库以及第三方的库,分别如下:
- SPI:SPI.h,驱动显示屏用
- 软串口:SoftwareSerial.h,板载调试器对应的串口为RX-PD5,TX-PD4,通过软串口,可以直接将信息输出到USB连接电脑后的串口,从而在电脑上查看输出。
- 显示驱动核心:使用了Adafruit_GFX,非常强大的显示驱动库
- 显示驱动库:Adafruit_ST7735、Adafruit_ST7789
- 背景音乐播放:TonePlayer.h,实现模拟背景音乐播放,song.h为背景音乐定义
- ZCD库:zcd.h,关于ZCD的具体使用,可以查看 microchip-pic-avr-examples/avr64dd32-getting-started-with-zcd-mplabx: Code examples for the ZCD peripheral. (github.com)
主要函数说明如下:
- *resetFunc():复位设备(重启)
- button():按键处理,使用中断调用,提高处理效率
- game_over():游戏结束处理
- update_game():游戏画面更新处理
- display_game():游戏画面主要显示逻辑处理
- drawBitmap():位图绘制
- setup():设置和初始化
- loop():主循环
- interruptFunction():ZCD中断调用
游戏的主界面如下:
- 在代码中,通过按键或者巡线传感器的中断,来触发小恐龙的跳跃操作。
- 在刷新过程中,树木会随机在右边出现,然后持续向左移动,直到到达最左边消失。
- 根据游戏进行的时长,换算为得分,在顶部进行显示。
- 画面中的草地,会定时进行更换。
- 顶部会显示两种不同类型的云,且会持续向右运动,直到消失。
一些要点:
1. 显示屏适配:
在实际适配本项目所使用的显示屏时,发现ST7735的驱动不好使,最后使用到了ST7789的驱动,但实际显示区域大于屏幕显示区域,所以做了一定的定位处理,使得显示窗口刚好匹配上这个显示屏。实际上,ST7735和ST7789主要是分辨率不同。
2. 背景音乐的处理:
前面说过,受限于AVR64DD32自身的运行频率限制,既要刷SPI屏,又要进行背景音乐播放,不可避免可能会导致背景音乐播放卡顿。如果屏幕绘制信息不多,情况尚好,内容多的时候,则明显受到影响。
实际处理中,应尽可能的减少不需要的画面更新。如源代码中,积分实在每次循环中,都进行显示,优化后,只在发生变化的时候才进行显示。
3. 角色运动效果的处理:
在游戏界面中,小恐龙的身体部分一般不变,但两只腿在不停交替运动,一变呈现运动的效果。这通过交替绘制不同的位图来实现。关于角色相关的位图信息,在sprite.c文件中有详细的定义。在按键后,小恐龙跳跃时,受限于刷新速度的影响,小恐龙本身显示会有闪烁。
4. 按键中断处理
代码中使用了按键中断进行处理,这样可以提高处理的效率。
考虑过使用声音传感器,当大声说跳的时候,控制小恐龙进行跳跃。但手头的声音传感器只有模拟输出,需要通过ADC读取数据,这会影响到实际代码的运行效率,所以最终没有实际采用。
5. ZCD的基本使用。
如果使用Arduino IDE进行开发,在示例代码中,有关于ZCD使用的基本例子。
一个基本的使用例子如下:
#include <ZCD.h>
int counter = 0;
void setup() {
// 禁止ZCD输出
zcd.output = out::disable;
// 初始化ZCD
zcd.init();
// 设置中断回调s (支持 RISING, FALLING , CHANGE)
zcd.attachInterrupt(interruptFunction, RISING);
// 启动ZCD
zcd.start();
}
void loop() {
if (counter>1000) {
cli(); // 禁止中断
// 输出信息,或者其他逻辑处理代码
counter = 0; // 归零
sei(); // 使用中断
delay(1);
}
}
// 频率计时中断
void interruptFunction() {
counter++;
}
在使用ZCD播放背景音乐的过程中,发现会导致使用PC2按键失效,进过研究ZCD的支持代码,发现了下面的代码:
PORTC.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc;
而我的按键的线是一个二线转三线的接口,刚好用到PC2以及旁边的GND。
所以又增加了一个巡线传感器,用于辅助按键处理,只有手指滑过,就能触发,效果也很不错。
参考资料:
在实际完成过程中,参考了不少资料,对各位作者个提供者表示感谢。
以下为部分参考资料列表:
- Dino-Game-Arduino-Edition/dino_FPS at main · TextZip/Dino-Game-Arduino-Edition (github.com)
- Dino Game for Arduino - Hackster.io
- [ESP32学习笔记] 一、点亮TFT屏幕 - 知乎 (zhihu.com)
- ZulNs/TonePlayer: Arduino library for playing melody in background process using the built-in tone() function (github.com)
- DFRobot_0.96
- Chrome Dino Game Online (dino-chrome.com)