65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Arduino Nano 上使用 4x 8x8 点阵显示“俄罗斯方块”

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2020 年 8 月 18 日

CPOL

26分钟阅读

viewsIcon

12021

downloadIcon

355

以下是我如何用我的 Arduino Nano、衣架和一些胶带制作一个“下落的方块”游戏

引言

自从 23 年前计算机工程二年级以来,我从未做过任何“电子”相关的事情,直到去年秋天晚些时候我订购了一个 Arduino 入门套件,并热切地期待它的到来。我甚至不知道我订购了什么,没有意识到 **Arduino** 是一个微控制器,我以为我会得到一堆晶体管、二极管和 LED。但是,瞧,它就在那里,就在我收到的邮件中的破塑料盒子里,还有所有其他与 **Arduino Uno** 相比黯然失色的小部件。

我在 Udemy 上上了一门在线初学者课程,火花四溅……是真的。

从那时起,我购买了很多在线 **Arduino**“引火物”。来自中国的慢船花了几个痛苦的月才最终到达这里。然后又发生了加拿大太平洋铁路封锁事件,进一步阻碍了我的微小乐趣到达它们的幸福家园,然后我们所有人都受到了冠状病毒的袭击。所以,我去了加拿大新不伦瑞克省蒙克顿当地的电子产品经销商,但他收取的费用是我在线支付的 5 倍。我太抠门了,无法接受。所以大多数时候我只是坐着等待下一次邮件递送,我的 **Arduino** 零件最终会到达。尽管在完成在线课程、观看了一些教学 YouTube 视频并阅读了几页 **Arduino** 语言参考后,我确信我知道自己在做什么,但我仍然在应该有蜂鸣器和陀螺仪 LED 的地方看到了冒烟的火花……但我坚持不懈,最终制作了一些简单的项目。

现在情况好多了。我终于有了一些必需品,比如稳压电源、可靠的万用表和一把像样的烙铁。所以我准备好了。

当你有一个或两三个 **Arduino** 时,有无限的可能性。我制作了一个机械臂,它的移动部件比编写代码更难制作,但结果却一点乐趣都没有,因为那些 SG-90 微型伺服电机太弱了,什么也做不了,所以我出去买了更大、更好、更强大的 MG995,所以很快就会很棒。还有一个 LCD1604 “奔跑者”游戏,你在迷宫中跑来跑去射击坏人,一个 Simon-Says 和其他一些小东西,因为我一直在忙于 C# 项目,最近,还有一个实际的 艺术项目 占用了我太多时间。所以,上周,一个全新的 **Max7219** 4x 菊花链点阵(8x8x4)到了,这足以让我每天晚上放下我的艺术项目几个小时来玩我的 **Arduino**。

第一晚,我将(由于与某个著名的俄罗斯经典视频游戏的版权问题,我将拒绝在本文中使用此项目的暂定标题)**下落的方块**块定义为一个类,它可以旋转并计算每个方块在下落时所在的位置,然后方块开始落在我的点阵游戏屏幕上。第二天晚上,它们随着我的面包板 **74HC165** 移位寄存器控制的按钮旋转并左右移动。第三天晚上,地图数据诞生了,方块现在遵守物理定律,落在它们所在的位置。行被擦除,当它们下落时发生了奇怪的事情,但我修复了所有这些。尽管我确实不得不重写一些东西,但整个过程只花了我整整一周。那一周的其余时间都花在了完善上。除了一个下午使用烙铁外,还有一些介绍性文字、滚动文本、不占用所有内存的滚动文本,最后是按位方式横向滚动的文本。然后是高分以及发现了一个叫做 EEPROM 的神奇东西,

“就像一个微型硬盘”,Arduino 参考。

这很酷!我的编程背景帮助我处理了将奇形怪状的方块通过 4x8 字节的点阵屏幕阵列下落所涉及的所有按位操作,而我从大学学到的那一点电子学基础知识足以完成它。

现在,不幸的是,我需要一个支持小组来帮助我戒掉玩“下落的方块”游戏……但我听说他们有甜甜圈和咖啡。所以没关系。

背景

这个项目是世界经典游戏俄罗斯方块的廉价劣质复制品。

根据 维基百科,俄罗斯方块的原始游戏由 Alexey PajitnovVadim Gerasimov 设计,他们于 1984 年在俄罗斯首次发布了该游戏。经过多年对他们创作权利的争议,Alexey Pajitnov 于 1996 年从任天堂手中夺回了这些权利,并创立了 **俄罗斯方块公司**。该游戏已售出超过 2.02 亿份,其中约 7000 万份实体版和 1.32 亿份付费下载版。该游戏保持着 **吉尼斯世界纪录**“移植平台最多的游戏”的记录,可在 65 个平台上玩,如果算上我的 **Arduino** **Nano**,那就是 66 个!

所需资源

  1. **Arduino Nano**(或任何你有的控制器)及配套的 USB 连接器
  2. 4 个菊花链连接的 **Max7219**
  3. **74HC165** 移位寄存器(可选)
  4. 6 个按钮
  5. 5V 直流蜂鸣器
  6. 5k 欧姆电位器
  7. 电线、焊料和烙铁、镊子、钳子、剥线钳
  8. 特百惠、汤罐盖和筷子是可选的

观看一段 4 分钟的视频,展示最终结果。

用户界面

“下落的方块”游戏通常使用操纵杆和两个按钮来玩,但由于我随 **Arduino** 入门套件收到的操纵杆在使用了很短时间后就损坏了,所以我选择了 6 个按钮(我恰好几个月前从旧的废弃电子设备中打捞出来的)。由于这个项目只有六个按钮,我成功地节省了 2 个输出引脚,代价是一个 **74HC165** 移位寄存器、6 个 180 欧姆电阻和一点焊料,但我坚持这个艰难的设计决定,因为它给了我一个玩弄烙铁的理由。谁不喜欢玩弄他们的烙铁呢?

**74HC165** 移位寄存器是一种微芯片,可以读取八个输入并仅使用四条线将这些输入的状态串行传输到您的微控制器。您可以将它们菊花链连接起来,一次读取 16、24、32 个或更多输入,仍然只使用四个数字 **Arduino** 引脚(**Nano** 只有 13 个,这意味着对于某些项目,移位寄存器是必不可少的)。在这个项目中使用一个有点大材小用,但无论如何我的架子上有十几个,而且按钮焊接到与 **74HC165** 相同的可拆卸板上,并且在我厌倦了“**下落的方块**”(或者将它固定在一起的筷子最终断裂)时可以拔下并重新用于不同的项目。下面的图表显示了如何使用 **74HC165** 将 **Arduino Uno** 连接到 8 个拨码开关。

在这个项目中,我没有使用拨码开关,而是使用了 6 个按钮,并使用母连接器将 **74HC165** 连接到 **Arduino**。按钮和下拉电阻器必须与 **74HC165** 一起焊接。这是该项目除了蜂鸣器和电位器之外唯一需要焊接的地方。关于该项目中移位寄存器唯一值得注意的是,引脚 5 和 6(分别为 D6 和 D7)都接地,因为只有 6 个(总共 8 个)按钮连接到它,这两条线被浪费了,最好不要浮空。此外,由于 **Arduino Nano** 只有两个接地引脚,并且有三根线需要接地(**74HC165** 移位寄存器、**Max7219** 点阵和蜂鸣器),我不得不使用旧的跳线端子制作一个 1:3 分线器,以便我仍然可以使用跳线连接这些外设而无需焊接,并为它们提供一个公共接地。

以下示意图显示了“下落的方块”组件如何连接到 **Arduino Nano**。

UI 代码

在游戏过程中,用户界面非常简单直观。左边的四个按钮是方向按钮,右边的两个按钮控制下落方块的旋转。

但是,由于“上”按钮在游戏中没有用处,因此它可以作为两按钮组合的控制按钮。

  1. 上 + 右旋转 = 暂停
  2. 上 + 左旋转 = 预览下一个方块
  3. 上 + 右 = 切换音乐

我最初实现了这些按钮组合,并让单个按钮仍然控制下落的游戏方块,这使得玩游戏变得困难,如果你旋转方块试图查看下一个会掉落什么,或者滑动一个方块试图切换音乐。所以我回忆起了一些关于有限状态机的遥远讲座,并在这个项目上使用了 FSM。我最终为所有这三种组合制作了 FSM,而我真正需要做的只是在“上”按钮按下时禁用所有控件,但当时我还没有确定“上”按钮是一个控制按钮,而是仍然使用以下两按钮组合

  1. 左 + 右 = 暂停
  2. 上 + 下 = 切换音乐
  3. 左旋转 + 右旋转 = 查看下一个方块

所以我会告诉你我的有限状态机,因为它们工作得足够好,尽管它们完全没有必要,而且在避免游戏时间控制问题上过于复杂。您可以通过下载 zip 文件查看包含这三个 FSM(暂停、音乐和显示下一个方块)的原始代码。但对于最终项目,我删除了这三个 FSM,并在“上”按钮按下时禁用了游戏控制。Bada-bing,bada-boom……简单多了。

UI 有限状态机

有限状态机是定义给定自动化系统所处不同状态的一种方式。一旦您知道程序可能处于的所有可能条件,您就可以创建从一个状态到下一个状态的路径,将它们全部连接起来,以便它们在您的数据、传感器或用户输入的条件下从一个状态转换到下一个状态。这是一个简单的例子

enum enuFSMMusic
{
 FSMMusicOn = 0,          // 0
 FSMMusicStartUpRight,    // 1
 FSMMusicStartUp,         // 2
 FSMMusicStartRight,      // 3
 FSMMusicOff,             // 4
 FSMMusicEndUpRight,      // 5
 FSMMusicEndUp,           // 6
 FSMMusicEndRight         // 7
}; 

用于切换音乐的有限状态机定义了上面显示的 8 个独特状态。当电源打开时,变量 eFSMMusic 被实例化并初始化为零(FSMMusicOn)。函数 PlaySong() 在播放音符之前调用 Music_UI(),而 Music_UI() 使用 switch-case 来处理 eFSMMusic 变量并决定要做什么。当它仍处于 FSMMusicOn 状态时,它会测试按钮**上**和**右**的条件。如果这两个按钮都按下,那么它会将其状态更改为 FSMMusicStartUpRight,直到它再次循环并通过该状态的 case 并再次测试相同的按钮,最终当玩家释放这两个按钮时进入 FSMMusicOFF 状态。在此期间,只有当变量 eFSMMusic 处于 FSMMusicOn 状态时,PlaySong() 才会播放音乐。

有限状态机是非常强大的工具,它们有自己的用途,并且可以大大简化原本困难的程序流程问题,但在这里它们完全没有必要。决定使用一个“上”按钮作为控制,并在按下该按钮时禁用所有其他与游戏相关的按钮控制,只需要三个布尔变量来定义所有三个选项(暂停、音乐和查看下一个方块)的条件。

“在所有条件都相等的情况下,越简单越好,”一位智者说道。

(或者可能是提利昂·兰尼斯特……我不记得了)

方块

“下落的方块”游戏中有七种不同类型的方块。在这个项目中,我使用一个名为 `classFallingBlocksPiece` 的类对它们进行了编码,该类跟踪方块在屏幕上掉落时的位置。每个下落的 **下落的方块** 方块都由四个 `classFallingBlocksPiece_Square` 实例的数组组成。

class classFalling BlocksPiece
{
  public:
    byte bytPieceType = 0;         // not necessary - only used for early debugging
    classPoint pt;                 // its location on the game map
    classFalling BlocksPiece_Square squares[4];

    classFalling BlocksPiece();
    void setType(byte bytTypeNew); // initializes each square 
    void InitRandom();
    void RotateRight();
    void RotateLeft();

    int PieceInBounds_Horizontal();
}; 

这些方块可以围绕自身旋转,并且第 0 个索引的方块是每个方块旋转的枢轴方块。在下图中,您可以看到每个方块是如何由这四个方块组成的,以及这些方块是如何配置以定义方块形状的。

`classFallingBlocksPiece_Square` 跟踪两个笛卡尔点。第一个点跟踪它相对于枢轴方块的位置,第二个点将其定位在游戏地图上。方块本身不需要知道它们的形状,只需要知道它们相对于枢轴方块的位置,所以当它们需要旋转时,每个方块都会改变它相对于枢轴方块的位置,本质上是改变了它们的形状,但有效地欺骗玩家相信方块已经旋转。土豆……西红柿。无论你叫它什么,它看起来都在旋转。

void classFalling BlocksPiece_Square::RotateRight()
{
 int intY_Temp = pt.X;
 pt.X = - pt.Y;
 pt.Y = intY_Temp;
}

void classFalling BlocksPiece_Square::RotateLeft()
{
 int intY_Temp = -pt.X;
 pt.X = pt.Y;
 pt.Y = intY_Temp;
}     

方块本身的位置记录在 `classFallingBlocksPiece` 中,并决定了枢轴方块的位置,从而设置了其他三个相对于它定位的方块的所有位置。

尽管游戏可以持续一段时间,并且在游戏结束滚动字幕之前可能会掉落一百个或更多方块,但游戏中实际上只有三个方块。所有三个方块都在一个数组中。

  1. 下落方块
  2. 下一个方块
  3. 测试方块

**下落方块**是你在屏幕上看到的由玩家控制下落的方块。**下一个方块**是屏幕顶部闪烁的方块,等待下落。有两个指向这两个方块的指针,每当**下落方块**停止移动时,这两个指针就会在两者之间交替。**测试方块**是**下落方块**的克隆,用于碰撞检测,以确定**下落方块**是否可以执行玩家 intended 的动作。

碰撞检测

当然,你需要测试方块是否可以下落、横向移动甚至旋转。为此,**下落方块**的变量被复制到**测试方块**上。然后根据玩家的意图操纵**测试方块**,如果**测试方块**的最终旋转和位置与游戏地图的当前状态(稍后会详细介绍)不冲突,则允许该移动,并将**测试方块**的更改值赋给**下落方块**,游戏继续进行。有时旋转会导致碰撞,在这种情况下,**测试方块**会向左或向右移动,并进行另一次碰撞测试。如果**测试方块**在这里的最终状态是允许的,那么**下落方块**不仅会旋转(根据玩家的意图),还会向旁边移动一个方块,以允许旋转进行而不会与游戏地图发生碰撞。在撰写本文时,旋转碰撞只通过一次向左或向右移动来改变,而不是第二次或第三次,这意味着在某些情况下,如果旋转需要移动不止一次才能避免碰撞,则方块将无法旋转。这可以纠正,我只是还没有做。

移动和旋转以及它们所需的碰撞测试在 `classFallingBlocksPiece` 之外完成,因为这些操作需要访问**地图数据**。

地图数据

当**下落方块**不能再移动时,它就会静止。在这种情况下,就不再需要将它作为一个由四个方块组成的完整**下落方块**进行跟踪了。与其记住哪些掉落的方块属于哪个方块,以及创建新的方块只会随着玩家一行一行地清除而分解和消失,不如使用**下落方块**的位置来设置游戏**地图数据**中的值。由于游戏屏幕由 4 个 8x8 点阵阵列组成,屏幕上有 8x32 个方块。每个方块都可以被一个**下落方块**-方块占用或不占用。**Max7219** 菊花链旨在侧向查看。然而,在这个**下落方块**项目中,它被配置为第 0 个矩阵位于游戏屏幕的顶部,而第 3 个矩阵位于底部。每个矩阵由 8 个字节(通常彼此叠放)组成,第 0 个字节在顶部,第 7 个字节在底部(当侧向查看时,如下面图表所示)。要在屏幕上绘制任何东西,您必须寻址您的 **Max7219** 菊花链的矩阵和行(这通常是侧向的)。下图显示了这些矩阵的设置方式。

游戏的**地图数据**由 8 个无符号长整数组成,这些整数构成游戏的列。当它们水平显示,如上图所示时,您会注意到这些无符号长整数的位索引从游戏屏幕的顶部(图像右侧)开始,到游戏屏幕的底部(图像左侧)结束,无符号长整数本身的索引顺序与 **Max7219** 自身的字节顺序相同,0 在屏幕左侧(图像顶部),7 在右侧(图像底部)。当一个方块掉落时,数据会被改变,并在**下落方块**的方块所在的位置设置位,然后下一个方块开始从顶部掉落。当需要绘制屏幕时,**地图数据**被复制到 8 个无符号长整数的临时数组中,并添加**下落方块**。然后每个临时无符号长整数向右移位 8 位,并将其最低有效字节用于

lc.setRows(intDisplayCounter, intColumnCounter, bytDisplay);

调用作为 `bytDisplay` 来绘制在屏幕上,如果自上次绘制屏幕以来,这 8 位中的任何一位发生了变化。使用两个变量来跟踪 4 个 **Max7219** 矩阵中每个 8 字节的哪些需要更新。

unsigned long ulngScreen_RowsNeedRefresh = -1;
byte bytScreen_ColumnsNeedRefresh = B11111111;

无符号长整数 `ulngScreen_RowsNeedRefresh` 跟踪哪些行已被修改,而 `bytScreen_ColumnsNeedRefresh` 跟踪哪些列被修改。当屏幕刷新时,这两个变量被重置为默认值(所有位都置高),并且它们保持这种状态,直到**下落方块**被移动或旋转,此时**下落方块**在此更改之前和之后占据的所有行和列都在上面的“`Refresh`”变量中设置,以便在下次调用 `drawScreen()` 函数时进行采样。

行塌陷

随着游戏的进行,被填满的行会闪烁然后被擦除,然后被擦除上方​​的行会下落以替换它们。为了实现这一点,**下落方块**会一直下落直到不能再下落。在这里,算法需要测试哪些行已完成。它可以测试所有 32 行,但既然它知道**下落方块**是唯一能填满一行的方块,为什么还要这样做呢?所以它只测试下落方块结束所在的行,使用上面提到的 `ulngScreen_RowsNeedRefresh` 变量。它创建一个另一个无符号长整数,并设置与玩家在游戏中填写的行对应的位。然后这个无符号长整数作为位操作中的掩码,在绘制到屏幕之前设置或重置**地图数据**中的位,以创建闪烁消失的行效果。之后,它们会一次一行地擦除,方法是将该行上方的所有位向左移位(这里的左是位左移,对应于屏幕上的“下”),适用于所有列,适用于所有消失的行,从游戏屏幕最底部的行开始。只有当所有消失的行都以这种方式擦除后,屏幕才会再次绘制。

滚动文本

**Max7219** 有详细的文档,并且网上有大量的教程可以为您提供所有您需要的功能,以在您的点阵屏上滚动文本。

所以我写了自己的。

我猜是愚蠢或固执,但我喜欢编码,而且只花了我几个小时就实现了一个简单的横向滚动文本功能,包括所有大小写字母和一些其他字符。

观看我在 Facebook 上发布的这个视频.

如果您的项目唯一想做的事情是横向滚动文本,那没问题,但是该文本滚动项目使用 8 字节内存定义每个字符的位图,总共 82x8 字节,即 656 字节,仅用于定义这 82 个字符。这听起来可能不多,但请记住,**Arduino Nano** 的内存非常少,没有多余的内存。对于任何其他项目,您可能会遇到内存问题。由于大多数微控制器中的内存非常宝贵,您必须优化内存使用或选择您想做的事情并放弃那些需要太多内存的事情。

这是滚动文本项目的内存使用情况(除了滚动一句话文本外什么都不做)

与此“下落的方块”项目的内存使用情况相比(这是一个“下落的方块”游戏,还有滚动文本)

这不仅限于 26 个大写字母、空格和数字 0-9,而且还简化了这些字符的定义方式。**下落方块**项目将每个滚动字符的位图横向定义在一个一维字节数组中。

String strCharList = "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789";
String strChrWidth = "4444444435445544544545555545344544444";

byte bytFont[]
{
 B11111110, // A 4
 B00010001,
 B00010001,
 B11111110,
 B11111111, // B 4
 B10001001,
 B10001001,
 B01110110,
 B01111110, // C 4
 B10000001,
 B10000001,
 B01000010,
 B11111111, // D 4
 B10000001,
 B10000001,
 B01111110,
 B11111111, // E 4
 B10010001,
 B10010001,
 B10000001,
 B11111111, // F 4
 B00001001,
 B00001001,
 B00000001,
 B01111110, // G 4
 B10000001,
 B10010001,
 B11110010

请看下图,它显示了相同的数据横向旋转,您可以更好地看到字符位图。

它们的位图定义中字母之间没有空格,并且上面看到的第一个四个大写字母都只使用 4 个字节,而不是第一次实现所需的 8 个字节。

每当需要字符位图时,该字符会在 `strCharList` 字符串中搜索,其在该字符串中的索引用于累加 `strCharWidth` 字符串中所有与 `strCharList` 长度匹配并跟踪每个字符位图宽度的整数值。通过这种方式,累加所需字符之前所有字符宽度的整数值,即可获得该字符在 `bytFont` 数组中的起始字节索引,然后将该字符的宽度值添加到其起始索引中,以确定其结束索引。通过这两个索引,位图从 `bytFont` 数组中读取,并创建一个 8 字节数组,该数组使字符居中并返回给调用函数,所有字符都使用类似于上面提到的滚动文本项目实现的算法绘制。

有限状态机确实有其目的

滚动文本项目只做一件事:无限地滚动一个字符串。但“下落的方块”游戏有不同的内容要说,这取决于它在游戏中的位置。在程序开始时,它会以无限循环向上滚动字符串“`Falling Blocks 2020 HIGH SCORE `”,直到玩家按下 `RotateRight` 按钮启动游戏。此时,滚动停止,游戏进行,直到玩家按下暂停按钮组合,或游戏结束。暂停模式会垂直滚动不同的字符串,就像介绍文本一样,但在游戏结束时,事情会变得有点复杂。最初,“游戏结束”文本也是垂直滚动的,但我发现它太慢了,需要太长时间才能让玩家知道他的最终分数。所以我实现了一个*横向滚动*功能,让 GAME 和 OVER 这两个四字母单词从右侧滚动进来。字母仍然像以前一样垂直,但第一个单词 GAME 从右侧一次一个位图列进入屏幕,直到单词水平居中,此时它会短暂等待,然后对第二个单词 OVER 重复此操作,再次等待,然后以类似的方式告诉玩家他的分数。在这里,它必须决定玩家是否击败了高分。如果有新高分,则 HIGH 和 SCRE 滚动进来,UI 允许用户输入他的名字。一旦 UI 完成并且高分者的名字已输入,它就会存储在 EEPROM 中(下面会详细介绍),显示屏会继续循环显示 GAME + OVER + + HIGH + SCORE + + ,就像玩家没有击败高分时通常显示的那样。

你可以看到,`if`、`else` 和嵌入的 `what else` 会把这弄得像意大利面条一样乱七八糟,所以使用 FSM 来实现这一切。

以下是该 FSM 可能的状态列表

enum enuFSMScrollingText
{
 FSMScrollingText_IntroInit,                     // 1
 FSMScrollingText_Intro,                         // 2
 FSMScrollingText_PauseInit,                     // 3
 FSMScrollingText_Pause,                         // 4
 FSMScrollingText_ScoreValue_Init,               // 5
 FSMScrollingText_ScoreValue,                    // 6
 FSMScrollingText_ScoreValue_Delay,              // 7
 FSMScrollingText_GameInit,                      // 8
 FSMScrollingText_Game,                          // 9
 FSMScrollingText_OverInit,                      // 10
 FSMScrollingText_Over,                          // 11
 FSMScrollingText_GameOver_Delay,                // 12
 FSMScrollingText_HighScore_High_Init,           // 13
 FSMScrollingText_HighScore_High,                // 14
 FSMScrollingText_HighScore_SCRE_Init,           // 15
 FSMScrollingText_HighScore_SCRE,                // 16
 FSMScrollingText_HighScore_Delay,               // 17
 FSMScrollingText_HighScoreValue_Init,           // 18
 FSMScrollingText_HighScoreValue,                // 19
 FSMScrollingText_HighScoreValue_Delay,          // 20
 FSMScrollingText_HighScore_PlayerName_UI,       // 21
 FSMScrollingText_HighScore_PlayerName_UI_Close, // 22
 FSMScrollingText_HighScore_PlayerName_Init,     // 23   
 FSMScrollingText_HighScore_PlayerName,          // 24
 FSMScrollingText_HighScore_PlayerName_Delay     // 25
};

由于本文前面已经解释了 FSM,因此这个特定的 FSM 应该不会太难理解。那些名称末尾带有“`Init`”的状态是定义要跟随的文本并准备好滚动到屏幕上的状态。每个“`Init`”状态之后的下一个状态是垂直滚动长字符串或从右侧水平滚动任何给定 4 字符字符串的状态。“`Delay`”表示屏幕保持静态一秒钟,然后进入下一个 FSM 状态。所有这些的例外是两个“`_UI`”状态,它们绕过任何滚动,让高分玩家输入他的名字,然后等待 Rotate-Right 按钮(玩家用于完成操作)被释放,以免被误认为是玩家开始新游戏的意图。

UI:输入高分者姓名

当滚动文本 FSM 进入 `FSMScrollingText_HighScore_PlayerName_UI` 状态时,因为玩家记录了高分,程序会进入一个函数,该函数会与用户交互,直到他按下 Rotate-Right 按钮,表示他已完成输入姓名。此函数会清除屏幕并将 `strHighScorePlayerName` 字符串重置为“`AAAA`”,并创建一个包含四个字符的数组,以跟踪玩家姓名中的每个字母。它使用一个整数变量来记住正在编辑哪个字符(对应于哪个矩阵数组正在闪烁),它会测试方向按钮的状态,并根据按下左或右按钮来滚动字母,或者在四个点阵显示器之间上下循环,允许玩家更改不同的字母。当玩家完成时,他可以按下 Rotate-Right 按钮,信息就会被保存。

记录高分

Arduino 微控制器都带有一些 EEPROM 存储器。即使断电,此存储器也会保存,并且可以在每次重新上电时用于初始化或重启您的项目。就 **下落的方块** 项目而言,我发现 EEPROM 对于存储高分值很有用。我可以用它来存储那 160 多个字节的字符位图,但我没有费心。因为 EEPROM 存储器的寿命为 100,000 次**写入**(读取不会损坏您的 EEPROM),所以最好不要将 EEPROM 存储器用于游戏变量或任何其他每天多次更改的变量,以防止您的 EEPROM 损坏并变得无用。他们建议,当您写入 EEPROM 时,首先测试您要覆盖的字节的值,看看它是否与您要覆盖它的值相同,因为如果是,您可以通过不必要地不覆盖内存字节来延长 EEPROM 的寿命。最好的方法是完全避免使用 `write()` 函数,而改用 `update()` 函数,因为它只会在旧值与您打算替换的新值不同时才写入目标地址。因此,使用 EEPROM 存储器来存储滚动字符的位图将是 EEPROM 的有效利用,如果需要的话我也会这样做,但由于该项目编译后还剩下 49% 的动态内存,因此绝不会出现内存不足的情况。

EEPROM 的 `read()`、`write()` 和 `update()` 函数都很简单,但我保存高分值的无符号整数时遇到了一些麻烦,因为我没有意识到一次只能保存一个字节,这在无符号整数的情况下需要两次操作而不是我所想象的一次。

构建

正如您在我在 YouTube 上发布的视频中看到的那样,这个项目的构建需要对回收箱进行一番翻找。首先,我剪断并拉直了一根旧衣架,然后将其弯曲成形并用电工胶带覆盖。我用镀锌钢丝将按钮控制器绑在弯曲衣架的底部,并确保没有任何裸露的钢材与按钮接线接触。然后我使用了两根塑料扎带,剪掉末端使其平整,并用胶带将其固定在盖子顶部,确保它们与弯曲衣架的形状对齐,以便稍后帮助固定。在这里,我在按钮控制器应该放置的地方测量了盖子旁边弯曲衣架,并使用丁烷打火机加热一把锋利的手术刀,整齐地切割特百惠塑料盖。此时,我在两个用胶带固定的扎带两侧的盖子上切了八个孔,以便我可以将未损坏的扎带穿过这些孔,并将衣架固定在盖子的底侧,将其紧密地贴合在我为它切出的边缘上。用胶带固定在盖子顶部的剪断扎带是为了确保穿过孔并围绕衣架的扎带不会撕裂特百惠塑料。两根扎带后,**Arduino Nano** 被牢牢固定在衣架上。我为电位器切了一个孔,将其穿过并拧紧,然后用热手术刀的尖端在蜂鸣器粘在底侧的上方烧了一系列小孔。我还必须切割特百惠的下半部分以将其安装到盖子上,然后测量 USB 线从微控制器伸出的位置,并在特百惠底部切了一个孔。

当我尝试这样玩游戏时,几乎没有什么可以握住的,所以我在Ragu意大利面酱盖子上打孔,并将其绑在衣架上,使其伸出按钮控制器下方。这仍然没有真正帮助,所以我在Ragu盖子的背面又打了两个孔,并用一根筷子横向绑住它,这样我的手掌底部就会向下按压它,而我的手指则握住控制器,我用拇指控制它。虽然不美观,但它确实有效。

这就是我的项目。我有一些可充电电池和将它们连接到我的 Arduino Nano 所需的部件,这样我就可以在没有 USB 线的情况下玩游戏,但这一切都还在从某个地方来的慢船上……所以也许在一个月、两个月或三个月后,我会向您更新。也许,在我完成之前,我会让这些部件移动两比特或更多。

在此之前,祝您居家 Arduino 愉快!

历史

  • 2020 年 8 月 18 日:首次发布
© . All rights reserved.