为什么这个系统值得单独设计
一个横版冒险游戏的港口关卡里,玩家从清晨码头跑到雾气弥漫的灯塔。近处是摇晃的吊机,中景是装货平台,远处是缓慢移动的云层和海面反光。第一眼看上去只是几层背景图,但真正上线时,镜头缩放、检查点回退、性能降级和关卡拼接都会考验这套系统。
视差背景最容易被写成 Scene 里的几行 scrollFactor。这样做演示很快,但一旦关卡变长、镜头有震动、UI 有独立摄像机、背景需要按区域切换,代码会马上变得不可控。更稳的做法是把背景层当成独立系统:它读取镜头状态和区域配置,决定每一层该显示什么、移动多少、何时加载、何时回收。 本文按实际项目会遇到的问题来拆,不停留在“能跑”的 Demo 层。重点会放在数据边界、状态流、玩家反馈、调试方式和后续维护成本上。Phaser 很适合快速做出手感,但越是能快速表现,越需要把规则层写清楚。
核心架构
flowchart TD
N1["CameraState"] --> N2["ParallaxProfile"]
N2["ParallaxProfile"] --> N3["LayerPool"]
N1["CameraState"] --> N4["CullWindow"]
N4["CullWindow"] --> N3["LayerPool"]
N3["LayerPool"] --> N5["DepthComposer"]
N5["DepthComposer"] --> N6["Phaser Cameras"]
N7["DebugOverlay"] --> N1["CameraState"]
N7["DebugOverlay"] --> N3["LayerPool"]
这套结构的原则是单向流动:输入或场景事件进入 CameraState,核心模型完成计算,再由 Phaser 表现层消费结果。ParallaxProfile、LayerPool、CullWindow、DepthComposer、DebugOverlay 都应尽量保持可序列化、可测试、可回放。不要让某个 Tween 完成回调、某个 Sprite 是否可见、某个按钮是否高亮成为玩法事实。
层配置要描述意图
每一层不要只记录 texture 和 scrollFactor。更实用的配置会包含 depth、repeatX、repeatY、parallaxX、parallaxY、anchor、区域可见条件、是否受天气影响、低端机是否可关闭。这样同一套背景系统可以服务港口、山谷、室内工厂和 Boss 房,而不是每个关卡复制一份 createBackground。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
镜头状态是唯一输入
背景层不应该直接监听玩家坐标。玩家坐标可能被过场镜头、回放镜头或死亡镜头覆盖,真正决定画面的是 CameraState。把 scrollX、scrollY、zoom、shakeOffset 和 bounds 组合成一个快照,背景系统只根据快照更新。这样切换到剧情镜头时,视差不会突然跳回玩家。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
长关卡要做窗口裁剪
横版关卡很长时,把所有背景块都创建出来会浪费显存和 draw call。CullWindow 可以根据镜头可见区域加安全边距,只激活附近的背景 tile。远景层移动慢,可以用更大的块和更低的更新频率;近景遮挡层则要跟镜头更紧。对象池负责复用 Sprite,避免滚动时反复创建销毁。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
深度排序要留给玩法空间
视差层的 depth 不能和敌人、子弹、掉落物随意混用。建议为背景、远景动态物、玩法对象、前景遮挡、屏幕特效划出固定区间。这样后续加雾、雨、光束或镜头遮罩时,不会出现某个背景突然盖住角色血条的事故。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
低端机降级不能只关特效
背景是移动端性能的大头之一。低端机可以减少远景层数量、降低粒子密度、关闭局部反光、把重复层合成更大的静态贴图。降级配置应写在 ParallaxProfile 里,而不是散在设备判断里。这样 QA 能明确知道每档画质少了什么。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
区域过渡要避免断层
从码头走进灯塔内部时,背景主题会变化。不要在触发线处直接销毁旧背景并创建新背景,视觉上会闪。可以让新旧 profile 在一段距离内交叉淡入淡出,或按层级逐步替换。近景层先换,中远景层延后,镜头会更自然。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
调试显示要可见
开发模式至少显示当前 profile、每层 scrollFactor、激活块数量、纹理尺寸、depth 和是否被裁剪。视差 bug 很多时候不是计算错,而是配置错:某层 repeat 方向写反,某张图宽度不是预期值,或某个区域没有安全边距。调试面板能把这些问题直接暴露出来。
落地时可以先用最朴素的调试图形验证规则,再替换成正式美术。这个顺序很重要:如果规则层还没稳定就开始堆特效,后面每一次调参都会同时牵动动画、音效和 UI,问题会变得难以判断。
TypeScript 实现骨架
interface ParallaxLayerConfig {
key: string;
texture: string;
depth: number;
factorX: number;
factorY: number;
repeatX: boolean;
quality: "low" | "medium" | "high";
}
interface CameraState { scrollX: number; scrollY: number; zoom: number; width: number; height: number }
class ParallaxLayer {
constructor(private sprite: Phaser.GameObjects.TileSprite, private config: ParallaxLayerConfig) {}
update(camera: CameraState) {
this.sprite.setDepth(this.config.depth);
this.sprite.tilePositionX = camera.scrollX * this.config.factorX;
this.sprite.tilePositionY = camera.scrollY * this.config.factorY;
this.sprite.setDisplaySize(camera.width / camera.zoom, camera.height / camera.zoom);
this.sprite.setPosition(camera.scrollX + camera.width / camera.zoom / 2, camera.scrollY + camera.height / camera.zoom / 2);
}
setVisibleByQuality(level: "low" | "medium" | "high") {
const order = { low: 0, medium: 1, high: 2 };
this.sprite.setVisible(order[this.config.quality] <= order[level]);
}
}
这段代码不是完整框架,而是把关键边界先立出来。实际项目里应继续补上配置加载、错误码、事件派发、性能统计和单元测试。只要骨架保持清楚,后续接入 Phaser 的 Graphics、Sprite、Matter、Tilemap 或 Sound 都不会污染规则层。
具体落地步骤
- 第一步,把 CameraState 和 ParallaxProfile 从 Scene 中拆出来,写成可以直接用 TypeScript 调用的模型。这个模型只接收普通对象,不接收 Sprite、Camera 或 Tween。只要这一步做到,后面的测试、调试、存档和工具预览都会简单很多。
- 第二步,在 Phaser Scene 里建立很薄的适配层。输入事件、物理回调、计时器和资源加载都可以在适配层发生,但它们只提交意图,不直接改核心状态。核心系统产出快照后,适配层再更新显示对象、音效、粒子和 HUD。
- 第三步,给每个关键状态准备调试可视化。不要等 QA 报问题才补日志。开发模式下至少能看到当前状态、最近输入、失败原因、候选列表、耗时和重要阈值。对复杂玩法来说,能看见中间状态比多写一层封装更重要。
- 第四步,用三类样例保护系统:正常流程、边界流程、错误配置。正常流程证明体验能跑通,边界流程证明快速输入、暂停、切场景和重复触发不会破坏状态,错误配置证明系统会给出明确报告,而不是静默失败。
项目检查清单
- 确认 CameraState 的输入输出能被 JSON 记录,便于复现玩家操作。
- 确认 ParallaxProfile 的配置有默认值、版本号和校验错误信息。
- 确认快速点击、暂停、切后台、重开场景和读档不会重复提交关键状态。
- 确认失败反馈比成功反馈更具体,玩家能理解自己为什么没有成功。
- 确认低端机或高负载场景有降级策略,而不是等帧率下降后再猜瓶颈。
- 确认调试面板能在不改代码的情况下打开,并能导出最近关键事件。
常见误区
第一类误区,是把 Phaser 的显示对象当成状态来源。显示对象适合表达结果,却不适合保存规则事实。它可能被对象池回收、被摄像机隐藏、被动画临时修改,也可能因为画质档变化而不存在。核心状态必须独立存在。
第二类误区,是只为当前关卡写逻辑。当前关卡对象少、节奏慢、输入简单,临时判断看起来没有问题。等到内容增加、节奏加快、平台变多,临时逻辑会互相覆盖。每个系统至少要提前考虑配置错误、重复触发和性能上限。
第三类误区,是没有把失败当成流程设计。复杂系统一定会失败:条件不满足、资源缺失、网络超时、玩家中断、配置非法。失败不应该只是 console 里的一行错误,而应该是玩家、QA 和内容团队都能理解的状态。
结语
视差背景与深度裁剪:远景层不是多放几张图 的难点不在某个 API,而在边界。把数据、规则、表现和调试分开后,Phaser 的优势会更明显:你可以很快做出反馈,也可以放心迭代规则。反过来,如果所有逻辑都散落在 Scene 的回调里,第一版越快,后续越难维护。
额外实践建议
- 把背景系统从具体关卡里抽出来后,关卡配置就能只描述层和区域,Scene 不再知道某张远景图应该怎么滚动。
- 如果项目有多摄像机,背景系统必须明确绑定世界摄像机,UI 摄像机不参与视差计算。
- 每次新增背景资源时,都要记录图片尺寸、是否可无缝平铺和推荐画质档,避免内容团队把不可平铺图误用于 repeat 层。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。