【Funpack2-4】移植Dino Game(恐龙游戏)到AVR64DD32 Curiosity Nano开发板
本项目基于Arduino开发框架,移植Dino Game在AVR64DD32 Nano运行
标签
Arduino
Funpack活动
显示
AVR64DD32
game
HonestQiao
更新2023-05-05
568

项目介绍:

本项目来源于小游戏"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自身的运行频率限制,在该方式下,依然会卡顿。在本项目中,也提供了该方式的背景音乐播放。

 

硬件介绍:

在本项目中,具体使用到的硬件如下:

  1. AVR64DD32 Nano开发板:【Funpack活动板卡】AVR64DD32 Curiosity Nano开发板 (eetree.cn)
  2. 显示屏:DFRobot_0.96高清分辨率显示屏
  3. 无源蜂鸣器:通用版本
  4. 普通按键:通用版本
  5. 一路巡线传感器:通用版本
  6. 导线若干

根据设计,线路原理图如下:

FhOUBJYRg60M1mza8YRn4p_3T7OA

实际硬件如下:

FvIdkvJqfoMdabsZDNL5o_-hRzhP

实现功能:

经过反复调教,本项目实现了如下的功能:

  1. 驱动DFRobot 0.96 高清分辨率 TFT显示屏显示
  2. 完整的Dino Game游戏逻辑
  3. 通过按键控制小恐龙的跳跃
  4. 通过巡线传感器检测手指滑过,控制小恐龙的跳跃
  5. 可通过代码配置,是播放动作音效,还是播放背景音乐
  6. 尝试了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中断调用

游戏的主界面如下:

Fvk2LoVwQuV_YrRhd6tvB-RaDe1V

  • 在代码中,通过按键或者巡线传感器的中断,来触发小恐龙的跳跃操作。
  • 在刷新过程中,树木会随机在右边出现,然后持续向左移动,直到到达最左边消失。
  • 根据游戏进行的时长,换算为得分,在顶部进行显示。
  • 画面中的草地,会定时进行更换。
  • 顶部会显示两种不同类型的云,且会持续向右运动,直到消失。

 

一些要点:

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_on_avr64dd32_nano.zip
团队介绍
一个狂热的开源爱好者和传播者,同时也是一名极客爱好者,长期关注嵌入式发展和少儿创客教育,既擅长互联网系统架构设计与研发,又拥有丰富的嵌入式研发经验。为人精力充沛,古道热肠,圈内人称乔大妈、乔帮主。
团队成员
HonestQiao
狂热的开源爱好者和传播者
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号