寒假在家一起练-基于树莓派rp2040实现复古游戏《俄罗斯方块》移植
1.编写(移植)《俄罗斯方块》游戏的运行逻辑; 2.依托Arduino提供的TFT_eSPI库编写LCD屏幕显示界面; 3.编写《俄罗斯方块》游戏的背景音乐; 4.依托RP2040游戏机平台进行软硬件逻辑结合;
标签
嵌入式系统
RP2040
俄罗斯方块
2022寒假在家练
bird
更新2022-03-04
1758

1.项目需求

  • 设计或移植一款经典的游戏
  • 可以通过LCD屏显示
  • 可以通过按键和四向摇杆控制游戏的动作
  • 在游戏中要通过蜂鸣器播放背景音乐

 

2.完成的功能及达到的性能

(1)完成设计(移植)经典游戏《俄罗斯方块》

  • 设计平台:Arduino
  • 编写语言:C
  • 编写环境:Win11
  • 硬件平台:RP2040

(2)完成通过LCD屏显示《俄罗斯方块》游戏界面

  • 使用代码库:TFT_eSPI
  • 总线协议:四线SPI(时钟SCLK,片选CS,从入主出MOSI,从出主入MISO)
  • 硬件平台:ST7789(240*240)

FniYZwuxG5sPA5kKXYfCzkaIGWMe

(3)完成通过按键和四向摇杆控制《俄罗斯方块》游戏的动作

  • 实现逻辑:电平检测、模拟量读取
  • 使用函数:digitalRead( ); analogRead( );
  • 硬件平台:Key按键、FJ08K四向摇杆

FlbIplBaxyBTIAJXhIAMjdXPCv3M

(4)完成在游戏中使用蜂鸣器播放《俄罗斯方块》游戏的背景音乐

  • 音乐选取:《Коробейники(货郎)》
  • 使用函数:tone( ); noTone( );
  • 硬件平台:Buzzer蜂鸣器

3.实现思路

  • 编写(移植)《俄罗斯方块》游戏的运行逻辑;
  • 依托Arduino提供的TFT_eSPI库编写LCD屏幕显示界面;
  • 编写《俄罗斯方块》游戏的背景音乐;
  • 依托RP2040游戏机平台进行软硬件逻辑结合;

 

4.实现过程

(1)程序框图

FpSBh92Ubwdr3VQCLdSKcYxMvkb_

(2)俄罗斯方块逻辑编写

(a)方块生成

这里运用了bool来判断是否要生成新的方块组合。代码如下:

bool GetSquares(Block block, Point pos, int rot, Point* squares) {
  bool overlap = false;
  for (int i = 0; i < 4; ++i) {
    Point p;
    p.X = pos.X + block.square[rot][i].X;
    p.Y = pos.Y + block.square[rot][i].Y;
    overlap |= p.X < 0 || p.X >= Width || p.Y < 0 || p.Y >= Height || screen[p.X][p.Y] != 0;
    squares[i] = p;
  }
  return !overlap;
}

(b)方块类型信息存储

构造结构体,存储颜色等信息,把方块用矩阵来表示,再用数组来将信息存储。代码如下:

struct Block {
  Point square[4][4];
  int numRotate;
  int color;
};
Block block;
Block blocks[7] = {
  {{{{ -1, 0}, {0, 0}, {1, 0}, {2, 0}}, {{0, -1}, {0, 0}, {0, 1}, {0, 2}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}, 2, 1},
  {{{{0, -1}, {1, -1}, {0, 0}, {1, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}, 1, 2},
  {{{{ -1, -1}, { -1, 0}, {0, 0}, {1, 0}}, {{ -1, 1}, {0, 1}, {0, 0}, {0, -1}}, {{ -1, 0}, {0, 0}, {1, 0}, {1, 1}}, {{1, -1}, {0, -1}, {0, 0}, {0, 1}}}, 4, 3},
  {{{{ -1, 0}, {0, 0}, {0, 1}, {1, 1}}, {{0, -1}, {0, 0}, { -1, 0}, { -1, 1}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}, 2, 4},
  {{{{ -1, 0}, {0, 0}, {1, 0}, {1, -1}}, {{ -1, -1}, {0, -1}, {0, 0}, {0, 1}}, {{ -1, 1}, { -1, 0}, {0, 0}, {1, 0}}, {{0, -1}, {0, 0}, {0, 1}, {1, 1}}}, 4, 5},
  {{{{ -1, 1}, {0, 1}, {0, 0}, {1, 0}}, {{0, -1}, {0, 0}, {1, 0}, {1, 1}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}}, 2, 6},
  {{{{ -1, 0}, {0, 0}, {1, 0}, {0, -1}}, {{0, -1}, {0, 0}, {0, 1}, { -1, 0}}, {{ -1, 0}, {0, 0}, {1, 0}, {0, 1}}, {{0, -1}, {0, 0}, {0, 1}, {1, 0}}}, 4, 7}
};

(c)移动和旋转问题

《俄罗斯方块》游戏最有魅力的点在于其移动和旋转的机制。GetNextPosRot()函数结合玩家在硬件操作反馈的结果,来判断方块组合该怎么移动。这里使用了数个if判断分支来实现这一功能。代码如下:

void GetNextPosRot(Point* pnext_pos, int* pnext_rot) {
  bool received = KeyPadLoop();
  if (digitalRead(SW)==0) started = true;
  if (!started) return;
  pnext_pos->X = pos.X;
  pnext_pos->Y = pos.Y;
  if ((fall_cnt = (fall_cnt + 1) % 10) == 0) pnext_pos->Y += 1;
  else if (received) {
    if (Button_LEFT) {
      Button_LEFT = false;
      pnext_pos->X -= 1;
    }
    else if(Button_RIGHT) {
      Button_RIGHT = false;
      pnext_pos->X += 1;
    }
    else if(Button_DOWN) {
      Button_DOWN = false;
      pnext_pos->Y += 1;
    }
    else if(Button_B) {
      Button_B = false;
      *pnext_rot = (*pnext_rot + 1)%block.numRotate;
    }
    else if(Button_A) {
      Button_A = false;
      *pnext_rot = (*pnext_rot + block.numRotate - 1)%block.numRotate; 
    }
  }
}

(d)怎么存储和更新积累的方块

利用for循环,不断扫描screen数组内数值,以判断下落的方块是否与底部接触,接触则更新数组并生成新的方块下落。当无法生成新的方块时,游戏结束。代码如下:

void ReviseScreen(Point next_pos, int next_rot) {
  if (!started) return;
  Point next_squares[4];
  for (int i = 0; i < 4; ++i) screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = 0;
  if (GetSquares(block, next_pos, next_rot, next_squares)) {
    for (int i = 0; i < 4; ++i) screen[next_squares[i].X][next_squares[i].Y] = block.color;
    pos = next_pos;
    rot = next_rot;
  }
  else {
    for (int i = 0; i < 4; ++i) screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = block.color;
    if (next_pos.Y == pos.Y + 1) {
      DeleteLine();
      PutStartPos();
      if (!GetSquares(block, pos, rot, next_squares)) {
        for (int i = 0; i < 4; ++i) screen[pos.X + block.square[rot][i].X][pos.Y + block.square[rot][i].Y] = block.color;
        GameOver();
      }
    }
  }
  Draw();
}

 

(e)怎样让障碍物满行后自动消除

从底部开始判断是否有空的一行方块,如果没有空白的就让这一行方块存储上一行方块的信息,以此类推,这样就可以达到消行的效果了。代码如下:

void DeleteLine() {
  int DeleteCount=0;
  for (int j = 0; j < Height; ++j) {
    bool Delete = true;
    for (int i = 0; i < Width; ++i) if (screen[i][j] == 0) Delete = false;
    if (Delete) 
    {
      for (int k = j; k >= 1; --k) 
        for (int i = 0; i < Width; ++i) 
          screen[i][k] = screen[i][k - 1];
          DeleteCount++;
    }
  }
      score+=DeleteCount*DeleteCount; 
}

 

4.3 LCD显示部分

(1)SPI总线协议

SPI(Serial Peripheral Interface - 同步外设接口)总线是一种用于短距离通信(主要是嵌入式系统中)的同步串行通信接口规范,虽然没有正式的国际标准,但这种接口协议由Motorola发明迄今经过很多厂商的支持,已经成了一种事实标准,被广泛用于各种MCU处理器中,同传感器,串行ADC、DAC、存储器、SD卡以及LCD等进行数据连接。由于没有统一的国际标准,SPI出现了很多不同的协议选项,例如不同的Word大小;每个设备都有自己的协议定义,包括是否支持命令;有些设备只发送,其它的则只是接收;有的片选是高有效,有的则是低有效;有的协议先发送最低位。

本硬件平台使用的是四线SPI(时钟SCLK,片选CS,从入主出MOSI,从出主入MISO)的ST7789LCD屏幕。

(2)TFT_eSPI库

TFT_eSPI库适用于 32 位处理器的 Arduino IDE 兼容图形和字体库。该库针对 32 位处理器,针对 STM32、ESP8266 和 ESP32 类型进行了性能优化。该库可以使用 Arduino IDE 的库管理器加载。直接内存访问 (DMA) 可与 ESP32、RP2040 和 STM32 处理器一起使用,以提高渲染性能。

为以下处理器合并了优化的驱动程序:

  • RP2040,例如树莓派 Pico
  • ESP32 和 ESP32-S2
  • ESP8266
  • STM32F1xx、STM32F2xx、STM32F4xx、STM32F767(建议使用更高 RAM 的处理器)

既然有这么好用的库,又何必自己去造轮子呢?在理解的基础上运用好它就行了。

(3)摇杆控制部分

FJ08K摇杆为一个双向的电阻器,随着摇杆方向不同,抽头的阻值随着变化。本模块使用 3.3V 供电, VRx,VRy (X、Y 轴)为模拟输入信号,连接到RP2040相应的IO口。VRx,VRy 的值:小于1000为左,大于3000为右;小于300为上,大于3000为下。其代码如下所示:

if (millis()-ButtonElsp<100) {return false;}
  ButtonElsp=millis();
  ClearKeys();
  if(analogRead(VRX)<1000) {
        Button_LEFT=true;
        return true; }
  if(analogRead(VRX)>3000) {
        Button_RIGHT=true;
        return true;}
  if(analogRead(VRY)<300) {
        Button_UP=true;
        return true;}
  if(analogRead(VRY)>3000) {
        Button_DOWN=true;
        return true;}

(4)背景音乐部分

《俄罗斯方块》作为一款经典游戏,其游戏背景音乐也是经典中的经典了。在本次任务中,我选用的是老版本《俄罗斯方块》的背景音乐:《Коробейники(货郎)》。

这是一首俄罗斯著名民歌,内容是一位年轻的货郎与一位少女顾客之间的对话,两人在交易中开始相互争执、讨价还价,后来渐生情窦,擦出了爱情的火花。歌曲的歌词来源于俄国诗人尼古拉·阿列克塞耶维奇·涅克拉索夫(Nikolay Alexeyevich Nekrasov,1821-1877年)的同名诗《货郎》的开头6段。这首诗于1861年刊登在杂志Sovremennik上,之后这首歌很快成为俄罗斯著名的民歌。

由于Arduino封装的代码库以及函数非常丰富和实用,所以我也就不重复造轮子了,在参考了网上的例程后,直接利Arduino中自带的tone( ); noTone( );两个函数实现了利用蜂鸣器演奏这首曲子。下面是主体函数部分:

// from https://blog.csdn.net/qq_35898865/article/details/105791890
void tetris_Music()
{
  //注意,该数组是notes (notes +期限)数量的两倍
  for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) 
  {
    //计算每个音符的持续时间
    divider = melody[thisNote + 1];
    if (divider > 0) 
    {
      noteDuration = (wholenote) / divider;
    } 
    else if (divider < 0) 
    {
      noteDuration = (wholenote) / abs(divider); //负数,是加点的音符
      noteDuration *= 1.5; //点状音符的持续时间为 二分音符 + 四分音符
    }
    //为确保音乐效果,防止出现连音,这里只用九层的时间放音,剩下的时间静音
    tone(buzzer, melody[thisNote], noteDuration*0.9);
    //在播放下一个音符之前,等待指定的持续时间。
    delay(noteDuration);
    //在下一个音符之前停止波形生成。
    noTone(buzzer);
  }
}

 

5.遇到的主要难题

(1)方块下落速度与更新速度缓慢

在没有使用TFT_eSPI库前,方块下落速度与更新速度缓慢问题几乎无解。一方面是因为新ST7789固件与以前版本有些许出入,另一方面是对SPI总线没有进行有利的使用。

但是后面在汤半泛(微信名)大佬贡献的TFT_eSPI库加持下,此问题得到了圆满的解决。

TFT_eSPI库对RP2040支持的改动如下:

  • 在User_Setup.h文件中,取消或更改下列代码注释或内容

    #define ST7789_DRIVER
    #define TFT_RGB_ORDER TFT_RGB
    #define TFT_WIDTH  240
    #define TFT_HEIGHT 240
    #define TFT_CS   -1  //在本硬件平台上,CS接地,始终为低电平
    #define TFT_DC   1  
    #define TFT_RST  0 
    #define TFT_MOSI 3
    #define TFT_SCLK 2

(2)计分系统的编写

理论上来说,计分系统的编写并不困难,在代码逻辑上也已经实现自洽,但是问题出现在LCD显示部分。当最下面一层方块消除后,计数确实发生了变化,这在通过串口调试时也是正常的。可在LCD显示上就是不正常的——其始终无法消除初始的“0”值。

这让我十分的困惑。一开始我以为是代码逻辑的问题,但是当代码运行在esp32上时,其又是正常的;后来我怀疑是Lcd驱动库的问题,但是换了几个函数之后还是无济于事。最后没有办法,我就暂时搁置了计分系统的编写。

(3)方块质感的体现

在传统《俄罗斯方块》游戏中,其方块都是极富质感的,有砖石般的晶体感。那怎么在单片机上去实现这样的效果呢?在参考了网上的案例以及TFT_eSPI库后,这个问题得到了解决——将图案取模后放入方块即可。

 

6.未来规划

该项目已经成功实现了编写(移植)《俄罗斯方块》游戏的功能,并达到了预期指标。然而其还有许多可以提升与扩展的地方:

  • 可以使背景图像更加美观。目前版本由于取模软件的原因,游戏背景无法插入图片,后续有时间会进行改进。
  • 可以添加一个游戏开始菜单。目前版本由于技术原因(不是),无法实现游戏菜单界面,后续会跟进学习。
  • 可以实现游戏音乐打开或者关闭。这个实现难度不大,但是单独实现不太美观,待添加游戏开始菜单后一并加入。
  • 可以实现计分系统。目前版本由于游戏背景干扰以及技术原因,无法进行计分操作,后续会跟进的。
软硬件
元器件
RP2040
树莓派基金会推出的双核Arm Cortex M0+微控制器,133MHz时钟速率,264KB SRAM,支持C/C++、MicroPython编程
附件下载
rp2040_Game_Tetris.rar
Arduino.ino文件
库文件.rar
Arduino库文件
团队介绍
杭州电子科技大学-蔡逸
团队成员
蔡逸
菜鸟一枚,还望大佬们多多关照~
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号