
1. 项目概述一个能“摸”到的复古像素游戏几年前我刚开始玩Arduino时总想着用它做点有趣的东西而不是仅仅点亮几个LED。后来我发现用那块小小的1602 LCD屏幕配合几个简单的按钮和传感器就能复刻出童年掌机游戏的乐趣。今天要分享的就是一个基于Arduino Uno的“跳跃方块”游戏。它的核心玩法很简单屏幕底部有一个由字符拼成的“小人”不断有方块从屏幕右侧向它移动你需要看准时机按下按钮让小人“跳”起来躲避方块。如果撞上了旁边的红色LED会亮起蜂鸣器也会发出“Game Over”的提示音整个游戏的开关由一个电位器控制转动它就像给这个微型游戏机“上发条”。这个项目麻雀虽小五脏俱全。它几乎涵盖了嵌入式系统开发中几个最核心的概念微控制器编程、GPIO通用输入输出接口控制、以及硬件交互设计。通过Arduino游戏开发你不仅能理解代码如何驱动硬件更能直观地看到“按下按钮”这个动作是如何通过一系列电信号转换最终让屏幕上的像素点产生变化的。对于想入门硬件编程又觉得单纯控制电机或传感器有些枯燥的朋友来说制作一个能玩的小游戏无疑是动力最强、成就感最高的学习路径。2. 核心硬件选型与电路设计思路2.1 主控与显示模块为什么是Arduino Uno和1602 LCD选择Arduino Uno作为大脑几乎是所有入门项目的共识。它基于ATmega328P微控制器有14个数字I/O口和6个模拟输入口对于本项目所需的资源控制LCD、读取按钮和电位器、驱动LED和蜂鸣器绰绰有余。更重要的是其庞大的社区和丰富的库支持能让你在遇到问题时快速找到解决方案。例如驱动1602 LCD我们直接使用Arduino IDE内置的LiquidCrystal库几行代码就能完成初始化无需深究底层时序。1602 LCD显示屏模块16x2字符是这个游戏的“舞台”。它之所以经典是因为其接口标准、价格低廉且易于驱动。所谓16x2意味着可以显示2行每行16个字符。请注意它显示的是字符而非像素点。这意味着我们的游戏角色“小人”和“方块”都需要用自定义字符Custom Character来设计。LiquidCrystal库允许我们定义最多8个5x8像素的自定义字符这正好为我们创造了绘制简单图形的基础。相比于更复杂的图形点阵屏1602 LCD在编程上更简单更能让我们专注于游戏逻辑本身。2.2 输入与反馈设备构建游戏的交互闭环一个完整的交互循环需要输入、处理和输出。在本项目中我们通过以下设备构建了这个循环电位器模拟输入这里它被创新性地用作“游戏开关”。电位器本质上是一个可变电阻。Arduino的模拟输入引脚A0-A5可以读取其分压后的电压值0-5V并映射为0-1023的整数。我们的逻辑是当读取到的值超过某个阈值例如512则认为游戏开启低于阈值则关闭。这比使用一个额外的开关按钮更节省I/O口也增加了操作的“仪式感”。轻触按钮数字输入作为唯一的动作按键控制“跳跃”。这里涉及按键消抖这个关键概念。机械按钮在按下和弹起的瞬间内部的金属触点会发生物理抖动导致Arduino会在极短时间内读取到多次高低电平变化误判为多次按下。解决方法通常是在代码中加入一个短暂的延时如10-50毫秒等待抖动过去后再确认按键状态。LED与蜂鸣器输出反馈它们是游戏的“感官”延伸。当碰撞发生时红色LED亮起提供视觉警示无源蜂鸣器发出特定频率的声音提供听觉警示。这种多模态反馈能极大增强游戏的沉浸感。驱动LED需要串联一个约220-330欧姆的限流电阻防止电流过大烧毁LED或单片机引脚。驱动蜂鸣器则通常使用PWM脉冲宽度调制引脚来产生不同频率的方波从而发出不同音调。注意关于无源蜂鸣器与有源蜂鸣器本项目更适合使用无源蜂鸣器。有源蜂鸣器内部自带振荡电路通电即响只能发出固定频率的声音。而无源蜂鸣器需要外部提供PWM信号才能发声我们可以通过代码精确控制其频率和时长从而播放简单的音调甚至旋律为游戏失败、跳跃成功等事件配上不同的音效体验更佳。2.3 电路连接详解与原理图解读搭建电路是硬件项目中最需要耐心的一环。一个清晰的接线图胜过千言万语但理解其背后的原理同样重要。以下是核心连接逻辑的解析LCD模块的连接采用4位数据模式 1602 LCD通常有16个引脚但我们无需使用全部。为了节省I/O口我们采用4位数据模式只使用DB4-DB7这4根数据线来传输数据。VSS (Pin 1): 接地GND。VDD (Pin 2): 接5V。VO (Pin 3): 液晶对比度调节。接一个10K电位器的中间抽头电位器两端分别接5V和GND。通过调节此电压0-5V来改变屏幕显示的深浅。RS (Pin 4): 寄存器选择。接数字引脚如12。高电平时选择数据寄存器发送要显示的数据低电平时选择指令寄存器发送命令。RW (Pin 5): 读写控制。直接接地GND因为我们只向LCD写数据不读取。E (Pin 6): 使能信号。接数字引脚如11。在数据线上数据稳定后需要一个高脉冲来锁存数据。DB4-DB7 (Pin 11-14): 4位数据线。分别接数字引脚如5, 4, 3, 2。A (Pin 15) / K (Pin 16): 背光电源正极和负极。如果LCD带背光将A通过一个限流电阻如220Ω接5VK接地以开启背光。其他元件连接按钮一端接5V另一端接一个10KΩ的下拉电阻到GND同时连接到Arduino的一个数字输入引脚如8。当按钮未按下时输入引脚通过下拉电阻被稳定拉低LOW按下时被拉高HIGH。电位器两侧引脚分别接5V和GND中间抽头接模拟输入引脚A0。LED正极长脚通过一个220Ω电阻连接到数字引脚如9负极接GND。无源蜂鸣器正极连接到支持PWM的数字引脚如10负极接GND。3. 游戏软件逻辑深度剖析与代码实现3.1 核心状态机与游戏循环设计任何游戏的核心都是一个高速运行的循环Game Loop在Arduino中就是loop()函数。在这个循环里我们需要按顺序处理几件事读取输入、更新游戏状态、渲染画面。对于“跳跃方块”这类简单游戏使用状态机State Machine模型来管理游戏状态非常清晰。我们可以定义几个游戏状态GAME_OFF: 游戏关闭状态电位器未开启。屏幕显示待机信息。GAME_STANDBY: 游戏就绪状态电位器已开启等待按下开始键。屏幕显示“Press to Start”。GAME_PLAYING: 游戏进行中。在此状态下执行核心游戏逻辑。GAME_OVER: 游戏结束状态发生碰撞。触发LED和蜂鸣器屏幕显示分数等待重启。在loop()中我们首先读取电位器值判断总开关然后根据当前状态执行相应的函数。这种设计将复杂的逻辑分解到不同状态中处理代码结构清晰易于调试和扩展。3.2 自定义字符设计与画面渲染1602 LCD的每个字符位置是一个5x8的点阵。LiquidCrystal库的createChar()函数允许我们定义自己的图形。我们需要至少定义两个自定义字符一个代表“小人”一个代表“方块”。例如小人可以设计成一个向上的箭头或简单的人形// 示例一个简单的小人字符 (5x8像素) byte playerChar[8] { B00100, // * B01110, // *** B00100, // * B11111, // ***** B10101, // * * * B00100, // * B01010, // * * B10001 // * * }; lcd.createChar(0, playerChar); // 将数组绑定到0号自定义字符在渲染时我们只需要在屏幕特定位置第二行某个固定列打印这个自定义字符即可lcd.write(byte(0))。方块的移动实质上是每隔一定时间游戏速度在屏幕第二行地面行将方块字符从右向左移动一列。这可以通过一个数组来记录第二行每个位置是空格还是方块然后在每一帧重新绘制整个第二行来实现。3.3 碰撞检测、物理与分数系统碰撞检测在这个游戏里极其简单只需要判断“小人”所在的列位置与当前所有“方块”所在的列位置是否重合。如果重合且小人处于“未跳跃”状态即在地面则判定为碰撞游戏状态切换至GAME_OVER。跳跃物理用一个简单的变量模拟即可。例如定义一个jumpHeight变量和isJumping状态。按下按钮时如果小人在地面则进入isJumping状态jumpHeight设为2表示可以向上跳两行。在游戏更新逻辑中如果处于跳跃状态则每帧将小人绘制在第一行空中并递减jumpHeight。当jumpHeight减到0时开始下落直到回到地面行。分数系统每成功躲避一个方块即方块移出屏幕最左侧且未发生碰撞分数加一。游戏速度可以随着分数增加而逐渐加快通过减少方块移动的帧间隔来实现增加游戏挑战性。3.4 完整代码框架与关键函数解析以下是精简后的核心代码框架展示了主要逻辑结构#include LiquidCrystal.h // 引脚定义 const int rs 12, en 11, d4 5, d5 4, d6 3, d7 2; const int buttonPin 8; const int potPin A0; const int ledPin 9; const int buzzerPin 10; LiquidCrystal lcd(rs, en, d4, d5, d6, d7); // 游戏变量 enum GameState { GAME_OFF, GAME_STANDBY, GAME_PLAYING, GAME_OVER }; GameState state GAME_OFF; int score 0; int gameSpeed 500; // 初始速度毫秒 bool isJumping false; int jumpCounter 0; int playerPos 0; // 小人在第二行的列位置 int obstaclePos 15; // 方块起始位置屏幕最右 // 自定义字符 byte playerChar[8] {...}; byte blockChar[8] {...}; void setup() { pinMode(buttonPin, INPUT); pinMode(ledPin, OUTPUT); pinMode(buzzerPin, OUTPUT); lcd.begin(16, 2); lcd.createChar(0, playerChar); lcd.createChar(1, blockChar); lcd.print(Jump Block Game); } void loop() { int potValue analogRead(potPin); bool buttonPressed digitalRead(buttonPin); // 状态机调度 switch(state) { case GAME_OFF: if(potValue 512) { state GAME_STANDBY; lcd.clear(); } break; case GAME_STANDBY: lcd.setCursor(0,0); lcd.print(Press to Start); if(buttonPressed) { state GAME_PLAYING; gameInit(); // 初始化游戏变量 lcd.clear(); } break; case GAME_PLAYING: updateGame(buttonPressed); // 更新游戏逻辑 drawGame(); // 绘制画面 delay(gameSpeed); // 控制游戏速度 break; case GAME_OVER: gameOverSequence(); // 失败提示 if(buttonPressed) { state GAME_STANDBY; resetFeedback(); } break; } } void updateGame(bool btn) { // 处理跳跃 if(btn !isJumping jumpCounter 0) { isJumping true; jumpCounter 2; // 跳跃高度 tone(buzzerPin, 523, 100); // 跳跃音效Do } if(isJumping) { jumpCounter--; if(jumpCounter 0) isJumping false; } // 移动方块 obstaclePos--; if(obstaclePos 0) { obstaclePos 15; score; gameSpeed max(100, gameSpeed - 10); // 速度加快设置下限 } // 碰撞检测 if(obstaclePos playerPos !isJumping) { state GAME_OVER; } } void drawGame() { lcd.setCursor(0,0); lcd.print(Score:); lcd.print(score); lcd.setCursor(0,1); // 绘制地面行 for(int i0; i16; i) { if(i playerPos) { lcd.write(byte(0)); // 绘制小人 } else if(i obstaclePos) { lcd.write(byte(1)); // 绘制方块 } else { lcd.print( ); } } } void gameOverSequence() { digitalWrite(ledPin, HIGH); tone(buzzerPin, 262, 200); // 失败低音Do delay(200); noTone(buzzerPin); delay(200); lcd.setCursor(0,0); lcd.print(Game Over! Score:); lcd.setCursor(0,1); lcd.print(score); }4. 系统调试、优化与功能扩展实战4.1 硬件调试常见问题与排查LCD屏幕不亮或显示乱码检查电源和背光确认VDD和背光引脚A/K接线正确特别是背光是否接了限流电阻。调整对比度这是最常见的问题。缓慢调节接在VO引脚上的电位器直到字符清晰显示。有时对比度电压不合适屏幕看似没显示其实已有内容。检查数据线连接确认RS、E、DB4-DB7的引脚号在代码和实际连接中完全一致。接触不良会导致乱码。初始化延时在setup()的lcd.begin()后加一个短暂的delay(500)给LCD模块足够的启动时间。按钮响应不灵或连跳消抖处理确保代码中实现了按键消抖。最简方法是在检测到按键按下后delay(50)再读取一次状态确认。上拉/下拉电阻确认使用了正确的电阻。如果使用了下拉电阻按钮接高电平代码中应检测HIGH为按下如果启用了Arduino内部上拉电阻pinMode(pin, INPUT_PULLUP)则按钮应接GND检测LOW为按下。两者不要混用。蜂鸣器不响或常响区分有源/无源确认你使用的是无源蜂鸣器。有源蜂鸣器接PWM引脚可能会响但无法控制音调。检查tone()函数tone(pin, frequency, duration)函数用于驱动无源蜂鸣器。确保引脚号、频率单位Hz正确。播放后如果需要停止使用noTone(pin)。驱动能力如果声音微弱可以尝试在蜂鸣器正极和Arduino引脚之间加一个NPN三极管如8050进行电流放大。4.2 软件优化与体验提升技巧非阻塞式延时游戏主循环中的delay(gameSpeed)会阻塞所有其他操作导致按键响应在延时期间不灵敏。更好的方法是使用millis()函数实现非阻塞定时。例如记录上次更新游戏逻辑的时间戳当millis() - lastUpdateTime gameSpeed时才执行方块移动和画面更新这样按键检测可以持续快速响应。unsigned long previousGameUpdate 0; void loop() { // ... 读取输入等 if (state GAME_PLAYING) { unsigned long currentMillis millis(); if (currentMillis - previousGameUpdate gameSpeed) { previousGameUpdate currentMillis; updateGame(buttonPressed); drawGame(); } } // ... 其他状态处理 }更平滑的跳跃动画当前的跳跃是“瞬移”到空中。可以引入“起跳”和“下落”两个中间状态字符让动画更细腻。随机性增强让方块的初始出现位置有一定随机性或者随机生成不同高度的障碍需要更多自定义字符可以大大增加游戏的可玩性。使用random(min, max)函数来实现。4.3 项目功能扩展思路这个基础框架有巨大的扩展潜力增加多种障碍物定义多个自定义字符如高方块、矮方块、坑等随机生成需要不同的跳跃时机或下蹲设计一个下蹲字符来躲避。引入“金币”收集在屏幕上随机出现代表金币的字符跳跃碰到可以加分。使用外部中断优化按键将跳跃按钮接到Arduino的中断引脚如Uno的2或3号引脚使用attachInterrupt()函数。这样无论主循环在执行什么按下按钮都能立即响应实现零延迟跳跃手感更佳。添加声音模块使用DFPlayer Mini等MP3模块在游戏开始、结束、得分时播放真实的音效或背景音乐取代简单的蜂鸣器单音。更换显示设备升级为OLED显示屏I2C接口可以显示更细腻的像素图形游戏画面将得到质的飞跃。驱动原理类似只是换用Adafruit_SSD1306等库。5. 从原型到作品的进阶思考完成这个项目后你收获的不仅仅是一个能玩的小游戏。你实践了嵌入式开发从需求分析、硬件选型、电路搭建、代码编写到调试优化的完整流程。LCD显示屏作为人机交互界面电位器作为模拟输入设备按钮作为数字输入设备LED和蜂鸣器作为输出反馈设备它们共同构成了一个典型的微型嵌入式系统。这个项目的代码和电路虽然简单但其架构——状态机管理、非阻塞编程、自定义图形渲染、简单的物理与碰撞系统——是许多复杂项目的缩影。当你下次用Arduino做一个智能温室控制器时你会发现状态机依然好用当你用ESP32做一个网络天气站时非阻塞的思想能保证网络请求不卡住界面更新。硬件项目的魅力在于软硬结合带来的实在感。屏幕上跳动的方块指尖按下按钮的触感失败时蜂鸣器发出的声响所有这些共同构成了一次完整的创造体验。我建议你在实现基础功能后一定要尝试至少一项扩展功能。无论是改一个更酷的角色造型还是增加一个积分排行榜需要用到EEPROM存储这个过程里遇到的挑战和解决问题的过程才是学习嵌入式开发最宝贵的部分。动手去试代码烧录进去电路接上看看会发生什么。所有的不确定都会在硬件通电的那一刻得到最确定的答案。