1. 项目概述与核心思路最近有个朋友因为特殊原因需要居家一段时间跟我抱怨日子有点无聊。他特别喜欢玩Chrome浏览器断网时出现的那个小恐龙跳跃游戏就是那个按空格键跳过仙人掌的经典玩法。我琢磨着能不能把这个小游戏从电脑浏览器里“搬”出来做成一个可以拿在手里玩的实体设备这不仅能给他解闷也是个挺有意思的嵌入式开发练手项目。我的想法是用一块ESP32开发板作为大脑它性能足够强能流畅运行游戏逻辑和图形显示。显示部分我选择了一块小巧的OLED屏幕这种屏幕自发光、对比度高显示像素游戏画面效果很棒。操作则用两个实体按钮来实现一个负责开始/暂停一个控制小恐龙跳跃。整个系统的核心是MicroPython这是一种为微控制器设计的Python语言它让写嵌入式代码变得像写普通Python脚本一样简单大大降低了开发门槛。这个项目的价值在于它完整地展示了如何将我们熟悉的软件应用一个网页小游戏转化为一个独立的、由硬件驱动的嵌入式系统涵盖了从硬件选型、电路连接、到图形处理、游戏逻辑编程的全过程非常适合想从纯软件转向硬件编程或者想深入理解嵌入式系统如何工作的朋友。2. 硬件选型与电路搭建解析2.1 核心控制器为什么是ESP32在众多微控制器中我选择了ESP32这背后有几个关键的考量。首先游戏运行需要一定的计算资源来处理画面刷新、碰撞检测和游戏状态管理。ESP32搭载了双核Xtensa处理器主频高达240MHz并且内置了520KB的SRAM这为运行MicroPython解释器和我们的游戏代码提供了充足的性能余量。其次ESP32原生支持Wi-Fi和蓝牙虽然我们这个离线游戏用不上这些功能但这意味着板子有丰富的GPIO通用输入输出引脚和强大的外设支持未来如果想给游戏加个联网排行榜功能升级起来会非常方便。最后也是很重要的一点ESP32对MicroPython的支持非常成熟社区活跃有大量现成的驱动库和教程能极大减少我们开发中踩坑的几率。注意市面上ESP32开发板型号很多建议选择带有USB转串口芯片如CH340、CP2102的版本这样连接电脑烧录程序会省去很多麻烦。我使用的是Makerfabs的MakePython ESP32它集成了OLED屏幕接口接线更简洁。2.2 显示与交互设备OLED与按钮显示设备我选用的是0.96英寸的128x64像素OLED屏幕SSD1306驱动。这种屏幕有几个无可替代的优点一是像素级控制每个像素点都可以独立开关非常适合显示我们这种由线条和色块构成的像素画风游戏角色二是它不需要背光显示黑色时像素点完全关闭因此对比度极高视觉效果清晰而且在显示静态画面时几乎不耗电。操作方面我用了两个最普通的轻触开关按钮。为什么是两个因为游戏操作逻辑很简单一个动作是“跳跃”另一个动作是“开始/暂停”。用两个独立的按钮符合直觉操作起来没有歧义。这里有一个细节按钮的一端需要连接到ESP32的GPIO引脚另一端则需要连接到电源3.3V和地GND。为了确保引脚在按钮未按下时有一个确定的状态通常是低电平我们必须使用“下拉电阻”。幸运的是ESP32的GPIO引脚可以软件配置内部上拉或下拉电阻这为我们省去了外接物理电阻的步骤让电路更加简洁。2.3 电路连接实战与原理图让我们把想法变成实际的连接。你需要准备一块面包板、若干杜邦线跳线、两个轻触开关、一块ESP32开发板和一块SSD1306 OLED屏幕。接线步骤与原理电源连接首先确保所有设备共地。将ESP32开发板的GND引脚用杜邦线连接到面包板的负电源轨通常标为蓝色。同样将OLED屏幕的GND引脚也连接到这条负电源轨。OLED屏幕连接OLED屏幕通常使用I2C通信协议只需要四根线。VCC- ESP32的3.3V引脚为屏幕供电。GND- 面包板负电源轨共地。SCL- ESP32的GPIO22引脚I2C时钟线。SDA- ESP32的GPIO21引脚I2C数据线。有些模块还有RES复位引脚如果存在可以接到一个空闲的GPIO如GPIO16方便软件复位屏幕如果不需要通常也可以不接。按钮连接两个按钮的接法完全一样。以“跳跃”按钮为例按钮的一个引脚连接ESP32的GPIO14同一个按钮的另一个引脚连接ESP32的3.3V引脚。以“开始/暂停”按钮为例按钮的一个引脚连接ESP32的GPIO15同一个按钮的另一个引脚连接ESP32的3.3V引脚。关键原理在代码中我们需要将GPIO14和GPIO15配置为输入模式并启用内部下拉电阻。这样当按钮未按下时引脚通过内部电阻连接到地GND我们读取到的值是0低电平。当按钮按下时引脚直接连接到3.3V我们读取到的值是1高电平。通过检测这个从0到1的跳变我们就知道按钮被按下了。实操心得接线时建议先接电源和地线再接信号线。每接好一部分就上电测试一下比如先只接OLED写个简单的显示测试程序确保屏幕能亮。然后再接按钮。这样做的好处是如果系统出现问题你能快速定位是哪个部分引起的避免所有线接好后面对一团乱麻无从下手。3. 软件开发环境与核心库准备3.1 MicroPython固件烧录与IDE选择硬件准备好之后我们要给ESP32“安装操作系统”也就是烧录MicroPython固件。首先去MicroPython官网下载针对ESP32的最新稳定版固件一个.bin文件。然后你需要一个烧录工具。对于Windows用户esptool.py是一个命令行工具功能强大。但如果你不习惯命令行像Flash Download Tools乐鑫官方提供这样的图形化工具也非常好用。烧录过程大致是让ESP32进入下载模式通常需要按住某个按钮再上电具体看开发板手册然后用工具选择固件文件、设置好端口和烧录地址点击开始即可。烧录成功后ESP32就变成一个能运行Python代码的微型电脑了。接下来是写代码的工具。我强烈推荐使用Thonny这款IDE。它是一款对MicroPython支持极好的开源软件界面简洁功能却一点不弱。它最大的优点是集成了串口终端、文件管理和代码编辑你可以在里面直接连接ESP32运行代码甚至像在电脑上一样浏览和管理ESP32板载存储空间里的文件。相比原文提到的uPyCraftThonny的更新更活跃社区支持更好。3.2 驱动库解析SSD1306与GFX我们的游戏依赖于两个核心的软件库ssd1306.py和gfx.py。理解它们的作用至关重要。ssd1306.py是一个底层的驱动库。它负责通过I2C总线与OLED屏幕硬件“对话”。这个库提供了最基础的功能初始化屏幕、清屏、向屏幕的显存缓冲区写入一个像素点的数据。你可以把它想象成一个最基础的画笔它只负责“在指定的坐标点个黑点或白点”。如果我们直接用这个库画游戏就需要自己计算恐龙和仙人掌每一个像素的位置然后调用成千上万次“画点”函数这效率太低代码也会变得无比冗长。于是我们需要gfx.pyGraphics Library。这是一个建立在ssd1306之上的图形库。它封装了更高级的绘图指令比如画直线、画矩形、画圆形、显示文字。在我们的游戏里恐龙和仙人掌本质上就是由多个矩形和线条组合而成的图形。gfx.py让我们可以用几句简单的代码就画出这些图形而无需关心底层每个像素如何设置。它充当了程序员和硬件驱动之间的桥梁极大提升了开发效率。注意这两个库通常不是MicroPython固件自带的。你需要将它们作为单独的.py文件上传到ESP32的文件系统中。使用Thonny可以非常方便地完成这个操作在“视图”中打开“文件”面板一边是你的电脑文件另一边就是ESP32的文件系统直接拖拽进去即可。3.3 图像素材处理PBM格式的奥秘游戏中的恐龙和仙人掌是像素画我们需要把它们转换成代码可以理解和显示的数据。这里用到了PBMPortable Bitmap格式。这是一种最简单的无损位图格式特别适合黑白图像。PBM文件的工作原理它是一个纯文本文件。开头是标识符P1接着是图像的宽度和高度以像素为单位然后就是一大串由0和1组成的数字矩阵。1代表黑色像素在我们的OLED上就是点亮0代表白色像素熄灭。例如一个3x3的“X”形图案可能表示为P1 3 3 1 0 1 0 1 0 1 0 1你可以用任何文本编辑器打开和编辑PBM文件也可以用像GIMP、Paint.net等图像软件打开画好你的像素画然后另存为PBM格式。如何用于项目在项目中我们并不需要在ESP32上实时解析PBM文件这需要额外的文件解析代码和存储空间。更常见的做法是在电脑上先用Python脚本将PBM文件里的0和1转换成MicroPython能直接使用的数据格式比如一个Python列表list。这个列表里存储的就是图像每一行的像素数据。然后我们将这个列表直接作为常量定义在游戏的代码文件里。当游戏需要显示恐龙时就直接将这个列表中的数据绘制到屏幕上。这种方法将耗时的文件解析过程放在了开发阶段保证了游戏运行时的流畅性。4. 游戏核心逻辑与代码实现拆解4.1 游戏状态机与主循环设计一个游戏本质上是一个状态机。我们的恐龙游戏至少有以下几个状态MENU开始菜单、PLAYING游戏中、PAUSED暂停、GAME_OVER游戏结束。游戏主循环一个永不停歇的while True循环每一帧都要根据当前状态来决定该做什么。例如在MENU状态主循环的任务是绘制菜单界面“按开始键游戏”并检测“开始”按钮是否被按下。一旦按下状态就切换到PLAYING。在PLAYING状态主循环的任务就繁重了更新恐龙的位置模拟重力下落和跳跃让仙人掌从屏幕右侧向左移动检查恐龙和仙人掌是否相撞碰撞检测绘制当前这一帧的所有图像并同时检测“跳跃”和“暂停”按钮。这就是游戏逻辑的核心骨架。代码结构示例state “MENU” while True: if state “MENU”: draw_menu() if start_button_pressed(): state “PLAYING” reset_game() # 重置分数、恐龙位置、仙人掌等 elif state “PLAYING”: update_dino() # 应用重力处理跳跃 update_cacti() # 移动所有仙人掌生成新的仙人掌 if check_collision(): state “GAME_OVER” draw_everything() # 画背景、恐龙、仙人掌、分数 if pause_button_pressed(): state “PAUSED” elif state “PAUSED”: draw_pause_screen() if pause_button_pressed(): # 再按一次暂停键继续 state “PLAYING” # … 处理其他状态 time.sleep(0.02) # 控制游戏帧率约50FPS4.2 物理模拟跳跃与重力让恐龙跳起来是游戏的关键体验。这里我们用了一个简化的物理模型。我们为恐龙定义一个垂直方向的位置dino_y以屏幕底部为基准和一个垂直方向的速度dino_velocity。在每一帧的update_dino()函数中应用重力dino_velocity GRAVITY。GRAVITY是一个小的正数常量例如0.5它模拟重力加速度使速度不断向下增加。更新位置dino_y dino_velocity。根据速度更新恐龙的位置。地面碰撞检测如果dino_y大于或等于地面高度说明恐龙落地了。我们将dino_y设置为地面高度并将dino_velocity重置为0。处理跳跃输入当检测到“跳跃”按钮被按下并且恐龙正处于地面上时我们给恐龙一个向上的初速度dino_velocity JUMP_STRENGTH。JUMP_STRENGTH是一个负的常数例如-10因为在我们坐标系里向上是Y轴减小的方向。通过调整GRAVITY和JUMP_STRENGTH这两个常量的值你可以改变游戏的手感让跳跃感觉更轻盈或更沉重。4.3 游戏对象管理仙人掌的生成与移动仙人掌是游戏中的障碍物。我们需要管理一个“仙人掌列表”列表里的每个元素都是一个字典或一个小的类实例记录着这个仙人掌的x坐标、y坐标固定在地面、宽度和高度用于碰撞检测。在每一帧的update_cacti()函数中移动遍历仙人掌列表将每个仙人掌的x坐标减去一个固定的速度值例如3像素。这样仙人掌就从右向左移动了。移除如果一个仙人掌的x坐标加上它的宽度已经小于0即完全移出屏幕左侧就从列表中删除它。生成我们需要一个机制来随机生成新的仙人掌。可以设置一个计时器或一个随机概率。例如每一帧有1%的概率生成一个新的仙人掌。生成时将其x坐标设置为屏幕最右侧128y坐标固定宽度和高度随机但要在合理范围内比如有高矮两种仙人掌然后将其加入列表。4.4 碰撞检测与分数计算碰撞检测是游戏逻辑的裁判。我们的恐龙和仙人掌都可以用轴对齐包围盒来近似表示。简单来说就是把恐龙和每个仙人掌都看作一个矩形。检测恐龙是否与某个仙人掌相撞就是判断这两个矩形是否重叠。判断逻辑是 如果恐龙右侧 仙人掌左侧且恐龙左侧 仙人掌右侧且恐龙底部 仙人掌顶部且恐龙顶部 仙人掌底部那么它们就撞上了。 一旦检测到碰撞游戏状态就切换到GAME_OVER。分数计算很简单。我们可以定义一个分数变量score。在游戏进行状态PLAYING下每过一帧或者每过一定时间分数就增加一点。更常见的做法是每成功避开一个仙人掌即一个仙人掌移出屏幕左侧分数就增加。这样分数直接反映了玩家的生存能力。5. 系统优化与深度调试技巧5.1 性能优化让游戏更流畅在资源受限的ESP32上每一毫秒的计算时间都很宝贵。以下是几个立竿见影的优化点局部刷新我们的游戏背景大部分是静态的天空和地面。不需要每一帧都重画整个屏幕。可以只刷新那些发生变化的部分比如恐龙、仙人掌和分数。SSD1306库通常支持设置一个“脏矩形”区域只更新这个区域内的内容。但为了代码简单在我们的低分辨率屏幕上全屏刷新的开销尚可接受。如果感觉卡顿可以优先考虑局部刷新。浮点数 vs 整数MicroPython处理整数的速度远快于处理浮点数。在物理模拟中尽量使用整数运算。例如将dino_y和dino_velocity都乘以一个倍数比如100变成整数来运算只在最后显示时再除以100。这能显著提升速度。对象池频繁地创建和删除Python对象比如生成和移除仙人掌会引发内存垃圾回收可能导致游戏偶尔卡顿。可以使用“对象池”技术预先创建好一定数量的仙人掌对象放在一个池子里需要时从池中取出一个激活它移出屏幕后不是删除而是将其状态设为“未使用”放回池中。这避免了运行时频繁的内存分配。控制帧率主循环末尾的time.sleep(0.02)非常重要。它一方面控制了游戏速度约50帧/秒另一方面也让CPU有时间休息减少功耗和发热。如果没有这个延时循环会跑得飞快CPU占用率100%但游戏速度也会失控。5.2 输入防抖与用户体验机械按钮在按下和弹起的瞬间由于触点物理振动会产生一系列快速的、不稳定的通断信号这种现象叫做“抖动”。如果不处理一次按键可能会被误判为多次按下。软件防抖实现我们不在检测到引脚变为高电平的瞬间就认为按键按下而是采用更稳健的方法。import time def is_button_pressed(pin): if pin.value() 1: # 初步检测到高电平 time.sleep_ms(20) # 等待20毫秒跳过抖动期 if pin.value() 1: # 再次确认仍然是高电平 return True return False在check_buttons()函数中调用这个防抖函数来判断按键状态。同时为了避免按住不放时连续触发我们通常检测的是“按键的上升沿”即从松开到按下的那个瞬间。这可以通过记录上一次的按键状态并与当前状态比较来实现。5.3 调试与问题排查实录在开发过程中你肯定会遇到各种问题。这里分享几个我踩过的坑和解决方法屏幕不亮或花屏检查接线这是最常见的问题。务必确认I2C的SDA和SCL线没有接反电源和地线连接牢固。检查I2C地址SSD1306屏幕常见的I2C地址是0x3C。但有些屏可能是0x3D。你可以写一个简单的I2C扫描程序来确认地址。from machine import I2C, Pin i2c I2C(sclPin(22), sdaPin(21)) print(‘I2C devices found:’, i2c.scan())检查初始化代码确保屏幕尺寸128, 64参数传递正确。按钮无反应检查引脚模式确认将按钮引脚设置为输入模式Pin.IN并且启用了内部下拉电阻Pin.PULL_DOWN。检查接线逻辑确认按钮另一端接的是3.3V而不是GND如果启用了下拉电阻的话。用串口打印调试在按键检测代码里加入print(‘Button pin value:’, button_pin.value())通过串口监视器观察按键按下时值是否从0变成了1。游戏运行卡顿使用time.ticks_ms()进行性能分析在可能耗时的函数前后记录时间戳计算执行时间。import time start time.ticks_ms() draw_complex_scene() end time.ticks_ms() print(‘Drawing took’, time.ticks_diff(end, start), ‘ms’)简化绘图如果发现绘图是瓶颈检查是否画了太多不必要的图形或者尝试关闭抗锯齿等效果。程序崩溃或重启内存不足这是MicroPython常见问题。使用gc.mem_free()打印剩余内存检查是否有内存泄漏比如列表无限增长。确保及时删除不再需要的大对象。看门狗复位如果程序陷入死循环ESP32的看门狗定时器会强制重启。确保你的主循环中有time.sleep()或定期调用machine.idle()。重要提示始终通过串口终端如Thonny的内置终端或Putty与ESP32保持连接。程序运行时的print()输出信息和错误回溯Traceback都会显示在这里这是你调试问题最宝贵的窗口。6. 功能扩展与项目进阶思考完成基础版本后这个项目还有很多可以玩和提升的空间这能让你更深入地学习嵌入式开发。6.1 增加声音与振动反馈“无声”的游戏缺乏沉浸感。我们可以添加一个蜂鸣器或无源扬声器来制造音效。将蜂鸣器的正极通过一个三极管或小电阻连接到ESP32的一个GPIO引脚如GPIO25负极接地。在代码中当恐龙跳跃、撞到仙人掌或游戏结束时通过PWM脉冲宽度调制功能让该引脚输出不同频率的方波就能驱动蜂鸣器发出“哔哔”声。你甚至可以尝试播放简单的8位音乐。同样可以添加一个微型振动马达在碰撞时提供触觉反馈这只需要一个GPIO口驱动一个MOS管即可。6.2 引入更多游戏元素让游戏更具可玩性多种障碍除了仙人掌可以加入会飞的翼龙需要玩家同时控制跳跃和蹲下来躲避。道具系统随机出现“加速鞋”暂时提升移动速度或“护盾”抵挡一次碰撞。昼夜交替让游戏背景在白天和黑夜之间周期性切换增加视觉变化。速度递增随着游戏时间增长仙人掌的移动速度逐渐加快游戏难度动态上升。6.3 数据持久化与游戏存档利用ESP32的flash存储空间我们可以实现简单的数据持久化。最高分记录将最高分保存在一个文件里如/highscore.txt。每次游戏结束后如果当前分数高于记录就更新这个文件。游戏启动时读取并显示最高分。游戏设置保存比如玩家调节了游戏难度或音量可以将这些设置保存下来下次开机依然有效。 MicroPython提供了简单的文件操作APIopen,read,write实现起来非常方便。但要注意频繁写入Flash会缩短其寿命所以不要每帧都写只在必要时如游戏结束才写入。6.4 外壳设计与电源管理一个完整的项目离不开好的“包装”。3D打印外壳使用Fusion 360或Tinkercad等软件为你的ESP32、屏幕和按钮设计一个紧凑的外壳。这不仅能保护电路还能提升作品的完成度和手感。可以在外壳上为屏幕开窗为按钮预留孔位。电池供电摆脱USB线的束缚你可以接入一块小型的锂电池如3.7V 18650或LiPo电池并通过一个TP4056充电模块为电池充电。ESP32的电压范围是3.0V-3.6V所以需要一个降压模块如AMS1117-3.3将电池电压稳定到3.3V。这样你的游戏机就真正便携了。低功耗优化在游戏暂停或菜单界面长时间无操作时可以调低屏幕亮度通过I2C命令控制SSD1306的对比度甚至让ESP32进入深度睡眠模式仅通过按键中断来唤醒从而极大延长电池续航。这个项目从一个小小的想法开始贯穿了硬件连接、驱动编写、游戏逻辑、状态管理、性能优化等多个嵌入式开发的核心环节。它最有趣的地方在于你能亲眼看到、亲手摸到自己编写的代码如何驱动硬件创造出一个有形的、可交互的作品。当你的朋友拿着这个自己制作的小游戏机玩得不亦乐乎时那种成就感远非纯软件项目可比。希望这个详细的拆解能给你提供一个清晰的路径也期待你在此基础上创造出更有趣的变体。