Arduino移植Chrome恐龙游戏:OLED显示与嵌入式图形编程实战
1. 项目概述与核心价值相信不少朋友在Chrome浏览器断网时都曾与那只像素风的小恐龙有过一面之缘。这个看似简单的跳跃游戏背后却蕴含着嵌入式图形编程的经典逻辑状态机控制、实时渲染、碰撞检测以及人机交互。今天我们不满足于在浏览器里玩而是要亲手将它“移植”到一块小小的Arduino开发板和0.96英寸的OLED屏幕上。这不仅仅是一个复刻游戏的项目更是一次深入理解微控制器如何驱动显示设备、处理图形数据并实现实时交互的绝佳实践。对于嵌入式开发初学者而言这个项目是一个完美的综合练习。它串联了硬件连接I2C通信、软件库使用Adafruit GFX SSD1306、图像数据处理将图片转换为微控制器可识别的字节数组以及游戏逻辑编程循环、事件检测、物理模拟等多个核心技能点。最终你将获得一个可以独立运行、通过串口指令控制的实体化恐龙游戏机其成就感远超单纯调用一个现成的API。无论你是想学习Arduino图形编程还是为你的智能硬件项目增加一个有趣的显示交互案例这个教程都将提供一条清晰的路径。2. 硬件选型与电路连接解析2.1 核心硬件清单与选型理由要实现这个项目你需要准备以下硬件每一件的选择都有其考量主控板Arduino Uno 或 Arduino Mega理由Arduino Uno是最常见且性价比高的选择其ATmega328P芯片的存储空间32KB Flash 2KB RAM和计算能力足以应对本游戏的逻辑。如果未来你想扩展更复杂的图形或音效Mega2560更大的内存和更多的I/O口会更有优势。本项目代码在Uno上运行流畅因此我们将以Uno为例进行讲解。显示模块0.96英寸 I2C接口 SSD1306 OLED屏分辨率128x64理由OLED屏幕自发光、对比度高、响应速度快非常适合动态图像显示。选择I2C接口通常有4个引脚VCC, GND, SCL, SDA而非SPI接口可以节省宝贵的I/O引脚仅需2个接线也更简单。128x64的分辨率在保持足够显示细节的同时又不会给Arduino的RAM和刷新带来过大压力。连接线杜邦线若干理由用于连接开发板与显示屏。建议使用公对公杜邦线。可选按键模块或导线理由原教程通过串口发送数字“5”来控制跳跃。为了获得更接近真实游戏的体验我们可以额外添加一个物理按键将其连接到Arduino的某个数字引脚并通过中断或轮询来检测按键从而替代串口控制。这将使项目完成度更高。注意购买OLED模块时请确认驱动芯片是SSD1306并且支持3.3V或5V逻辑电平。大多数模块都自带稳压芯片可以直接用Arduino的5V供电。2.2 电路连接图与原理说明连接非常简单遵循I2C设备的通用接法OLED显示屏引脚连接至 Arduino Uno 引脚作用说明VCC5V电源正极。为模块供电。GNDGND电源地线。与Arduino共地。SCLA5I2C时钟线。在Arduino Uno上I2C的SCL固定为模拟引脚A5。SDAA4I2C数据线。在Arduino Uno上I2C的SDA固定为模拟引脚A4。连接示意图文字描述 将OLED模块的4个引脚按上述表格用杜邦线依次连接到Arduino Uno的对应引脚即可。确保连接牢固避免虚接。为什么是A4和A5在Arduino的底层Wire库I2C通信库已经将A4和A5引脚映射为专用的SDA和SCL功能。即使你使用的是数字引脚编号其物理引脚也是这两个。这是硬件设计决定的不可更改。如果使用物理按键 将一个常开按键的一端连接到Arduino的某个数字引脚例如引脚2另一端接地。同时在该数字引脚和5V之间连接一个10kΩ的上拉电阻Arduino内部有上拉电阻可通过pinMode(pin, INPUT_PULLUP)启用这样外部就可以省略电阻。当按键按下时引脚从高电平5V被拉低到低电平0V程序通过检测这个变化来触发跳跃。3. 软件开发环境搭建与核心库详解3.1 Arduino IDE配置与库安装安装Arduino IDE从Arduino官网下载并安装最新版的IDE。这是最基础的步骤。安装必需的库本项目依赖于Adafruit的两个核心图形库。Adafruit GFX Library这是一个底层的图形库提供了画点、线、圆、矩形、文本以及绘制位图Bitmap的基础函数。它是所有Adafruit显示驱动的基石。Adafruit SSD1306 Library这是专门为SSD1306驱动芯片的OLED屏编写的驱动库。它基于GFX库实现了针对该型号屏幕的初始化、缓冲区和显示控制。安装方法 打开Arduino IDE点击工具-管理库...打开库管理器。在搜索框中分别搜索“Adafruit GFX”和“Adafruit SSD1306”找到并安装它们。通常安装SSD1306库时IDE会提示你依赖的GFX库尚未安装点击“安装所有”即可。选择开发板与端口将Arduino Uno通过USB线连接至电脑。在IDE中点击工具-开发板-Arduino AVR Boards-Arduino Uno。点击工具-端口选择对应的COM口Windows或/dev/cu.usbmodemXXXMac。3.2 核心代码结构深度解析原教程提供了代码片段我们将在此基础上构建一个更健壮、更易理解的完整项目。首先理解整个程序的骨架。// 1. 包含必要的头文件 #include Wire.h // I2C通信库 #include Adafruit_GFX.h // 核心图形库 #include Adafruit_SSD1306.h // OLED驱动库 // 2. 定义屏幕和游戏对象的常量 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 // 如果模块没有RESET引脚则设为-1 #define SCREEN_ADDRESS 0x3C // I2C地址通常是0x3C或0x3D #define DINO_WIDTH 25 #define DINO_HEIGHT 26 #define DINO_INIT_X 10 #define DINO_INIT_Y 29 // 恐龙初始位置Y坐标基于地面线计算 #define BASE_LINE_Y 54 // 地面线的Y坐标 #define JUMP_HEIGHT 22 // 最大跳跃像素高度 // 3. 声明全局对象和变量 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); // 恐龙和障碍物的位图数据字节数组后续会讲解如何生成 static const unsigned char PROGMEM dino_bitmap[] { ... }; static const unsigned char PROGMEM cactus1_bitmap[] { ... }; static const unsigned char PROGMEM cactus2_bitmap[] { ... }; // 游戏状态变量 int dinoY DINO_INIT_Y; int cactusX SCREEN_WIDTH; // 第一个障碍物从屏幕右侧外出现 int cactusType 0; bool isJumping false; bool isFalling false; int score 0; void setup() { Serial.begin(9600); // 初始化串口用于调试和接收指令 // 初始化OLED显示 if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F(SSD1306分配失败请检查连线)); for(;;); // 卡死在这里不再继续执行 } display.clearDisplay(); // 清空显示缓冲区 display.display(); // 将缓冲区内容刷到屏幕上此时是黑屏 showStartScreen(); // 显示开始游戏界面 } void loop() { // 主循环将根据游戏状态开始、游戏中、结束调用不同的函数 // 这是一个简单的状态机实现 if (gameState STATE_PLAYING) { runGameLogic(); // 处理输入、更新物体位置、检测碰撞 renderGameFrame(); // 绘制当前帧 } else if (gameState STATE_GAME_OVER) { showGameOverScreen(); } // 添加一个小的延时来控制游戏帧率避免刷新过快 delay(20); // 大约50帧/秒 }关键点解析PROGMEM关键字这是Arduino特有的修饰符用于将大型的常量数据如图像字节数组存储在Flash程序存储器中而不是宝贵的SRAM中。这对于内存有限的Uno来说至关重要。Adafruit_SSD1306 display(...)实例化一个显示对象。参数依次是宽度、高度、通信方式Wire表示I2C、复位引脚。SSD1306_SWITCHCAPVCC这是一个参数告诉驱动库从芯片内部产生一个高压来驱动OLED像素通常传入这个值即可。双缓冲思想display.clearDisplay()、drawBitmap()、drawLine()等函数都是在操作一个位于内存中的“显示缓冲区”。只有调用display.display()时缓冲区的内容才会一次性发送到OLED屏幕。这避免了屏幕闪烁是图形编程的常见技巧。4. 从图片到代码图像数据处理实战这是嵌入式图形项目中最具特色的一环。我们无法直接在代码里写“画一个恐龙”而是需要将恐龙的图片转换成微控制器能理解的一串十六进制数字。4.1 图像转换工具与原理原教程提到了javl.github.io/image2cpp/这个在线工具它非常好用。其工作原理如下准备图片你需要一张恐龙的图片。最好是黑白1位深度的BMP或PNG格式尺寸为25x26像素与我们定义的DINO_WIDTH和DINO_HEIGHT一致。可以在画图工具中手动绘制或从网上寻找像素图后缩放。像素到字节工具会按行扫描你的图片。每个像素如果是白色或透明对应位就是1如果是黑色就是0。每8个像素位组合成一个字节Byte。例如一行25个像素需要ceil(25/8) 4个字节来表示。生成数组工具将所有这些字节按顺序排列生成一个C语言格式的数组就是我们代码里的dino_bitmap。4.2 使用image2cpp的详细步骤与避坑指南打开https://javl.github.io/image2cpp/。上传图片点击“选择图片”上传你的恐龙、仙人掌图片。关键设置Canvas size保持默认或与图片尺寸一致。如果图片尺寸不对可以在这里调整。Brightness threshold阈值。调整滑块确保预览图中你的图案是白色的背景是黑色的。如果原图不是纯黑白这个设置很重要。扫描方向默认的“自上而下从左到右”通常就正确。输出格式Code format选择Arduino Code。Draw mode选择Vertical - 1 bit per pixel。这是SSD1306库drawBitmap函数期望的格式。Use PROGMEM务必勾选这将把数组放入Flash。Variable name填写你想要的数组名如dino_bitmap。点击“Generate code”工具会生成类似下面的代码static const unsigned char PROGMEM dino_bitmap [] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x00, 0x00, 0x06, 0xff, 0x00, 0x00, 0x0e, 0xff, 0x00, // ... 更多字节数据 };复制粘贴将生成的整个数组声明复制到你的Arduino代码中替换掉原来的占位符。实操心得图片背景最好是纯黑图案是纯白这样转换最准确。转换后务必在代码中检查数组名和尺寸定义DINO_WIDTH,DINO_HEIGHT是否与图片实际像素尺寸匹配。不匹配会导致显示错乱。可以先在工具中预览生成的位图确认图案正确后再复制代码。5. 游戏核心逻辑实现与代码逐行解读现在进入最核心的部分让游戏动起来。我们将构建一个简单的游戏循环处理跳跃、障碍物移动和碰撞检测。5.1 游戏状态机与主循环设计一个游戏通常由几个状态组成开始、进行中、结束。我们用enum或整数变量来管理。enum GameState { STATE_START, STATE_PLAYING, STATE_GAME_OVER }; GameState gameState STATE_START; void loop() { switch (gameState) { case STATE_START: // 等待开始指令如串口输入‘1’或按键按下 if (checkStartCommand()) { resetGame(); // 重置分数、恐龙和障碍物位置 gameState STATE_PLAYING; } break; case STATE_PLAYING: handleInput(); // 处理跳跃指令 updateGameWorld(); // 更新恐龙和障碍物位置 if (checkCollision()) { gameState STATE_GAME_OVER; // 碰撞检测 } renderFrame(); // 绘制画面 break; case STATE_GAME_OVER: showGameOverScreen(); // 等待重新开始指令 if (checkRestartCommand()) { gameState STATE_START; } break; } delay(GAME_FRAME_DELAY); // 控制游戏速度 }5.2 跳跃物理的简化模拟真实的跳跃有加速度和减速度这里我们用一个简化的两段式模型来模拟既节省计算资源效果也足够好。#define JUMP_HEIGHT 22 // 最大跳跃高度像素 #define GRAVITY 1 // 下落加速度像素每帧 int dinoY DINO_INIT_Y; // 恐龙当前Y坐标 int jumpVelocity 0; // 跳跃速度 bool isJumping false; void updateDino() { if (isJumping) { // 上升阶段给一个向上的初速度然后受“重力”减速 dinoY - jumpVelocity; jumpVelocity - GRAVITY; // 速度递减 // 当速度减到0或以下开始下落 if (jumpVelocity 0) { // 注意这里没有立刻切换状态而是让速度变负自然进入下落计算 // 也可以设计为上升和下落两个明确的状态 } // 如果落回地面 if (dinoY DINO_INIT_Y) { dinoY DINO_INIT_Y; isJumping false; jumpVelocity 0; } } } void initiateJump() { if (!isJumping) { // 只有在地面上才能起跳 isJumping true; jumpVelocity 10; // 赋予一个向上的初速度这个值影响跳跃高度和手感 } }为什么这样设计jumpVelocity初始为10每帧减少GRAVITY1。恐龙的位置变化是dinoY - velocity。所以第一帧上升10像素第二帧上升9像素...直到速度减为0达到最高点。之后速度变为负值dinoY - (-1)等价于dinoY 1开始下落。这种模拟比原教程中简单的“上升22像素再下降22像素”更平滑、更接近真实物理。5.3 障碍物生成与移动逻辑障碍物仙人掌需要从屏幕右侧不断向左移动移出屏幕左侧后在右侧重新生成并且类型大小随机。int cactusX SCREEN_WIDTH; // X坐标从屏幕最右侧开始 int cactusType 0; // 0代表小仙人掌1代表大仙人掌 int cactusWidth; // 根据类型确定的宽度 void updateCactus() { // 向左移动 cactusX - 2; // 移动速度影响游戏难度 // 如果障碍物完全移出屏幕左侧 if (cactusX -cactusWidth) { // 重置到屏幕右侧外 cactusX SCREEN_WIDTH; // 随机选择下一个障碍物类型 cactusType random(0, 2); // random(min, max) 生成 [min, max) 区间的整数 cactusWidth (cactusType 0) ? TREE1_WIDTH : TREE2_WIDTH; // 增加分数 score; } }5.4 碰撞检测的精髓与优化碰撞检测是游戏逻辑的“裁判”。我们需要判断恐龙的碰撞框和障碍物的碰撞框是否发生了重叠。bool checkCollision() { // 定义恐龙的碰撞框。通常比位图视觉大小稍小以提升游戏体验不那么容易死。 int dinoLeft DINO_INIT_X; int dinoRight DINO_INIT_X DINO_WIDTH - 5; // 右侧留点余量 int dinoTop dinoY; int dinoBottom dinoY DINO_HEIGHT; // 定义障碍物的碰撞框 int cactusLeft cactusX; int cactusRight cactusX cactusWidth; int cactusTop TREE_Y; // 障碍物固定的Y坐标 int cactusBottom TREE_Y TREE1_HEIGHT; // 假设高度一致 // 轴对齐边界框AABB碰撞检测算法 // 原理如果两个矩形在X轴和Y轴上的投影都重叠则它们碰撞。 bool overlapX (dinoRight cactusLeft) (dinoLeft cactusRight); bool overlapY (dinoBottom cactusTop) (dinoTop cactusBottom); return overlapX overlapY; // 只有X和Y方向都重叠才算碰撞 }注意事项碰撞框调整直接使用位图尺寸作为碰撞框往往会让游戏变得“不近人情”。适当缩小碰撞框例如恐龙右侧和底部留出几个像素的空白是游戏设计的常见技巧能让玩家感觉更公平。性能考量AABB检测是效率最高的2D碰撞检测之一只涉及简单的数值比较非常适合在资源受限的微控制器上运行。避免使用复杂的几何运算。6. 功能增强与优化实践原教程实现了最基本的功能。作为一个完整的项目我们可以从以下几个方面进行增强使其更完善、更专业。6.1 用物理按键替代串口控制串口控制对于演示可行但体验不佳。改为物理按键是必须的一步。#define JUMP_BUTTON_PIN 2 // 假设按键接在数字引脚2 void setup() { // ... 其他初始化代码 pinMode(JUMP_BUTTON_PIN, INPUT_PULLUP); // 启用内部上拉电阻按键按下时引脚为LOW } void handleInput() { // 检测按键是否被按下低电平有效 if (digitalRead(JUMP_BUTTON_PIN) LOW) { // 简单的防抖延时一小段时间再读一次 delay(50); // 50毫秒防抖延时 if (digitalRead(JUMP_BUTTON_PIN) LOW) { initiateJump(); // 等待按键释放避免长按连续触发 while(digitalRead(JUMP_BUTTON_PIN) LOW) { delay(10); } } } }按键消抖机械按键在按下和弹起时触点会产生短暂的、快速的通断抖动程序可能会误判为多次按下。通过按下后延时几十毫秒再判断状态可以有效地滤除这个抖动这是嵌入式开发中处理按键输入的标准操作。6.2 实现多个障碍物与速度递增一个障碍物太简单。我们可以创建一个障碍物数组来管理多个并让游戏速度随着分数增加而变快。#define MAX_CACTI 2 // 最多同时存在2个障碍物 struct Cactus { int x; int type; int width; bool active; }; Cactus cacti[MAX_CACTI]; int gameSpeed 2; // 基础移动速度 int speedIncreaseThreshold 10; // 每得10分加速一次 void initCacti() { for (int i 0; i MAX_CACTI; i) { cacti[i].x SCREEN_WIDTH i * 60; // 让它们间隔出现 cacti[i].type random(0, 2); cacti[i].width (cacti[i].type 0) ? TREE1_WIDTH : TREE2_WIDTH; cacti[i].active true; } } void updateCacti() { for (int i 0; i MAX_CACTI; i) { if (cacti[i].active) { cacti[i].x - gameSpeed; if (cacti[i].x -cacti[i].width) { cacti[i].x SCREEN_WIDTH; cacti[i].type random(0, 2); cacti[i].width (cacti[i].type 0) ? TREE1_WIDTH : TREE2_WIDTH; score; // 每得一定分数增加游戏速度 if (score % speedIncreaseThreshold 0 gameSpeed 8) { gameSpeed; } } } } } // 碰撞检测也需要遍历所有活动的障碍物6.3 添加音效与视觉反馈进阶虽然OLED屏不能播放复杂音频但我们可以通过无源蜂鸣器发出简单的提示音并通过屏幕闪烁增加游戏反馈。添加蜂鸣器将一个无源蜂鸣器正极接Arduino的一个PWM引脚如~9负极接GND。在跳跃和游戏结束时用tone(pin, frequency, duration)函数发出声音。#define BUZZER_PIN 9 void playJumpSound() { tone(BUZZER_PIN, 523, 100); // 发出Do音持续100ms } void playGameOverSound() { tone(BUZZER_PIN, 349, 200); // Fa delay(250); tone(BUZZER_PIN, 294, 400); // Re }游戏结束闪烁效果void showGameOverScreen() { for (int i 0; i 3; i) { // 闪烁3次 display.clearDisplay(); display.display(); delay(200); // 绘制“Game Over”和分数 display.setTextSize(2); display.setCursor(20, 20); display.print(Game Over); // ... 绘制分数 display.display(); delay(200); } }7. 完整代码整合与烧录测试将以上所有模块整合成一个完整的.ino文件。由于代码较长这里提供整合后的框架和关键部分完整的代码文件你可以根据上述章节自行组装或从提供的GitHub链接获取。项目目录结构建议Dino_Game_Arduino/ ├── Dino_Game_Arduino.ino (主程序文件) ├── bitmaps.h (存放所有图像字节数组的头文件可选使主程序更整洁) └── README.md (项目说明)主程序文件 (Dino_Game_Arduino.ino) 核心框架#include Wire.h #include Adafruit_GFX.h #include Adafruit_SSD1306.h // 所有常量定义 #define SCREEN_WIDTH 128 // ... 其他定义 // 显示对象 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, Wire, OLED_RESET); // 图像数据放在这里或单独的.h文件 static const unsigned char PROGMEM dino_bitmap[] { ... }; // ... 其他位图 // 游戏全局变量 enum GameState { STATE_START, STATE_PLAYING, STATE_GAME_OVER }; GameState gameState STATE_START; int dinoY DINO_INIT_Y; // ... 其他变量 void setup() { Serial.begin(9600); // 初始化显示 if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { for(;;); } display.clearDisplay(); // 初始化按键引脚等 pinMode(JUMP_BUTTON_PIN, INPUT_PULLUP); showStartScreen(); } void loop() { switch (gameState) { case STATE_START: if (checkStartCommand()) { // 检查按键或串口 resetGame(); gameState STATE_PLAYING; } break; case STATE_PLAYING: handleInput(); updateGameWorld(); if (checkCollision()) { playGameOverSound(); gameState STATE_GAME_OVER; } renderFrame(); break; case STATE_GAME_OVER: showGameOverScreen(); if (checkRestartCommand()) { gameState STATE_START; } break; } delay(20); // 控制帧率 } // 以下是所有具体的函数实现 void handleInput() { ... } void updateGameWorld() { ... } bool checkCollision() { ... } void renderFrame() { ... } void showStartScreen() { ... } void showGameOverScreen() { ... } void resetGame() { ... } // ... 其他辅助函数烧录与测试步骤在Arduino IDE中打开整合好的.ino文件。确认开发板和端口选择正确。点击“验证”✓编译代码确保无错误。点击“上传”→将程序烧录到Arduino Uno。观察OLED屏幕应该先显示开始界面。按下连接好的跳跃按键或通过串口监视器发送1开始游戏发送5跳跃测试游戏是否正常运行。8. 常见问题排查与调试技巧在制作过程中你可能会遇到以下问题。这里提供一套排查思路。8.1 屏幕不亮或显示乱码检查电源和连线这是最常见的问题。确保VCC和GND没有接反SCL和SDA线连接正确且接触良好。可以用万用表测量OLED模块的VCC引脚是否有5V电压。检查I2C地址SSD1306的常见地址是0x3C但也有部分模块是0x3D。你可以运行一个简单的I2C扫描程序来确认地址。#include Wire.h void setup() { Wire.begin(); Serial.begin(9600); Serial.println(I2C Scanner ...); } void loop() { byte error, address; for(address 1; address 127; address ) { Wire.beginTransmission(address); error Wire.endTransmission(); if (error 0) { Serial.print(设备发现于地址 0x); Serial.println(address, HEX); } } delay(5000); }检查库和初始化确认Adafruit SSD1306库已正确安装。初始化失败时display.begin()会返回false程序中的for(;;);会使其卡住。检查串口监视器是否有“SSD1306 allocation failed”的错误信息。8.2 游戏运行卡顿或闪烁严重帧率控制loop()中如果没有delay刷新会极快可能导致屏幕刷新跟不上甚至让Arduino忙于绘图而无法及时检测输入。delay(20)约50FPS是一个合理的值。太小的延迟会导致卡顿太大的延迟会导致游戏不跟手。优化绘制代码只绘制变化的部分增量渲染但在这个小游戏中全屏清空重绘更简单且Arduino能胜任。确保display.display()只在完整绘制完一帧后调用一次。不要在绘制过程中多次调用。内存不足如果添加了太多图像或变量可能导致内存不足。使用Serial.println(freeMemory())函数需要额外库来监控剩余内存。将常量数据放入PROGMEM是关键。8.3 碰撞检测不准确或手感奇怪调试碰撞框在renderFrame()函数中临时添加代码用drawRect()函数将恐龙和障碍物的碰撞框画出来直观地看它们是否如你所想。// 在renderFrame()中绘制完所有游戏元素后临时添加调试图形 display.drawRect(dinoLeft, dinoTop, dinoRight-dinoLeft, dinoBottom-dinoTop, SSD1306_WHITE); display.drawRect(cactusLeft, cactusTop, cactusWidth, TREE1_HEIGHT, SSD1306_WHITE);调整碰撞框大小如第5.4节所述适当缩小视觉图像的碰撞框是标准做法。多试几次找到感觉最公平的尺寸。检查坐标计算确保恐龙和障碍物的坐标、宽度、高度变量在更新和检测时使用的是最新值。特别是恐龙的dinoY在跳跃过程中是实时变化的。8.4 按键无响应或连跳检查接线和上拉电阻如果使用内部上拉INPUT_PULLUP按键另一端应接地。按下时引脚应为低电平LOW。完善消抖逻辑第6.1节提供的消抖逻辑是基础版。更稳健的做法是使用状态机或记录按下/释放的时间戳来消抖但基础版对于这个游戏已足够。防止空中连跳在initiateJump()函数中通过if (!isJumping)这个条件确保了恐龙只有在地面状态时才能触发新的跳跃。这是防止空中连跳的关键。完成以上所有步骤后你应该得到了一个运行流畅、控制灵敏的Arduino版Chrome恐龙游戏。这个项目麻雀虽小五脏俱全涵盖了嵌入式开发从硬件到软件的多个核心概念。你可以在此基础上继续发挥比如添加更多的障碍物类型比如飞鸟、设计关卡、保存最高分到EEPROM甚至用多个OLED屏拼接成更大的显示区域。最重要的是通过这个实践你获得的不仅仅是游戏本身而是解决一系列实际问题的能力。