所有项目

汽车迷宫 · Pygame 多关卡小游戏

Pygame 搭的 2D 汽车迷宫游戏。吃完所有星星才能到终点,多关卡地图 + 矩形/圆形混合碰撞检测 + BGM 音效。打包成独立 exe。

📅 2026PythonPygamePyInstaller

🚗 👉 在首页玩一把(Hero 下方那块) Canvas 2D 手搓的轻量版,零依赖、首屏 < 100ms。 下面记录原版 Pygame 桌面版与首页迷你版两套实现的取舍。

两个版本

从一个 Pygame 本地练手小游戏,到"首页零依赖轻量入口",两种实现各有取舍:

版本入口体积技术栈
🖥️ 原版 EXE本地双击~18 MBPython 3.12 + Pygame + PyInstaller
🚗 首页迷你版博客首页 Hero 下方< 20 KB 单文件Vue + Canvas 2D + TypeScript,DFS 生成 + 自适应难度

下面按这两个顺序说各自做了什么、踩了什么坑。


A · 原版:Python + Pygame

最初的练手项目,目的是复习 Pygame 的 sprite / group 机制。

玩法

开一辆小车在迷宫里 吃完所有星星,然后开到终点 —— 过关。撞墙直接 crash。每关地图越来越复杂,全部过完显示 Win 画面。

墙壁 (矩形) + 星星 (圆形) + 终点 (圆形) + 玩家 (旋转的矩形)

代码结构

maze/
├── main.py          · Pygame 主循环
├── config.py        · SCREEN_WIDTH / HEIGHT / FPS
├── game_manager.py  · 场景管理器(加载 / 碰撞 / 渲染)
├── player.py        · 玩家汽车(移动 + 旋转 + 碰撞处理)
├── wall.py / star.py / target.py · 三种 sprite
├── utils/
│   ├── collide.py   · 矩形 AABB + 圆形距离两种检测
│   └── draw_text.py
└── static/
    ├── images/  sounds/
    └── maps/level1.txt  level2.txt  ...

地图格式(纯文本)

每关一个 .txt,格式是结构化几段数字:

3                # 3 堵墙
10 10 200 20     # 每行 "x y 宽 高"
...
5                # 5 颗星星
100 100          # 每行 "x y"
...
1                # 1 个终点
400 300
400 300 0        # 玩家初始 "x y 朝向角度"

GameManager.load() 逐行解析。加关卡写一份 level3.txt 就行,不用动代码。

碰撞检测 · 两种 shape 混用

  • 玩家 vs 墙:spritecollide(..., collided_rect) 矩形 AABB
  • 玩家 vs 星星 / 终点:spritecollide(..., collided_circle) 圆形距离判定

星星视觉是圆的,圆形检测旋转时不会有"边角冲突"感。

通关条件

if self.stars_cnt == 0:              # 所有星星吃完
    if spritecollide(self.player, self.targets, True, collided_circle):
        self.success_sound.play()
        return True                   # 过关

不是简单"碰终点就赢",而是逼玩家把星星全吃了再收尾,强制绕路。

打包 & 音效

  • 打包:PyInstaller 单文件 main.exe (~18 MB),static/--add-data 塞进去
  • BGMpygame.mixer.music + play(-1) 循环,音量 0.1(不抢戏)
  • SFXpygame.mixer.Sound 吃星星 / 通关两个短音,音量 0.3

B · 首页迷你版:Canvas 2D + 自适应难度

👉 就在首页 Hero 下方那块,无需加载直接玩

桌面 EXE 不可能让随便逛博客的访客每个都下载来玩。所以首页放了个 < 20 KB 单文件 Vue 组件 当轻量入口 —— 看完博客顺手玩两关,零等待、零下载。

组件:components/games/MiniMaze.vue

设计取舍

  • 纯 Canvas 2D:不引 Phaser / Pixi,< 10 KB gzip
  • 零音频文件:Web Audio API 现场合成(三角波拾星 + C-E-G-C 大三和弦胜利音)
  • 关键视觉不依赖 emoji 字体:星星 / 终点用 emoji(字体缺了也不致命),车是 Canvas 手绘矢量

关卡生成 · DFS backtracker + BFS 最远点

前两关手设当暖身,第 3 关起按玩家水平程序化生成:

function generateLevel(skill: number): string[] {
    const s = clamp(skill, 0, 1)
    // 网格尺寸随 skill 线性插值;强制奇数(backtracker 每步跨 2 格)
    const cols = (Math.round(10 + s * 8) | 1)   // 11 → 19
    const rows = (Math.round(6  + s * 4) | 1)   //  7 → 11

    // 1) DFS backtracker 从 (1,1) 开始雕通道
    // 2) Braid:随机打通死胡同形成环路,skill 低时多开(更宽松)
    // 3) BFS 从起点算距离场,最远可达点放终点 —— 强制走最长对角线
    // 4) 星星 2~6 颗随 skill 增加,只放在深度 > 2 的地板
}

关键在第 3 步:BFS 测距找最远点当终点,不是随便扔。保证玩家至少走一遍"最长对角线",不会出现起点旁边就是终点的水关。

自适应难度

每关记录移动次数 + 用时,通关时算"每步平均秒",映射回 skill:

function onLevelWin() {
    const spm = secs / moves                   // seconds per move
    const raw = (3 - spm) / (3 - 0.5)          // 0.5s/步 → skill 1.0,3s/步 → 0.0
    const thisSkill = clamp(raw, 0, 1)
    // 滑动平均(旧 40% + 新 60%)—— 避免单关波动跌宕
    skillLevel.value = skillLevel.value * 0.4 + thisSkill * 0.6
}

每步 0.5s 内 → 算你老手,下关迷宫放大、星星加多;每步 3s 以上 → 新手,下关小而空旷。难度标签写在通关面板(简单 / 适中 / 有挑战 / 高手级),让玩家意识到系统在调。

手绘车 · 角度旋转

最早用 emoji 🚗 —— 单 emoji 没法转方向,朝左时看起来像倒着走。换成 Canvas 手绘俯视角小车,默认朝 +x,移动方向直接设 facingAnglectx.rotate(...) 搞定:

if      (dx > 0) player.facingAngle = 0
else if (dx < 0) player.facingAngle = Math.PI
else if (dy < 0) player.facingAngle = -Math.PI / 2
else if (dy > 0) player.facingAngle =  Math.PI / 2

车身是 lilac 主调的圆角矩形,加了车顶座舱 / 挡风玻璃反光 / 4 个轮子 / 前黄灯后红灯。dark mode 切更深的紫,跟主站色板一致。

移动插值 · 消除卡顿

最初每按一次键走一格 + setTimeout 触发下一步 —— 连按方向键时看到明显顿挫。现在:

  • linear 插值player.progress 从 0 到 1,STEP_MS = 90(90ms 跨一格,刚好看到动画又不拖沓)
  • 零间隙续接:当帧 progress >= 1 立即检查 keysDown,有按键直接启动下一步 —— 不等任何 timer

结果:长按方向键时,车在格子之间 一帧间隙都没有,手感接近原版 Pygame。


回头看

同一个游戏两次重实现,触到的东西远不止游戏本身:

  • A · 原版:Pygame 的 sprite / group + PyInstaller 打包
  • B · 迷你版:Canvas 2D 渲染 + DFS backtracker 生成 + BFS 距离场 + 自适应难度 + Web Audio 现场合成 + 手绘矢量小车

从"Pygame 桌面打包"到"浏览器零依赖手搓",同一玩法换两套技术栈,是有趣的对照。