基于树莓派RP2040_game_kit屏幕驱动方法及Flappy_bird游戏移植
这一期活动主角是“2022寒假在家练”pico_game_kit,重点介绍game_kit在Arduino环境st7789屏幕的驱动问题及利用该屏幕驱动移植了一款经典小游戏,供大家借鉴参考。
标签
游戏
RP2040
2022寒假在家练
pico-game-kit
flappy bird
ST7789驱动
genvex
更新2022-03-02
1363

   这一期活动主角是“2022寒假在家练”pico_game_kit,它的MCU是树莓派的RP2040,相对于esp32主流的原型,这是一款比较新的产品。目前已经有多个平台的支持,如micropython、circuitpython、Arduino、pico_sdk等环境的支持,用户群也在慢慢壮大起来,但是这些平台也是刚刚建立起来,还需要时间去不断完善和熟化。据悉主办方在筹备这期活动也遇到了一些困难,例如全球“缺芯”的问题,最终这款游戏板的显示屏采用了国产的替代品。苏老师在群上感叹到“但凡以后有渠道能买到欧美的器件,尽量不用国产的,耽误太多时间,而且有潜在的风险,尽量远离自己不熟悉的品牌”,而“风险”就是本期项目的一个挑战,也为本次活动增添了不少乐趣。

下面请欣赏苏老师语录:

“任何新产品的发展都有一个过程,如果你选择体验新品,那就要自己手动改装不少还不完善的地方,当然你也可以等待几年,不用动脑就可以使用成熟的产品。”

“我们活动的目的就是让大家尽可能遇到坑,然后自己学会从坑里爬出来,并且知道坑是如何造成的,遇到类似的坑如何更快地爬出来。”

                                                                                  ---苏公雨

本项目的主要内容

   重点介绍game_kit的Arduino环境下st7789屏幕的驱动问题及利用该屏幕驱动移植了一款经典小游戏,供大家借鉴参考。

FkK123lpo5I6eTmmzCr_Tg7MyE0t

(一)Arduino环境下的st7789屏幕驱动

    前面说过了Pico支持的平台很多,有非常容易上手的micropython,强大支持库的circuitpython,功能强大的pico_sdk 环境,Arduino 介于中间位置。但相对于本期项目内容Arduino还是具有一定优势,因为Arduino项目具有很高的可移植性,如果得到强大的图形库支持,就可以拥有广阔得发展空间。按原本计划只要能够用Arduino驱动ST7789屏幕,项目就可以很顺利的开展了,可是该款产品用的屏幕是国产替代品,进展并没有想象中那么顺利。在收到板子前面几天一直尝试用不同的图形驱动库来驱动屏幕,均以失败告终。后来在微信群上得到了“中量大-叶开”、“笛子”老师两位大神的深入分析研究,终于取得重大突破,完成了采用TFT_eSPI库驱动ST7789,为项目后期的工作奠定了坚实的基础,为后面的小程序移植开发带來了极大的便利。驱动这款屏幕是本项目终极难点,当大神成功在屏幕上成功打出“Hello World”时,标志这该板全面支持Arduino,假设没有学习群,光靠自己单兵作战,可能就要弃坑了。

下面介绍TFT_eSPI图形库具体设置要点。

  口诀是:

  “1. 0xff要发, 发完hard reset, 2. cpol=1, cpha=1 ”

                                                           --- 笛子老师

(1)修改spi数据发送模式:

参考“https://github.com/Bodmer/TFT_eSPI/pull/1547”中对TFT_eSPI的修理方法。

FnVkZ0ysqH7m4NFls5C8eXDo-oUN

                       (叶开同学提供)

(2)在复位时出发送0xff

参考下图红色标记:

  • FoLn-W9P4eljqrlcRTPDvhXaJGNB

        (笛子老师图片)

(3)St7789屏幕用户参数设置。

Fg0Z649bObmPFD8Cd2RNMC1EPeAz

FolxdF2UNgUoXxm7iMPq5awcTkkY

  

    按照这三步走就可以驱动屏幕了,由于具体设置过程相对复杂,细节较多,为了快速上手的同学可以直接下载文末的TFT_eSPI驱动包,替代”C:\Users\Administrator\Documents\Arduino\librariesArduino” 目录下的TFT_eSPI,前提是已经在Arduino安装了TFT_eSPI库,然后该库是专门针对game_pico专用的驱动其他的屏幕正常来说是不适用的,所以建议使用新产品的时候记得删除该库重装。

(二)Flappy bird小游戏的移植

     移植的思路是只要是用TFT_eSPI驱动的小游戏都能够移植到game_kit上面来,例如现有的在esp32上成功移植的游戏都能转换过来,遵循这一点同学们可以做更多的尝试。但是在移植过程总会遇到些大大小小的问题。目前成功移植的Flappy bird来自于M5Stack上的开源项目。由于原项目是基于M5Stack的二次开发的TFT_eSPI驱动的,里面有些功能的也基于esp32特性实现的,暂时在pico 上面没有现成办法实现,例如用esp32读写eeprom的方法断电保存数据的方法,目前折中的办法是只要不关机本轮的最佳成绩是在的,过去的事情就当没发生过好了。

    游戏的移植具体实现如下,代码中全面的英文注释和中文对关键函数的理解。相信机智的同学都能够轻松理解,不理解也没关系,下载下来玩耍一下,新年期间或者疫情隔离期间都可以拿出来玩玩,也是极好的。 

#include <Arduino.h>
#include <TFT_eSPI.h>
#include "Button.h"
TFT_eSPI tft;

// Button API
#define DEBOUNCE_MS 10
Button BtnB = Button(5, true, DEBOUNCE_MS); //实例化按键,可以使用waspressed功能,起到消抖作用。

#include <SPI.h>


#define TFTW            240     // screen width
#define TFTH            240     // screen height
#define TFTW2           120     // half screen width
#define TFTH2           120     // half screen height
// game constant
#define SPEED             1     //加快往前的速度难度更大。
#define GRAVITY         9.8     //正常重力加速度是9.8,下降速度比较快,减少加速度,降低游戏难度。
#define JUMP_FORCE     2.15     //小鸟起跳力度,可以调节到自己适合的数值。
#define SKIP_TICKS     20.0     // 1000 / 50fps
#define MAX_FRAMESKIP     5
...省略若干变量定义

static int maxScore = 0;   //存储最高分值。

static unsigned int birdcol[] = //这个愤怒的小鸟长得有点丑。16列*8行。
{ C0, C0, C1, C1, C1, C1, C1, C0, C0, C0, C1, C1, C1, C1, C1, C0,
  C0, C1, C2, C2, C2, C1, C3, C1, C0, C1, C2, C2, C2, C1, C3, C1,
  C0, C2, C2, C2, C2, C1, C3, C1, C0, C2, C2, C2, C2, C1, C3, C1,
  C1, C1, C1, C2, C2, C3, C1, C1, C1, C1, C1, C2, C2, C3, C1, C1,
  C1, C2, C2, C2, C2, C2, C4, C4, C1, C2, C2, C2, C2, C2, C4, C4,
  C1, C2, C2, C2, C1, C5, C4, C0, C1, C2, C2, C2, C1, C5, C4, C0,
  C0, C1, C2, C1, C5, C5, C5, C0, C0, C1, C2, C1, C5, C5, C5, C0,
  C0, C0, C1, C5, C5, C5, C0, C0, C0, C0, C1, C5, C5, C5, C0, C0};

// bird structure
static struct BIRD {
  long x, y, old_y; //小鸟的位置信息
  long col;         
  float vel_y;
} bird;

// pipe structure
static struct PIPES {
  long x, gap_y;
  long col;
} pipes;

// score
int score;
// temporary x and y var
static short tmpx, tmpy;

// ---------------
// draw pixel
// ---------------

#define drawMPixel(a, b, c) tft.setAddrWindow(a, b, a, b); tft.pushColor(c) //加速图形绘画,但在do循环中不能使用,有bug.


void setup() {
  // put your setup code here, to run once:
  tft.begin();

}

void loop() {
  // put your main code here, to run repeatedly:
  game_start();
  game_loop();
  game_over();
}

// ---------------
// game loop
// ---------------
void game_loop() {
  // ===============
  // prepare game variables
  // draw floor
  // ===============
  // instead of calculating the distance of the floor from the screen height each time store it in a variable
  unsigned char GAMEH = TFTH - FLOORH;
  // draw the floor once, we will not overwrite on this area in-game
  // black line
  tft.drawFastHLine(0, GAMEH, TFTW, TFT_BLACK);
  // grass and stripe
  tft.fillRect(0, GAMEH+1, TFTW2, GRASSH, GRASSCOL);
  tft.fillRect(TFTW2, GAMEH+1, TFTW2, GRASSH, GRASSCOL2);
  // black line
  tft.drawFastHLine(0, GAMEH+GRASSH, TFTW, TFT_BLACK);
  // mud
  tft.fillRect(0, GAMEH+GRASSH+1, TFTW, FLOORH-GRASSH, FLOORCOL);
  // grass x position (for stripe animation)
  long grassx = TFTW;
  // game loop time variables
  double delta, old_time, next_game_tick, current_time;
  next_game_tick = current_time = millis();
  int loops;
  // passed pipe flag to count score
  bool passed_pipe = false;
  // temp var for setAddrWindow //void TFT_eSPI::setAddrWindow(int32_t x0, int32_t y0, int32_t w, int32_t h)
  unsigned char px;

  while (1) {
    loops = 0;
    while( millis() > next_game_tick && loops < MAX_FRAMESKIP) {
      // ===============
      // input
      // ===============
      if (BtnB.wasPressed()) {
        // if the bird is not too close to the top of the screen apply jump force
        if (bird.y > BIRDH2*0.5) bird.vel_y = -JUMP_FORCE;
        // else zero velocity
        else bird.vel_y = 0;
      }
       BtnB.read();
      
      // ===============
      // update
      // ===============
      // calculate delta time
      // ---------------
      old_time = current_time;
      current_time = millis();
      delta = (current_time-old_time)/1000;

      // bird
      // ---------------
      bird.vel_y += GRAVITY * delta;
      bird.y += bird.vel_y;

      // pipe
      // ---------------
      pipes.x -= SPEED;
      // if pipe reached edge of the screen reset its position and gap
      if (pipes.x < -PIPEW) { //-PIPEW 柱子完全消失。
        pipes.x = TFTW;
        pipes.gap_y = random(10, GAMEH-(10+GAPHEIGHT));//柱子缺口的位置,y刻度10开始,这是顶部距离,底部 GAMEH-(10+GAPHEIGHT), GAMEH游戏区间高度、10底部占用,GAPHEIGHT缺口高度)
      }

      // ---------------
      next_game_tick += SKIP_TICKS;
      loops++;
    }

    // ===============
    // draw
    // ===============
    // pipe
    // ---------------
    // we save cycles if we avoid drawing the pipe when outside the screen
    if (pipes.x >= 0 && pipes.x < TFTW) {
      // pipe color
      tft.drawFastVLine(pipes.x+3, 0, pipes.gap_y, PIPECOL);
      tft.drawFastVLine(pipes.x+3, pipes.gap_y+GAPHEIGHT+1, GAMEH-(pipes.gap_y+GAPHEIGHT+1), PIPECOL);
      // highlight
      tft.drawFastVLine(pipes.x, 0, pipes.gap_y, PIPEHIGHCOL);
      tft.drawFastVLine(pipes.x, pipes.gap_y+GAPHEIGHT+1, GAMEH-(pipes.gap_y+GAPHEIGHT+1), PIPEHIGHCOL);
      // bottom and top border of pipe
      drawMPixel(pipes.x, pipes.gap_y, PIPESEAMCOL);
      drawMPixel(pipes.x, pipes.gap_y+GAPHEIGHT, PIPESEAMCOL);
      // pipe seam
      drawMPixel(pipes.x, pipes.gap_y-6, PIPESEAMCOL);
      drawMPixel(pipes.x, pipes.gap_y+GAPHEIGHT+6, PIPESEAMCOL);
      drawMPixel(pipes.x+3, pipes.gap_y-6, PIPESEAMCOL);
      drawMPixel(pipes.x+3, pipes.gap_y+GAPHEIGHT+6, PIPESEAMCOL);
    }
    // erase behind pipe
    if (pipes.x <= TFTW) tft.drawFastVLine(pipes.x+PIPEW, 0, GAMEH, BCKGRDCOL);

    // bird
    // ---------------  //本游戏的核心关键技术
    tmpx = BIRDW-1;     //妥善处理bird的绘制不留残影,更新不晃眼。
    do {
          px = bird.x+tmpx+BIRDW;
          // clear bird at previous position stored in old_y
          // we can't just erase the pixels before and after current position
          // because of the non-linear bird movement (it would leave 'dirty' pixels)
          tmpy = BIRDH - 1;
          do {
            tft.drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);//先纵向用背景色填充原来小鸟的位置,tmpy--意思是从底部往上洗。
          } while (tmpy--);
          // draw bird sprite at new position
          tmpy = BIRDH - 1;
          do {//birdcol[tmpx + (tmpy * BIRDW)  //最为烧脑的地方,小鸟图形颜色,用二维数组可能好理解些。
            tft.drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);//bird.y + tmpy 新位置的底部,
            //tft.drawPixel(px, bird.y + tmpy, birdcol[1000]);//数组超出界限了也没报错。
            //[tmpx + (tmpy * BIRDW)]//颜色只给了16列8行,但鸟的大小是16*16,看到小鸟是下半部分是黑色的。小鸟长得丑的原因找到了。
          } while (tmpy--);//tmpy--同样采用从底部往上填充。
    } while (tmpx--);
    // save position to erase bird on next draw
    bird.old_y = bird.y; //小鸟的y\高度起始位置,左顶角位置。

    // grass stripes
    // ---------------
    grassx -= SPEED;
    if (grassx < 0) grassx = TFTW;
    tft.drawFastVLine( grassx    %TFTW, GAMEH+1, GRASSH-1, GRASSCOL);
    tft.drawFastVLine((grassx+64)%TFTW, GAMEH+1, GRASSH-1, GRASSCOL2);

    // ===============
    // collision
    // ===============
    // if the bird hit the ground game over
    if (bird.y > GAMEH-BIRDH) break;
    // checking for bird collision with pipe
    if (bird.x+BIRDW >= pipes.x-BIRDW2 && bird.x <= pipes.x+PIPEW-BIRDW) {//进入管道缺口区间的判断。
      // bird entered a pipe, check for collision
      if (bird.y < pipes.gap_y || bird.y+BIRDH > pipes.gap_y+GAPHEIGHT) break;//跟管道缺口上下接触的判断。
      else passed_pipe = true;
    }
    // if bird has passed the pipe increase score
    else if (bird.x > pipes.x+PIPEW-BIRDW && passed_pipe) {
      passed_pipe = false; //复位
      // erase score with background color
      tft.setTextColor(BCKGRDCOL);
      tft.setCursor( TFTW2, 4);
      tft.print(score);
      // set text color back to white for new score
      tft.setTextColor(TFT_WHITE);
      // increase score since we successfully passed a pipe
      score++;
    }

    // update score
    // ---------------
    tft.setCursor( TFTW2, 4);
    tft.print(score);
  }
  
  // add a small delay to show how the player lost
  delay(1200);
}


// ---------------
// game start
// ---------------
void game_start() {
  tft.fillScreen(TFT_BLACK);
  tft.fillRect(10, TFTH2 - 20, TFTW-20, 1, TFT_WHITE);
  tft.fillRect(10, TFTH2 + 32, TFTW-20, 1, TFT_WHITE);
  tft.setTextColor(TFT_WHITE);
  tft.setTextSize(3);
  // half width - num char * char width in pixels
  tft.setCursor( TFTW2-(6*9), TFTH2 - 16);
  tft.println("FLAPPY");
  tft.setTextSize(3);
  tft.setCursor( TFTW2-(6*9), TFTH2 + 8);
  tft.println("-BIRD-");
  tft.setTextSize(2);
  tft.setCursor( 10, TFTH2 - 36);
  tft.println("Pico Game Kit");
  tft.setCursor( TFTW2 - (10*9), TFTH2 + 36);
  tft.println("GenVex_updated");
  while (1) {
    // wait for push button
      if(BtnB.wasPressed()) {
        break;
      }
      BtnB.read();    //更新按键数据。   
    }
      // init game settings
      game_init();
}

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  //randomSeed(analogRead(4));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH-60);
  //pipes.gap_y = 40;
}

// ---------------
// game over
// ---------------
void game_over() {
  tft.fillScreen(TFT_BLACK);
//  EEPROM_Read(&maxScore,0);//pico还没看到读写EEPROM的方法,断电不消失数据保存的方法,记录本机的历史最高分。
//  
  if(score>maxScore)
  {
    maxScore = score;
    tft.setTextColor(TFT_RED);
    tft.setTextSize(2); 
    tft.setCursor( TFTW2 - (13*6), TFTH2 - 26);
    tft.println("NEW HIGHSCORE");
  }
  
  tft.setTextColor(TFT_WHITE);
  tft.setTextSize(3);
  // half width - num char * char width in pixels
  tft.setCursor( TFTW2 - (9*9), TFTH2 - 6);
  tft.println("GAME OVER");
  tft.setTextSize(2);
  tft.setCursor( 10, 10);
  tft.print("score: ");
  tft.print(score);
  tft.setCursor( TFTW2 - (12*6), TFTH2 + 18);
  tft.println("press button");
  tft.setCursor( 10, 28);
  tft.print("Max Score:");
  tft.print(maxScore);
  while (1) {
    // wait for push button
      if(BtnB.wasPressed()) {
        break;
      }    
      BtnB.read();
  }
}

 

FgwOzf3WBTpG-gQcQhAmQZfO6hBB

(这游戏难度也太大了吧,最好成绩猜到5分)

 

心得总结

   (1)Pico_game_kit是基于树莓派RP2040核心开发的新产品,屏幕是国产替代品,增加了项目的挑战性。再次出现苏老师语录:“对于我们来讲,能用起来,有的玩就已经谢天谢地,本身这个过程就是遇坑填坑,而不是寻找迅速做出来得到满足感的过程”。在把这个屏幕驱动的坑填上之后,我们就可以开心地玩耍了。目前的驱动只用TFT_eSPI完成了驱动,实现的办法好像还有点笨拙,应该还有提升的空间。尝试加深对该屏幕驱动的理解或许能够完成LovyanGFX的驱动支持。基于Arduino庞大的用户群和海量的成功案例,相信利用这块板子玩出更多精彩内容,让我们拭目以待期待小伙伴们带来更多精彩的项目。

Fmx32D-J1C7nTdIYyfuAeonV2b-x

FiFjzJMuF_EdJLppwwX8SgmyY7Dq

 

(2)游戏移植过程需要对代码的逐句理解,才能充分理解作者的意图,学习他们核心关键技术,然后就可以尝试对游戏进行改良创新。例如,本游戏中小鸟的动态绘制技术就比较时实用,不过代码比较复杂,不好理解。目前已经尝试将小鸟提前绘制好,放在图层(sprite)里,只需知道小鸟的更新位置,用简单两行代码就可以替代原有复杂的循环,小鸟就可以随意飞翔,再也用考虑残影问题。

    然而,移植别人的游戏只是站在别人肩膀之上轻易的摘到果子,那么能不能自己尝试开发一个新的游戏呢!

 

在此再次对本项目提供帮助的小伙伴致敬!!!

祝大家玩的愉快,学业猛进!

 

附件下载
PicoFlappyBird.rar
Arduino数据包
TFT_eSPI.rar
团队介绍
这家伙很帅什么也没留下。
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号