前言
继光影拼图之后,本文将介绍博客系列第六款小游戏——Flappy Byte(小鸟闯关) 的开发。这是一款向经典 Flappy Bird 致敬的像素风作品,也是系列中第一款实时物理模拟游戏。
相比之前几款游戏,小鸟闯关在技术实现上有本质区别:
| 游戏 | 核心机制 | 技术难点 | 交互方式 |
|---|---|---|---|
| 五子棋 | AI 博弈 | Minimax 搜索树 | 点击落子 |
| 贪吃蛇 | 方向控制 | 蛇身移动逻辑 | 键盘方向键 |
| 俄罗斯方块 | 方块堆叠 | 碰撞消除 | 键盘操作 |
| 扫雷 | 逻辑推理 | 递归展开算法 | 点击标记 |
| 光影拼图 | 图片切割+拖拽 | 吸附算法+响应式 | 拖拽碎片 |
| 小鸟闯关 | 物理模拟+避障 | 实时碰撞+像素渲染 | 点击/触屏/键盘 |
Flappy Byte 的核心挑战:
- 物理模拟:重力加速度、跳跃反弹、实时碰撞检测
- 像素渲染:纯 Canvas 像素艺术绘制,无需外部图片资源
- 难度曲线:随分数动态调整管道间距和速度
- 交互优化:结束面板 3 秒延时防止误触
- 性能管理:单文件
requestAnimationFrame游戏循环
项目结构
相比拼图游戏的模块化架构,小鸟闯关采用单文件紧凑设计,所有逻辑集中在 flappy-game.js 一个文件中:
1 | source/ai-games/flappy/ |
这种设计选择是因为 Flappy Bird 类游戏的逻辑天然紧凑——物理、渲染、事件三者紧密耦合,拆分成多个模块反而增加复杂度。
游戏状态机
游戏采用简洁的三态状态机管理流程:
1 | ┌─────────────────────────────────────────────────────────────┐ |
状态切换的核心代码:
1 | function handleInput() { |
关键设计:三种状态的输入响应不同。ready 状态开始游戏,playing 状态让鸟跳跃,gameover 状态需要等待 3 秒冷却后才能重置。
物理模拟与碰撞检测
重力与跳跃
小鸟的运动模拟了真实物理中的重力加速度:
1 | // 重力加速度(每帧) |
参数设计逻辑:
GRAVITY = 0.5:每帧增加下落速度,模拟重力JUMP_FORCE = -6:跳跃瞬间给一个向上的初速度MAX_SPEED = 10:限制最大下落速度,防止飞出画面- 旋转角度根据速度动态计算:上升时仰头(
-0.35),下落时俯冲(vy × 0.1,最大1.4)
AABB 碰撞检测
碰撞检测采用轴对齐包围盒(AABB) 算法,检测小鸟与管道、地面的碰撞:
1 | function checkCollision() { |
AABB 检测的核心条件:两个矩形在 X 轴和 Y 轴上同时重叠才算碰撞。对于管道,X 轴检查小鸟是否进入管道区域,Y 轴检查是否碰到了管道上下部分(间隙之外)。
像素风格渲染系统
全局像素对齐策略
为了保持像素风格的一致性,所有绘制都基于像素块大小 PX = 4 对齐:
1 | var PX = 4; // 场景元素像素块大小 |
所有位置和尺寸都通过 Math.round(x / PX) * PX 对齐到像素网格,确保画面干净利落。
天空与云
天空采用色带渐变替代平滑渐变,云朵用像素网格定义:
1 | function drawBg() { |
小鸟:像素网格精灵
小鸟是整个游戏中最复杂的像素绘制对象,使用 16×12 像素网格(每个像素 2×2 画布像素)定义:
1 | var palette = ['', '#1d746f', '#1d8a84', '#37c6c0', '#ffd93d', '#ffffff', '#222222', '#2aa39e']; |
颜色编码说明:
| 编码 | 颜色 | 用途 |
|---|---|---|
0 |
透明 | 背景 |
1 |
#1d746f |
身体深色 |
3 |
#37c6c0 |
胸部浅色 |
4 |
#ffd93d |
喙(金色) |
5 |
#ffffff |
眼白 |
6 |
#222222 |
瞳孔 |
7 |
#2aa39e |
翅膀 |
翅膀动画通过网格中值为 7 的像素行上下偏移实现:
1 | var yOff = (c === 7 && bird.wingUp) ? -2 : 0; |
像素数字字模
得分使用自定义 3×5 像素字模渲染,而非系统字体:
1 | var PIXEL_DIGITS = [ |
每个数字由 15 位二进制值定义(5 行 × 3 列),1 表示点亮像素块。这种方案的好处:
- 像素风格统一:与游戏其他元素风格一致
- 无依赖:不需要加载外部字体
- 可定制:可以精确控制每个像素
地面与管道
地面采用条纹像素地面设计,配合滚动装饰块模拟运动感:
1 | function drawGround() { |
管道的像素化处理包括位置对齐、纯色填充和像素金边:
1 | function drawPipes() { |
绘制顺序
正确的绘制顺序保证层次关系:
1 | function draw() { |
难度曲线系统
游戏难度随得分递增,采用双参数动态调整:
1 | function getDifficulty() { |
参数变化曲线:
| 得分区间 | 管道间距 | 移动速度 | 难度感受 |
|---|---|---|---|
| 0-7 | 170 | 1.5 | 简单 |
| 8-15 | 165 | 1.5 | 略紧 |
| 16-23 | 160 | 1.7 | 中等 |
| 24-31 | 155 | 1.9 | 困难 |
| 32+ | 150 | 2.0 | 极限 |
- 间距递减:每 8 分缩小
5px,下限150px(防止出现无法通过的缝隙) - 速度递增:每 10 分增加
0.2,上限2.0(避免速度过快导致性能问题)
积分系统与本地存储
游戏采用浏览器 localStorage 持久化最高分:
1 | // 读取最佳成绩 |
计分逻辑:小鸟通过管道时,管道对象的 scored 标记确保每个管道只计分一次:
1 | if (!pipes[i].scored && pipes[i].x + pipes[i].w < bird.x) { |
用户交互优化:3 秒关闭延时
游戏结束面板的 3 秒延时是体验优化的重要设计:
设计动机
- 防止误触:玩家刚死亡时可能条件反射点击屏幕,导致立即重开看不到成绩
- 成绩展示:给玩家时间查看本局得分与最佳成绩
- 情绪缓冲:从失败到重开之间提供一个自然的停顿
实现方案
1 | // 游戏结束时设置计时器 |
倒计时精度:使用 Date.now() 时间戳判断而非计数递减,避免 setInterval 漂移导致的不准确。
资源清理
计时器必须在游戏重置和页面切换时正确清理:
1 | function clearCountdown() { |
clearCountdown() 在三个地方被调用:
resetGame()— 玩家手动重置cleanupGame()— PJAX 页面切换- 倒计时自身 — 到 0 时自动清理
PJAX 兼容性
小鸟闯关复用了博客统一的 GameManager 生命周期管理方案:
1 | if (window.GameManager) { |
清理逻辑详解
1 | function cleanupGame() { |
清理要点:
- 动画帧:必须
cancelAnimationFrame,否则循环持续消耗 GPU - 事件监听:移除
click、touchstart、touchend、keydown,避免 PJAX 切换后重复绑定 - 倒计时:
setInterval必须清理,否则切页面后仍在运行
事件监听器通过 window._flappyClick 等全局引用保存,便于 removeEventListener 移除:
1 | window._flappyClick = handleClick; |
总结
小鸟闯关是博客游戏系列中技术路线最独特的一款——它不依赖 AI 算法或复杂数据结构,而是用实时物理模拟和像素艺术渲染带来完全不同的游戏体验。
通过本文,我们深入探讨了:
- 物理引擎:重力加速度、跳跃冲量、AABB 碰撞检测
- 像素渲染:像素对齐策略、网格精灵定义、点阵字模
- 难度曲线:双参数动态调整保证渐进式挑战
- 交互优化:3 秒关闭延时,兼顾展示与防误触
- 状态管理:三态状态机的干净切换与资源清理
对比系列中的其他游戏:
| 游戏 | 技术标签 | 复杂度 |
|---|---|---|
| 五子棋 | AI 博弈、Minimax | ⭐⭐⭐⭐ |
| 贪吃蛇 | 队列、方向控制 | ⭐⭐ |
| 俄罗斯方块 | 矩阵旋转、碰撞消除 | ⭐⭐⭐ |
| 扫雷 | 递归展开、逻辑推理 | ⭐⭐⭐ |
| 光影拼图 | 图片处理、吸附算法 | ⭐⭐⭐⭐⭐ |
| 小鸟闯关 | 物理模拟、像素渲染 | ⭐⭐⭐ |
Flappy Byte 的成功说明:即使是最简单的游戏机制,通过精致的像素艺术和流畅的物理反馈,也能带来完整的游戏体验。
相关链接: