
1. 项目概述当经典游戏遇上开源硬件相信不少朋友在浏览器断网时都遇到过那个经典的“小恐龙游戏”——一只像素风的小恐龙在无尽的沙漠中奔跑、跳跃躲避仙人掌和翼龙。这个由Chrome浏览器内置的彩蛋因其简单又上瘾的玩法成为了许多人的“摸鱼”神器。但你是否想过把这个虚拟世界里的游戏搬到现实中的桌面上让它变成一个看得见、摸得着的实体装置这就是我们今天要聊的项目用Arduino打造一个全自动运行的“小恐龙游戏”模拟器。这个项目的核心远不止是复现一个游戏那么简单。它本质上是一个集成了传感器、执行器和逻辑控制的微型自动化系统。我们不再用键盘的空格键控制恐龙跳跃而是通过超声波或红外传感器来“感知”障碍物然后驱动舵机或电机让一个实体的小恐龙模型做出跳跃动作。整个游戏逻辑包括障碍物的生成、速度的递增、分数的计算都由一块小小的Arduino开发板来运算和控制。对于硬件爱好者、创客或者嵌入式入门的学习者来说这是一个绝佳的练手项目。它涵盖了从传感器数据采集、信号处理、控制算法到执行机构驱动这一完整的嵌入式开发链路而且最终成果趣味性十足非常有成就感。我最初做这个项目是想给工作室做一个有趣的桌面摆件。在实现过程中从最初的简单触发跳跃到后来加入多种障碍物类型、可调的游戏难度甚至用OLED屏幕显示分数和游戏状态踩了不少坑也积累了很多实战经验。接下来我就把这套从零到一的实现方案包括硬件选型、电路连接、代码逻辑以及那些只有亲手做过才会知道的注意事项毫无保留地分享给大家。2. 核心硬件选型与电路设计思路动手之前先把“武器装备”整理清楚。一套稳定可靠的硬件是项目成功的基础。我们的目标是用最低的成本、最易得的模块实现核心的游戏逻辑。下面这张表格梳理了核心部件及其选型理由你可以根据自己的预算和需求进行调整。部件名称推荐型号/规格核心作用选型理由与备选方案主控板Arduino Uno R3系统大脑运行游戏逻辑处理传感器数据控制执行器。资源丰富14个数字I/O6个模拟输入社区支持强大性价比高。备选Arduino Nano更小巧或ESP32自带Wi-Fi/蓝牙可为未来联网功能留余地。障碍物检测传感器HC-SR04 超声波模块模拟游戏中“视线”检测前方是否有障碍物及距离。成本低测距范围2cm-400cm完全满足桌面场景精度足够。备选红外测距传感器如GP2Y0A21YK0F但易受环境光干扰或直接使用红外对管更便宜但只能判断有无无法测距。跳跃执行机构SG90 9g 微型舵机驱动小恐龙模型完成“跳跃”动作。扭矩足够驱动小型模型控制简单PWM信号价格低廉。备选直流电机连杆机构速度更快但需要额外的电机驱动板和更复杂的机械结构。游戏状态显示0.96寸 I2C OLED 屏幕显示实时分数、游戏速度、历史最高分及“Game Over”提示。I2C接口仅需2根线节省I/O口显示清晰功耗低比LCD1602显示效果更生动。备选LCD1602液晶屏更便宜但显示内容单调。恐龙与场景模型3D打印或手工制作构成游戏的实体视觉部分。核心是恐龙模型需要与舵机连接并能灵活地上下运动。可以用硬纸板、乐高积木或者3D打印制作。地形仙人掌、翼龙可以用轻质材料固定。电源5V 2A 直流电源适配器为整个系统供电。Arduino Uno和舵机同时工作时电流需求较大USB供电可能不足导致舵机抖动或板子重启因此推荐独立电源。注意电源是关键许多新手会直接用电脑USB口或者一个手机充电器供电当舵机动作时瞬间电流可能超过500mA导致电压被拉低整个系统不稳定。务必使用能提供5V/2A或以上电流的稳压电源并确保正负极连接正确。电路连接并不复杂但需要有条理。核心连接如下Arduino与HC-SR04超声波模块的Vcc接Arduino 5VGND接GNDTrig触发接数字引脚9Echo回响接数字引脚10。Arduino与SG90舵机舵机的红线正极接5V棕线负极接GND橙线信号线接数字引脚6Arduino上带PWM功能的引脚。Arduino与OLED屏幕OLED的VCC接5VGND接GNDSCL接A5SDA接A4这是Arduino Uno上I2C通信的固定引脚。独立供电将5V/2A电源适配器的正极通常是中心针接入Arduino的Vin引脚负极接入GND引脚。此时切记不要同时连接USB线供电以免电压冲突。所有连接建议使用面包板先进行测试确认功能无误后再考虑焊接或使用杜邦线永久连接。接线时特别是电源部分一定要再三确认接反了很可能烧毁模块。3. 游戏逻辑的代码实现与核心算法硬件是躯体代码才是灵魂。我们要在Arduino上模拟出小恐龙游戏的核心逻辑。这包括障碍物的随机生成、游戏速度的渐进增加、碰撞检测以及分数计算。由于Arduino性能有限我们无法渲染复杂的图形但可以用抽象的逻辑和实体的动作来完美模拟。3.1 核心状态机与变量定义首先我们需要定义游戏的状态。一个典型的有限状态机FSM就够用了START等待开始、RUNNING游戏中、JUMPING跳跃中、GAME_OVER游戏结束。我们用枚举类型来定义它。enum GameState { START, RUNNING, JUMPING, GAME_OVER }; GameState currentState START;接着定义核心游戏变量// 游戏核心变量 unsigned long score 0; // 分数 int gameSpeed 20; // 游戏速度值越小速度越快控制障碍物移动和循环延迟 int obstaclePosition 128; // 障碍物在“逻辑屏幕”上的位置假设屏幕宽度128像素对应超声波测距范围 int obstacleType; // 障碍物类型0-小仙人掌1-大仙人掌2-翼龙影响所需的跳跃高度 int dinoHeight 0; // 恐龙当前高度0为地面0为跳跃中 unsigned long lastUpdateTime 0; // 用于非阻塞延时的计时器 const int updateInterval 50; // 游戏主循环更新间隔毫秒 // 硬件相关变量 const int trigPin 9; const int echoPin 10; const int servoPin 6; const int jumpHeight 60; // 舵机转动角度对应恐龙跳跃高度 const int safeDistance 15; // 安全距离厘米小于此距离视为碰撞这里的关键是“非阻塞延时”。绝对不要在循环里用delay()来控制游戏节奏因为它会冻结整个程序导致传感器数据无法及时读取。我们使用millis()函数来记录时间确保游戏逻辑、传感器读取和显示刷新都能流畅并行。3.2 障碍物生成与碰撞检测算法障碍物系统是游戏性的核心。我们在逻辑上维护一个“游戏画面”假设画面宽度是128个单位障碍物从最右侧位置128向左移动。每个游戏周期障碍物位置减少一定值由gameSpeed决定。void updateGameLogic() { if (currentState ! RUNNING) return; // 1. 移动障碍物 obstaclePosition - (gameSpeed / 5); // 移动步进数值可调 // 2. 如果障碍物移出屏幕左侧则生成新障碍物 if (obstaclePosition 0) { score 1; // 成功躲避一个障碍物得分 // 每得10分增加一点游戏速度 if (score % 10 0 gameSpeed 5) { gameSpeed - 1; } obstaclePosition 128; // 重置到屏幕最右侧 obstacleType random(0, 3); // 随机生成0,1,2三种障碍物类型 } // 3. 碰撞检测核心 // 我们通过超声波实时测量前方障碍物距离 long distance measureDistance(); // 只有当障碍物逻辑位置进入“危险区域”例如位置30且恐龙处于地面高度时才进行精确碰撞判断 if (obstaclePosition 30 dinoHeight 0) { // 根据障碍物类型判断当前跳跃高度是否足够 bool isCollided false; switch (obstacleType) { case 0: // 小仙人掌需要任意高度跳跃即可 isCollided (distance safeDistance); // 如果实际距离太近撞上了 break; case 1: // 大仙人掌需要跳得更高 isCollided (distance safeDistance 5); // 判定距离更严格 break; case 2: // 翼龙需要精准时机在翼龙下方或上方 // 翼龙有飞行高度这里简化处理如果距离很近且恐龙没跳则撞上 isCollided (distance safeDistance dinoHeight 0); break; } if (isCollided) { currentState GAME_OVER; } } }measureDistance()函数封装了超声波测距的代码。这里有一个重要的实操心得超声波测距在近距离小于2cm时可能不准确或返回超大值需要在代码中加入数据过滤。我通常采用“中值滤波法”连续读取3-5次去掉最大最小值后取平均能有效消除偶发的错误数据。3.3 舵机控制与跳跃动画舵机控制恐龙跳跃需要模拟一个“起跳-悬空-下落”的过程而不是瞬间弹起落下。这可以通过控制舵机角度随时间变化来实现。void controlDinoJump() { static unsigned long jumpStartTime 0; static bool isJumping false; if (currentState JUMPING !isJumping) { // 开始跳跃 isJumping true; jumpStartTime millis(); dinoHeight jumpHeight; // 设置逻辑高度 } if (isJumping) { unsigned long jumpDuration millis() - jumpStartTime; // 跳跃周期约600ms300ms上升300ms下降 if (jumpDuration 300) { // 上升期角度从90度地面平滑增加到150度最高点 int angle map(jumpDuration, 0, 300, 90, 150); myServo.write(angle); } else if (jumpDuration 600) { // 下降期角度从150度平滑减少到90度 int angle map(jumpDuration, 300, 600, 150, 90); myServo.write(angle); } else { // 跳跃结束 isJumping false; dinoHeight 0; if (currentState JUMPING) { currentState RUNNING; // 跳跃完成后回到奔跑状态 } } } }这里用到了map()函数来做线性映射让舵机的转动更平滑。同时将跳跃状态isJumping和游戏状态JUMPING分开管理使得逻辑更清晰。触发跳跃的条件可以是在START状态时检测到有物体靠近传感器模拟“游戏开始”或者在RUNNING状态时检测到障碍物进入警戒范围。4. 传感器数据处理与抗干扰优化硬件项目中传感器数据的稳定性和可靠性直接决定了系统的成败。HC-SR04超声波模块在桌面环境下容易受到其他超声波源、复杂表面如绒毛桌布或供电波动的影响产生“飞点”突然一个极大或极小的错误值。4.1 实现软件滤波算法单纯的单次读取不可靠。我们必须对原始数据进行滤波。前面提到的中值滤波是简单有效的方法这里给出一个更健壮的实现const int numReadings 5; // 采样次数 int readings[numReadings]; // 采样数组 int readIndex 0; // 当前读数索引 long total 0; // 总和 long average 0; // 平均值 long getFilteredDistance() { // 移出旧的读数 total total - readings[readIndex]; // 读取新的原始距离 readings[readIndex] getRawDistance(); // 加入新的读数到总和 total total readings[readIndex]; // 移动到下一个位置 readIndex (readIndex 1) % numReadings; // 计算平均值 average total / numReadings; return average; } long getRawDistance() { // 标准的HC-SR04测距代码 digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); digitalWrite(trigPin, LOW); long duration pulseIn(echoPin, HIGH, 30000); // 设置超时30ms对应约5米 // 计算距离厘米声音速度按340m/s计算 long distance duration * 0.034 / 2; // 如果超时或距离异常返回一个安全值如200cm if (distance 0 || distance 200) { distance 200; } return distance; }这个滤波函数会维护一个最近5次测量的滑动窗口并始终输出其平均值。它能有效平滑掉单次的突变数据。4.2 设置动态警戒阈值在游戏中我们并非一直需要精确的距离值。我们可以根据游戏逻辑状态来动态调整传感器的“敏感度”。例如当obstaclePosition 50障碍物还很远我们只需要很低频率比如每500ms检查一次距离用于判断是否有物体靠近以开始游戏。当20 obstaclePosition 50障碍物进入中期提高检测频率到每100ms并开始预判。当obstaclePosition 20障碍物进入碰撞危险区全速检测每50ms或更快并采用最严格的滤波数据用于碰撞判断。这种策略既能减少不必要的计算又能在关键时刻保证响应速度。实现上可以在updateGameLogic()函数中根据obstaclePosition来改变调用getFilteredDistance()的频率或切换不同的滤波参数。5. 系统集成、调试与性能优化当各个模块传感器、舵机、显示、逻辑的代码都准备好后就需要将它们整合到一个协调的主循环中并处理它们之间的状态同步问题。5.1 主循环结构与状态切换Arduino的loop()函数是我们的主舞台。它的设计原则是快速执行永不阻塞。void loop() { unsigned long currentMillis millis(); // 1. 状态机分发处理 switch (currentState) { case START: handleStartState(); break; case RUNNING: handleRunningState(); break; case JUMPING: // 跳跃动作由 controlDinoJump() 函数持续控制此处主要处理状态逻辑 handleJumpingState(); break; case GAME_OVER: handleGameOverState(); break; } // 2. 实时控制任务与状态无关或需持续运行 controlDinoJump(); // 控制舵机动画 updateDisplay(); // 刷新OLED屏幕显示当前分数、速度、状态 // 3. 基于时间的游戏逻辑更新非阻塞 if (currentMillis - lastUpdateTime updateInterval) { lastUpdateTime currentMillis; if (currentState RUNNING) { updateGameLogic(); // 更新障碍物位置、分数、速度等 } } // 4. 传感器数据读取可根据状态调整频率 static unsigned long lastSensorRead 0; if (currentMillis - lastSensorRead getSensorReadInterval()) { // 动态获取读取间隔 lastSensorRead currentMillis; long currentDistance getFilteredDistance(); processSensorData(currentDistance); // 根据当前距离触发状态切换 } }handleStartState()函数可能包含在OLED上显示“Press to Start”并闪烁同时持续监测传感器前方是否有手挥动距离突然变小再变大来触发游戏开始。processSensorData()函数则根据当前状态和测得的距离决定是否触发跳跃例如在RUNNING状态且距离小于某个阈值时将状态切换到JUMPING。5.2 调试技巧与常见问题排查即使代码逻辑清晰第一次上电也难免遇到问题。下面是一些我踩过坑后总结的排查清单现象可能原因排查步骤与解决方案舵机不动或抽搐1. 供电不足。2. 信号线接触不良或接错。3. 代码中舵机控制引脚定义错误。1.首要检查电源换用5V/2A以上独立电源确保Arduino和舵机供电充足。2. 用servo.write()函数写一个固定角度如90度的测试程序单独测试舵机。3. 确认信号线连接的是Arduino上支持PWM带~号的引脚如3,5,6,9,10,11。超声波读数一直为0或超大值1. 接线错误Trig和Echo接反。2. 测量对象太近2cm或太远400cm超出模块范围。3. 模块本身损坏。1. 对照数据手册确认Trig触发和Echo回响引脚连接正确。2. 在代码中加入超时处理和异常值过滤如前文所述。3. 使用Arduino串口监视器打印出pulseIn()函数返回的原始duration值看是否正常。游戏逻辑混乱状态切换异常1. 全局变量在多个地方被意外修改。2. 状态切换条件有重叠或漏洞。3. 非阻塞延时逻辑出错时间基准混乱。1. 使用串口打印关键变量如currentState,obstaclePosition,score的值观察其变化是否符合预期。2. 画出清晰的状态转换图检查每个转换条件是否互斥且完备。3. 检查所有基于millis()的计时确保没有使用int类型导致溢出并且减法逻辑正确。OLED屏幕不显示或乱码1. I2C地址不对。2. 库文件未安装或版本不兼容。3. SCL/SDA接错引脚。1. 运行一个I2C扫描程序确认OLED模块的地址通常是0x3C或0x3D。2. 在Arduino IDE中安装正确的OLED驱动库如Adafruit_SSD1306和Adafruit_GFX。3. 确认Arduino Uno上SCL接A5SDA接A4。系统运行一段时间后复位1. 电源不稳定有大电流负载时电压骤降。2. 代码中有内存泄漏在Arduino C中较少见但递归或大型数组可能导致栈溢出。1.还是电源问题用万用表测量在舵机动作时Arduino 5V引脚的实际电压如果低于4.8V必须升级电源。2. 简化代码移除不必要的全局变量和字符串操作优化内存使用。调试时串口监视器是你最好的朋友。养成将关键变量、函数执行步骤、传感器原始值打印出来的习惯能帮你快速定位问题所在。6. 功能扩展与创意改进基础版本实现后这个项目还有巨大的扩展空间可以让它变得更有趣、更智能。1. 增加多种交互模式声控模式添加一个声音传感器如KY-038通过拍手或大喊来触发跳跃。需要注意环境噪声的过滤。手势模式换用TOF飞行时间测距传感器如VL53L0X或小型雷达模块可以识别更复杂的手势例如手从左向右挥动开始游戏向上挥动跳跃。实体按钮模式保留最经典的实体按键操作增加复古街机感。2. 增强游戏性与反馈音效反馈加入一个无源蜂鸣器在跳跃、得分、碰撞时发出不同的音效体验立刻提升一个档次。振动反馈在恐龙底座安装一个微型振动电机硬币马达碰撞时产生震动增强沉浸感。多难度选择通过一个旋转编码器或按钮在游戏开始前选择“简单”、“普通”、“困难”模式影响初始游戏速度和障碍物密度。3. 记录与连接最高分记录利用Arduino的EEPROM电可擦可编程只读存储器保存历史最高分断电不丢失。蓝牙/Wi-Fi连接如果使用ESP32作为主控可以添加蓝牙功能将实时分数同步到手机APP上甚至实现多人分数排行榜。4. 外观与机械结构优化精致的场景建模使用3D打印制作更精美的恐龙、仙人掌、云朵和沙漠背景模型并涂装上色。多关节恐龙使用两个或更多舵机让恐龙不仅能上下跳还能做出奔跑摆臂的姿势这需要更复杂的机械结构和协同控制代码。可移动的障碍物用另一个舵机或步进电机带动障碍物如仙人掌在轨道上横向移动模拟游戏中原障碍物的左右位置变化大大增加游戏难度和真实性。我个人在完成基础版后尝试了加入蜂鸣器音效和EEPROM保存最高分。音效部分需要根据音符频率和节拍自己编写简单的函数或者使用现成的tone()函数库。而保存最高分则需要注意EEPROM有写入寿命限制约10万次不要在每个游戏循环中都去写入只在游戏结束且新分数破纪录时才写入一次。这个项目最吸引我的地方在于它从一个简单的想法出发却可以像一棵树一样生长出无数枝丫。你可以止步于一个能自动玩的小玩具也可以深挖下去把它变成一个融合了机械设计、电路设计、嵌入式编程和交互设计的综合性作品。无论你选择哪种路径亲手将代码逻辑转化为物理世界中有趣动作的过程那种成就感是纯粹软件编程难以比拟的。希望这份详细的指南能帮你顺利启动自己的“自动小恐龙”项目享受创造的乐趣。