1. 项目概述当经典文字游戏遇上开源硬件“Hangman”猜词游戏这个经典游戏相信大家都不陌生。它考验的是玩家的词汇量和逻辑推理能力。但你想过没有如果把这个游戏从纸笔或屏幕上搬下来变成一个看得见、摸得着的实体设备会是什么样子这就是“Arduino Hangman Game”项目的核心魅力。它不是一个简单的软件模拟而是一个融合了硬件交互、逻辑控制和趣味性的综合性电子制作项目。简单来说这个项目就是用一块Arduino开发板作为大脑驱动一个LED点阵屏来显示猜词进度比如被猜的单词用下划线表示已猜对的字母亮起再用几个物理按钮让玩家输入猜测的字母同时通过一个数码管或更多的LED来显示剩余的错误次数也就是“上吊小人”的绘制进度。当错误次数用尽游戏结束猜对所有字母则玩家获胜。整个过程玩家是在与一个实实在在的硬件设备互动这种体验远比点击鼠标或触摸屏幕要来得有趣和深刻。这个项目非常适合已经掌握了Arduino基础如数字输入输出、串口通信并想向综合应用迈进的爱好者。通过它你将系统地实践如何将一个问题分解为硬件模块选型、电路连接、状态机软件设计等多个环节最终整合成一个可运行的整体。它涉及的核心技术点包括多路数字输入按钮防抖处理、LED点阵屏的驱动行列扫描、游戏状态机的逻辑实现以及如何让有限的硬件资源呈现出清晰的游戏界面。接下来我将带你从零开始拆解这个项目的每一个环节分享我在实际制作中踩过的坑和总结的技巧。2. 核心硬件选型与电路设计思路做硬件项目第一步永远是“用什么”和“怎么连”。对于Arduino猜词游戏硬件清单并不复杂但每一样的选择都关系到最终体验和代码复杂度。2.1 主控与显示单元大脑和面孔主控芯片首推Arduino Uno R3。它拥有14个数字IO口和6个模拟输入口对于这个项目绰绰有余。Uno的生态最为成熟资料丰富排查问题也最方便。像Nano、Leonardo虽然体积小但初期调试时没有独立的USB转串口芯片有时会遇到驱动问题增加不必要的麻烦。所以从稳定性出发Uno是新手和老手都不会错的选择。显示设备这是项目的视觉核心有两种主流方案8x8 LED点阵屏MAX7219驱动这是我强烈推荐的方案。一个8x8的点阵屏有64个LED足以显示一个单词每个字母用5x7的点阵字体加上间隔一行显示4-5个字母很轻松。关键是MAX7219是一个专用的LED驱动芯片它通过SPI接口与Arduino通信你只需要连接3根线DIN, CLK, CS它就能帮你搞定所有繁琐的行列扫描和电流驱动大大节省了Arduino的IO口和CPU资源。市面上常见的红色8x8点阵模块价格低廉集成度高。OLED显示屏I2C接口另一种更“高端”的选择。比如0.96寸的128x64像素OLED屏。它的优势是分辨率高可以显示更漂亮的字体甚至图形比如精细的“上吊小人”图案。通过I2C接口也只需要连接2根线SDA, SCL。缺点是成本稍高且驱动库相对复杂一点对于纯粹显示字母和简单图形来说有点“杀鸡用牛刀”。但如果你追求显示效果这是个好选择。实操心得对于第一次做这个项目我建议使用MAX7219点阵屏。原因很简单库成熟LedControl库接线简单编程直观。你不需要关心底层扫描只需调用setLed()或setChar()函数就能控制每一个像素点。这让你能把精力集中在游戏逻辑本身。2.2 输入与反馈单元如何与游戏互动字母输入我们需要26个字母的输入方式。显然接26个独立按钮不现实。最实用的方案是使用一个4x4矩阵键盘。它只用8个IO口4行4列就能实现16个按键的检测足够容纳A-Z的字母通常可以复用一些键或者通过“模式切换”键来输入更多字符。矩阵键盘的扫描程序有现成的库如Keypad实现起来非常方便。错误次数显示/“上吊小人”这部分是游戏的灵魂反馈。有几种实现方式一位共阴数码管最简单直接。用7段数码管显示数字如从6倒数到0。只需要7个IO口如果用74HC595移位寄存器则只需3个IO口。直观但缺乏趣味性。多个LED灯用6-8个独立的LED每错一次就点亮一个模拟“小人”身体部位被画出。接线稍多但效果生动。在点阵屏或OLED上绘制这是最集成化的方案。在点阵屏的空白区域或者OLED屏上用图形方式画出“绞架”和“小人”。这需要一定的图形编程能力但整体感最强。其他可选硬件蜂鸣器用于游戏开始、猜对、猜错、胜利或失败的音效反馈极大增强体验。一个无源蜂鸣器接一个IO口即可。复位按钮除了软件复位一个硬件复位按钮能让玩家快速开始新游戏。2.3 电路连接图与供电考量电路连接的核心是理清各模块与Arduino的接口。假设我们采用Arduino Uno MAX7219点阵屏 4x4矩阵键盘 一位数码管 蜂鸣器的方案MAX7219点阵屏VCC - Arduino 5VGND - Arduino GNDDIN - 引脚11 (MOSI)CLK - 引脚13 (SCK)CS - 引脚10 (可自定义)4x4矩阵键盘行线R1-R4 - 引脚9, 8, 7, 6列线C1-C4 - 引脚5, 4, 3, 2具体引脚可自定义需与代码中Keypad库初始化一致一位共阴数码管通过74HC595驱动节省IO口74HC595的DS (数据) - 引脚12SH_CP (时钟) - 引脚13 (可与MAX7219的CLK共用但建议分开以防干扰或用另一引脚)ST_CP (锁存) - 引脚11 (可与MAX7219的DIN共用同样建议分开)数码管段选通过595输出控制位选直接接GND因为只有一位。蜂鸣器正极 - 引脚A0 (或其他数字PWM引脚用于播放不同频率)负极 - GND注意事项当模块增多特别是使用数码管和点阵屏这种电流较大的设备时务必注意供电。单个USB口供电可能不足导致屏幕闪烁或Arduino重启。建议准备一个5V/2A的直流电源适配器通过Arduino的DC接口供电确保系统稳定运行。3. 软件架构与核心逻辑实现硬件搭好了接下来就是赋予它灵魂的软件部分。这个项目的代码结构比简单的闪烁LED要复杂因为它是一个典型的**状态机State Machine**应用。3.1 游戏状态机设计猜词游戏可以清晰地划分为几个状态初始化状态INIT设备上电显示欢迎信息或游戏标题。准备状态READY等待玩家按下“开始”键。显示提示信息。游戏进行状态PLAYING核心状态。在这个状态下系统需要持续做几件事扫描矩阵键盘获取玩家输入的字母。判断字母是否在目标单词中。更新点阵屏上单词的显示揭示猜对的字母。更新错误次数数码管或LED。绘制“上吊小人”进度如果采用图形绘制。播放相应的音效。判断游戏是否结束胜利或失败。胜利状态WIN显示胜利动画或信息播放欢快音效等待复位。失败状态LOSE显示失败动画或完整的“上吊小人”播放悲伤音效显示正确单词等待复位。在代码中我们会用一个全局变量如gameState来标记当前状态在主循环loop()中通过一个switch-case语句来执行不同状态下的特定任务。enum GameState {INIT, READY, PLAYING, WIN, LOSE}; GameState gameState INIT; void loop() { switch(gameState) { case INIT: showWelcomeMessage(); delay(2000); gameState READY; break; case READY: if (startButtonPressed()) { setupNewGame(); // 随机选词重置错误次数等 gameState PLAYING; } break; case PLAYING: char key keypad.getKey(); // 获取按键 if (key) { processGuess(key); // 处理猜测 } checkGameOver(); // 检查游戏是否结束并可能切换至WIN/LOSE状态 break; case WIN: playVictoryTune(); showWinAnimation(); break; case LOSE: playLoseTune(); showCorrectWord(); break; } }3.2 核心函数拆解猜词逻辑与显示更新processGuess(char guessedLetter)是这个游戏最核心的函数之一。它的伪代码如下void processGuess(char letter) { // 1. 将字母转换为大写确保一致性 letter toupper(letter); // 2. 检查这个字母是否已经被猜过维护一个已猜字母数组 if (isLetterAlreadyGuessed(letter)) { beepWarning(); // 提示已猜过 return; } // 3. 标记该字母为已猜 addToGuessedLetters(letter); // 4. 判断是否在目标单词中 bool correctGuess false; for (int i 0; i targetWordLength; i) { if (targetWord[i] letter) { // 5. 如果在更新显示数组在对应位置显示该字母 revealedWord[i] letter; correctGuess true; } } // 6. 根据猜测结果更新显示和状态 if (correctGuess) { updateDisplay(); // 刷新点阵屏显示被揭示的字母 playCorrectSound(); // 检查是否所有字母都被猜出即revealedWord中没有‘_’了 if (isWordComplete()) { gameState WIN; } } else { // 7. 猜错了减少剩余错误次数 wrongAttemptsLeft--; updateWrongAttemptsDisplay(); // 更新数码管或LED drawNextHangmanPart(); // 绘制下一部分身体 playWrongSound(); // 检查错误次数是否用尽 if (wrongAttemptsLeft 0) { gameState LOSE; } } }updateDisplay()函数负责将revealedWord数组的内容输出到点阵屏上。这里涉及点阵屏的字符显示。通常我们需要一个字体库将字母的‘A’-‘Z’映射到5x7的点阵数据。使用LedControl库的setChar()函数可以方便地显示标准字符但如果你需要自定义位置或同时显示多个字母可能需要自己编写一个函数循环调用setLed()来点亮特定像素。3.3 单词库的管理与随机选择单词库可以硬编码在程序中的一个字符串数组里。例如const char* wordList[] {ARDUINO, HANGMAN, PROJECT, COMPUTER, PROGRAMMING};使用random()函数从中随机选择一个作为targetWord。实操心得硬编码的单词库大小有限。一个进阶技巧是利用Arduino的EEPROM存储单词库或者通过串口在上传程序后由电脑发送单词。更高级的玩法是连接一个SD卡模块从文本文件中读取大量单词。但初期建议从硬编码开始确保核心逻辑稳定。4. 分步实现与代码详解让我们将上述设计落地写出一段可运行的代码。这里以Arduino Uno MAX7219点阵屏 4x4矩阵键盘 一位数码管直接驱动的配置为例。4.1 步骤一库安装与引脚定义首先在Arduino IDE中安装必要的库LedControl用于点阵屏和Keypad用于矩阵键盘。可以通过“工具”-“管理库”进行搜索安装。接着进行引脚定义和对象初始化#include LedControl.h #include Keypad.h // 1. MAX7219点阵屏引脚定义 #define DIN_PIN 11 #define CLK_PIN 13 #define CS_PIN 10 LedControl lc LedControl(DIN_PIN, CLK_PIN, CS_PIN, 1); // 我们只有一个MAX7219 // 2. 4x4矩阵键盘定义 const byte ROWS 4; const byte COLS 4; char hexaKeys[ROWS][COLS] { {1,2,3,A}, {4,5,6,B}, {7,8,9,C}, {*,0,#,D} }; byte rowPins[ROWS] {9, 8, 7, 6}; // 连接行线的引脚 byte colPins[COLS] {5, 4, 3, 2}; // 连接列线的引脚 Keypad customKeypad Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS); // 3. 数码管引脚定义假设共阴直接驱动使用引脚A1-A7 const int digitPins[7] {A1, A2, A3, A4, A5, A6, A7}; // 对应a,b,c,d,e,f,g段 // 数码管显示数字0-9的段码共阴 const byte digitPattern[10] { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 }; // 4. 蜂鸣器引脚 #define BUZZER_PIN A0 // 5. 游戏全局变量 enum GameState {INIT, READY, PLAYING, WIN, LOSE}; GameState gameState INIT; String targetWord ARDUINO; String revealedWord; // 如 “A _ _ U _ _ O” int wrongAttemptsLeft 6; // 初始错误次数 String guessedLetters ; // 已猜字母记录4.2 步骤二初始化设置setup函数在setup()函数中我们需要初始化所有硬件模块。void setup() { Serial.begin(9600); // 用于调试可选 // 初始化点阵屏 lc.shutdown(0, false); // 唤醒MAX7219 lc.setIntensity(0, 8); // 设置亮度0-15 lc.clearDisplay(0); // 清屏 // 初始化数码管引脚为输出 for (int i 0; i 7; i) { pinMode(digitPins[i], OUTPUT); digitalWrite(digitPins[i], HIGH); // 共阴数码管段码高电平点亮先全部熄灭 } // 初始化蜂鸣器引脚 pinMode(BUZZER_PIN, OUTPUT); // 初始化游戏 setupNewGame(); // 显示欢迎信息 displayString(HI, 0); // 在点阵屏0位置显示“HI” delay(1000); lc.clearDisplay(0); gameState READY; }setupNewGame()函数负责重置游戏变量随机选择单词这里简化固定为“ARDUINO”并初始化显示。void setupNewGame() { // 1. 随机选择单词此处简化 // targetWord wordList[random(0, wordListSize)]; targetWord ARDUINO; // 2. 初始化revealedWord为下划线 revealedWord ; for (int i 0; i targetWord.length(); i) { revealedWord _; } // 3. 重置错误次数和已猜字母 wrongAttemptsLeft 6; guessedLetters ; // 4. 更新显示 updateDisplay(); updateWrongAttemptsDisplay(); // 5. 清空点阵屏上可能残留的“小人”图形如果之前画了 // ... 具体图形清除代码取决于你的绘制方式 }4.3 步骤三主循环与状态处理loop函数这是游戏逻辑的调度中心。void loop() { char key customKeypad.getKey(); // 非阻塞式读取按键 switch(gameState) { case READY: // 等待‘A’键作为开始键根据你的键盘布局定义 if (key A) { gameState PLAYING; lc.clearDisplay(0); displayString(revealedWord.c_str(), 0); beepStart(); } break; case PLAYING: if (key) { processGuess(key); } // 可以在这里加入一些动画效果比如闪烁光标可选 break; case WIN: playVictory(); // 胜利后等待复位键比如‘#’键 if (key #) { setupNewGame(); gameState READY; displayString(PLAY, 0); } break; case LOSE: playGameOver(); // 显示正确单词 displayString(targetWord.c_str(), 0); // 失败后等待复位键 if (key #) { setupNewGame(); gameState READY; displayString(PLAY, 0); } break; } // 更新数码管显示错误次数 updateWrongAttemptsDisplay(); }4.4 步骤四关键功能函数实现1. 更新点阵屏显示 (updateDisplay和displayString):由于LedControl库的setChar函数一次只能显示一个字符且位置固定我们需要自己实现一个简单的字符串显示函数让单词在点阵屏上水平展开。// 一个简单的在点阵屏上显示字符串的函数每个字符宽约5列留一列间隔 void displayString(const char* str, int startCol) { lc.clearDisplay(0); int currentCol startCol; for (int i 0; str[i] ! \0; i) { // 注意setChar需要字符的小数点数据这里显示大写字母 lc.setChar(0, currentCol, str[i], false); // false表示不带小数点 currentCol 6; // 移动到下一个字符位置5列字符1列间隔 if (currentCol 8) break; // 防止超出屏幕 } } void updateDisplay() { displayString(revealedWord.c_str(), 0); }2. 处理猜测 (processGuess):这是最核心的逻辑函数实现前面伪代码描述的功能。void processGuess(char key) { // 只处理字母键根据你的键盘布局映射这里假设B-Z是字母 if (key B || key Z) return; char guess key; // 我们的键盘布局里A是开始B-Z是字母 // 检查是否已猜过 if (guessedLetters.indexOf(guess) ! -1) { beepWarning(); return; } guessedLetters guess; bool correct false; // 检查猜测是否在目标单词中 for (int i 0; i targetWord.length(); i) { if (targetWord[i] guess) { revealedWord[i] guess; correct true; } } if (correct) { updateDisplay(); beepCorrect(); // 检查是否获胜 if (revealedWord.indexOf(_) -1) { gameState WIN; } } else { wrongAttemptsLeft--; updateWrongAttemptsDisplay(); drawHangman(); // 根据错误次数绘制小人 beepWrong(); if (wrongAttemptsLeft 0) { gameState LOSE; } } }3. 更新错误次数显示 (updateWrongAttemptsDisplay):控制数码管显示剩余错误次数。void updateWrongAttemptsDisplay() { if (wrongAttemptsLeft 0 wrongAttemptsLeft 9) { showDigit(wrongAttemptsLeft); } } void showDigit(int num) { byte pattern digitPattern[num]; for (int i 0; i 7; i) { // 从段码的最低位a段开始依次设置各引脚 // 注意这里假设digitPins数组顺序是a,b,c,d,e,f,g bool segmentState bitRead(pattern, i); digitalWrite(digitPins[i], segmentState ? LOW : HIGH); // 共阴LOW点亮 } }4. 绘制“上吊小人” (drawHangman):在8x8点阵屏的右侧区域绘制。我们可以为每个错误次数定义一组像素坐标。void drawHangman() { int errors 6 - wrongAttemptsLeft; // 已经错了多少次 switch(errors) { case 1: // 画头 lc.setLed(0, 1, 5, true); // (列, 行) 注意坐标可能需调整 lc.setLed(0, 2, 5, true); lc.setLed(0, 1, 6, true); lc.setLed(0, 2, 6, true); break; case 2: // 画身体 for (int row 3; row 4; row) { lc.setLed(0, 1, row, true); lc.setLed(0, 2, row, true); } break; case 3: // 左臂 lc.setLed(0, 0, 3, true); break; case 4: // 右臂 lc.setLed(0, 3, 3, true); break; case 5: // 左腿 lc.setLed(0, 0, 2, true); break; case 6: // 右腿 lc.setLed(0, 3, 2, true); // 同时画上绞架可选 for (int col 4; col 7; col) { lc.setLed(0, col, 7, true); // 底部横梁 } lc.setLed(0, 5, 6, true); // 竖梁 lc.setLed(0, 5, 5, true); // 顶部横梁 lc.setLed(0, 5, 4, true); // 绳子 break; } }5. 音效函数 (beepStart,beepCorrect,beepWrong,playVictory,playGameOver):使用tone()函数产生不同频率和时长的声音。void beepStart() { tone(BUZZER_PIN, 523, 200); // Do delay(200); tone(BUZZER_PIN, 659, 200); // Mi delay(200); tone(BUZZER_PIN, 784, 300); // Sol } void beepCorrect() { tone(BUZZER_PIN, 698, 150); // Fa delay(150); tone(BUZZER_PIN, 880, 150); // La } void beepWrong() { tone(BUZZER_PIN, 220, 300); // 低音 } void playVictory() { // 一小段胜利旋律 int melody[] {659, 659, 659, 523, 659, 784, 392}; int duration[] {200, 200, 200, 200, 200, 400, 400}; for (int i 0; i 7; i) { tone(BUZZER_PIN, melody[i], duration[i]); delay(duration[i] * 1.3); noTone(BUZZER_PIN); } } void playGameOver() { tone(BUZZER_PIN, 196, 500); // 低沉的音 delay(600); }5. 调试、优化与功能扩展即使代码写完直接运行很可能也会遇到各种问题。这一部分就是分享如何让项目从“能跑”到“跑得稳、跑得好”。5.1 常见问题与排查技巧点阵屏不亮或显示乱码检查接线确保VCC、GND、DIN、CLK、CS五根线连接牢固且DIN、CLK、CS接在了正确的数字引脚上。检查库和初始化确认安装了LedControl库且LedControl对象初始化时的引脚参数与实物连接一致。lc.shutdown(0, false)是唤醒屏幕的关键。亮度设置尝试lc.setIntensity(0, 15)调到最亮看看。电源问题点阵屏全亮时电流较大尝试使用外部电源供电。矩阵键盘按键无反应或串键引脚定义检查rowPins和colPins数组中的引脚号是否与实际焊接一致。上拉电阻Arduino内部有上拉电阻在Keypad库初始化后通常不需要外接上拉电阻。但如果发现按键不稳定可以在行线或列线上接10kΩ上拉电阻到5V。防抖Keypad库自带防抖但如果仍有问题可以尝试在代码中获取按键后加一个短延时delay(50)再判断。串口调试在loop()里加入if(key){ Serial.println(key); }通过串口监视器查看按下的键值是否正确这是最有效的调试手段。数码管显示错误或暗淡共阴/共阳搞错确认你的数码管是共阴还是共阳。示例代码是针对共阴的。如果是共阳需要将段码取反并且位选如果是多位数码管或共阳端接法不同。限流电阻每个段码引脚都应串联一个220Ω-1kΩ的电阻防止电流过大烧毁LED或Arduino引脚。电流不足如果所有段都亮时很暗可能是Arduino引脚输出电流不足每个引脚最大约40mA。考虑使用74HC595或ULN2003等驱动芯片。游戏逻辑错误如字母判断不准大小写问题确保targetWord和输入的key在比较前统一转为大写或小写使用toupper()或tolower()。已猜字母记录检查guessedLetters字符串的追加和查找逻辑是否正确。indexOf()返回-1表示未找到。串口打印在processGuess函数的关键步骤用Serial.print()打印出targetWord、guessedLetter、revealedWord等变量的值观察程序实际运行逻辑。5.2 性能与体验优化非阻塞式延迟游戏中的欢迎信息显示、胜利/失败动画如果用了delay()会导致整个程序卡住无法检测按键。可以使用millis()函数来实现非阻塞的定时操作保持系统响应。unsigned long previousMillis 0; const long interval 1000; // 1秒间隔 void loop() { unsigned long currentMillis millis(); if (currentMillis - previousMillis interval) { previousMillis currentMillis; // 执行需要定时执行的任务比如闪烁光标 blinkCursor(); } // ... 其他状态处理 }更丰富的单词库将单词库存储在PROGMEM程序存储器中可以节省宝贵的RAM空间。使用F()宏将字符串常量存储在Flash中。const char* const wordList[] PROGMEM {ARDUINO, HANGMAN, ...}; // 读取时需要特殊函数如strcpy_P添加难度选择通过按键如‘1’‘2’‘3’在游戏开始前选择单词长度或错误次数增加可玩性。使用OLED屏增强表现力换上OLED屏后你可以显示更美观的字体绘制更精细的绞架和小人图案甚至加入简单的动画。这需要学习U8g2或Adafruit_SSD1306等图形库的使用。5.3 项目扩展思路这个基础框架搭建好后你可以从多个方向进行扩展把它变成一个更酷的个人项目无线化加入蓝牙模块如HC-05或Wi-Fi模块ESP8266让手机APP可以发送单词或直接作为输入界面。语音输入接入一个简单的语音识别模块如LD3320实现“说字母猜单词”的功能。多人游戏增加一个按钮作为“玩家切换”两个玩家轮流猜用另一个数码管显示当前玩家分数。词库外设接入一个SD卡模块从存储卡中随机读取单词实现海量词库。网络更新词库如果使用ESP32可以连接网络定期从服务器更新热门词汇或每日谜题。这个Arduino猜词游戏项目从硬件焊接、软件编程到调试优化完整地走完了一个嵌入式小产品的开发流程。它没有用到特别高深的理论但非常考验将想法分解、模块化实现和系统整合的能力。我最深的体会是硬件项目成功的关键往往不在最复杂的代码而在最基础的细节一个没焊好的接头一个忘记初始化的变量一个没考虑到的电流问题都可能导致整个系统失灵。所以耐心和细致的调试永远是硬件开发者最好的朋友。当你最终看到自己制作的设备流畅地运行起来那种成就感是纯软件项目难以比拟的。希望这份详细的拆解能帮你顺利实现自己的Arduino Hangman Game并在此基础上玩出更多花样。