尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Arduino Uno实现Chrome小恐龙游戏:2KB内存下的嵌入式开发实战

Arduino Uno实现Chrome小恐龙游戏:2KB内存下的嵌入式开发实战 1. 项目缘起当Arduino遇上Chrome小恐龙如果你和我一样是个喜欢鼓捣硬件的玩家那你肯定对Chrome浏览器断网时出现的那只小恐龙不陌生。那个像素风的“Dino Game”几乎成了我们等待网络恢复时唯一的慰藉。有一天我看着手边吃灰的Arduino Uno和一块闲置的OLED屏幕突然冒出一个念头能不能让这块小小的开发板独立地跑起这个经典游戏不是简单地用Arduino模拟键盘去控制浏览器里的游戏而是让Arduino自己成为游戏主机从逻辑运算、画面渲染到玩家交互全部在板子上完成。这个想法听起来有点“杀鸡用牛刀”毕竟Arduino Uno的核心只是一颗8位的AVR单片机主频16MHz内存只有2KB。用它来运行一个哪怕是最简单的图形游戏似乎都有些捉襟见肘。但正是这种在有限资源下挑战极限的过程才是嵌入式开发的乐趣所在。它迫使你去思考每一个字节的用途优化每一行代码的效率最终在硬件限制的框架内创造出一个流畅、可玩的体验。这不仅仅是复刻一个游戏更是一次对嵌入式系统资源管理、实时渲染和状态机设计的实战演练。接下来我将带你一步步实现这个“Arduino运行小恐龙游戏”的项目。我们会从最核心的游戏逻辑与状态机设计讲起探讨如何在内存中高效地表示游戏世界然后深入到如何在低性能的I2C OLED屏幕上实现流畅的帧动画接着是交互部分如何用最少的硬件实现跳跃和开始功能最后我们还会聊聊性能调优以及如何将这个项目扩展得更酷比如加上音效或者联网排行榜。无论你是刚接触Arduino的新手还是想挑战极限的老鸟相信这个项目都能给你带来一些启发。2. 核心架构在2KB内存里构建一个游戏世界要让Arduino Uno跑起游戏首要任务就是设计一个极度精简且高效的数据结构与游戏循环。我们不能像在PC上那样随心所欲地创建对象、调用复杂的图形库必须精打细算。2.1 游戏世界的抽象与数据定义小恐龙游戏的核心元素非常简单一个可以跳跃的恐龙、随机生成的仙人掌障碍物、地面以及分数。我们需要用最小的数据结构来刻画它们。首先是恐龙的角色。它只需要几个状态站立、跳跃、下蹲如果我们实现这个功能。跳跃是一个抛物线运动我们需要记录它的纵坐标Y轴位置和垂直速度。在Arduino上我们通常用整数来表示坐标以节省浮点数运算的开销。// 恐龙结构体定义 struct Dino { int16_t x; // 横坐标固定值 int16_t y; // 纵坐标屏幕底部为基准 int16_t velocityY; // 垂直速度用于模拟跳跃重力 bool isJumping; // 跳跃状态标志 uint8_t width; // 碰撞框宽度 uint8_t height; // 碰撞框高度 };这里我选择了int16_t16位有符号整数而不是int是为了明确数据范围避免在不同平台上int长度不同带来的潜在问题。velocityY用整数模拟速度每次游戏循环更新位置时加上速度值同时速度再减去一个代表“重力”的常量就能模拟出跳跃上升和下落的过程。其次是障碍物。仙人掌是游戏难度递增的关键。我们需要一个数组来管理屏幕上同时存在的多个障碍物。每个障碍物需要记录其横坐标、类型不同高度的仙人掌和是否被越过用于计分和回收。// 障碍物结构体定义 struct Obstacle { int16_t x; // 横坐标 uint8_t type; // 类型0: 小仙人掌1: 大仙人掌等 bool passed; // 是否已被恐龙越过用于计分 uint8_t width; uint8_t height; }; // 由于内存有限我们不可能生成无限个定义一个最大数量池 #define MAX_OBSTACLES 3 Obstacle obstacles[MAX_OBSTACLES];地面通常用滚动的背景条来表示。我们可以用一个变量记录地面贴图的水平偏移量每帧向左移动一定像素当偏移超过一个贴图宽度时复位形成循环滚动的效果。最后是游戏状态机。游戏至少包含几个状态开始画面START、游戏中PLAYING、游戏结束GAME_OVER。用一个枚举变量来管理当前状态能让我们清晰地组织代码逻辑。enum GameState { START, PLAYING, GAME_OVER }; GameState currentGameState START;2.2 游戏主循环与时间控制Arduino的程序核心是loop()函数它会以尽可能快的速度循环执行。但对于游戏来说恒定且合适的帧率FPS至关重要。如果帧率不稳定游戏速度就会忽快忽慢如果帧率太高可能会因为计算和渲染来不及完成导致画面撕裂或逻辑错误。因此我们必须引入“帧时间控制”。思路是记录上一帧结束的时间点然后等待直到达到我们设定的帧间隔例如每秒30帧间隔约33毫秒再开始下一帧的计算与渲染。#define FRAME_TIME_MS 33 // 目标每帧耗时约30 FPS unsigned long lastFrameTime 0; void loop() { unsigned long currentTime millis(); // 如果距离上一帧的时间还没达到目标间隔就空等或处理其他轻量级任务 if (currentTime - lastFrameTime FRAME_TIME_MS) { return; // 或者 delay(1) 但不如return高效 } // 更新帧时间 lastFrameTime currentTime; // 1. 处理输入读取按钮 handleInput(); // 2. 更新游戏逻辑根据当前状态 updateGameLogic(); // 3. 渲染画面 renderGraphics(); }这种模式被称为“固定时间步长游戏循环”。它确保了游戏逻辑的更新频率是稳定的不受loop()执行速度波动的影响。在updateGameLogic()函数里我们会根据currentGameState来调用不同的更新函数比如在PLAYING状态下更新恐龙位置、障碍物位置、检查碰撞、增加分数等。注意这里使用return而不是delay(FRAME_TIME_MS)来等待。delay()函数会阻塞整个程序期间无法响应任何输入如按钮按下这对于需要实时交互的游戏是致命的。使用return则让loop()函数快速返回Arduino的底层系统可以继续处理其他事务虽然简单但这是保持响应性的好习惯直到时间条件满足再执行核心逻辑。3. 图形渲染让OLED屏“动”起来我们常用的0.96寸或1.3寸OLED屏幕分辨率通常是128x64或128x32像素通过I2C或SPI接口与Arduino通信。I2C版本更为常见但速度较慢。在这个项目中我们选择使用I2C接口的SSD1306驱动芯片的128x64 OLED屏并依赖Adafruit_SSD1306和Adafruit_GFX这两个库来简化绘图操作。3.1 初始化与基础绘图首先需要在代码中包含库并初始化屏幕对象。#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 // 如果屏幕有复位引脚则接其编号否则用-1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); void setup() { // 初始化I2C通信和显示屏 Wire.begin(); if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // I2C地址通常是0x3C或0x3D for(;;); // 初始化失败死循环 } display.clearDisplay(); // 清屏 display.setTextSize(1); display.setTextColor(SSD1306_WHITE); // 单色屏白色即点亮像素 }Adafruit_GFX库提供了丰富的绘图函数如drawPixel(),drawLine(),drawRect(),fillRect(),setCursor(),print()等。但要注意所有这些绘图函数都只是在内存中的一个“显示缓冲区”display buffer里修改像素数据并不会立刻在屏幕上显示。你必须调用display.display()函数才会将缓冲区的内容一次性发送到OLED屏幕。这个“双缓冲”机制对于防止画面撕裂至关重要。3.2 实现游戏元素的绘制我们的游戏元素都是像素风格的非常适合用绘制矩形和位图的方式来实现。恐龙我们可以用几个矩形拼出一只简单的恐龙。为了表现跳跃动画可以绘制两种姿态。更高级的做法是使用“位图数组”即用一个二维数组来定义每一帧恐龙的样子但这会消耗更多内存。为了简单和节省内存我们选择用动态计算坐标来绘制矩形。void drawDino(int16_t x, int16_t y, bool isJumping) { display.fillRect(x, y, 10, 20, SSD1306_WHITE); // 身体 display.fillRect(x8, y-5, 4, 5, SSD1306_WHITE); // 头 display.fillRect(x-2, y15, 4, 6, SSD1306_WHITE); // 左腿 display.fillRect(x8, y15, 4, 6, SSD1306_WHITE); // 右腿 // 可以根据isJumping状态调整腿的绘制表现奔跑动画 }障碍物仙人掌同样用矩形绘制。可以定义几种不同高度和宽度的类型。void drawObstacle(int16_t x, uint8_t type) { switch(type) { case 0: // 小仙人掌 display.fillRect(x, SCREEN_HEIGHT - 20, 5, 20, SSD1306_WHITE); display.fillRect(x-2, SCREEN_HEIGHT - 15, 2, 10, SSD1306_WHITE); display.fillRect(x5, SCREEN_HEIGHT - 15, 2, 10, SSD1306_WHITE); break; case 1: // 大仙人掌 display.fillRect(x, SCREEN_HEIGHT - 30, 8, 30, SSD1306_WHITE); // ... 更多细节 break; } }地面地面是循环滚动的。我们用一个变量groundOffset记录偏移量。int16_t groundOffset 0; void drawGround() { // 绘制一条地平线 display.drawFastHLine(0, SCREEN_HEIGHT - 1, SCREEN_WIDTH, SSD1306_WHITE); // 可以绘制一些不规则的地面纹理用短线段表示 for(int i 0; i SCREEN_WIDTH; i 10) { int height random(1, 4); // 随机高度 display.drawFastVLine((i - groundOffset) % SCREEN_WIDTH, SCREEN_HEIGHT - height, height, SSD1306_WHITE); } // 每帧更新偏移量实现滚动 groundOffset--; if(groundOffset -10) { groundOffset 0; } }分数和游戏状态文本使用setCursor()和print()函数即可。void drawScore(uint32_t score) { display.setCursor(SCREEN_WIDTH - 30, 0); display.print(SCORE:); display.print(score); } void drawGameOver() { display.setCursor(SCREEN_WIDTH/2 - 30, SCREEN_HEIGHT/2 - 5); display.print(GAME OVER); display.setCursor(SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2 5); display.print(Press to restart); }在每一帧的renderGraphics()函数中我们需要遵循一个固定的绘制顺序先清空缓冲区clearDisplay()然后绘制背景地面、云朵等接着绘制前景障碍物、恐龙最后绘制UI分数、状态文本最终调用display.display()提交。关键技巧局部刷新以提升性能。clearDisplay()和display.display()是相对耗时的操作尤其是display.display()需要传输整个128x648192位1024字节的缓冲区数据。如果你的游戏画面只有小部分区域变化比如恐龙跳跃、障碍物移动可以尝试只清除和重绘变化的部分而不是全屏刷新。但这会大大增加代码复杂度。对于小恐龙游戏这种几乎每帧全屏都在变化的场景全屏刷新是更简单直接的选择。提升性能的关键在于优化逻辑计算和减少绘图函数的调用次数例如用fillRect代替多个drawPixel。4. 玩家交互从按钮到“手感”游戏离不开交互。我们需要至少一个按钮来控制恐龙跳跃另一个按钮或复用同一个用于游戏开始/重新开始。4.1 硬件连接与消抖将按钮的一端连接到Arduino的某个数字引脚如引脚2另一端接地。在代码中将该引脚设置为INPUT_PULLUP模式。这样当按钮未按下时引脚通过内部上拉电阻读到高电平HIGH按下时引脚直接接地读到低电平LOW。#define BUTTON_JUMP_PIN 2 #define BUTTON_START_PIN 3 void setup() { pinMode(BUTTON_JUMP_PIN, INPUT_PULLUP); pinMode(BUTTON_START_PIN, INPUT_PULLUP); // ... 其他初始化 }机械按钮在按下和弹起时触点会因为物理抖动而在几毫秒内产生多次电平跳变。如果直接读取一次按压可能会被误判为多次。因此必须进行“消抖”。软件消抖的简单方法是当检测到引脚电平变化后等待一小段时间比如10-50毫秒再次读取引脚状态如果状态稳定则确认为一次有效的动作。bool isButtonPressed(int pin) { if (digitalRead(pin) LOW) { // 按钮按下为LOW delay(20); // 等待抖动过去 if (digitalRead(pin) LOW) { return true; // 确认按下 } } return false; }但是在游戏主循环中使用delay(20)是绝对要避免的它会严重破坏游戏帧率。正确的做法是使用状态机和时间戳来非阻塞地检测按钮。4.2 非阻塞式按钮检测与跳跃机制我们为每个按钮维护一个状态记录。struct Button { uint8_t pin; bool lastState; // 上一次读取的状态 bool pressed; // 当前是否处于“按下”状态已消抖 unsigned long lastDebounceTime; // 上次状态变化的时间 }; Button jumpButton {BUTTON_JUMP_PIN, HIGH, false, 0}; Button startButton {BUTTON_START_PIN, HIGH, false, 0}; void updateButton(Button btn) { bool reading digitalRead(btn.pin); // 如果读取到的状态和上次记录的状态不同重置消抖计时器 if (reading ! btn.lastState) { btn.lastDebounceTime millis(); } // 如果状态变化后已经过去了足够长的时间比如50ms if ((millis() - btn.lastDebounceTime) 50) { // 那么当前读取的状态就是稳定状态 // “按下”事件发生在上次稳定状态是未按下当前稳定状态是按下 if (!btn.pressed reading LOW) { btn.pressed true; // 标记为按下事件发生 } else if (reading HIGH) { btn.pressed false; // 按钮释放重置状态 } } btn.lastState reading; // 保存本次读取状态 } void handleInput() { updateButton(jumpButton); updateButton(startButton); if (currentGameState START startButton.pressed) { startGame(); } else if (currentGameState PLAYING jumpButton.pressed) { // 触发跳跃只有在恐龙在地面时才允许起跳 if (!dino.isJumping) { dino.velocityY JUMP_FORCE; // 赋予一个向上的初速度 dino.isJumping true; } } else if (currentGameState GAME_OVER startButton.pressed) { resetGame(); } // 注意这里处理完后并没有立刻将btn.pressed置为false。 // 我们通常在一帧的逻辑处理完毕后或者在确认动作已被响应后再重置它。 // 更常见的做法是将pressed视为一个“事件”在handleInput末尾统一清除。 }跳跃物理模拟在updateGameLogic()的PLAYING状态部分我们需要更新恐龙的位置。#define GRAVITY 1 #define JUMP_FORCE -15 // 负值表示向上 void updateDino() { if (dino.isJumping) { dino.y dino.velocityY; // 更新位置 dino.velocityY GRAVITY; // 速度受重力影响增加下落 // 如果恐龙落到地面以下则结束跳跃复位到地面 if (dino.y GROUND_Y) { dino.y GROUND_Y; dino.velocityY 0; dino.isJumping false; } } }这里GROUND_Y是地面所在的纵坐标。通过调整JUMP_FORCE和GRAVITY的值你可以改变跳跃的高度和手感。这是一个简单的欧拉积分模拟对于这种小游戏来说完全足够。避坑心得按钮事件的“边缘触发”与“电平触发”。上面我们实现的是“边缘触发”Edge Trigger即只在按钮从高到低变化的瞬间按下事件做出响应。这对于跳跃操作是合适的。但如果你希望实现“长按加速”等功能就需要检测“电平触发”Level Trigger即只要按钮处于按下状态就持续生效。区分并正确使用这两种模式是做好交互设计的基础。在我们的代码中btn.pressed在按钮稳定按下期间只会为true一次直到按钮释放并再次按下。如果你想在下一帧还能知道按钮是否被按住需要另一个变量如btn.isHeld来记录。5. 游戏逻辑的筋骨碰撞、生成与难度曲线游戏的核心玩法的实现集中在updateGameLogic()函数里。除了更新恐龙我们还需要管理障碍物和游戏规则。5.1 障碍物的生成与移动障碍物应该从屏幕右侧随机生成并以恒定的速度向左移动。当障碍物移出屏幕左侧后应该被回收并可以重新放入生成池。#define OBSTACLE_SPEED 5 #define OBSTACLE_MIN_GAP 80 // 障碍物间的最小水平间隔 #define OBSTACLE_MAX_GAP 150 void updateObstacles() { for (int i 0; i MAX_OBSTACLES; i) { if (obstacles[i].x -obstacles[i].width) { // 障碍物已完全移出屏幕可以回收并准备重新生成 obstacles[i].x SCREEN_WIDTH; // 先移到屏幕外右侧 obstacles[i].passed false; // 可以在这里随机化类型 obstacles[i].type random(0, 2); // 假设有2种类型 } else { // 向左移动障碍物 obstacles[i].x - OBSTACLE_SPEED; } } // 尝试生成新障碍物 static unsigned long lastSpawnTime 0; unsigned long currentTime millis(); // 每隔一个随机时间间隔尝试生成 if (currentTime - lastSpawnTime random(OBSTACLE_MIN_GAP/OBSTACLE_SPEED, OBSTACLE_MAX_GAP/OBSTACLE_SPEED) * FRAME_TIME_MS) { // 找一个空闲的在屏幕外右侧的障碍物槽位 for (int i 0; i MAX_OBSTACLES; i) { if (obstacles[i].x SCREEN_WIDTH) { // 空闲槽位 obstacles[i].x SCREEN_WIDTH; // 设置其类型、宽度、高度等属性 obstacles[i].type random(0, 2); obstacles[i].width (obstacles[i].type 0) ? 5 : 8; obstacles[i].height (obstacles[i].type 0) ? 20 : 30; obstacles[i].passed false; lastSpawnTime currentTime; break; // 一次只生成一个 } } } }这里用时间间隔来控制生成频率比单纯依赖距离更灵活也更容易实现随机的间隔。OBSTACLE_MIN_GAP和OBSTACLE_MAX_GAP除以速度再乘以帧时间换算成了以毫秒为单位的时间间隔范围。5.2 碰撞检测碰撞检测是游戏逻辑的关键。我们使用“轴对齐包围盒”AABB算法即检查两个矩形在X轴和Y轴上是否都有重叠。bool checkCollision(const Dino dino, const Obstacle obs) { // 计算两个矩形中心的相对距离 int16_t dinoCenterX dino.x dino.width / 2; int16_t dinoCenterY dino.y dino.height / 2; int16_t obsCenterX obs.x obs.width / 2; int16_t obsCenterY (SCREEN_HEIGHT - 1) - obs.height / 2; // 障碍物底部贴地 // 计算半宽高 int16_t dinoHalfW dino.width / 2; int16_t dinoHalfH dino.height / 2; int16_t obsHalfW obs.width / 2; int16_t obsHalfH obs.height / 2; // 判断在X轴和Y轴上是否分离 if (abs(dinoCenterX - obsCenterX) (dinoHalfW obsHalfW)) return false; if (abs(dinoCenterY - obsCenterY) (dinoHalfH obsHalfH)) return false; // 如果两个轴上都未分离则发生碰撞 return true; }在每一帧遍历所有在屏幕内的障碍物与恐龙进行碰撞检测。如果发生碰撞游戏状态切换到GAME_OVER。void checkCollisions() { for (int i 0; i MAX_OBSTACLES; i) { if (obstacles[i].x SCREEN_WIDTH obstacles[i].x -obstacles[i].width) { // 在屏幕内 if (checkCollision(dino, obstacles[i])) { currentGameState GAME_OVER; return; } // 检查是否越过障碍物用于计分 if (!obstacles[i].passed obstacles[i].x obstacles[i].width dino.x) { obstacles[i].passed true; score; } } } }5.3 难度递增机制一个有趣的游戏需要难度曲线。我们可以随着分数增加逐渐提高障碍物的移动速度或者减少生成间隔。// 在全局变量中定义 uint8_t baseObstacleSpeed 5; uint16_t score 0; // 在updateGameLogic中根据分数调整速度 void updateDifficulty() { // 每得100分速度增加1 uint8_t speedBonus score / 100; currentObstacleSpeed baseObstacleSpeed speedBonus; // 确保速度不超过一个上限 if (currentObstacleSpeed 12) { currentObstacleSpeed 12; } // 也可以调整生成间隔 // obstacleSpawnInterval max(MIN_INTERVAL, BASE_INTERVAL - score/50); }将currentObstacleSpeed应用到updateObstacles()函数中替换固定的OBSTACLE_SPEED。这样游戏玩得越久速度越快挑战性也越大。6. 性能优化与内存管理实战当把所有功能拼凑起来后你可能会发现游戏运行得不够流畅或者偶尔出现卡顿。在Arduino Uno的极限环境下性能优化是必须面对的挑战。6.1 profiling与瓶颈定位首先你需要知道时间花在哪里。一个简单的方法是在loop()中不同阶段用micros()记录时间戳。unsigned long logicTime, renderTime; void loop() { unsigned long frameStart micros(); // ... 处理输入 handleInput(); unsigned long logicStart micros(); // ... 更新游戏逻辑 updateGameLogic(); logicTime micros() - logicStart; unsigned long renderStart micros(); // ... 渲染图形 renderGraphics(); renderTime micros() - renderStart; unsigned long totalFrameTime micros() - frameStart; // 可以通过串口打印出来观察 // Serial.print(Logic: ); Serial.print(logicTime); // Serial.print( Render: ); Serial.print(renderTime); // Serial.print( Total: ); Serial.println(totalFrameTime); // ... 帧时间控制 }通过串口监视器你可以看到logicTime和renderTime各占多少微秒。通常渲染特别是display.display()是最大的开销。6.2 渲染优化策略减少绘制调用检查你的drawDino、drawObstacle等函数是否用了很多个drawPixel或drawLine尽量用fillRect代替一次函数调用能绘制多个像素。Adafruit_GFX库的fillRect是经过优化的。避免不必要的清屏我们目前每帧都clearDisplay()这是正确的。但如果你能精确知道哪些像素从上一帧的“亮”变成了这一帧的“灭”你可以只绘制那些需要改变的部分但这非常复杂。对于动态性强的游戏全清全绘更可靠。使用更快的通信协议如果你使用的是I2C OLED时钟频率通常默认100kHz或400kHz可以尝试将I2C时钟频率提高到最大允许值对于SSD1306很多库支持800kHz。在display.begin()之后可以尝试Wire.setClock(800000L);。注意这需要屏幕和连接线支持过高的频率可能导致通信失败。考虑SPI屏幕如果可能使用SPI接口的OLED屏幕。SPI的通信速率远高于I2C可以显著提升画面刷新率。但需要占用更多的引脚MOSI, SCK, CS, DC, RST。6.3 内存优化策略编译完成后查看Arduino IDE的输出信息关注“全局变量使用了多少字节的动态内存”。2KB的RAM非常紧张。使用PROGMEM存储常量数据比如恐龙的多种姿态位图、障碍物的多种形状位图这些数据在程序运行期间不会改变应该存放在Flash程序存储器中而不是RAM中。使用pgm_read_byte()函数来读取。// 将位图数据存放在Flash中 const uint8_t dinoBitmap[] PROGMEM { 0b00011100, 0b00011100, // ... 位图数据 }; // 绘制时从Flash读取 for(int i0; ibitmapHeight; i) { uint8_t line pgm_read_byte(dinoBitmap[i]); // ... 根据line的每一位来设置像素 }这能节省宝贵的RAM但会稍微增加代码复杂度和CPU开销。精简变量类型我们已经用了int16_t、uint8_t等。仔细检查每个变量是否可以用更小的类型比如分数score如果只是显示用uint16_t0-65535足够如果追求高分可能需要uint32_t。坐标x可能超过255所以用int16_t是合适的。减少全局变量将一些只在特定函数内使用的临时变量改为局部变量。局部变量在栈上分配函数返回后释放可以复用内存空间。使用F()宏包装字符串在串口打印或display.print()中直接使用字符串字面量如GAME OVER这些字符串默认也会被复制到RAM中。使用F()宏可以将其保留在Flash中。display.print(F(GAME OVER)); // 正确节省RAM // display.print(GAME OVER); // 错误消耗RAM6.4 逻辑计算优化避免浮点数运算如前所述全部使用整数运算。将速度、重力等参数放大一定倍数比如100倍作为整数处理在需要显示时再缩小。简化碰撞检测我们的AABB碰撞检测已经比较高效。如果性能还是吃紧可以先进行“粗略检测”比如只当障碍物与恐龙在X轴方向足够近时才进行精确的AABB检测。使用查表法对于一些复杂的计算如三角函数如果取值范围有限可以预先计算好结果存入数组放在PROGMEM中运行时直接查表。经过这些优化你应该能让游戏在Arduino Uno上以稳定30帧运行。如果仍然卡顿可能需要考虑降低帧率目标比如20FPS或者进一步简化图形比如使用更小的分辨率或更简单的图形元素。7. 项目扩展与进阶玩法让基础版本运行稳定后你可以尝试加入更多功能让项目更有趣。1. 添加音效使用一个无源蜂鸣器连接到PWM引脚。你可以为跳跃、碰撞、得分定义不同的简短旋律。使用tone()函数播放特定频率的声音。注意tone()会占用一个定时器可能与某些库冲突且播放时是阻塞的。对于简单的“哔”声影响不大对于复杂音乐可能需要非阻塞的库如Tone。2. 使用更好的显示设备升级到SPI接口的OLED或者使用IPS TFT液晶屏如ST7735驱动的屏幕可以获得更快的刷新率和更丰富的颜色如果是彩色屏。但需要更多的引脚和更复杂的驱动库。3. 加入“下蹲”动作增加一个按钮控制恐龙下蹲以躲避飞行的障碍物如果未来加入。这需要为恐龙增加一个下蹲状态并相应调整其碰撞框的高度和绘制图形。4. 制作实体外壳与按钮用3D打印或激光切割一个迷你游戏机外壳将Arduino、屏幕、按钮集成进去并用电池供电做成一个真正的便携式怀旧游戏机。5. 联网与排行榜高阶使用ESP8266或ESP32模块替换Arduino Uno。这些模块自带Wi-Fi功能性能也强大得多。你可以在游戏结束后将分数上传到某个网络服务如Thingspeak, Blynk甚至自己搭建的简单服务器实现全球排行榜。这涉及到网络编程和JSON数据处理是一个更大的挑战但也更有成就感。6. 双人对战模式脑洞使用两块Arduino通过串口或I2C通信让两个玩家在各自的屏幕上玩但障碍物是同步的比拼谁活得久。这需要设计一套简单的通信协议。实现这个项目的过程中最深的体会是嵌入式开发中“资源约束”与“创意实现”之间的博弈。每一字节的RAM每一微秒的CPU时间都变得无比珍贵。当你最终看到那只由你自己编写的代码驱动的小恐龙在小小的OLED屏幕上跳跃、奔跑成功躲过一个又一个障碍时那种成就感远超在PC上运行一个3A大作。它让你真切地感受到你不仅是在写软件更是在与硬件对话在物理世界的限制下创造出生动的数字生命。这或许就是硬件编程最原始的乐趣所在。
返回列表