十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Arduino与1602 LCD的跳跃游戏开发:从硬件连接到状态机设计

基于Arduino与1602 LCD的跳跃游戏开发:从硬件连接到状态机设计 1. 项目概述用Arduino复刻经典跳跃游戏最近在整理工作室的物料翻出来几块闲置的1602 LCD屏和一个4x4矩阵键盘看着它们一个念头突然冒了出来能不能用这些最简单的元件做点好玩的东西想起小时候在掌机上玩过的那些像素风跳跃游戏虽然画面简陋但乐趣十足。于是“用Arduino做个跳跃小游戏”这个想法就成型了。这个项目我称之为“JumpMan Game”核心目标就是用最基础的Arduino Uno、一块字符型LCD和一个键盘实现一个可以互动游玩的跳跃闯关游戏。这不仅仅是为了怀旧。对于刚接触Arduino和嵌入式开发的朋友来说这是一个绝佳的练手项目。它涵盖了数字输入键盘、字符输出LCD、游戏逻辑状态机、计时控制等多个核心知识点。整个系统没有用到任何复杂的图形库或高级外设所有画面都由16x2的字符拼凑而成角色“JumpMan”可能就是一个“^”符号障碍物是“#”这种极致的简约反而能让你更专注于程序逻辑本身。通过这个项目你能深刻理解如何用代码在有限的硬件资源上构建一个完整的交互应用这种“螺蛳壳里做道场”的能力对嵌入式开发者至关重要。2. 核心硬件选型与电路设计思路2.1 主控与显示单元为何是Arduino Uno 1602 LCD选择Arduino Uno作为大脑几乎是所有入门项目的首选。它拥有14个数字I/O口和6个模拟输入口对于驱动一个LCD和键盘绰绰有余。更重要的是其庞大的社区和库支持比如驱动LCD的经典LiquidCrystal库能让我们免于纠缠在底层时序里快速上手。而选用1602字符型LCD而非更炫酷的图形OLED或TFT屏是本次设计的精髓所在。它的显示内容非常有限只有两行每行16个字符。这种限制本身就是一种挑战和乐趣。你无法绘制精细的精灵图必须用ASCII字符来“画”出游戏世界一个向上箭头^可以代表跳跃的小人一串#####可以代表地面或平台|可以代表尖刺障碍。这种表达方式迫使游戏设计回归本质专注于玩法和逻辑。从电气连接上看1602 LCD采用标准的并行接口需要连接6根控制线RS, RW, E, D4-D7到Arduino的数字引脚供电简单。2.2 输入设备4x4矩阵键盘的扫描原理游戏需要输入我们选择了4x4矩阵键盘。它只有16个按键但通过矩阵扫描只占用8个I/O口4行4列。相比独立按键这大大节省了宝贵的引脚资源。其工作原理是动态扫描程序依次将每一行Row设置为低电平然后读取所有列Column的状态。如果某个按键被按下对应的列线就会被拉低通过行列坐标就能唯一确定是哪个键。在游戏设计中我们可以定义其中几个键比如“2”、“8”、“4”、“6”分别代表“上”、“下”、“左”、“右”“5”或“A”键代表“确认/开始”。剩下的键可以用于“暂停”、“重新开始”等功能。使用矩阵键盘库可以简化扫描代码但自己实现一遍扫描逻辑对于理解嵌入式系统的输入处理非常有帮助。2.3 电路连接图与供电考量整个系统的连接非常清晰Arduino Uno核心控制器通过USB线供电和下载程序。1602 LCDVSS和K背光阴极接GNDVDD和A背光阳极接5V。RS、E、D4-D7分别接至Arduino的12、11、5、4、3、2引脚。RW引脚直接接地因为我们只进行写操作。通过一个10K电位器连接到VO引脚可以调节显示对比度。4x4矩阵键盘其8个引脚R1-R4 C1-C4分别接至Arduino的A0-A7这8个模拟引脚当作数字引脚使用。当然你也可以接其他任意8个数字引脚只需在代码中对应修改即可。注意在连接时务必确保电源5V和GND连接正确且稳定。LCD背光如果过亮或耗电大可以在背光阳极串联一个约220欧姆的限流电阻。整个系统电流不大USB供电完全足够。3. 游戏软件架构与核心逻辑实现3.1 状态机游戏逻辑的骨架对于这样一个小游戏使用状态机State Machine来管理游戏流程是最清晰、最可靠的方法。我们可以定义几个核心状态MENU_STATE游戏菜单状态显示“START”、“EXIT”等选项等待玩家用上下键选择和确认键进入。PLAY_STATE游戏进行状态这是最核心的状态包含游戏主循环。PAUSE_STATE游戏暂停状态按下暂停键时进入屏幕显示“PAUSED”再次按下恢复。GAME_OVER_STATE游戏结束状态角色死亡或通关后进入显示分数并提示“PRESS RESTART”。状态机的实现很简单就是一个全局变量gameState存储当前状态。主循环loop()函数根据gameState的值调用不同的处理函数如handleMenu()handleGame()等。状态之间的转换由按键事件触发比如在MENU状态下按下确认键就将gameState设为PLAY_STATE。3.2 游戏世界与角色的数据表示如何在代码中描述这个简单的世界地图World我们可以用一个二维字符数组worldMap[2][16]来模拟LCD的两行16列。初始化时第一行可能是空或有一些固定障碍第二行是地面一串-或#。更复杂的关卡可以预定义多个这样的“地图帧”在游戏中切换。角色Player用一个结构体来保存角色属性非常合适。struct Player { int x; // 水平位置 (0-15) int y; // 垂直位置 (0-1 0代表第一行1代表第二行) bool isJumping; // 是否处于跳跃状态 int jumpCounter; // 跳跃帧计数器 int lives; // 生命值 };这里y坐标只有0和1因为我们的LCD只有两行跳跃可以表现为角色从地面行y1短暂“消失”或变换成跳跃造型出现在空中行y0然后落下。3.3 核心游戏循环与物理模拟在PLAY_STATE的处理函数中游戏主循环按以下步骤运行读取输入调用键盘扫描函数获取当前按下的按键。更新逻辑移动根据左右键更新角色的x坐标并确保其不超出屏幕边界0-15。跳跃如果检测到上键按下且角色在地面y1且!isJumping则启动跳跃。设置isJumping true并初始化jumpCounter。跳跃过程可以用一个简单的抛物线模拟在跳跃的前半段角色“出现”在第一行y0后半段落回第二行y1。jumpCounter用于控制这个时序。重力与碰撞这是一个简化版“物理”。如果没有跳跃角色默认在地面。我们需要检测角色当前位置的“地图元素”是什么。如果脚下worldMap[y1][x]是空的角色应该下落这需要扩展地图行数到屏幕外或用变量记录角色真实高度。在我们的简易版中可以只处理“碰到尖刺|即减命”的碰撞。渲染画面这是最有趣的一步。我们需要根据最新的worldMap和player状态重新生成要显示到LCD上的两行字符串。char line0[17] “ “; // 第一行16空格结束符 char line1[17] “ “; // 第二行 // 先拷贝地图背景 strcpy(line0, worldMap[0]); strcpy(line1, worldMap[1]); // 根据角色位置和状态在对应位置画上角色 if(player.isJumping player.jumpCounter JUMP_HEIGHT) { line0[player.x] ‘^‘; // 跳跃时显示在第一行 } else { line1[player.x] ‘A‘; // 地面时显示在第二行 } // 将字符串设置到LCD lcd.setCursor(0,0); lcd.print(line0); lcd.setCursor(0,1); lcd.print(line1);延时控制使用delay()函数控制游戏帧率例如delay(100)使得游戏每秒更新约10帧这对于一个字符游戏来说足够了。更专业的做法是使用millis()进行非阻塞延时以便更精确地控制逻辑更新和渲染节奏。4. 关键代码模块详解与避坑指南4.1 LCD驱动与自定义字符创建LiquidCrystal库让显示文字变得简单但如果我们想显示一个简单的“小人”或“怪物”标准字符集可能不够。这时可以使用自定义字符Custom Character功能。1602 LCD的控制器允许用户定义8个5x8像素的自定义字符。例如我们可以定义一个跳跃的小人byte jumpMan[8] { B00100, B01110, B00100, B11111, B00100, B01010, B10001, B00000 };在setup()函数中使用lcd.createChar(0, jumpMan)将其注册到0号位置。之后在打印时用lcd.write(byte(0))即可显示。这比单纯用^或A看起来更有趣。但要注意自定义字符会占用显示控制器内存不宜过多定义。实操心得自定义字符的数组低位对应像素行的上方。B00100表示该行中间一个像素亮。设计时可以在纸上先画好格子。另外LCD在初始化后需要一定时间稳定在setup()中初始化LCD后最好加一个短暂的delay(100)。4.2 矩阵键盘的可靠读取与消抖自己实现键盘扫描并不复杂但必须处理按键消抖。机械按键在按下和释放的瞬间会产生快速的电平抖动可能导致一次按压被误读为多次。char readKeypad() { char key ‘\0‘; // 默认无按键 for (int row 0; row 4; row) { // 将当前行拉低 digitalWrite(rowPins[row], LOW); // 检查每一列 for (int col 0; col 4; col) { if (digitalRead(colPins[col]) LOW) { delay(50); // 简单延时消抖 if (digitalRead(colPins[col]) LOW) { // 再次确认 key keymap[row][col]; // 从映射表获取对应字符 while(digitalRead(colPins[col]) LOW); // 等待按键释放 } } } digitalWrite(rowPins[row], HIGH); // 恢复行线为高电平 } return key; }keymap是一个4x4的字符数组定义了每个按键对应的字符如{‘1‘,‘2‘,‘3‘,‘A‘}, {‘4‘,‘5‘,‘6‘,‘B‘}, ...}。避坑指南while(digitalRead(colPins[col]) LOW);这行代码实现了“等待释放”但它是一个阻塞循环。在游戏主循环中这会导致游戏卡住直到按键松开。对于游戏来说这通常不可接受。更好的做法是在扫描到按键后记录按键值并立即返回在主逻辑中处理“按键按下”事件。同时设置一个“按键已处理”标志防止同一按键持续触发。真正的消抖可以通过检查按键状态是否稳定保持了一段时间比如20毫秒来实现这需要用到millis()记录时间。4.3 游戏逻辑的时序与帧率控制直接使用delay()控制游戏速度虽然简单但会阻塞所有其他操作包括键盘扫描。这使得游戏响应变得迟钝。推荐使用基于millis()的非阻塞定时。unsigned long previousGameUpdate 0; const long gameUpdateInterval 100; // 100毫秒更新一帧 void loop() { char key readKeypadNonBlocking(); // 非阻塞读键 unsigned long currentMillis millis(); if (currentMillis - previousGameUpdate gameUpdateInterval) { previousGameUpdate currentMillis; // 在这里执行游戏逻辑更新和渲染 updateGameLogic(key); renderGame(); } // 此处可以处理其他非实时任务如更新分数显示等 }readKeypadNonBlocking()函数需要改造它只进行快速扫描并返回当前按键状态不等待、不阻塞。这样无论玩家按键多久游戏都能以固定的帧率平滑运行体验好很多。5. 功能扩展与优化实践5.1 增加游戏元素敌人、金币与关卡基础版本完成后可以丰富游戏内容敌人在worldMap中随机或固定位置放置E代表Enemy。如果玩家碰到敌人生命值减1。敌人可以设计成简单的左右移动。金币用*或自定义字符代表金币。玩家碰到后分数增加并从地图上移除。可以在worldMap之外单独维护一个金币坐标的数组。关卡定义多个不同的worldMap数组。当玩家到达屏幕最右端player.x 15时加载下一个关卡的地图并重置玩家位置到最左端。实现这些功能后游戏的数据结构可能需要升级。例如用一个byte类型的二维数组gameGrid[2][16]来代替字符数组每个字节的不同位代表不同的元素如第0位代表地面第1位代表金币第2位代表敌人这样便于进行多种元素的碰撞检测。5.2 音效与反馈优化虽然Arduino Uno没有高级音频模块但我们可以通过蜂鸣器或无源扬声器连接一个数字引脚产生简单的提示音增强游戏反馈。跳跃音短促的“嘀”声。吃金币音欢快的两个音调。受伤音低沉的长音。 使用tone(pin, frequency, duration)函数可以轻松实现。注意播放声音时可能会对时序产生微小影响最好将其放在非关键的时间路径上或者使用非阻塞的方式播放。5.3 代码结构优化与封装随着功能增加代码会变得冗长。良好的封装能提升可读性和可维护性。将硬件操作封装创建DisplayManager类负责所有LCD操作包括初始化、清屏、打印游戏画面。创建独立的游戏对象将Player结构体升级为类包含移动、跳跃、绘制等方法。同样可以为Enemy、Coin创建类。状态机明确化使用枚举enum定义游戏状态并用单独的函数处理每个状态使主循环极其简洁。6. 常见问题排查与调试技巧在开发过程中你肯定会遇到各种问题。下面是一些典型问题及其解决方法问题现象可能原因排查步骤与解决方案LCD白屏或显示乱码1. 对比度不对2. 接线错误或虚焊3. 初始化时序不对1. 调节VO引脚连接的电位器直到显示清晰。2. 用万用表逐根检查连线确认RS、E、D4-D7与代码中定义一致。3. 确保在setup()中lcd.begin(16,2)已执行。尝试在初始化后加delay(500)。按键无反应或反应混乱1. 行列引脚定义错误2. 上拉电阻未启用3. 消抖逻辑问题1. 核对代码中rowPins和colPins数组的顺序与实际焊接是否一致。2. Arduino内部有上拉电阻在pinMode(pin, INPUT_PULLUP)启用。确认扫描逻辑是“拉低行读列”。3. 简化代码先实现单个按键的可靠读取再扩展为矩阵。使用串口打印出扫描到的行列值进行调试。游戏画面闪烁严重1. 帧率过高LCD刷新跟不上2. 清屏操作过于频繁1. 增加游戏更新间隔如从delay(50)改为delay(150)。2. 避免在每帧都调用lcd.clear()。只更新屏幕上发生变化的部分字符使用lcd.setCursor()定位后print。角色移动有拖影1. 旧图像未清除2. 渲染逻辑错误在绘制新角色位置前先在旧位置绘制地图背景空格或地面字符。即“先擦除再绘制”。程序运行一段时间后卡死1. 内存泄漏堆碎片2. 数组越界3. 中断冲突1. 避免在循环中动态分配内存如String拼接。使用静态字符数组。2. 严格检查所有数组索引确保x和y坐标在边界内。3. 如果使用了tone()函数或某些库的中断可能与键盘扫描产生冲突。尝试简化代码定位问题。调试王牌技巧串口打印。在代码关键位置插入Serial.print()语句输出变量值如玩家坐标、按键值、游戏状态这是排查逻辑错误最直接的方法。例如在键盘扫描函数中打印按下的键值在游戏更新函数中打印玩家位置能让你清晰看到程序的实际运行流程快速定位问题所在。
返回列表