从一个真实问题开始
横版关卡上线前最后一天,策划要求把 Boss 出场时镜头拉近一点。改动看似很小,却引发了两个问题:技能按钮在部分手机上偏移,弹幕碰撞看起来也不准。真正的问题不是镜头缩放,而是项目没有区分世界坐标、屏幕坐标和 UI 坐标。
这个问题发生在一款横屏平台射击 H5 游戏里。当时我的角色是负责相机和 UI 适配的客户端工程师,最先做的不是马上改代码,而是把玩家路径、设备环境、资源状态和场景切换顺序重新走了一遍。Phaser 项目很容易给人一种“代码都在前端,问题应该很好定位”的错觉;实际到了线上,浏览器、渠道容器、资源缓存、输入焦点和玩家习惯会一起参与结果。
这篇文章讨论的核心是:Camera 应该改变玩家如何观看世界,而不应该顺手改变世界规则本身。如果只看 API,很容易把 Phaser 学成一组函数;如果从项目交付看,就必须关心边界、生命周期、失败兜底和调试证据。下面会围绕 玩家进入 Boss 房间、镜头锁定、震屏、弹幕躲避和结算 UI 展示 展开,把经验落到可执行的工程判断上。
先看整体结构
flowchart TD
A[World Objects 世界坐标] --> B[Main Camera 滚动/缩放/震屏]
B --> C[玩家看到的游戏画面]
D[HUD Objects 屏幕坐标] --> E[HUD Camera 或 HUD Scene]
E --> C
F[输入系统] --> G{命中区域}
G -->|UI| D
G -->|玩法| A
这张图不是为了显得复杂,而是提醒我们:玩家看到的是一个连续体验,工程上却是多个系统串起来的结果。main camera、world bounds、scroll、zoom、HUD camera、screen space 都有自己的职责,任何一个环节偷懒,最后都会变成“怎么偶尔不对”的线上问题。
一段可以落地的代码切口
下面这段示例不是完整框架,只是为了说明 镜头与 UI 分层 应该如何从一开始就留下边界。真实项目里可以继续封装,但不要在还没说清职责前就追求抽象。
const hud = this.scene.get('HudScene');
this.cameras.main.startFollow(player, true, 0.12, 0.12);
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
// HUD 使用独立 Scene 或独立 Camera,不跟随世界滚动。
this.cameras.main.ignore(hudLayer);
代码里的重点不是语法,而是控制权。Phaser 的对象和插件都很好调用,难点是不要让每个回调都直接修改全局状态。只要控制权分散,后续就会出现“这个字段到底是谁改的”“为什么第二次进入场景不一样”“为什么关闭弹窗后玩法状态变了”之类的问题。
三套坐标必须分清
Phaser 项目里至少有三套坐标:世界坐标、屏幕坐标和 UI 坐标。世界坐标描述玩家、地图、怪物、子弹的位置;屏幕坐标描述浏览器画布上的点;UI 坐标描述按钮、血条、弹窗在界面里的位置。Camera 的 scroll 和 zoom 会影响世界如何投影到屏幕,但不应该改变玩法对象的真实位置。
很多 Bug 都来自坐标混用。比如用 pointer.x 直接判断玩家是否点击世界物体,在镜头滚动后就会偏;把血条挂在世界层,缩放时文字跟着模糊;把技能按钮放进 GameScene 的 world container,震屏时按钮也抖。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
HUD 最好独立
HUD 可以放在独立 Scene,也可以用独立 Camera 或固定容器处理。核心原则是:HUD 不跟随世界滚动,不被玩法镜头缩放影响,不参与物理世界。这样 Boss 镜头拉近、关卡震屏、玩家死亡慢动作都不会把按钮和分数带歪。
如果 HUD 需要显示指向世界物体的标记,例如任务箭头或敌人血条,可以在每帧把世界坐标转换到屏幕坐标,再决定是否显示。转换是边界,不要把 HUD 对象直接塞回世界里省事。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
跟随镜头要有阻尼
直接 startFollow 玩家能很快实现跟随,但体验不一定好。横版动作游戏需要前视空间,赛车游戏需要速度方向偏移,弹幕游戏需要稳定视野。镜头跟随不是把玩家永远放在中心,而是为当前玩法提供最有用的信息。
阻尼、dead zone、look ahead、边界限制都要按玩法调。跟随太紧会晕,太慢会让操作没有反馈。镜头系统要接受玩法事件,例如进入 Boss 房间临时锁定、剧情演出移动到指定点、释放大招时短暂拉远。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
震屏要克制
震屏能增强打击感,也很容易破坏可读性。低端屏幕、小尺寸手机、密集弹幕场景里,过强震屏会让玩家看不清危险。震屏应该有强度、时长、衰减和优先级,不应该每个系统随手调用 camera.shake。
建议做一个 CameraEffects 服务,统一接收 hit、explosion、bossLanding 等语义事件,再决定震屏参数。这样可以防止多个震屏叠加过量,也方便做设备档位降级。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
缩放适配不是 CSS 拉伸
H5 游戏面对的屏幕比例非常杂。简单把 canvas 拉满,会导致 UI 被刘海挡住、可玩区域过窄、世界视野不公平。Phaser Scale Manager 提供了模式选择,但项目仍然要定义自己的安全区和设计分辨率策略。
横屏游戏通常要关心左右安全区,竖屏游戏要关心底部手势区域。UI 元素应根据安全区锚定,玩法世界则根据设计视野裁剪或扩展。不要让 UI 适配规则反过来改变物理世界尺寸,除非这是明确的玩法设计。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
多 Camera 要防止输入混乱
多摄像机可以实现小地图、分屏、独立 UI,但输入判断会更复杂。一个 pointer 事件到底命中了哪个 camera 下的对象,需要有明确规则。否则玩家点击小地图时可能同时触发世界移动,点击弹窗按钮时也可能开枪。
通常应该先让 UI 层消费输入,再让玩法层处理剩余事件。对可交互对象,要确认它属于哪个 Scene、哪个 Camera、是否被遮罩或弹窗覆盖。输入优先级比视觉层级更重要,因为玩家感受到的是点击结果。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
镜头调试要可视化
开发版可以显示 camera scroll、zoom、world bounds、dead zone、safe area 和 pointer world position。调镜头时如果只凭肉眼,很容易把 UI 适配问题误判成地图问题。
尤其在移动设备上,要截图记录不同屏幕比例:16:9、19.5:9、平板、折叠屏模拟宽度。镜头系统一旦稳定,后续内容制作会顺很多;如果一开始混乱,后面每张地图都要单独救火。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
落地清单
检查 Camera 设计时可以问:世界坐标和 UI 坐标是否分离;HUD 是否不会被主镜头震动和缩放影响;pointer 是否正确转换到 world;震屏是否集中管理;安全区是否覆盖主流手机;Boss 锁镜是否能恢复;镜头效果是否不会改玩法对象真实位置。
好的镜头系统不是炫技,而是让玩家稳定理解世界。Phaser Camera 足够灵活,关键是把观看、输入和规则分开。镜头可以让世界更有戏剧性,但不要让它悄悄改变世界本身。
在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。
排查问题时的顺序
遇到相关问题时,不建议先凭经验改参数。更稳的顺序是先复现,再缩小范围,最后才动代码。复现时要记录设备、浏览器、渠道容器、网络、页面可见状态、游戏版本和资源版本。很多 H5 游戏问题只在特定容器里出现,如果只在桌面 Chrome 里验证,很容易得到错误结论。
缩小范围时,可以把链路拆成输入、状态、资源、表现和持久化几段。先确认玩家意图有没有被收到,再确认状态机有没有接受,再确认 Phaser 对象有没有正确执行,再确认表现层有没有被镜头、缩放、缓存或音频策略影响。这样的排查路径比“看哪里像问题就改哪里”慢一点,但能避免改出新问题。
最后是留证据。开发版日志、调试面板、可视化边界、状态快照和小型回放,都比口头描述可靠。尤其是涉及 镜头与 UI 分层 的问题,录屏只能告诉你现象,不能告诉你内部状态。把内部状态展示出来,团队才有共同语言。
团队协作里的责任划分
Phaser 项目经常由少数工程师快速推进,因此容易忽略协作边界。可是一旦项目进入运营,策划会改配置,美术会换资源,运营会调整活动,渠道会接 SDK,测试会覆盖多设备。工程代码如果没有把责任划清,每个角色都会被迫理解太多底层细节。
比较健康的方式是让配置描述意图,让服务层解释规则,让 Scene 编排生命周期,让表现对象执行动画和反馈,让平台适配器处理浏览器或渠道差异。这样策划新增内容时不需要知道 Scene 的内部结构,美术替换资源时不会改变玩法规则,运营关闭活动时不会留下半开半关的 UI 状态。
这不是大团队才需要的流程。越小的团队越需要减少隐性沟通成本。一个清楚的边界,可以让后续每一次临时需求都少一点风险。
上线前最后一轮检查
最后一轮检查不要只点一遍主流程。至少要覆盖首次进入、第二次进入、弱网、低端设备、后台恢复、快速重复点击、资源失败、配置缺字段和旧数据升级。很多 Phaser Bug 都出现在“第二次”或者“恢复后”:第二次开局、第二次打开弹窗、第二次播放音频、第二次加载同一图集。
如果这篇文章讨论的系统已经接近上线,我会要求团队给出三类证据。第一是功能证据,证明主流程确实可用;第二是边界证据,证明失败和异常路径不会把玩家卡死;第三是观测证据,证明线上再出问题时能定位。只有这三类证据都存在,才算不是靠运气发布。
结语
Phaser 的优势是轻、快、直接。它能让一个想法很快变成可以玩的东西,也正因为如此,项目很容易在“先跑起来”之后忽略工程边界。Camera 应该改变玩家如何观看世界,而不应该顺手改变世界规则本身。把这个原则落实到代码里,项目就不会因为功能增加而迅速失控。
真正可靠的 Phaser 游戏,不是每个模块都写得很重,而是关键链路有清楚的生命周期、明确的责任、可降级的失败路径和能解释问题的调试证据。做到这些,即使项目仍然保持轻量,也能承受上线后的真实流量和频繁改动。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。