Day 089 — 游戏循环与事件:图解
游戏循环架构图
┌─────────────────────┐
│ pygame.init() │
│ 创建窗口、加载资源 │
└──────────┬──────────┘
▼
┌──────────────────────────────┐
│ while running: │◄─────────────────────┐
└──────────────┬───────────────┘ │
▼ │
┌────────────────────────────────┐ │
│ for event in events: │ │
│ 处理 QUIT → running=False │ │
│ 处理 KEYDOWN → 响应输入 │ │
│ 处理 MOUSE → 响应鼠标 │ │
└───────────────┬────────────────┘ │
▼ │
┌────────────────────────────────┐ │
│ update(dt) │ │
│ 移动物体 / 碰撞检测 / AI 逻辑 │ │
└───────────────┬────────────────┘ │
▼ │
┌────────────────────────────────┐ │
│ render(screen) │ │
│ 清屏 → 绘制 → display.flip() │ │
└───────────────┬────────────────┘ │
▼ │
┌────────────────────────────────┐ │
│ clock.tick(60) │ │
│ 控制帧率 / 获取 dt 时间间隔 │ │
└───────────────┬────────────────┘ │
│ │
├── running=True ───────────────────────┘
│
▼
┌─────────────────────┐
│ pygame.quit() │
│ 程序退出 │
└─────────────────────┘
事件处理流程图
┌─────────────────┐
│ 用户操作触发 │
│ (键盘/鼠标/...) │
└────────┬────────┘
▼
┌─────────────────┐
│ 事件入队到 │
│ pygame 事件队列 │
└────────┬────────┘
▼
┌──────────────────────────────┐
│ for event in events: │
│ pygame.event.get() │
└──────────────┬───────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ KEYDOWN │ │ MOUSEBUTTONDOWN │ │ QUIT │
│ 按键按下 │ │ 鼠标点击 │ │ 窗口关闭 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 改变方向 │ │ 发射子弹 │ │ running │
│ 触发动作 │ │ 选择目标 │ │ = False │
└──────────────┘ └──────────────┘ └──────────────┘
两种输入模式对比
┌─────────────────────────────────────────────────────────────┐
│ KEYDOWN 模式(事件驱动) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 按下键的瞬间: │
│ ├─ 触发 KEYDOWN 事件 │
│ ├─ 只执行一次 │
│ └─ 适合: 跳跃、射击、菜单选择(一次性动作) │
│ │
│ 时间线: │
│ ──按下──|──────按住期间──────|──松开── │
│ ↑ ↑ │
│ KEYDOWN KEYUP │
│ (触发一次) (触发一次) │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ get_pressed() 模式(状态轮询) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 每一帧都检查: │
│ ├─ 返回 True/False 的状态数组 │
│ ├─ 按住期间每帧都为 True │
│ └─ 适合: 移动、持续攻击(持续性动作) │
│ │
│ 时间线: │
│ ──按下──|──────按住期间──────|──松开── │
│ ┌──── True ────────────────┐ │
│ │ 每帧都检测到 True │ │
│ │ → 每帧都执行移动 │ │
│ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
帧率控制原理
Clock.tick(60) 的工作原理:
目标: 每帧至少 1/60 秒 (约 16.67ms)
帧 1: 处理(5ms) + 更新(3ms) + 渲染(4ms) = 12ms
tick(60) 等待 4.67ms → 实际 16.67ms ✓
帧 2: 处理(5ms) + 更新(3ms) + 渲染(4ms) = 12ms
tick(60) 等待 4.67ms → 实际 16.67ms ✓
帧 3: 处理(5ms) + 更新(20ms) + 渲染(4ms) = 29ms ← 慢帧!
tick(60) 等待 0ms (已超时) → 实际 29ms ✗
→ FPS 降低,但游戏不会加速追赶
┌──────┐ ┌──────┐ ┌──────────────┐ ┌──────┐
│ 12ms │ │ 12ms │ │ 29ms │ │ 12ms │ ...
└──────┘ └──────┘ └──────────────┘ └──────┘
60FPS 60FPS ~34FPS 60FPS
贪吃蛇游戏逻辑流程
┌─────────────────────────────────────┐
│ 初始化蛇和食物 │
└──────────────┬──────────────────────┘
▼
┌─────────────────────┐
│ 等待输入事件 │◄─────────────────┐
└──────────┬──────────┘ │
▼ │
┌─────────────────────┐ │
│ 计算新蛇头位置 │ │
│ head + direction │ │
└──────────┬──────────┘ │
▼ │
┌─────────────────────┐ 是 │
│ 蛇头撞墙或撞自己? │─────────────→ GAME OVER
└──────────┬──────────┘
│ 否
▼
┌─────────────────────┐ 是
│ 蛇头 == 食物位置? │──────→ 分数+10, 吃掉食物
└──────────┬──────────┘ (不缩短蛇身)
│ 否
▼
┌─────────────────────┐
│ 添加新蛇头 │
│ 移除蛇尾 │
└──────────┬──────────┘
│
▼
渲染画面 ──────────────────→ 回到等待输入