用Arduino打造自动小恐龙游戏:从传感器到执行器的嵌入式实战
2026/8/19 2:57:05 网站建设 项目流程

1. 项目概述:当经典游戏遇上开源硬件

相信不少朋友在浏览器断网时,都遇到过那个经典的“小恐龙游戏”——一只像素风的小恐龙在无尽的沙漠中奔跑、跳跃,躲避仙人掌和翼龙。这个由Chrome浏览器内置的彩蛋,因其简单又上瘾的玩法,成为了许多人的“摸鱼”神器。但你是否想过,把这个虚拟世界里的游戏,搬到现实中的桌面上,让它变成一个看得见、摸得着的实体装置?这就是我们今天要聊的项目:用Arduino打造一个全自动运行的“小恐龙游戏”模拟器。

这个项目的核心,远不止是复现一个游戏那么简单。它本质上是一个集成了传感器、执行器和逻辑控制的微型自动化系统。我们不再用键盘的空格键控制恐龙跳跃,而是通过超声波或红外传感器来“感知”障碍物,然后驱动舵机或电机,让一个实体的小恐龙模型做出跳跃动作。整个游戏逻辑,包括障碍物的生成、速度的递增、分数的计算,都由一块小小的Arduino开发板来运算和控制。对于硬件爱好者、创客或者嵌入式入门的学习者来说,这是一个绝佳的练手项目。它涵盖了从传感器数据采集、信号处理、控制算法到执行机构驱动这一完整的嵌入式开发链路,而且最终成果趣味性十足,非常有成就感。

我最初做这个项目,是想给工作室做一个有趣的桌面摆件。在实现过程中,从最初的简单触发跳跃,到后来加入多种障碍物类型、可调的游戏难度,甚至用OLED屏幕显示分数和游戏状态,踩了不少坑,也积累了很多实战经验。接下来,我就把这套从零到一的实现方案,包括硬件选型、电路连接、代码逻辑,以及那些只有亲手做过才会知道的注意事项,毫无保留地分享给大家。

2. 核心硬件选型与电路设计思路

动手之前,先把“武器装备”整理清楚。一套稳定可靠的硬件是项目成功的基础。我们的目标是:用最低的成本、最易得的模块,实现核心的游戏逻辑。下面这张表格梳理了核心部件及其选型理由,你可以根据自己的预算和需求进行调整。

部件名称推荐型号/规格核心作用选型理由与备选方案
主控板Arduino Uno R3系统大脑,运行游戏逻辑,处理传感器数据,控制执行器。资源丰富(14个数字I/O,6个模拟输入),社区支持强大,性价比高。备选: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或以上电流的稳压电源,并确保正负极连接正确。

电路连接并不复杂,但需要有条理。核心连接如下:

  1. Arduino与HC-SR04:超声波模块的Vcc接Arduino 5V,GND接GND,Trig(触发)接数字引脚9,Echo(回响)接数字引脚10。
  2. Arduino与SG90舵机:舵机的红线(正极)接5V,棕线(负极)接GND,橙线(信号线)接数字引脚6(Arduino上带PWM功能的引脚)。
  3. Arduino与OLED屏幕:OLED的VCC接5V,GND接GND,SCL接A5,SDA接A4(这是Arduino Uno上I2C通信的固定引脚)。
  4. 独立供电:将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; // 跳跃周期约600ms:300ms上升,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_SSD1306Adafruit_GFX)。
3. 确认Arduino Uno上,SCL接A5,SDA接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万次),不要在每个游戏循环中都去写入,只在游戏结束且新分数破纪录时才写入一次。

这个项目最吸引我的地方在于,它从一个简单的想法出发,却可以像一棵树一样,生长出无数枝丫。你可以止步于一个能自动玩的小玩具,也可以深挖下去,把它变成一个融合了机械设计、电路设计、嵌入式编程和交互设计的综合性作品。无论你选择哪种路径,亲手将代码逻辑转化为物理世界中有趣动作的过程,那种成就感,是纯粹软件编程难以比拟的。希望这份详细的指南能帮你顺利启动自己的“自动小恐龙”项目,享受创造的乐趣。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询