Arduino与Processing串口通信实现Flappy Bird物理交互游戏

发布时间:2026/8/19 23:18:44
Arduino与Processing串口通信实现Flappy Bird物理交互游戏 1. 项目缘起当经典游戏遇上物理世界几年前当那个像素风的小鸟在屏幕上蹦蹦跳跳让无数人摔手机、砸键盘的时候我就在想如果这只鸟能跳出屏幕变成一个我可以用手按动的实体按钮让游戏过程更有“触感”会不会更有趣这就是我做这个“Arduino Processing版Flappy Bird”项目的初衷。它不是一个简单的软件复刻而是一个软硬件结合的交互实验用Arduino Uno读取物理按钮的信号用Processing在电脑屏幕上渲染游戏画面和逻辑让虚拟的游戏世界通过一根USB线与真实的物理按键连接起来。这个项目的核心价值在于它为你打开了一扇门让你看到如何用最低的成本一块几十块的Arduino板子、一个按钮、几根杜邦线和最简单的代码去构建一个属于自己的、可触摸的交互系统。无论你是刚接触Arduino和Processing的新手想找一个有趣的项目练手还是有一定经验的开发者想了解串口通信和游戏逻辑如何结合这个项目都能提供一条清晰的路径。你会发现从“灯闪一下”到“做一个能玩的游戏”中间的距离并没有想象中那么遥远。2. 核心架构解析软硬件如何“握手”整个系统的架构非常清晰可以概括为“一个硬件输入一个软件处理一条数据通道”。理解这个架构是后续一切操作的基础。2.1 硬件端Arduino的角色与电路搭建Arduino在这里扮演了一个纯粹的“输入采集器”角色。它不负责复杂的游戏逻辑计算也不负责图形渲染它的任务只有一个实时监测连接在它上面的物理按钮的状态并将这个状态变化通过串口Serial Port发送给电脑。硬件清单与选型理由Arduino Uno或兼容板这是最经典、资料最丰富的入门级开发板。它的ATmega328P芯片性能足够处理按键检测和串口通信USB接口供电和编程都非常方便。选择它意味着你遇到的绝大多数问题都能在网上找到解决方案。按键模块或轻触开关这是游戏的“灵魂”。我推荐使用那种四脚带帽的轻触开关或者已经集成了上拉电阻的按键模块。前者更便宜但需要自己处理电路后者即插即用对新手更友好。选择哪种取决于你想在电路知识上深入到什么程度。杜邦线公对公若干用于连接。10kΩ电阻如果使用裸开关这是实现“上拉”的关键。后面会详细解释为什么需要它。电路连接原理以使用裸轻触开关为例这是整个硬件部分最容易出错的地方务必理解其原理。按键一端连接到Arduino的某个数字引脚例如我常用D2引脚。按键另一端连接到GND地。在D2引脚和5V引脚之间连接一个10kΩ的上拉电阻。这个连接方式构成了一个经典的“上拉电阻”电路。其工作原理是当按键没有被按下时D2引脚通过10kΩ电阻被“拉”到了高电平5VArduino读取到的值是HIGH或数字1。当按键被按下时D2引脚通过按键直接与GND相连由于导线的电阻远小于10kΩ此时D2引脚被“拉”到了低电平0VArduino读取到的值是LOW或数字0。这个电阻的作用是防止引脚悬空既不接高也不接低导致电平不确定从而产生误触发。注意很多Arduino教程会教你使用板子内部的上拉电阻通过pinMode(pin, INPUT_PULLUP)来设置。这种方法更简单无需外接电阻。此时你的按键连接需要改变按键一端接D2另一端接5V。按下时D2从内部被拉高的状态HIGH变为接到5V还是HIGH不这里有个关键点。实际上当使用内部上拉时按键应该连接在引脚和GND之间。按下按键引脚从HIGH被拉低到LOW。我个人的经验是对于新手先从外接上拉电阻的电路开始理解更能建立清晰的物理概念。后续优化时可以改用内部上拉以简化电路。2.2 软件端Processing的游戏世界Processing在这个项目中承担了所有“高级”任务游戏画面绘制、物理碰撞检测、分数计算、关卡难度递增逻辑以及最重要的——通过串口与Arduino对话。为什么选择Processing为视觉交互而生Processing的语法极其简洁专为图形、动画和交互设计。用它来画一只小鸟、几根管道比用其他语言要直观和快速得多。与Arduino“天生一对”Processing有一个官方的Serial库专门用于串口通信其API设计得非常容易上手几乎是为Arduino通信量身定做。跨平台Windows, macOS, Linux全支持确保你的项目可以在任何电脑上运行。Processing程序的结构通常围绕setup()和draw()两个主函数。setup()在程序开始时运行一次用于初始化串口、设置窗口大小、加载资源等。draw()则每秒运行很多次默认60次形成60FPS的动画在这里更新小鸟的位置、绘制每一帧的画面、检测碰撞。2.3 通信桥梁串口协议设计硬件和软件之间需要一种共同的语言这就是串口通信协议。我们的协议可以设计得非常简单。Arduino发送什么Arduino只需要在按键状态发生变化时发送一个简单的字符。例如当检测到按键从“松开”变为“按下”下降沿时发送字符p代表 press。或者更简单粗暴且常用的方式每次在loop()中读取引脚状态如果当前是按下状态LOW就持续发送字符1如果是松开状态HIGH就发送0。Processing端则根据连续收到的1来判断按键被持续按着。Processing接收与解析Processing的Serial库会监听指定串口如 COM3, /dev/ttyUSB0。当有数据到达时我们可以在draw()循环中调用serial.read()读取。如果读到我们约定的字符如p就触发一次“小鸟跳跃”的动作给小鸟一个向上的速度。这种“事件驱动”的通信方式效率很高Arduino只在有输入变化时才通信避免了不必要的串口数据拥堵。3. 分步实现从零到一的完整流程理解了架构我们开始动手。我会先完成Arduino端的“固件”程序确保硬件信号能正确发出然后再编写Processing端的游戏逻辑。3.1 Arduino端程序简洁的输入哨兵首先用Arduino IDE编写并上传以下代码。假设我们使用D2引脚并采用了外部上拉电阻的电路即按键按下时D2读为LOW。// Flappy Bird - Arduino Controller // 引脚定义 const int buttonPin 2; // 变量记录上一次按键状态用于检测变化 int lastButtonState HIGH; int currentButtonState HIGH; void setup() { // 初始化串口通信波特率设为9600。这个值必须与Processing端一致。 Serial.begin(9600); // 初始化按键引脚为输入模式 pinMode(buttonPin, INPUT); // 注意这里用的是INPUT因为我们用了外部上拉电阻。 // 如果用内部上拉应改为 INPUT_PULLUP同时按键另一端接GND。 } void loop() { // 读取当前按键状态 currentButtonState digitalRead(buttonPin); // 检测状态是否发生变化从高到低即按下瞬间 if (lastButtonState HIGH currentButtonState LOW) { // 按键被按下的瞬间通过串口发送字符 p Serial.write(p); // 也可以打印到串口监视器查看调试时有用正式运行时可以注释掉 // Serial.println(Button Pressed!); } // 更新上一次的状态 lastButtonState currentButtonState; // 一个短暂的延时防止检测过于频繁去抖动。10-20毫秒通常足够。 delay(10); }上传与测试将代码上传到你的Arduino Uno。打开Arduino IDE的“串口监视器”工具 - 串口监视器。确保右下角波特率设置为9600。按下连接好的物理按键你应该能在串口监视器中看到不断的p字符输出或者你取消注释的“Button Pressed!”文字。这说明你的硬件电路和Arduino程序工作正常已经能够可靠地发送按键事件。实操心得按键去抖动。机械按键在按下和松开的瞬间内部的金属触点会发生物理弹跳导致在几毫秒内电平快速变化多次。如果不处理一次按压会被误判为多次。上面的代码通过两个方式处理一是delay(10)在每次检测后等待一段时间避开抖动期二是检测“下降沿”从HIGH到LOW而不是单纯的低电平。对于要求更高的场景可以使用更精确的“状态机”或库来处理但对我们这个游戏delay(10)边沿检测已经足够稳定。3.2 Processing端程序构建游戏的核心逻辑接下来是重头戏。打开Processing创建一个新项目。你需要先导入串口库Sketch - Import Library... - Serial。然后开始编写代码。为了清晰我将代码分块解释。第一部分变量声明与初始化 (setup)import processing.serial.*; // 导入串口库 Serial myPort; // 串口对象 String portName; // 串口名称 // 游戏角色小鸟 float birdX, birdY; // 小鸟位置 float birdVelocity 0; // 小鸟垂直速度 float gravity 0.5; // 重力加速度 float jumpForce -10; // 跳跃力度向上为负 // 游戏障碍物管道 ArrayListPipe pipes new ArrayListPipe(); // 使用动态数组存储管道 int pipeWidth 60; // 管道宽度 int pipeGap 150; // 管道上下之间的空隙小鸟能通过的高度 int pipeInterval 180; // 管道出现的帧间隔 int pipeTimer 0; // 计时器用于控制管道生成频率 // 游戏状态 boolean gameRunning true; int score 0; PFont font; // 字体 // 管道类用于管理单个管道对 class Pipe { float x; float topHeight; // 上管道的高度 float bottomY; // 下管道的Y坐标 Pipe(float tempX) { x tempX; // 随机生成上管道的长度留出中间空隙 topHeight random(50, height - pipeGap - 50); bottomY topHeight pipeGap; } void update() { x - 2; // 管道向左移动的速度 } void display() { fill(70, 200, 70); // 绿色管道 // 绘制上管道 rect(x, 0, pipeWidth, topHeight); // 绘制下管道 rect(x, bottomY, pipeWidth, height - bottomY); } // 检测小鸟是否与当前管道碰撞 boolean hits(float bx, float by, float bw) { if (by topHeight || by 20 bottomY) { // 小鸟高度假设为20 if (bx 20 x bx x pipeWidth) { return true; } } return false; } // 检测小鸟是否已通过管道用于计分 boolean passed(float bx) { // 如果小鸟的x坐标超过了管道的x坐标宽度且之前未计分则计分 // 这里简化处理每对管道只计分一次的逻辑需要在主循环中额外维护一个布尔变量 return (bx x pipeWidth); } }setup()函数void setup() { size(800, 600); // 设置游戏窗口大小 birdX width / 4; // 小鸟初始水平位置在屏幕左侧1/4处 birdY height / 2; // 小鸟初始垂直位置在屏幕中央 // 初始化字体 font createFont(Arial, 32); textFont(font); // 串口初始化这里需要你手动修改 // 打印可用的串口列表找到你的Arduino对应的端口 printArray(Serial.list()); // 根据打印的结果选择正确的端口索引。例如Windows可能是COM3macOS/Linux可能是/dev/tty.usbmodem14101 // portName Serial.list()[0]; // 通常第一个是当前连接的但不绝对 portName COM3; // 请替换为你的实际端口名 myPort new Serial(this, portName, 9600); // 波特率必须与Arduino端匹配 // 生成第一对管道 pipes.add(new Pipe(width)); }第二部分游戏主循环 (draw) 与串口读取draw()函数每秒执行60次是游戏的心脏。void draw() { background(135, 206, 235); // 绘制天蓝色背景 // --- 1. 处理串口输入 --- while (myPort.available() 0) { char inChar myPort.readChar(); // 读取一个字符 if (inChar p gameRunning) { // 收到Arduino发来的跳跃指令 birdVelocity jumpForce; // 给小鸟一个向上的速度 } } if (!gameRunning) { // 游戏结束画面 fill(255, 0, 0); textAlign(CENTER); text(Game Over! Score: score, width/2, height/2 - 40); text(Press R to Restart, width/2, height/2 20); return; // 游戏结束不再更新游戏逻辑 } // --- 2. 更新小鸟状态 --- birdVelocity gravity; // 应用重力 birdY birdVelocity; // 更新位置 // 边界检测碰到顶部或底部则游戏结束 if (birdY height || birdY 0) { gameRunning false; } // --- 3. 更新与绘制管道 --- pipeTimer; if (pipeTimer pipeInterval) { pipes.add(new Pipe(width)); // 在屏幕右侧生成新管道 pipeTimer 0; } // 遍历所有管道更新、绘制并检测碰撞 // 注意从后往前遍历方便安全地删除已移出屏幕的管道 for (int i pipes.size() - 1; i 0; i--) { Pipe p pipes.get(i); p.update(); p.display(); // 碰撞检测 if (p.hits(birdX, birdY, 20)) { gameRunning false; } // 计分检测简化版小鸟中心通过管道中心即算分 // 更严谨的做法是为每个Pipe对象添加一个isScored布尔标记 if (!p.passed(birdX) birdX p.x pipeWidth/2) { // 这里是一个简单的计分触发点实际需要更精确的逻辑避免重复计分 // 为了示例我们用一个全局变量记录上一帧是否在管道右侧本帧是否在左侧来判断 // 此处省略精确逻辑用一个粗略的每对管道计分一次的方法示意 } // 如果管道移出屏幕左侧则删除它以释放内存 if (p.x -pipeWidth) { pipes.remove(i); score; // 每成功移除一对管道即小鸟通过了一对得分加一 } } // --- 4. 绘制小鸟 --- fill(255, 255, 0); // 黄色小鸟 ellipse(birdX, birdY, 20, 20); // 用圆形代表小鸟 // --- 5. 绘制当前分数 --- fill(0); textAlign(LEFT); text(Score: score, 20, 40); }第三部分添加键盘重启功能为了让游戏在结束后可以重新开始我们添加一个键盘事件处理。void keyPressed() { if (key r || key R) { // 重置游戏状态 gameRunning true; birdY height / 2; birdVelocity 0; score 0; pipes.clear(); // 清空所有管道 pipeTimer 0; pipes.add(new Pipe(width)); // 添加第一对管道 } }3.3 连接与运行修改端口号在Processing代码的setup()函数里找到portName COM3;这一行。运行一次程序查看控制台输出的串口列表将COM3替换成你的Arduino实际连接的端口名如/dev/ttyUSB0。确保Arduino已连接并通电且已上传了之前的按键检测程序。点击Processing的“运行”按钮。如果一切正常你将看到一个游戏窗口黄色的小鸟开始下坠。此时按下连接在Arduino上的物理按钮小鸟应该会向上跳跃。试着控制它穿过移动的绿色管道吧4. 调试与优化让游戏更“跟手”第一次运行很可能不会完美。以下是几个常见的坑和优化点。问题1按键无反应或反应延迟。检查串口连接确认Processing代码中的端口号是否正确。这是最常见的问题。检查波特率确保Arduino代码的Serial.begin(9600)和Processing代码的new Serial(this, portName, 9600)波特率一致。检查电路用万用表测量按键按下时D2引脚对GND的电压是否接近0V。或者在Arduino IDE的串口监视器里观察按下按键时是否有数据输出。Processing读取方式我们使用了while (myPort.available() 0)来读取所有缓冲区的数据。如果游戏卡顿可能是draw()循环太慢导致串口缓冲区累积了大量数据处理不过来。可以尝试改为if (myPort.available() 0)只读一个字符但这可能在快速连按时丢失指令。对于这个游戏while循环通常是安全的。问题2游戏画面卡顿。简化绘图我们的管道和小鸟都是简单图形卡顿可能性不大。但如果后期想添加背景、动画要注意Processing的绘图效率。检查碰撞检测效率我们的碰撞检测hits函数是每帧对每个管道进行的矩形检测在管道数量不多时20完全没问题。如果卡顿可能是其他原因。问题3按键感觉“不跟手”。这是输入延迟问题是硬件交互游戏的关键体验。延迟来自几个方面Arduino端采样延迟我们的loop()中有一个delay(10)。这意味着Arduino最快每10毫秒检测一次按键。对于需要快速响应的游戏这个延迟有点大。优化方案移除delay(10)改用非阻塞的时间戳方式检测去抖动。例如记录上次按下时间只有当按键状态变化且距离上次变化时间大于5毫秒时才认为是有效动作并发送数据。串口通信延迟字符传输本身很快但系统串口缓冲和调度可能有微小延迟。通常这不是主因。Processing帧处理延迟draw()函数一帧内的所有计算和绘图必须在下一次draw()调用前完成。如果一帧计算量太大导致帧率从60FPS降到30FPS那么从按下按键到画面响应延迟就从约16ms增加到33ms手感差异会很明显。优化后的Arduino代码去抖动优化版const int buttonPin 2; int lastButtonState HIGH; int currentButtonState HIGH; unsigned long lastDebounceTime 0; // 上次状态稳定时间 unsigned long debounceDelay 5; // 去抖动延时毫秒 void setup() { Serial.begin(9600); pinMode(buttonPin, INPUT_PULLUP); // 这次使用内部上拉电阻按键另一端接GND。 } void loop() { int reading digitalRead(buttonPin); // 如果读数与上次稳定状态不同则重置去抖动计时器 if (reading ! lastButtonState) { lastDebounceTime millis(); } // 如果经过去抖动延时后状态仍然与当前稳定状态不同则认为是有效变化 if ((millis() - lastDebounceTime) debounceDelay) { if (reading ! currentButtonState) { currentButtonState reading; // 只有从高到低的变化按下瞬间才发送 if (currentButtonState LOW) { Serial.write(p); } } } lastButtonState reading; // 移除了 delay(10)让 loop() 跑得更快 }这个版本使用了更精确的毫秒级计时去抖动并且去除了固定的delay使得Arduino能以更高的频率检测引脚响应速度显著提升。你会立刻感觉到按键“跟手”了很多。5. 扩展思路不止于“能玩”一个基础版本完成后你可以从这个核心出发进行无限扩展让这个项目成为你的创意试验场。硬件扩展多种输入方式除了按钮可以尝试使用超声波传感器HC-SR04来控制小鸟。用手在传感器前上下移动通过测量距离来映射小鸟的升降。这需要修改Arduino代码读取传感器数据并将其映射为一个范围值发送给Processing。增加反馈在Arduino上连接一个蜂鸣器。当小鸟碰撞时Arduino收到Processing发回的指令需要建立双向通信让蜂鸣器发出“悲鸣”。或者连接一个振动电机碰撞时产生触觉反馈。实体化游戏角色用舵机、LED和纸板做一个物理小鸟Processing通过串口控制舵机上下移动让游戏角色真正“活”在桌面上。软件与游戏性扩展双向通信目前是Arduino单向发送。可以改为Arduino发送按键信号Processing判断碰撞后通过串口发送一个字符如c回Arduino触发蜂鸣器或LED。图形美化在Processing中加载真正的Flappy Bird精灵图片绘制带有渐变色的背景让管道有纹理。游戏逻辑丰富增加地面移动、多种障碍物类型如移动的方块、道具系统如护盾、本地高分记录等。数据可视化在游戏界面旁边用曲线图实时绘制小鸟的飞行高度、速度或者记录每次按键的力度和时机用于分析自己的操作。这个项目的魅力在于它像一块积木。你掌握了“Arduino采集输入 - 串口发送 - Processing接收并处理 - 图形化输出”这个核心流程后就可以替换掉其中任何一个环节。把按钮换成摇杆把Processing换成Unity游戏引擎把Flappy Bird换成你自己的创意游戏。这条连接物理世界和数字世界的通道一旦打通你会发现无数可能性。