1. 项目概述:当复古像素风遇上开源硬件
最近在捣鼓我的PyBadge,一个基于CircuitPython的开源游戏掌机开发板,突然想给它注入点不一样的灵魂。市面上那些简单的贪吃蛇、打砖块玩多了总觉得差点意思,于是萌生了一个想法:能不能把经典的“太空侵略者”那种像素风射击游戏,搬到这块小小的屏幕上,并且加入一点“人脸识别”的趣味互动?这就是“PyBadge Face Invaders”项目的由来。本质上,它是一个为Adafruit PyBadge(或功能近似的PyGamer)量身定制的游戏项目,核心玩法是玩家通过移动自己的脸部(或任何可识别的物体)来控制屏幕上的飞船,躲避并击落由像素点阵组成的“入侵者”。
这个项目听起来有点跨界,但它完美地结合了几个有趣的技术点:首先是嵌入式开发,在资源受限的微控制器上运行游戏逻辑;其次是计算机视觉的轻量化应用,通过摄像头捕捉图像并进行实时处理;最后是复古游戏设计,在有限的屏幕分辨率(160x128像素)和色彩下创造流畅的体验。它解决的不仅仅是“做一个游戏”,而是如何在极简的硬件上,实现软硬件协同与创意交互。无论你是想学习CircuitPython、对嵌入式游戏开发感兴趣,还是单纯想做一个炫酷的、可以和朋友互动的电子玩具,这个项目都能提供一条清晰的实践路径。接下来,我会详细拆解从思路到实现的每一个环节。
2. 核心设计思路与硬件选型解析
2.1 为什么是PyBadge?
选择Adafruit PyBadge作为开发平台,是经过一番考量的。市面上能跑Python的微控制器板子不少,比如Raspberry Pi Pico、ESP32系列等。但PyBadge有几个难以替代的优势,正好契合我们这个项目。
第一是高度集成与开箱即用。PyBadge本质上是一块基于ATSAMD51微控制器的游戏掌机形态开发板。它集成了1.8英寸彩色LCD屏幕、5向方向摇杆、4个动作按钮、2个菜单按钮、一个蜂鸣器,甚至还有5个NeoPixel LED。这意味着我们不需要额外焊接屏幕、连接按钮,省去了大量的硬件调试时间,可以专注于游戏逻辑和视觉算法的开发。
第二是对CircuitPython的完美支持。CircuitPython是MicroPython的一个分支,由Adafruit大力维护,以其极简的API和丰富的硬件驱动库著称。对于PyBadge,Adafruit提供了完整的“显示库”(displayio)来驱动屏幕绘制精灵和背景,以及“摇杆/按钮库”(gamepad)来读取输入。更重要的是,它有成熟的社区和大量的学习资源,遇到问题很容易找到解决方案。
第三是扩展能力。PyBadge预留了摄像头接口,可以无缝连接OV5640或OV7670等摄像头模块,这是我们实现人脸追踪功能的基础。同时,其GPIO引脚也允许连接其他传感器,为未来功能扩展(比如增加体感控制)留下了空间。
注意:如果你手头是PyGamer,它和PyBadge硬件几乎一致,只是外壳形状不同,本项目代码完全兼容。如果使用其他开发板,则需要自行适配屏幕、输入设备和摄像头驱动,工作量会大很多。
2.2 游戏机制与视觉方案设计
游戏的核心机制需要兼顾经典玩法的还原与新颖交互的可行性。经典的“太空侵略者”中,玩家飞船只能左右移动,发射子弹,击落上方规律移动并下压的敌人。我们计划保留这个核心循环,但将左右移动的控制权从摇杆/按钮移交给人脸位置。
视觉方案是整个项目的技术难点。在PyBadge的ATSAMD51(120MHz Cortex-M4,256KB RAM)上运行完整的OpenCV是不现实的。因此,我们必须采用极其轻量级的图像处理算法。我们的目标是:在低分辨率(建议QVGA 320x240或更低)下,以可接受的帧率(>10fps)检测到一个“脸”或“显著物体”的横向位置。
一个可行的方案是基于颜色或亮度的简单阈值分割与质心计算。具体思路是:
- 图像采集:从摄像头获取一帧灰度图像。
- 预处理:进行高斯模糊以减少噪声。
- 二值化:设定一个亮度阈值。因为通常人脸(或我们举着的物体)与背景的亮度有差异,我们可以通过阈值将图像分为前景(物体)和背景。
- 寻找轮廓或计算质心:在二值图像中,找到最大的连通区域(即我们的“脸”),并计算这个区域的中心点(质心)的x坐标。
- 坐标映射:将摄像头x坐标(0-图像宽度)线性映射到屏幕的x坐标(0-160)。这个映射后的坐标就作为玩家飞船的横坐标。
这个方案完全可以在CircuitPython中,借助imagecapture和基本的数学运算实现,无需复杂的机器学习模型。为了提升鲁棒性,我们可以让玩家在游戏开始前举着一个颜色鲜明或高亮的物体(比如手机屏幕)进行校准,以确定最佳的亮度阈值。
游戏循环设计则采用经典的状态机模式:
- 初始化状态:加载资源,初始化摄像头,进行亮度校准。
- 准备状态:显示标题和简易教程。
- 游戏进行状态:
- 读取摄像头,计算玩家飞船位置。
- 更新敌人位置(规律性的左右移动并逐步下移)。
- 更新子弹位置。
- 检测碰撞(子弹 vs 敌人,敌人 vs 飞船,敌人 vs 底部边界)。
- 根据碰撞结果更新分数、生命值或游戏状态。
- 重绘整个屏幕。
- 游戏结束状态:显示最终分数,等待重启。
3. 开发环境搭建与核心库详解
3.1 CircuitPython固件与驱动安装
第一步是让PyBadge运行最新的CircuitPython。前往CircuitPython官网的下载页面,找到“Adafruit PyBadge”对应的最新稳定版.uf2文件。用USB线连接PyBadge到电脑,快速双击板子上的复位按钮,此时电脑上会出现一个名为PYBADGEBOOT的U盘。将下载的.uf2文件拖入这个U盘,板子会自动重启。重启后,会出现一个名为CIRCUITPY的新U盘,这就是我们的可编程磁盘。
接下来,需要安装必要的库文件。由于我们要用到显示、游戏手柄、摄像头和图像处理,需要将以下库文件(.mpy或.py)从Adafruit的CircuitPython库包中复制到CIRCUITPY磁盘的lib文件夹下:
- 必须库:
adafruit_display_text:用于在屏幕上显示分数、生命值等文本。adafruit_displayio_layout:高级UI布局(非必须,但可简化菜单设计)。displayio:核心显示驱动,已内置于固件,无需额外安装。gamepad:读取摇杆和按钮输入。adafruit_ov5640.mpy:用于驱动OV5640摄像头模块(根据你的摄像头型号选择,也可能是adafruit_ov7670.mpy)。adafruit_imageload:加载精灵图(sprite)资源。
- 图像处理辅助库:
adafruit_bitmap_font:加载自定义字体(用于美化分数显示)。adafruit_pixelbuf:NeoPixel LED控制,可用于游戏状态光效反馈。
实操心得:库文件的管理是个细活。建议直接从Adafruit的CircuitPython库Bundle中一次性下载完整包,然后按需取用。避免从不同来源混用库版本,容易导致兼容性问题。另外,
lib文件夹内的库不宜过多,否则会占用宝贵的内存并可能影响启动速度。
3.2 项目文件结构规划
一个清晰的文件结构能让代码维护变得轻松。在CIRCUITPY磁盘的根目录下,我建议这样组织:
CIRCUITPY/ ├── code.py # 主程序入口 ├── lib/ # 库文件夹 ├── assets/ # 资源文件夹 │ ├── sprites.bmp # 精灵图集(飞船、敌人、子弹、爆炸效果) │ └── font.bdf # 自定义点阵字体文件 └── config.json # 配置文件(可存放亮度阈值、灵敏度等参数)code.py是CircuitPython设备启动后自动执行的文件。我们将主要的游戏逻辑写在这里。assets文件夹存放所有二进制资源文件,如图片和字体。使用BMP格式的精灵图是因为displayio和adafruit_imageload对其支持良好,并且可以方便地使用图像编辑软件制作。config.json是一个可选但很好的实践,它允许我们在不修改代码的情况下调整游戏参数,比如通过一个简单的校准程序将计算出的最佳亮度阈值写入这个文件。
4. 核心代码模块拆解与实现
4.1 摄像头初始化与轻量级人脸追踪
首先,我们需要初始化摄像头并实现那个轻量级的“人脸”(物体)追踪算法。这里以OV5640为例。
import board import busio import digitalio import adafruit_ov5640 import displayio from ulab import numpy as np # 使用ulab(numpy的子集)进行高效的数组运算 # 初始化I2C和摄像头 i2c = busio.I2C(board.SCL, board.SDA) cam = adafruit_ov5640.OV5640( i2c, data_pins=board.CAMERA_DATA, clock=board.CAMERA_PCLK, vsync=board.CAMERA_VSYNC, href=board.CAMERA_HREF, mclk=board.CAMERA_XCLK, reset=board.CAMERA_RESET, size=adafruit_ov5640.OV5640_SIZE_QVGA, # 使用QVGA分辨率平衡速度与精度 pixformat=adafruit_ov5640.OV5640_COLOR_GRAYSCALE # 关键:使用灰度图,减少数据量 ) # 设置一个较低的帧率,保证处理时间 cam.framerate = 10 def get_face_x(): """从摄像头捕获一帧,计算前景物体的中心x坐标。返回一个0-1之间的归一化值,失败返回0.5(屏幕中央)。""" buf = cam.capture() # 获取图像缓冲区 # 将缓冲区转换为ulab数组以便进行数值计算 # 注意:这里需要根据摄像头实际输出的缓冲区格式进行解析,OV5640 Grayscale模式下,buf通常是字节数组 # 简化示例,假设我们取图像中间一行进行快速处理(牺牲垂直精度换取速度) height = cam.height width = cam.width scanline_y = height // 2 # 计算中间一行在缓冲区中的起始位置 scanline_start = scanline_y * width scanline = np.frombuffer(buf[scanline_start: scanline_start + width], dtype=np.uint8) # 简单的动态阈值:取扫描行的平均亮度作为阈值基准 mean_brightness = np.mean(scanline) threshold = mean_brightness + 20 # 增加一个偏移量,让阈值比平均亮度高一些 # 二值化:找出亮度高于阈值的像素位置 bright_pixels = np.where(scanline > threshold)[0] if len(bright_pixels) > 10: # 如果找到足够多的亮点 # 计算这些亮点的中心位置 center_x = np.mean(bright_pixels) # 归一化到0-1范围 normalized_x = center_x / width return max(0.0, min(1.0, normalized_x)) # 钳制在0-1之间 else: # 没有检测到显著物体,返回屏幕中央 return 0.5这段代码的关键在于极致的简化。我们放弃了处理整幅图像,而是只分析中间的一行(或几行)扫描线,这能极大减少计算量。动态阈值基于扫描线的平均亮度,使其能适应不同的环境光。ulab库提供了类似NumPy的接口,在CircuitPython上执行数组运算比纯Python循环快得多。
注意事项:摄像头捕获和数组运算会消耗较多时间和内存。在实际游戏中,你可能需要进一步降低分辨率(如QQVGA 160x120),或者每两帧处理一次,以确保游戏主循环的流畅性。此外,环境光变化剧烈时,简单的动态阈值可能会失效,因此游戏开始前的“校准环节”非常重要。
4.2 游戏精灵管理与显示系统
PyBadge的displayio模块采用“显示组”的层级结构来管理屏幕元素。我们需要创建精灵(Sprite)、图块(TileGrid)来显示飞船、敌人和子弹。
import displayio import adafruit_imageload from adafruit_display_text import label from adafruit_bitmap_font import bitmap_font # 初始化显示 display = board.DISPLAY main_group = displayio.Group() # 主显示组,所有元素都放在这里 # 1. 加载精灵图集 sprite_sheet, palette = adafruit_imageload.load("/assets/sprites.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette) # 假设我们的图集是16x16像素的精灵,排列在一行 SPRITE_WIDTH = 16 SPRITE_HEIGHT = 16 # 创建精灵的图块网格 player_sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=SPRITE_WIDTH, tile_height=SPRITE_HEIGHT) player_sprite[0] = 0 # 使用图集中第一个图块作为玩家飞船 player_sprite.x = display.width // 2 player_sprite.y = display.height - SPRITE_HEIGHT - 5 main_group.append(player_sprite) # 2. 创建敌人精灵组 enemy_group = displayio.Group() NUM_ENEMIES_PER_ROW = 5 NUM_ROWS = 3 ENEMY_SPACING = 20 for row in range(NUM_ROWS): for col in range(NUM_ENEMIES_PER_ROW): enemy = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=SPRITE_WIDTH, tile_height=SPRITE_HEIGHT) enemy[0] = 1 # 使用图集中第二个图块作为敌人 enemy.x = col * ENEMY_SPACING + 10 enemy.y = row * ENEMY_SPACING + 10 enemy_group.append(enemy) main_group.append(enemy_group) # 3. 创建子弹列表(使用精灵组管理) bullet_group = displayio.Group() MAX_BULLETS = 3 for _ in range(MAX_BULLETS): bullet = displayio.TileGrid(sprite_sheet, pixel_shader=palette, width=1, height=1, tile_width=4, tile_height=8) # 子弹精灵较小 bullet[0] = 2 # 第三个图块是子弹 bullet.y = -10 # 初始置于屏幕外 bullet_group.append(bullet) main_group.append(bullet_group) # 4. 创建UI文本 font = bitmap_font.load_font("/assets/font.bdf") score_text = label.Label(font, text="SCORE: 0", color=0xFFFFFF) score_text.x = 5 score_text.y = 10 main_group.append(score_text) life_text = label.Label(font, text="LIFE: 3", color=0xFFFFFF) life_text.x = display.width - 60 life_text.y = 10 main_group.append(life_text) # 将主显示组推送到屏幕 display.show(main_group)displayio的核心思想是分层与组合。我们将所有可视元素(精灵、文本)作为Group的子项,通过修改这些子项的属性(如x,y,[0]索引)来更新画面。这种方式比直接操作帧缓冲区更高效,也更容易管理。
4.3 游戏主循环与逻辑控制
游戏主循环需要整合输入、更新、渲染和碰撞检测。我们将使用gamepad读取按钮输入(用于发射子弹),用前面实现的get_face_x()函数控制飞船。
import time import gamepad import random pad = gamepad.GamePad(board.DPAD_UP, board.DPAD_DOWN, board.DPAD_LEFT, board.DPAD_RIGHT, board.BUTTON_A, board.BUTTON_B) # 游戏状态变量 player_speed = 3 enemy_speed_x = 1 enemy_speed_y = 0 enemy_direction = 1 # 1向右,-1向左 enemy_move_counter = 0 enemy_move_interval = 20 # 每20帧移动一次敌人 bullets = [] # 存储活跃子弹的列表,每个元素为[x, y]坐标 score = 0 lives = 3 game_over = False last_button_state = False while not game_over: # --- 输入处理 --- buttons = pad.get_pressed() # 用A键发射子弹 if buttons & gamepad.GamePad.BUTTON_A: if not last_button_state: # 检测按键按下边缘 if len(bullets) < MAX_BULLETS: # 从飞船中心发射一颗新子弹 new_bullet = [player_sprite.x + SPRITE_WIDTH//2 - 2, player_sprite.y] bullets.append(new_bullet) last_button_state = True else: last_button_state = False # 人脸追踪控制飞船 face_x_normalized = get_face_x() target_x = int(face_x_normalized * (display.width - SPRITE_WIDTH)) # 平滑移动,避免跳跃 player_sprite.x = target_x # --- 更新游戏状态 --- # 1. 更新子弹位置 for i in range(len(bullets)-1, -1, -1): # 倒序循环,便于安全删除 bullets[i][1] -= 5 # 子弹向上飞 if bullets[i][1] < -10: # 飞出屏幕上边界 bullets.pop(i) else: # 更新对应精灵的位置 bullet_group[i].x = bullets[i][0] bullet_group[i].y = bullets[i][1] # 2. 更新敌人位置 enemy_move_counter += 1 if enemy_move_counter >= enemy_move_interval: enemy_move_counter = 0 move_down = False # 检查敌人是否碰到屏幕边缘 for enemy in enemy_group: if enemy.x <= 0 or enemy.x >= display.width - SPRITE_WIDTH: enemy_direction *= -1 move_down = True break # 移动所有敌人 for enemy in enemy_group: enemy.x += enemy_speed_x * enemy_direction if move_down: enemy.y += 5 # 下移一行 # 检查是否有敌人到达底部 for enemy in enemy_group: if enemy.y >= player_sprite.y: game_over = True break # --- 碰撞检测 --- # 子弹 vs 敌人 for bullet_idx in range(len(bullets)-1, -1, -1): bx, by = bullets[bullet_idx] for enemy in enemy_group: if (enemy.x < bx + 4 and enemy.x + SPRITE_WIDTH > bx and enemy.y < by + 8 and enemy.y + SPRITE_HEIGHT > by): # 碰撞发生 enemy_group.remove(enemy) # 移除敌人 bullets.pop(bullet_idx) # 移除子弹 score += 100 score_text.text = f"SCORE: {score}" # 播放一个简单的音效(通过蜂鸣器) board.DAC.output(440, 0.1) # 示例,实际使用可能需要audioio库 time.sleep(0.05) board.DAC.output(0, 0) break # 跳出内层循环,一颗子弹只能击中一个敌人 # 敌人 vs 玩家(简易的矩形碰撞) for enemy in enemy_group: if (player_sprite.x < enemy.x + SPRITE_WIDTH and player_sprite.x + SPRITE_WIDTH > enemy.x and player_sprite.y < enemy.y + SPRITE_HEIGHT and player_sprite.y + SPRITE_HEIGHT > enemy.y): lives -= 1 life_text.text = f"LIFE: {lives}" enemy_group.remove(enemy) # 撞到的敌人也消失 if lives <= 0: game_over = True break # --- 渲染(displayio自动处理)--- # 注意:由于我们直接修改了精灵对象的属性,显示会自动更新。 # 但需要给系统一点时间处理 time.sleep(0.016) # 尝试维持约60Hz的循环,实际帧率受处理速度限制 # 游戏结束,显示分数 game_over_text = label.Label(font, text=f"GAME OVER! SCORE: {score}", color=0xFF0000) game_over_text.anchor_point = (0.5, 0.5) game_over_text.anchored_position = (display.width//2, display.height//2) main_group.append(game_over_text) while True: time.sleep(1)主循环是游戏的心脏。这里有几个关键优化点:一是倒序遍历列表以便在循环中安全删除元素;二是使用计数器控制敌人移动速度,而不是每帧都移动,这能创造更经典的、有节奏感的敌人移动模式;三是简单的矩形碰撞检测,对于像素风游戏来说足够用且高效。
5. 性能优化与调试技巧实录
在PyBadge这样的资源受限设备上开发游戏,性能是永恒的挑战。以下是我在开发过程中总结的几个关键优化点和调试技巧。
5.1 内存与帧率瓶颈排查
CircuitPython提供了gc(垃圾回收)和micropython模块来帮助监控资源。
import gc import micropython def print_memory_info(): gc.collect() # 手动触发垃圾回收 print(f"Free memory: {gc.mem_free()} bytes") print(f"Allocated memory: {gc.mem_alloc()} bytes") micropython.stack_use() # 查看栈使用(可选) # 在游戏初始化后和主循环的关键点调用,观察内存变化如果发现内存持续下降(内存泄漏),检查是否有全局列表或字典在无限增长(比如未清理的子弹或敌人对象)。对于帧率,可以在主循环中计算帧间隔:
import time last_time = time.monotonic() frame_count = 0 while True: # ... 游戏逻辑 ... frame_count += 1 current_time = time.monotonic() if current_time - last_time >= 1.0: fps = frame_count print(f"FPS: {fps}") frame_count = 0 last_time = current_time如果FPS过低(<10),瓶颈通常在于:
- 摄像头处理:尝试降低分辨率(如改用QQVGA),或减少
get_face_x()函数中处理的扫描线数量。 - 碰撞检测:当屏幕上对象很多时,两两检测(O(n²)复杂度)会成为瓶颈。可以引入空间划分,如简单的“网格法”,只检测相邻网格内的对象。
- 显示刷新:
displayio的自动刷新本身高效,但频繁地添加/移除Group中的对象(如敌人被击中时)会触发重排。可以考虑对象池模式:将敌人精灵设为不可见(hidden=True)而非移除,需要时再复用。
5.2 视觉追踪稳定性提升
基于亮度的简单追踪在复杂背景下很容易失效。除了开始前的校准,我们可以加入一些滤波和状态保持逻辑:
class SimpleFaceTracker: def __init__(self, smooth_factor=0.3): self.last_x = 0.5 self.smooth_factor = smooth_factor # 平滑系数,0-1之间,越大越平滑但延迟越高 def update(self, new_raw_x): """使用一阶低通滤波器平滑输入""" if new_raw_x is None: # 如果本次检测失败,使用上一次的值 smoothed_x = self.last_x else: # 应用平滑:smoothed = old * (1-factor) + new * factor smoothed_x = self.last_x * (1 - self.smooth_factor) + new_raw_x * self.smooth_factor self.last_x = smoothed_x return smoothed_x # 在主循环中使用 tracker = SimpleFaceTracker(smooth_factor=0.2) # ... raw_x = get_face_x() # 可能返回None或0.5(失败) smooth_x = tracker.update(raw_x) player_sprite.x = int(smooth_x * (display.width - SPRITE_WIDTH))此外,可以增加一个置信度机制:只有当检测到的亮点区域面积大于某个阈值,或连续多帧检测成功时,才更新跟踪位置,否则认为目标丢失,飞船缓慢回中或停止移动。
5.3 常见问题与快速排查表
下表列出了开发中可能遇到的典型问题及解决方法:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕一片空白或花屏 | 1. 显示初始化失败 2. 精灵图文件损坏或格式不对 3. 内存不足导致显示缓冲区错误 | 1. 检查board.DISPLAY是否正确,displayio库是否存在。2. 确认 sprites.bmp是索引色位图,并且颜色深度与PyBadge屏幕兼容(通常16位色)。用图像软件另存为“BMP - 16色”或“BMP - 256色”。3. 在代码开头打印内存信息,确保有足够内存。精简资源,如图片尺寸。 |
| 摄像头无法初始化或捕获黑屏 | 1. 摄像头模块接触不良 2. 引脚定义错误 3. 供电不足 | 1. 重新插拔摄像头排线。 2. 对照PyBadge原理图,检查 board.CAMERA_*引脚定义是否与你的摄像头模块匹配(OV5640 vs OV7670)。3. PyBadge的3.3V输出可能功率有限。尝试外接一个3.3V电源到摄像头的VCC引脚。 |
| 人脸追踪延迟高或抖动严重 | 1. 图像处理计算量太大 2. 阈值设置不合理 3. 环境光变化 | 1. 降低摄像头分辨率,减少处理区域(如只分析1/4的图像区域)。 2. 实现一个校准模式,让用户将脸/物体置于框内,自动计算最佳阈值并保存到 config.json。3. 使用上述的平滑滤波器( SimpleFaceTracker)。 |
| 游戏运行几秒后卡死或重启 | 1. 内存泄漏 2. 主循环阻塞(如 time.sleep过长)3. 硬件过热(少见) | 1. 使用gc.collect()和打印内存信息定位泄漏点。检查是否有列表、字典无限增长。2. 避免在主循环中使用长延时。用 time.monotonic()记录时间点来实现非阻塞延时。3. 确保代码中没有死循环。 |
| 按钮无响应 | 1.gamepad引脚定义错误2. 按钮消抖处理不当 | 1. 确认board.BUTTON_A等常量与你板子的实际连接一致。用简单测试程序单独测试每个按钮。2. 使用边缘检测(如上文代码所示),而不是持续检测按键状态。 |
5.4 音效与灯光反馈增强
虽然蜂鸣器音效简单,但恰当的反馈能极大提升游戏体验。我们可以用pwmio或audioio(如果固件支持)来生成不同频率和时长的声音。
import pwmio import time def play_tone(pin, frequency, duration): """使用PWM播放一个简单音调""" pwm = pwmio.PWMOut(pin, duty_cycle=0, frequency=frequency) pwm.duty_cycle = 32768 # 50%占空比 time.sleep(duration) pwm.deinit() # 定义不同音效 def sfx_shoot(): play_tone(board.SPEAKER, 800, 0.05) def sfx_hit(): play_tone(board.SPEAKER, 300, 0.1) def sfx_explosion(): play_tone(board.SPEAKER, 150, 0.2)对于PyBadge周围的5个NeoPixel LED,可以用来表示生命值或特殊状态:
import neopixel pixels = neopixel.NeoPixel(board.NEOPIXEL, 5, brightness=0.1) def update_lights(lives): pixels.fill((0, 0, 0)) # 清空 for i in range(lives): pixels[i] = (0, 20, 0) # 绿色表示生命 pixels.show()在主循环中,当生命值变化时调用update_lights(lives),就能获得直观的视觉反馈。
6. 项目扩展与进阶玩法
完成基础版本后,这个项目还有很大的扩展空间,可以把它变成一个功能更丰富的创意平台。
1. 多模式与难度系统:可以在游戏开始前增加一个菜单(用方向键选择),提供不同模式。例如,“经典模式”就是上述玩法;“挑战模式”下敌人移动更快、射击频率更高;“无尽模式”下敌人会一波接一波地出现,分数无限累积。难度参数(敌人速度、数量、子弹速度)可以做成可配置的。
2. 更复杂的视觉交互:升级视觉算法。例如,可以尝试在CircuitPython上集成一个极简的Haar Cascade或HOG特征检测(需要预先在PC上训练一个非常小的模型并转换为整数权重数组),实现真正的人脸检测。或者,利用颜色追踪,让玩家手持不同颜色的物体来控制发射不同颜色的子弹,对应消灭不同颜色的敌人。
3. 数据持久化与排行榜:利用PyBadge的板载存储,将最高分数保存到一个文件中。每次游戏结束后,与历史最高分比较并更新。甚至可以做一个简单的字母输入界面(用方向键选择字母),让玩家在创造新纪录时录入名字。
4. 双人对战模式:利用PyBadge的第二个摇杆(如果扩展)或另一套按钮,或者通过红外/无线电模块连接两个PyBadge,实现双人对战。一个玩家控制飞船,另一个玩家可以控制敌人的发射节奏或设置障碍,增加互动趣味性。
5. 物理效果与粒子系统:虽然资源有限,但可以模拟简单的效果。比如敌人被击中时,不是立刻消失,而是播放一个由几个白色像素点向外扩散的“爆炸”动画。这只需要在几帧内连续修改几个精灵的位置和颜色即可实现。
这个项目的魅力在于,它从一个简单的想法出发,串联起了嵌入式编程、实时系统、计算机视觉和游戏设计多个领域。每一步优化和扩展,都是对有限资源下创造无限可能的一次探索。当你看到自己通过移动脑袋就能控制屏幕上的飞船击落像素敌人时,那种亲手创造交互魔法的成就感,是单纯玩游戏无法比拟的。