1. 项目概述当经典记忆游戏遇上双人对决如果你玩过那种会按顺序亮灯、然后让你重复按对的“西蒙”记忆游戏可能会觉得一个人玩久了有点孤单。这次我们把这个经典游戏彻底改造让它变成一场紧张刺激的双人对决——Simon Standoff。这不仅仅是一个简单的Arduino项目它融合了嵌入式系统编程、人机交互界面设计、基础电路搭建甚至用到了激光切割来制作一个精致的“竞技场”外壳。对于创客、电子爱好者或者任何想深入理解如何将代码逻辑转化为实体互动体验的人来说这个项目都是一个绝佳的练手机会。项目的核心是使用两块Arduino Nano 33 IoT作为“大脑”分别控制两位玩家的操作面板。每个面板上有四个带LED背光的瞬时按钮红、黄、绿、蓝。游戏开始时系统会生成并演示一个随机的灯光序列然后两位玩家需要竞速输入正确的序列。一旦有人按错或超时他面板上的所有LED就会快速闪烁提示出局而另一位玩家则继续挑战更长的序列直到最终决出胜者。更有趣的是在待机状态下这些按钮的LED会循环呼吸变色像一个无声的邀请吸引人们前来对战。接下来我将从设计思路、硬件搭建、代码逻辑到外壳制作完整拆解这个项目的每一个环节并分享我在复现过程中踩过的坑和总结的经验。2. 整体设计与核心思路拆解2.1 从单人到双人游戏逻辑的演变传统的Simon游戏是人机对战玩家记忆并复现机器给出的序列。而Simon Standoff的核心创新在于引入了“竞争”维度。这里的设计难点在于如何公平地让两个玩家同时响应同一个序列并实时、准确地判断对错。我采用的思路是“中央序列独立响应”。系统由其中一块Arduino担任主控生成并演示灯光序列这个序列对两位玩家是相同的。演示结束后两块Arduino同时进入监听状态等待各自连接的按钮被按下。关键在于时序判断系统不仅判断按下的按钮颜色是否正确还要判断按键的时机是否在合理的“回合”内。如果玩家A在玩家B之前按下了正确的下一个颜色那么系统会立即点亮玩家A对应的按钮LED作为正确反馈并等待序列中的下一个颜色被按下如果玩家按错则立即判定该玩家本轮失败。这种设计确保了竞争的实时性和公平性。注意这里没有采用“轮流应答”的模式而是真正的“抢答”。这要求代码必须有极高的响应速度和去抖动处理能力防止因按键抖动误判为多次按压。2.2 硬件架构选型为何是双Arduino Nano 33 IoT你可能会问用一个更强大的主控比如Arduino Mega或ESP32来控制所有8个按钮不行吗当然可以但这会带来两个主要问题一是接线复杂所有16根信号线8个LED控制8个按钮读取需要集中到一个板子上线路杂乱且容易干扰二是IO口资源紧张普通的Arduino Uno的IO口可能不够用。使用两块Arduino Nano 33 IoT的方案则优雅得多模块化与对称性每位玩家一个独立的控制单元电路完全对称便于搭建、调试和复用。代码也可以几乎相同只需稍作修改区分玩家身份。资源充足Nano 33 IoT虽然小巧但数字IO口足够驱动4个LED和读取4个按钮并且还有余量。通信需求可选扩展Nano 33 IoT自带WiFi和蓝牙模块。在这个基础版本中我们通过同步电源启动和相同的随机种子来实现序列同步。但如果你想让游戏更有趣完全可以利用其无线功能让两块板子无线同步游戏状态实现物理分离的两个控制台对战这为项目留下了巨大的升级空间。成本与复杂度平衡相比一个高端主板两块Nano的成本可能更低且将复杂度分解更易于初学者理解和分步实现。2.3 交互设计灯光、按钮与状态反馈人机交互的核心是提供清晰、即时的反馈。在这个项目中我们通过多种灯光模式来传达信息序列演示模式所有按钮LED熄灭系统按顺序点亮特定颜色的LED每个点亮持续约500毫秒间隔250毫秒。为了增加难度也可以让播放速度逐渐加快。玩家输入模式玩家按下按钮时该按钮的LED应立刻给予视觉反馈如亮度提高或短暂闪烁表示输入已被接收。正确反馈玩家按对序列中的下一个按钮时该按钮LED常亮约300毫秒然后熄灭提示正确并进入等待下一个输入的状态。错误反馈玩家按错时该玩家所有的四个LED快速闪烁如每秒5次2-3秒。这是一个非常强烈且直观的“出局”信号。待机吸引模式游戏未开始时所有LED执行缓慢的呼吸灯效果或彩虹循环吸引注意力。按钮选择16mm带灯自锁瞬时开关是关键。它把LED和按钮开关集成在一个组件里简化了电路和结构安装。我们需要区分它的四个引脚两个是LED的阳极()和阴极(-)另外两个是按钮开关的两个触点通常不分正负。3. 核心电路解析与搭建要点3.1 元器件清单与选型考量除了项目正文中列出的基础清单在实际制作中我建议你准备以下物品会让过程更顺利焊接工具一把好用的电烙铁、焊锡丝、助焊剂。焊接集成按钮的引脚是必须的。线材建议使用不同颜色的硅胶导线如红色用于正极黑色用于负极其他颜色用于信号线这样在复杂的面包板电路中更容易排查错误。正文中提到的“实芯导线”更适合在面包板上插拔但焊接时多股铜芯的导线更容易操作。电源两个Arduino Nano 33 IoT可以通过USB独立供电但为了整洁也可以使用一个5V/2A的USB电源适配器配合一个面包板电源模块为两个板子和所有LED统一供电。切记Nano 33 IoT的工作电压是3.3V其IO口输出电压也是3.3V这与传统的5V Arduino不同。电阻计算LED限流电阻的选择很重要。假设我们使用的LED在3.3V下正向压降约为2.0V不同颜色略有差异期望电流为15mA足够亮且安全。根据欧姆定律R (Vcc - Vf) / I (3.3V - 2.0V) / 0.015A ≈ 86.7Ω。选择330Ω是一个更为保守和通用的值它能将电流限制在更安全的约4mA虽然亮度稍低但非常稳定能有效保护Arduino的IO口和LED。如果你追求亮度可以选用220Ω或150Ω的电阻但务必先测试。3.2 电路连接详解从原理图到面包板这是整个项目最需要耐心的一步。我们以一位玩家的一个按钮例如红色为例详解连接方法电源总线建立在面包板上用跳线将两侧的“正极轨”通常标有红线连接起来同样连接两侧的“负极轨”通常标有蓝线。将Arduino Nano 33 IoT的3.3V引脚连接到正极轨GND引脚连接到负极轨。这样就在整个面包板上建立了统一的电源网络。带灯按钮引脚辨识拿到按钮翻到背面通常会有标记。最常见的标记是“”、“-”表示LED部分“NO”常开、“C”公共端或“1”、“2”表示开关部分。用万用表的二极管档或电阻档可以轻松确认LED引脚之间有单向导电性开关引脚在未按下时开路按下后短路。LED部分电路连接将按钮的LED引脚通过一根导线连接到Arduino的一个数字IO口例如D18。这个IO口将用于输出PWM信号来控制LED亮度。将按钮的LED-引脚先连接到面包板的一个空行然后从这个空行串联一个330Ω的限流电阻最后连接到电源的负极轨GND。这里必须串联电阻直接接地会烧毁LED或损坏Arduino端口。按钮开关部分电路连接将按钮的一个开关引脚如C通过一根导线连接到Arduino的另一个数字IO口例如D9。这个IO口在代码中需要配置为INPUT_PULLUP模式即启用内部上拉电阻。将按钮的另一个开关引脚如NO直接连接到电源的正极轨3.3V。为什么这样连接当按钮未按下时D9引脚通过内部上拉电阻连接到芯片内部的3.3V因此我们读取到的是HIGH高电平。当按钮按下时开关闭合D9引脚通过导线直接与3.3V正极相连但更重要的是它也通过一个几乎为零电阻的路径接到了3.3V此时读取的仍然是HIGH等等这里有个关键点实际上在启用内部上拉电阻的情况下当按钮将引脚直接接到正极3.3V这与内部上拉提供的电压一致引脚电平仍是HIGH。为了检测按下动作更常见的接法是将开关一端接GND另一端接IO口。这样未按下时内部上拉IO口为HIGH按下时IO口被短接到GND变为LOW。原文的描述可能容易引起误解。我强烈建议采用“按钮接GND”的方式因为它更符合常规逻辑按下低电平。重复与扩展为红色按钮的LED和开关连接好后完全按照相同的逻辑将黄、绿、蓝色按钮分别连接到(D19, D3),(D20, D4),(D21, D7)。另一位玩家的电路在另一块面包板和Arduino上镜像搭建。实操心得在面包板上搭建复杂电路时一定要“分区块、分功能”进行。先搭建好一个按钮的完整电路并编写一个简单的测试程序例如按下按钮点亮对应LED确认其工作正常。然后再复制出第二个、第三个。全部接完再调试如果出问题排查起来会非常痛苦。3.3 电路图与实物对应关系原文提到了Fritzing图中元件不匹配的问题这在实际创客项目中非常普遍。我们的策略是“功能等效替换”。在绘图软件中我们可以用一个普通的LED和一个独立的瞬时按钮拼凑成一个“带灯按钮”的符号并在旁边做好文字标注。关键是理清电气连接关系LED的阳极- Arduino PWM引脚。LED的阴极- 限流电阻 - GND。按钮的一个引脚- Arduino数字输入引脚配置为上拉输入。按钮的另一个引脚- GND推荐或VCC需调整代码逻辑。画出清晰的示意图即使元件符号不标准也能极大帮助你在焊接和组装时保持头脑清醒。4. 代码逻辑深度剖析与实现代码是这个项目的灵魂它决定了游戏的流畅度、公平性和趣味性。我将核心逻辑分解为几个模块。4.1 引脚定义与全局变量首先我们需要定义所有硬件连接的引脚并声明游戏状态变量。// 玩家1的引脚定义 (假设使用Arduino Nano 33 IoT #1) const int playerLEDs[] {18, 19, 20, 21}; // 红黄绿蓝 LED控制引脚 const int playerButtons[] {9, 3, 4, 7}; // 对应按钮的输入引脚 const int ledCount 4; // 游戏序列相关 int gameSequence[100]; // 存储生成的序列100足够长了 int sequenceLength 1; // 当前回合的序列长度 int currentStep 0; // 玩家当前需要按下的序列位置 // 游戏状态 enum GameState { ATTRACT, PLAYBACK, INPUT, CORRECT, WRONG, WIN }; GameState state ATTRACT; // 计时相关 unsigned long playbackStartTime; int playbackIndex 0; const int playbackDuration 500; // 每个灯亮的时间(ms) const int pauseDuration 250; // 灯灭间隔时间(ms) // 玩家输入相关 bool buttonPressed[4] {false, false, false, false}; bool lastButtonState[4] {HIGH, HIGH, HIGH, HIGH}; // 上拉初始为HIGH unsigned long lastDebounceTime[4] {0, 0, 0, 0}; const unsigned long debounceDelay 50; // 消抖时间4.2 状态机游戏流程的核心引擎整个游戏运行由一个“状态机”控制。这是处理复杂时序逻辑的经典方法。程序在任何时刻只处于一个状态并根据条件切换到下一个状态。void loop() { switch (state) { case ATTRACT: runAttractMode(); // 运行吸引模式呼吸灯 if (检测到开始游戏信号如某个按钮长按) { initializeGame(); state PLAYBACK; } break; case PLAYBACK: playSequence(); // 播放序列 if (序列播放完毕) { currentStep 0; state INPUT; } break; case INPUT: listenForInput(); // 监听玩家输入 // 在listenForInput函数内部会判断对错并切换状态 break; case CORRECT: // 正确反馈如点亮按钮后熄灭 delay(300); currentStep; if (currentStep sequenceLength) { // 本轮序列全部输入正确 sequenceLength; state PLAYBACK; // 进入下一轮播放更长的序列 } else { state INPUT; // 继续输入当前序列的下一个 } break; case WRONG: // 错误反馈所有LED快速闪烁 triggerWrongAnimation(); // 游戏结束逻辑可以返回ATTRACT状态或显示失败画面 state ATTRACT; break; case WIN: // 胜利反馈如喝彩灯光秀 runWinAnimation(); delay(3000); state ATTRACT; break; } }4.3 关键函数详解1. 初始化与序列生成void initializeGame() { sequenceLength 1; randomSeed(analogRead(A0)); // 利用未连接的模拟引脚噪声作为随机种子 // 确保两位Arduino使用相同的随机种子例如都读A0或通过其他方式同步 for (int i 0; i 100; i) { gameSequence[i] random(0, 4); // 生成0-3的随机数对应四个颜色 } playbackIndex 0; playbackStartTime millis(); }2. 播放序列函数void playSequence() { unsigned long currentTime millis(); unsigned long elapsed currentTime - playbackStartTime; int currentPhase elapsed / (playbackDuration pauseDuration); int phaseTime elapsed % (playbackDuration pauseDuration); if (currentPhase sequenceLength) { // 处于“亮灯”或“灭灯”阶段 if (phaseTime playbackDuration) { // 亮灯阶段 digitalWrite(playerLEDs[gameSequence[currentPhase]], HIGH); } else { // 灭灯阶段 allLEDsOff(); } } else { // 序列播放完毕 allLEDsOff(); playbackStartTime currentTime; // 重置计时器为可能的需要做准备 // 状态切换由loop()中的条件判断处理 } }3. 带消抖的输入监听函数这是确保游戏响应准确无误的核心。机械按钮在按下和释放时会产生快速的电平抖动如果不处理会被误判为多次按压。void listenForInput() { for (int i 0; i ledCount; i) { int reading digitalRead(playerButtons[i]); // 检查信号是否变化从HIGH到LOW即按下 if (reading ! lastButtonState[i]) { lastDebounceTime[i] millis(); // 重置消抖计时器 } // 如果信号稳定时间超过了消抖延时 if ((millis() - lastDebounceTime[i]) debounceDelay) { // 确认信号状态已稳定 if (reading ! buttonPressed[i]) { buttonPressed[i] reading; // 如果按钮被按下稳定在LOW状态 if (buttonPressed[i] LOW) { onButtonPressed(i); // 处理按钮按下事件 } } } lastButtonState[i] reading; } } void onButtonPressed(int buttonIndex) { // 立刻给予视觉反馈例如让对应LED更亮 analogWrite(playerLEDs[buttonIndex], 255); // 判断对错 if (buttonIndex gameSequence[currentStep]) { // 按对了 state CORRECT; } else { // 按错了 state WRONG; } }4. 吸引模式与动画效果为了让待机界面不枯燥我们可以实现一个简单的呼吸灯效果。void runAttractMode() { long currentTime millis(); int brightness (exp(sin(currentTime / 2000.0 * PI)) - 0.36787944) * 108.0; // 生成0-255的平滑正弦波值 for (int i 0; i ledCount; i) { // 可以给每个灯不同的相位形成波浪效果 int phaseShift i * 1000; int individualBrightness (exp(sin((currentTime phaseShift) / 2000.0 * PI)) - 0.36787944) * 108.0; analogWrite(playerLEDs[i], individualBrightness); } }5. 双板同步与通信策略基础版本中两位玩家的游戏体验是独立的相当于各自在和同一个“幽灵西蒙”比赛。要实现真正的“Standoff”对峙需要让两块Arduino知道对方的状态例如一方出错后另一方自动获胜。这里有几种同步策略硬件同步简单用一根导线连接两块Arduino的某个IO口。当一方游戏失败时将该引脚拉低或拉高另一方持续检测这个引脚。一旦检测到信号变化立即宣布自己获胜并播放胜利动画。这种方法简单可靠延迟极低。软件序列同步基础在initializeGame()函数中使用相同的随机种子。例如都使用randomSeed(analogRead(A0))并在上电后等待几秒由主持人同时按下两个“开始”按钮。由于模拟引脚A0的浮动噪声在短时间内是相似的这样生成的随机序列也大概率相同。但这并非绝对精确。无线通信进阶利用Nano 33 IoT的蓝牙或WiFi功能。可以设置一个为主机负责生成序列并判断对错另一个为从机只负责输入和显示。主机通过无线模块将游戏状态当前序列、轮到谁、对错结果实时发送给从机。这需要学习ArduinoBLE或WiFiNINA库复杂度较高但最具扩展性。注意事项如果采用无线方案务必处理好通信失败的情况。例如加入超时重传、连接状态指示灯以及通信中断时的降级处理逻辑比如默认判平局或进入单机模式。6. 结构设计与激光切割制作一个坚固、美观的外壳能极大提升项目的完成度和质感。激光切割亚克力是创客项目的常见选择。6.1 设计要点尺寸规划根据你的面包板、Arduino和内部走线空间来确定盒子内部尺寸。文中提到12x8x4约30x20x10厘米是一个较大的尺寸适合将所有元件平铺。你可以量取所有元件布局后的最大长、宽、高每边额外增加1-2厘米作为余量。按钮孔位顶板需要开8个直径为15mm的圆孔。孔距至关重要。你需要根据按钮的实际直径和面板布局来精确计算。按钮的裙边法兰通常比15mm大所以孔不能开得太大否则按钮会掉进去也不能太小否则装不进去。最好先购买按钮实物测量后在设计图中预留比按钮主体直径小0.1-0.2mm的紧配孔或者设计一个带卡扣的结构。指接榫设计这是激光切割盒子最常用的连接方式。通过设计像齿轮一样交错的榫头可以让板材无需胶水就能拼插起来方便拆装。常用的设计软件如Fusion 360有专门的插件如Slicer for Fusion 360可以自动生成指接榫盒子你只需输入尺寸和板材厚度如1/8英寸约3mm。通风与走线孔别忘了在盒子侧面或背面设计一些小孔用于USB线穿出供电以及必要的散热。6.2 制作与组装流程文件准备使用矢量绘图软件如Inkscape, Adobe Illustrator或CAD软件如Fusion 360绘制DXF文件。确保所有线条都是连续的路径并将不同切割/雕刻的线条分到不同图层如红色线条为切割蓝色线条为雕刻。激光切割材料使用3mm厚的透明或有色亚克力。第一次制作建议先用便宜的椴木板或MDF板试切验证尺寸和结构。参数激光切割机的功率、速度和焦距需要根据材料和厚度进行设置。通常供应商或机器手册会提供参考值。例如对于3mm亚克力可能使用较高功率如70%和中等速度如15mm/s进行切割。安全第一全程佩戴防护眼镜机器工作时不要离开确保通风良好附近有灭火设备。组装与粘合先将所有指接榫部分小心地拼插起来检查是否严丝合缝。如果使用亚克力胶水氯仿类它通过溶解亚克力表面使其融合。操作时用针头瓶点在接缝处利用毛细作用吸入几秒钟即可粘牢。务必在通风极好的环境下操作避免吸入蒸汽。也可以使用透明的超级胶水氰基丙烯酸酯但粘接强度和美观度可能不如专用胶水。内部安装先将按钮从顶板内侧穿出用配套的螺母锁紧。将焊接好长导线的按钮以及Arduino、面包板等元件用尼龙扎带、双面泡棉胶或螺丝固定在盒底。最后整理导线用扎带捆好确保没有短路风险再盖上顶板。7. 调试、测试与问题排查实录即使按照教程一步步来也难免会遇到问题。下面是我在复现过程中遇到的一些典型问题及解决方法。7.1 常见问题速查表问题现象可能原因排查步骤与解决方案LED完全不亮1. 电源未接通或电压不对。2. LED正负极接反。3. 限流电阻阻值过大或断路。4. Arduino引脚未正确设置为输出模式。1. 用万用表测量面包板正负极轨电压是否为3.3V。2. 检查LED引脚定义交换正负极试试。3. 检查电阻焊接是否牢固尝试更换一个220Ω电阻。4. 在setup()函数中确认使用了pinMode(pin, OUTPUT)。LED常亮不受控1. LED控制引脚与VCC短路。2. 程序逻辑错误一直输出高电平。1. 断电用万用表通断档检查该引脚与3.3V是否意外连通。2. 编写一个最简单的闪烁测试程序隔离硬件问题。按钮无反应1. 按钮接线错误特别是上拉/下拉逻辑。2. 引脚模式未设置为INPUT_PULLUP。3. 消抖代码有误或延时过长。4. 按钮本身损坏。1.最可能确认按钮是否一端接IO口另一端接GND推荐。如果用原文接VCC的方式代码逻辑需反转按下读HIGH。2. 检查pinMode(buttonPin, INPUT_PULLUP)。3. 简化代码去掉消抖逻辑直接打印引脚状态看是否变化。4. 用万用表通断档测试按钮按下时是否导通。游戏序列不同步1. 两块Arduino的随机种子不同。2. 游戏开始时机不一致。1. 尝试使用固定种子如randomSeed(1234)先测试同步性。2. 设计一个“准备就绪”指示灯由主持人统一触发开始。或采用硬件同步线。程序运行不稳定偶尔复位1. 电源功率不足特别是所有LED全亮时电流过大。2. 导线接触不良。3. 代码中有内存泄漏或数组越界。1. 计算总电流4个LED * 2块板子 * 约15mA 120mA加上MCU自身建议使用能提供1A以上的USB电源。2. 检查所有跳线和焊接点。3. 检查数组如gameSequence访问索引是否可能越界。7.2 分模块调试法强烈建议采用“分模块调试”策略不要试图一次性写完所有代码并让它工作。LED测试模块写一个程序循环点亮、熄灭每一个LED确保每个LED及其电路工作正常。按钮测试模块写一个程序每按下一个按钮就在串口监视器打印对应的编号确保每个按钮都能被正确识别且消抖有效。单机游戏逻辑模块在一个Arduino上实现完整的单人Simon游戏包括序列生成、播放、输入判断和反馈。这是最核心的部分确保逻辑无误。双机通信/同步模块在单人逻辑稳定的基础上再添加双板同步功能。外壳整合最后将调试好的电子部分装入外壳。装入前最好再次进行完整功能测试。7.3 关于Nano 33 IoT的特别提醒3.3V逻辑电平它的IO口是3.3V耐受的。如果你要连接一些5V的传感器或模块可能需要电平转换器。本项目所有元件在3.3V下工作所以没问题。引脚功能注意D0和D1通常用于串口通信RX/TX在上传程序时不要连接其他东西否则可能导致上传失败。D2,D4,D7等是普通的数字IO可以安全使用。模拟写入控制LED亮度使用的是analogWrite(pin, value)它实际上是通过PWM脉冲宽度调制模拟的。Nano 33 IoT的PWM引脚是那些旁边有波浪线~标记的如D3,D5,D6,D9,D10等。确保你连接的LED控制引脚支持PWM。8. 项目优化与扩展思路当你成功复现基础版本后可以尝试以下优化让项目更具挑战性和趣味性增加声音反馈加入一个无源蜂鸣器为不同的游戏事件正确、错误、胜利、待机配上不同的音效或旋律体验立刻提升一个档次。难度分级在游戏开始前通过某个按钮选择难度。简单模式序列播放速度慢困难模式速度快甚至可以在序列中加入短暂的“干扰灯光”。分数系统与显示加入一个OLED或LCD屏幕实时显示当前回合数、玩家的反应时间甚至历史最高分。更多游戏模式除了“竞速模式”可以增加“合作模式”两位玩家需要交替正确输入序列或者“镜像模式”一位玩家看到的序列需要另一位玩家镜像输入。网络排行榜利用Nano 33 IoT的WiFi功能将玩家的最高分上传到某个网络服务器实现全球排名。更酷的外壳使用半透明或磨砂亚克力配合内部的RGB LED可以做出更炫酷的灯光效果。甚至可以用3D打印制作更有设计感的按钮支架和外壳。这个Simon Standoff项目就像一棵技能树的主干掌握了它你就同时点亮了嵌入式编程、数字电路、人机交互和数字制造这几个重要的技能点。最重要的是它充满了亲手创造的乐趣和与朋友对战的竞技快感。从点亮第一个LED到完成一场紧张的对决每一步的调试和成功都会带来巨大的成就感。希望这份超详细的拆解能帮你绕过我踩过的那些坑顺利打造出属于你自己的记忆游戏竞技场。如果在制作过程中遇到任何新问题随时可以回溯到对应的模块进行排查记住耐心和模块化思维是创客最好的朋友。