基于Arduino Uno与12864液晶屏的俄罗斯方块游戏开发全解析 1. 项目缘起为什么用Arduino做俄罗斯方块如果你玩过电子制作大概率听说过Arduino。它简单、开源是无数创客和电子爱好者的入门神器。但用Arduino Uno、一块12864液晶屏和一个红外遥控器来复刻经典的俄罗斯方块游戏这个想法听起来有点“复古”甚至有点“大材小用”毕竟现在随便一个手机或电脑上的模拟器画面和体验都要好得多。我最初做这个项目还真不是为了“玩”游戏本身。核心驱动力有三个相信也是很多硬件爱好者会共鸣的点第一验证硬件驱动与底层交互的可靠性。12864液晶屏通常指128x64像素的图形点阵屏是嵌入式开发中非常经典的显示模块驱动它需要理解并操作其内部的控制器如ST7920或KS0108涉及底层通信协议SPI或并行、显存映射、绘图函数编写。这比调用现成的图形库要“硬核”得多能让你真正理解“像素是如何被点亮”的。红外遥控则涉及38kHz载波信号的接收与解码是学习异步串行通信和状态机编程的绝佳案例。把这两者结合起来做一个实时交互的游戏是对硬件驱动稳定性和程序实时性的一次综合压力测试。第二在资源极度受限的环境下进行软件架构设计。Arduino Uno的核心是ATmega328P仅有2KB的SRAM和32KB的Flash。在这个“螺蛳壳里做道场”你需要精心设计每一个变量、每一段代码。游戏逻辑方块旋转、碰撞检测、消行判断、画面渲染、输入响应、计时器管理所有这些功能必须在几十毫秒的循环周期内协同工作不能有内存泄漏也不能有逻辑阻塞。这迫使你写出高效、模块化、可维护的代码这种锻炼对任何嵌入式开发者都至关重要。第三获得从无到有的完整创造乐趣。从点亮第一个像素到画出第一个方块再到方块受控下落、旋转最后实现完整的游戏逻辑和计分系统——这个过程充满了挑战和成就感。它不是一个简单的“库函数调用”而是一个从硬件连接到软件逻辑完全自主实现的微型系统工程。最终当你手握一个自己焊接、自己编程的“游戏机”用遥控器操控方块时那种满足感是无可替代的。所以这个项目远不止是“做一个游戏”。它是一个综合性的嵌入式系统开发练手项目涵盖了硬件接口、驱动开发、实时系统、状态机、数据结构与算法等多个核心知识点。接下来我将从硬件选型、驱动剖析、核心逻辑实现到调试优化完整拆解这个项目的每一个环节。2. 硬件选型与电路连接搭建你的微型游戏平台工欲善其事必先利其器。硬件是项目的基石选型和连接方式直接决定了后续软件开发的复杂度和最终效果的稳定性。2.1 核心控制器为什么是Arduino Uno在众多Arduino板卡中Uno是最经典、资源最“标准”的一款。它的ATmega328P微控制器运行在16MHz主频拥有32KB Flash程序存储空间和2KB SRAM运行内存。对于俄罗斯方块项目这个配置是“刚好够用”的典型。Flash空间编译后的游戏程序包含驱动、逻辑、字库通常在15-25KB之间32KB的Flash留有足够的余量方便后期增加功能如音效、更复杂的菜单。SRAM空间这是最紧张的资源。我们需要存储屏幕显存缓冲区如果使用双缓冲128x64像素单色需要1024字节1KB。游戏地图数组通常用一个二维数组表示10x20的网格每个格子占1字节约200字节。当前方块、下一个方块的数据结构。各种状态变量、计时器、分数等。栈空间和全局变量。 2KB的SRAM要求我们必须精打细算避免使用动态内存分配如malloc谨慎使用字符串并尽量使用PROGMEM将常量数据如方块形状数据、字模存放在Flash中使用时再读取到SRAM。选择Uno的另一个原因是其极佳的生态和稳定性。相关的库如IRremote用于红外U8g2或Adafruit_GFX用于屏幕对其支持非常成熟社区资料也最丰富。2.2 显示核心详解12864液晶屏市面上常见的12864屏主要有两种控制器ST7920和KS0108。它们驱动方式不同选错了库就无法点亮。ST7920控制器通常支持串行SPI和8位并行两种模式。SPI模式只需要3-4根数据线CS, SID, SCLK有时加RS接线简单节省IO口但刷新速度相对较慢。对于俄罗斯方块这种画面变化不极端频繁的应用SPI模式完全足够也是我推荐的方式。这种屏内部常带中文字库。KS0108控制器通常只支持8位并行模式需要至少11根线8根数据线3根控制线占用IO口多但刷新速度快。它一般不带字库需要自己取模。如何识别最可靠的方法是看屏幕背面主控芯片的型号。如果看不清可以尝试ST7920的屏在SPI模式下时序要求相对宽松而网上流传的“12864 Arduino例程”大多针对ST7920。为了项目简化我强烈建议你购买明确标注支持Arduino且控制器为ST7920的12864屏。接线示例以ST7920 SPI模式为例Arduino Uno 5V - 屏幕 VCCArduino Uno GND - 屏幕 GNDArduino Uno Pin 10 (SS) - 屏幕 CS (片选)Arduino Uno Pin 11 (MOSI) - 屏幕 SID (串行数据)Arduino Uno Pin 13 (SCK) - 屏幕 SCLK (串行时钟)Arduino Uno Pin 9 (可自定义) - 屏幕 RS (数据/命令选择)。有些屏的RS也叫A0。注意务必查阅你屏幕的具体资料表电源电压可能是5V也可能是3.3V接错会烧毁屏幕。背光可能需要串联一个限流电阻如220Ω再接到5V上。2.3 输入设备红外遥控器的解码我们使用最常见的NEC编码格式的红外遥控器就是家里电视空调那种。它通过发射38kHz的载波用脉冲间隔来表示“0”和“1”。我们需要一个红外接收头如VS1838B、HS0038。它有三个引脚VCC接5V、GND、OUT信号输出。OUT脚接到Arduino的一个中断引脚如Uno的Pin 2或Pin 3上以便能及时响应遥控信号。工作原理简述当按下遥控器按键时接收头会输出一系列高低电平脉冲。IRremote库会帮我们完成载波过滤和解码最终将这一串脉冲解析成一个具体的十六进制键值。我们的程序只需要判断这个键值并映射到相应的游戏操作左、右、下、旋转、暂停即可。接线Arduino 5V - 接收头 VCCArduino GND - 接收头 GNDArduino Pin 2 (中断0) - 接收头 OUT硬件连接好后建议先分别测试屏幕和红外接收头是否工作正常再进入复杂的游戏逻辑编写。3. 软件架构与核心驱动让硬件“活”起来硬件是躯体软件是灵魂。这一部分我们将深入代码层面构建游戏的基础框架。3.1 驱动层屏幕驱动库的选择与封装对于ST7920的12864屏有两个主流库可选U8g2和Adafruit_GFXAdafruit_ST7920。我推荐使用U8g2。为什么是U8g2单库全能U8g2支持超过250种显示器包括我们用的ST7920。你不需要组合多个库。性能与内存平衡它提供了完整的图形原语点、线、矩形、圆、位图和字体渲染功能。虽然它本身有一定内存开销但其设计考虑了微控制器的限制。缓冲机制灵活U8g2默认使用页面缓冲Page Buffer即一次只渲染一部分内容到屏幕而不是全屏双缓冲。这极大地节省了SRAM全屏双缓冲需要1KB而页面缓冲可能只需128字节。对于俄罗斯方块我们完全可以采用页面缓冲因为游戏区域更新是局部的。初始化与基础绘图#include U8g2lib.h // 使用硬件SPICS10, RS9 U8G2_ST7920_128X64_1_HW_SPI u8g2(U8G2_R0, /* cs*/ 10, /* dc*/ 9); void setup() { u8g2.begin(); u8g2.setFont(u8g2_font_6x10_tf); // 设置一个小的字体 u8g2.setDrawColor(1); // 设置绘制颜色为白色1 } void loop() { u8g2.firstPage(); // 开始页面缓冲绘制循环 do { // 在这里调用所有的绘图函数 u8g2.drawStr(0, 10, Hello World!); // 在(0,10)位置绘制字符串 u8g2.drawFrame(10,20,30,40); // 画一个矩形框 } while (u8g2.nextPage()); // 循环直到所有页面绘制完成并发送到屏幕 delay(1000); }这段代码是U8g2的典型用法。firstPage()和nextPage()构成的循环会智能地将整个屏幕分成若干“页”进行绘制和传输优化了内存使用和刷新效率。3.2 输入层红外解码与状态映射我们使用IRremote库来解码。安装库后代码非常简单#include IRremote.h #define IR_RECEIVE_PIN 2 // 对应中断0 void setup() { Serial.begin(9600); IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK); // 启动红外接收 } void loop() { if (IrReceiver.decode()) { // 如果解码到信号 Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX); // 打印原始键值16进制 IrReceiver.resume(); // 准备接收下一个信号 } }运行这段代码按下遥控器不同按键在串口监视器里记下每个按键对应的十六进制值。例如你可能会得到0xFF6897数字0、0xFF30CF数字1等。接下来我们需要将这些原始键值映射到游戏操作。一个好的做法是定义一个枚举和映射函数让主循环更清晰enum GameAction { ACTION_NONE, ACTION_LEFT, ACTION_RIGHT, ACTION_DOWN, ACTION_ROTATE, ACTION_PAUSE, ACTION_DROP }; GameAction getActionFromIR(unsigned long irValue) { switch (irValue) { case 0xFFA25D: // 假设这是CH-键 return ACTION_LEFT; case 0xFF629D: // CH return ACTION_RIGHT; case 0xFFE21D: // CH return ACTION_DOWN; case 0xFF22DD: // | return ACTION_ROTATE; case 0xFF02FD: // || return ACTION_PAUSE; case 0xFFC23D: // | return ACTION_DROP; // 一键下落 default: return ACTION_NONE; } }3.3 游戏逻辑层核心数据结构的定义这是游戏的大脑。我们需要定义几个核心的数据结构游戏地图 (Game Board)用一个二维数组board[HEIGHT][WIDTH]表示其中HEIGHT20行WIDTH10列。每个元素为0空或1有方块。为了简化碰撞检测我们可以在顶部额外设置几行不可见的“缓冲区”。方块 (Tetromino)俄罗斯方块有7种基本形状I, J, L, O, S, T, Z。每种形状有1-4种旋转状态。我们可以用一个三维数组来预定义所有形状的所有状态// 形状I, J, L, O, S, T, Z // 每种形状用一个4x4的矩阵表示其一个旋转状态 const uint8_t PROGMEM TETROMINOS[7][4][4] { // I { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} }, // ... 其他形状的定义 };将数据存放在PROGMEM中可以节省宝贵的SRAM。当前方块状态我们需要几个变量来跟踪当前正在下落的方块int currentType; // 当前方块类型 (0-6) int currentRotation; // 当前旋转状态 (0-3) int currentX, currentY; // 方块左上角在游戏地图中的坐标有了这些数据结构游戏的主循环框架就清晰了初始化清空地图生成第一个方块。循环开始检查红外输入更新方块位置或旋转。碰撞检测判断方块移动/旋转后是否与地图边界或已固定的方块冲突。下落每隔一定时间随等级加快方块自动下落一格。锁定与消行当方块无法下落时将其“固化”到地图中并检查是否有满行进行消除和计分。生成新方块检查新方块是否立即碰撞游戏结束。渲染将地图和当前方块绘制到屏幕上。4. 核心算法实现碰撞、旋转与消行这是游戏逻辑中最精妙的部分直接决定了游戏的手感和正确性。4.1 碰撞检测无处不在的边界检查在任何试图改变方块状态移动、旋转、自然下落之前都必须进行碰撞检测。我们定义一个函数bool checkCollision(int newX, int newY, int newRotation)。实现思路根据newRotation从PROGMEM中读取当前方块currentType对应的4x4形状矩阵。遍历这个4x4矩阵中的每一个“有效格子”值为1的格子。计算这个有效格子映射到游戏地图board中的坐标mapX newX col; mapY newY row;。判断条件是否超出地图左右边界mapX 0 || mapX WIDTH是否超出地图底部mapY HEIGHT注意顶部缓冲区允许负值。是否与地图board中已固化的方块重叠board[mapY][mapX] 1如果以上任何一条为真则发生碰撞返回true。这个函数是游戏物理规则的核心守卫。例如当玩家按下“左”键时程序会先计算newX currentX - 1然后调用checkCollision(newX, currentY, currentRotation)。如果返回false才执行currentX newX。4.2 方块旋转Wall Kick的实现简单的旋转就是改变currentRotation然后进行碰撞检测。但如果旋转后发生碰撞大多数现代俄罗斯方块游戏遵循标准规则会尝试进行“墙踢”Wall Kick——即让方块在旋转的同时尝试向旁边移动一格如果还是不行再尝试另一个方向给玩家更多的操作空间。基础Wall Kick表对于每种形状和从旋转状态A到状态B定义一组尝试的偏移量(x, y)。例如从状态0旋转到状态1时先尝试(0,0)原位置如果碰撞再尝试(-1,0)左移一格接着(-1,1)(0,-2)(-1,-2)。这个表是标准化的可以在Tetris指南中找到。简化实现对于第一个项目我们可以实现一个简化版旋转后如果发生碰撞则尝试将方块向左或向右微移一格例如先左后右如果移开后不碰撞则旋转成功且位置更新。这能解决大部分靠墙旋转的问题虽然不是完全标准但大大提升了游戏体验。bool tryRotate() { int newRotation (currentRotation 1) % 4; // 尝试1直接旋转 if (!checkCollision(currentX, currentY, newRotation)) { currentRotation newRotation; return true; } // 尝试2左移一格再旋转 if (!checkCollision(currentX - 1, currentY, newRotation)) { currentRotation newRotation; currentX--; return true; } // 尝试3右移一格再旋转 if (!checkCollision(currentX 1, currentY, newRotation)) { currentRotation newRotation; currentX; return true; } // 所有尝试都失败旋转被阻止 return false; }4.3 消行逻辑高效的数据搬移当方块被锁定后我们需要遍历地图的每一行检查是否全为1满行。高效消行算法从地图底部y HEIGHT-1向上扫描。如果发现某一行y是满行则计分例如消1行得100分同时消多行有倍数奖励。将该行以上的所有行整体向下移动一行。这是一个内存搬移操作for (int yy y; yy 0; yy--) { memcpy(board[yy], board[yy-1], WIDTH * sizeof(uint8_t)); }最顶行y0清零。由于移下来了一行当前行索引y需要再次检查因为可能移下来的又是一行满的所以y或者用while循环更合适。根据消除的行数提升游戏等级并相应减少方块自动下落的时间间隔加快游戏速度。这个算法是“原地”操作的不需要额外的缓冲区效率很高。5. 渲染优化与用户体验让游戏“跟手”在资源受限的Arduino上渲染效率直接影响游戏流畅度和操作手感。5.1 局部刷新避免全屏重绘U8g2的页面缓冲机制本身是一种优化但我们还可以在应用逻辑上做更进一步的优化——脏矩形更新。原理是只重绘屏幕上发生变化的部分。对于俄罗斯方块画面变化主要来自当前方块的移动/旋转需要擦除方块旧位置绘制新位置。方块锁定和消行动画需要更新一大片游戏区域。分数/等级/下一个方块预览的更新需要更新侧边栏信息区。实现策略在内存中维护一个和屏幕逻辑对应的“上一帧地图”副本。当方块移动后比较新旧位置计算出需要擦除和重绘的最小矩形区域。对于消行由于涉及多行清除和下落可以直接标记整个游戏区域为“脏区域”。在U8g2的绘制循环中只绘制这些“脏区域”内的内容。这需要你手动管理绘制坐标和范围稍微增加了代码复杂度但对于维持高帧率尤其是游戏后期速度很快时非常有效。一个更简单的折中方案是将屏幕分为几个逻辑区域如主游戏区、信息区每次循环固定重绘游戏区而信息区只在分数/等级变化时才重绘。这比全屏刷新要好。5.2 控制响应消除输入延迟红外接收是中断驱动的响应本身很快。延迟主要来自两个方面主循环阻塞如果loop()中某次操作如复杂的消行计算或全屏渲染耗时过长就会错过红外信号的处理。必须保证主循环每一次执行都非常快理想在几毫秒内。将耗时操作拆分、避免在循环中使用delay()是关键。下落逻辑与按键响应的协调经典的设计是使用一个状态机和定时器。方块的下落由定时器如millis()触发而按键响应是即时的。这确保了玩家可以随时快速移动或旋转方块而下落节奏保持不变。unsigned long lastDropTime 0; int dropInterval 1000; // 初始下落间隔1秒 void loop() { unsigned long currentTime millis(); // 1. 处理输入最高优先级 handleInput(); // 这个函数里调用getActionFromIR并立即改变currentX/Y/Rotation // 2. 定时下落 if (currentTime - lastDropTime dropInterval) { if (!moveDown()) { // 尝试下落一格如果失败则锁定 lockPiece(); clearLines(); spawnNewPiece(); if (checkGameOver()) { gameOver(); return; } } lastDropTime currentTime; } // 3. 渲染有条件刷新 if (screenDirty) { renderGame(); screenDirty false; } }5.3 视觉增强简单的动画与反馈即使是在单色屏上也可以提供良好的视觉反馈。锁定闪烁方块锁定前可以快速闪烁2-3次提示玩家。消行动画消行时可以让被消除的行闪烁几次再清除增加成就感。下一个方块预览在屏幕角落绘制下一个将要出现的方块让玩家有所准备。边框与网格绘制游戏区域的边框和淡淡的网格线能极大提升美观度和可玩性。这些效果只需要在渲染函数中增加一些简单的逻辑即可实现成本低体验提升明显。6. 调试、优化与深度踩坑实录做项目不可能一帆风顺尤其是这种软硬件结合的项目。下面分享几个我实际遇到的关键问题和解决方案。6.1 内存溢出最隐蔽的“杀手”症状程序运行一段时间后Arduino无故重启或者行为异常如屏幕乱码、按键无响应。排查过程首先怀疑堆栈溢出在SRAM仅2KB的系统里递归函数、大型局部数组非常危险。检查代码确保没有深递归并将大型数组如屏幕缓冲区备份定义为全局变量或静态变量。使用freeMemory()函数检查在网上可以找到一段代码用于输出当前剩余内存。在setup()和loop()的关键位置打印内存值观察其变化趋势。如果内存持续减少说明有内存泄漏。在Arduino C中内存泄漏通常源于错误使用new/delete或malloc/free。最安全的做法是完全避免动态内存分配。我的踩坑案例我最初为了“灵活”使用了一个String对象来拼接分数信息用于显示。String类在背后会进行动态内存分配和释放。在频繁的loop()中这导致了内存碎片并最终耗尽内存。解决方案对于微控制器永远使用字符数组char array和snprintf()来格式化字符串。char scoreStr[10]; // 固定大小的字符数组 snprintf(scoreStr, sizeof(scoreStr), S:%06lu, score); // 安全格式化 u8g2.drawStr(90, 10, scoreStr);检查PROGMEM的使用确保将常量数据如方块形状、字模正确存放在Flash中。使用pgm_read_byte()来读取它们。错误地直接访问PROGMEM数据如同访问普通数组会导致读取错误的数据也可能引发奇怪的问题。6.2 红外接收干扰与连击问题症状按键偶尔失灵或者按一次却触发多次动作连击。根因分析环境光干扰日光灯、节能灯等可能发出类似38kHz的红外脉冲干扰接收头。确保接收头不要直接对着强光源可以用深色热缩管或胶带包裹其感光部分。电源噪声电机、舵机等大电流设备与Arduino共用电源可能引起电压波动导致接收头工作不稳定。为Arduino使用独立的稳压电源或在接收头的VCC和GND之间并联一个10uF和0.1uF的电容进行滤波。软件消抖与信号处理IRremote库本身会处理一部分但遥控器信号可能不稳定。我们需要在应用层增加状态去抖。unsigned long lastValidIRTime 0; const unsigned long IR_DEBOUNCE_MS 150; // 去抖时间150毫秒 void handleInput() { if (IrReceiver.decode()) { unsigned long now millis(); if (now - lastValidIRTime IR_DEBOUNCE_MS) { unsigned long irCode IrReceiver.decodedIRData.decodedRawData; GameAction action getActionFromIR(irCode); if (action ! ACTION_NONE) { processGameAction(action); // 处理游戏动作 lastValidIRTime now; } } IrReceiver.resume(); } }这段代码确保在IR_DEBOUNCE_MS毫秒内只处理第一个有效的红外信号忽略之后的重复信号完美解决了连击问题。这个时间可以根据手感调整通常100-200ms比较合适。6.3 屏幕闪烁与撕裂感症状画面更新时能看到明显的闪烁或部分图像残留。分析与解决SPI通信速度Arduino的硬件SPI速度很快通常不是问题。但如果你用的是软件模拟SPIU8G2_ST7920_128X64_1_SW_SPI速度可能会慢到足以让人眼察觉到绘制过程。首选硬件SPI。U8g2的缓冲模式我们使用的是_1_页面缓冲。如果一次绘制操作跨越了U8g2内部定义的“页”的边界它可能会分两次发送导致短暂的画面不一致。解决方法是优化绘制顺序先绘制背景如网格、边框再绘制动态物体方块。并且尽量让一个完整的图形对象在同一“页”绘制周期内完成。全局更新与局部更新如果整个游戏区域每帧都重绘闪烁感会减轻但速度可能变慢。如果采用局部更新必须确保“擦除旧图形”和“绘制新图形”这两个操作在视觉上是连续的即在一个firstPage()...nextPage()循环内完成。绝对避免在两个独立的页面循环中分别执行擦除和绘制。实测技巧在我的项目中将屏幕的SPI时钟频率稍微调低一点通过U8g2的构造函数或setBusClock函数有时反而能提高稳定性减少乱码。这可能是由于屏线较长或干扰导致的。这需要根据实际情况试验。6.4 游戏逻辑的“幽灵”Bug症状方块有时会穿墙或者旋转后卡进已固定的方块里。排查这几乎总是碰撞检测函数checkCollision的边界条件没处理好。仔细检查坐标计算newX col和newY row。确保col和row遍历的是0到3。检查地图数组的索引board[mapY][mapX]。确保mapY和mapX在访问前是有效的。特别是mapY当方块在顶部缓冲区时它可能是负值在访问board数组前必须判断if (mapY 0)。旋转的特殊情况I型方块和O型方块的旋转中心点与其他方块不同。你的形状数据定义和碰撞检测计算是否考虑到了这一点确保4x4矩阵中方块的“重心”在旋转时是合理的。调试大法当出现诡异穿透时通过串口打印出currentX, currentY, currentRotation以及尝试移动/旋转后的新坐标然后手动计算是否应该碰撞。这是最直接的定位方法。7. 功能扩展与进阶玩法完成基础版本后你可以考虑添加更多功能让项目更具挑战性和趣味性。7.1 添加音效与振动反馈虽然Arduino Uno没有真正的模拟音频输出但可以通过无源蜂鸣器或压电扬声器在数字引脚上产生不同频率的方波实现简单的“哔哔”声。接线蜂鸣器正极接一个PIN如8负极接GND。最好串联一个100Ω电阻保护引脚。库可以使用tone()和noTone()函数。设计音效移动/旋转短促的“滴”声。锁定一个中频音。消行一个欢快的上升琶音快速播放几个不同频率。游戏结束一个低沉的长音。注意发声函数是阻塞的tone()会持续发声。为了避免影响游戏主循环音效应设计得非常短促或者使用状态机在非关键时间点播放。7.2 实现“下一个方块”预览与暂存功能下一个方块在游戏区域旁开辟一个小区域如4x4格子大小。在内存中维护一个nextType变量。在spawnNewPiece()函数中将nextType赋给currentType然后随机生成一个新的nextType。渲染时在预览区绘制nextType对应的方块。暂存方块这是一个经典功能Hold。按某个键如遥控器上的“功能”键可以将当前下落的方块存到一个holdType变量中并立即生成下一个方块或取出之前暂存的方块。如果之前没有暂存过则生成全新的方块。规则一次只能暂存一个方块且在下次暂存前不能连续使用。这增加了策略深度。7.3 使用EEPROM保存最高分ATmega328P内部有1KB的EEPROM可以掉电保存数据。我们可以用它来保存历史最高分。#include EEPROM.h #define EEPROM_ADDR_HIGHSCORE 0 // 定义一个地址 unsigned long highScore 0; void setup() { // 从EEPROM读取最高分 EEPROM.get(EEPROM_ADDR_HIGHSCORE, highScore); // EEPROM初始值可能是255需要判断 if (highScore 0xFFFFFFFF) { // 通常初始值 highScore 0; } } void gameOver() { if (score highScore) { highScore score; // 将新纪录写入EEPROM EEPROM.put(EEPROM_ADDR_HIGHSCORE, highScore); } // ... 显示游戏结束画面包含最高分 }重要提示EEPROM有写入寿命约10万次。不要在每个循环中都写入只在破纪录时写一次。7.4 移植到其他硬件平台这个项目的核心逻辑游戏状态、碰撞检测、消行算法是平台无关的。你可以轻松地将它移植到性能更强的平台以获得更炫酷的效果。ESP32拥有更快的双核处理器、更多内存、Wi-Fi/蓝牙。你可以驱动彩色OLED屏添加联网排行榜甚至通过蓝牙连接手机作为手柄。STM32性能强大生态丰富。你可以使用更高效的HAL库或直接寄存器操作来驱动屏幕实现更高的帧率。升级显示将12864单色屏换成IPS TFT彩色屏给不同形状的方块赋予不同的颜色游戏视觉效果将得到质的飞跃。移植的关键是重写驱动层显示和输入和主循环调度。游戏逻辑层几乎可以原封不动地复用。这正是良好软件架构设计的价值体现——关注点分离。从点亮第一颗像素到完成一个可以流畅游戏、带有分数和记忆的完整作品这个过程充满了硬件调试的曲折和代码逻辑的博弈。它不仅仅是一个游戏更是一个微缩的嵌入式系统原型涵盖了从底层驱动到上层应用、从资源管理到用户体验设计的完整链条。当你成功运行起自己编写的俄罗斯方块时所获得的不仅仅是玩游戏的快乐更是对“机器如何工作”的深刻理解以及那种亲手从零构建一个复杂系统的、无可替代的成就感。希望这份详尽的拆解能为你点亮自己那盏创意之灯提供足够的燃料和清晰的路径。