用 Phaser Arcade Physics 做战斗手感:碰撞盒比特效更诚实

以动作类 H5 游戏为例,讨论 Phaser Arcade Physics 中速度、碰撞、命中窗口、击退和手感调试的实践方法。

从一个真实问题开始

一个角色攻击动画做得很漂亮,刀光也足够夸张,但玩家反馈只有两个字:不爽。录屏慢放后发现,视觉上刀已经砍到怪物,伤害却晚了半拍;怪物看起来碰到玩家时,实际碰撞盒还隔着几像素。特效骗不了手感,碰撞盒才是最诚实的部分。

这个问题发生在一款俯视角刷怪 H5 动作游戏里。当时我的角色是战斗客户端工程师,最先做的不是马上改代码,而是把玩家路径、设备环境、资源状态和场景切换顺序重新走了一遍。Phaser 项目很容易给人一种“代码都在前端,问题应该很好定位”的错觉;实际到了线上,浏览器、渠道容器、资源缓存、输入焦点和玩家习惯会一起参与结果。

这篇文章讨论的核心是:Arcade Physics 适合快速、明确、低成本的 2D 碰撞,但它需要被玩法时间轴约束。如果只看 API,很容易把 Phaser 学成一组函数;如果从项目交付看,就必须关心边界、生命周期、失败兜底和调试证据。下面会围绕 玩家普攻、怪物受击、击退、短暂无敌和掉落结算 展开,把经验落到可执行的工程判断上。

先看整体结构

sequenceDiagram
    participant Input as 输入
    participant FSM as 角色状态机
    participant Physics as Arcade Physics
    participant Target as 目标
    participant VFX as 表现层
    Input->>FSM: attack pressed
    FSM->>Physics: 开启命中窗口
    Physics->>Target: overlap 命中
    Target->>Target: 扣血/硬直/无敌帧
    Target->>VFX: 受击闪白和击退表现
    FSM->>Physics: 关闭命中窗口

这张图不是为了显得复杂,而是提醒我们:玩家看到的是一个连续体验,工程上却是多个系统串起来的结果。body velocity、overlap、collider、hitbox、hurtbox、knockback 都有自己的职责,任何一个环节偷懒,最后都会变成“怎么偶尔不对”的线上问题。

一段可以落地的代码切口

下面这段示例不是完整框架,只是为了说明 战斗判定链路 应该如何从一开始就留下边界。真实项目里可以继续封装,但不要在还没说清职责前就追求抽象。

const attack = {
  windup: 90,
  active: 120,
  recovery: 180,
  damage: 12,
  knockback: 180
};

function updateAttack(timeInMs) {
  hitbox.active = timeInMs >= attack.windup &&
    timeInMs < attack.windup + attack.active;
}

代码里的重点不是语法,而是控制权。Phaser 的对象和插件都很好调用,难点是不要让每个回调都直接修改全局状态。只要控制权分散,后续就会出现“这个字段到底是谁改的”“为什么第二次进入场景不一样”“为什么关闭弹窗后玩法状态变了”之类的问题。

Arcade Physics 的边界

Arcade Physics 的优势是简单、快、可预测。它适合平台跳跃、弹幕、轻量动作、碰撞触发区和大量小物体。它不适合复杂刚体堆叠、真实物理模拟或高精度连续碰撞。如果项目需要的是清楚的战斗判定,而不是真实世界物理,它往往比更重的方案更稳。

边界明确以后,团队就不会期待 Arcade Physics 自动解决所有手感问题。速度曲线、命中窗口、硬直、取消、霸体、击退,这些都应该由战斗逻辑定义,物理系统只负责空间关系和简单移动。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

先分清 hitbox 和 hurtbox

很多新项目直接用角色 sprite 的 body 做碰撞,这会让表现和规则绑死。角色动画变大,碰撞跟着变大;美术调整帧图,战斗手感悄悄改变。更合理的做法是区分 hurtbox 和 hitbox:hurtbox 表示角色可被打中的区域,hitbox 表示攻击在某个时间段内覆盖的区域。

在 Phaser 里可以用不可见的 rectangle body 或独立 GameObject 承载这些盒子。调试时打开 debug 绘制,慢放录屏,对齐动画帧和判定窗口。一个好的碰撞盒通常不是完全贴合美术,而是贴合玩家的感知。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

命中窗口不要绑死在动画事件上

动画事件很方便,但不应该成为唯一权威。浏览器帧率波动、动画速度缩放、角色攻速加成都会影响事件触发的时机。战斗判定最好有自己的时间轴:起手、有效、收招,每段用毫秒或逻辑帧描述。动画可以跟随时间轴,也可以被表现层微调。

这样做的好处是策划调手感时不用反复改动画资源。动画师可以把刀光做得更自然,战斗配置仍然保持稳定。尤其在多角色、多武器的项目里,判定和表现分离能减少大量回归成本。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

速度不是越快越爽

动作游戏常见误区是把移动速度、击退速度和攻击位移都调高。短期看起来刺激,实际上会放大碰撞误差,让玩家觉得飘。手感来自可预期的加速、停止、转向和反馈,而不是单纯的数值大。

Arcade Physics 默认速度控制直接,适合用代码明确设置 velocity。但建议把输入速度、攻击位移、受击击退、地形阻挡分别处理。玩家主动移动要响应快,攻击位移要有起止,受击击退要能被状态机接管,不能让多个系统同时写 velocity。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

overlap 和 collider 要按语义选

overlap 适合命中检测、拾取、触发区;collider 适合墙体阻挡和实体推挤。如果所有东西都用 collider,可能会出现子弹把怪物顶开、攻击盒被地形挡住、掉落物挤开玩家等奇怪现象。如果所有东西都用 overlap,又会丢掉必要的阻挡。

建议按层设计碰撞矩阵:玩家与墙体 collider,玩家攻击与敌人 hurtbox overlap,敌人与玩家 hurtbox overlap,掉落物与玩家拾取区 overlap,敌人与敌人是否 collider 根据玩法决定。矩阵比到处写 if 更容易检查。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

调试工具决定迭代速度

战斗手感不能只靠主观描述。开发版应该能显示 hurtbox、hitbox、当前状态、攻击时间轴、最近命中记录、无敌帧剩余时间和 velocity。再加一个 0.25 倍速或逐帧推进功能,很多争论会立刻消失。

我经历过最有效的一次调参,是策划、动画和客户端三个人盯着同一个 debug overlay。大家不再争“感觉晚了”,而是直接看到有效帧比刀光晚了 70ms。问题被量化以后,修改也更有信心。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

低端机上的判定一致性

H5 动作游戏经常面对帧率不稳定。update 的 delta 变大时,快速移动物体可能穿过很薄的目标。Arcade Physics 不是银弹,需要通过限制速度、增大关键碰撞盒、拆分移动步长或提高逻辑检查频率来降低风险。

不要只在开发机上测手感。低端安卓机、低电量模式、浏览器后台恢复后的第一秒都要测。战斗系统越依赖精确时机,越要关心帧率波动下的表现。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

落地清单

检查一个 Phaser 战斗系统,可以从这些问题开始:攻击判定是否独立于美术帧;hitbox 和 hurtbox 是否分离;多个系统是否同时写 velocity;碰撞矩阵是否清楚;debug overlay 是否能解释一次命中;低帧率下是否会穿透;击退和无敌帧是否由状态机管理。

手感不是玄学,它由一批可观察的小决策组成。Phaser Arcade Physics 足够完成大量 2D 战斗项目,前提是团队把它当作清楚的空间判断工具,而不是把所有战斗规则都塞进物理回调。

在实际协作中,我会把这部分写进开发检查表,而不是只放在口头约定里。因为 Phaser 项目的迭代速度通常很快,今天为了活动临时加的逻辑,三周后就可能变成复用模板。越是轻量项目,越要在关键位置把规则写清楚。

排查问题时的顺序

遇到相关问题时,不建议先凭经验改参数。更稳的顺序是先复现,再缩小范围,最后才动代码。复现时要记录设备、浏览器、渠道容器、网络、页面可见状态、游戏版本和资源版本。很多 H5 游戏问题只在特定容器里出现,如果只在桌面 Chrome 里验证,很容易得到错误结论。

缩小范围时,可以把链路拆成输入、状态、资源、表现和持久化几段。先确认玩家意图有没有被收到,再确认状态机有没有接受,再确认 Phaser 对象有没有正确执行,再确认表现层有没有被镜头、缩放、缓存或音频策略影响。这样的排查路径比“看哪里像问题就改哪里”慢一点,但能避免改出新问题。

最后是留证据。开发版日志、调试面板、可视化边界、状态快照和小型回放,都比口头描述可靠。尤其是涉及 战斗判定链路 的问题,录屏只能告诉你现象,不能告诉你内部状态。把内部状态展示出来,团队才有共同语言。

团队协作里的责任划分

Phaser 项目经常由少数工程师快速推进,因此容易忽略协作边界。可是一旦项目进入运营,策划会改配置,美术会换资源,运营会调整活动,渠道会接 SDK,测试会覆盖多设备。工程代码如果没有把责任划清,每个角色都会被迫理解太多底层细节。

比较健康的方式是让配置描述意图,让服务层解释规则,让 Scene 编排生命周期,让表现对象执行动画和反馈,让平台适配器处理浏览器或渠道差异。这样策划新增内容时不需要知道 Scene 的内部结构,美术替换资源时不会改变玩法规则,运营关闭活动时不会留下半开半关的 UI 状态。

这不是大团队才需要的流程。越小的团队越需要减少隐性沟通成本。一个清楚的边界,可以让后续每一次临时需求都少一点风险。

上线前最后一轮检查

最后一轮检查不要只点一遍主流程。至少要覆盖首次进入、第二次进入、弱网、低端设备、后台恢复、快速重复点击、资源失败、配置缺字段和旧数据升级。很多 Phaser Bug 都出现在“第二次”或者“恢复后”:第二次开局、第二次打开弹窗、第二次播放音频、第二次加载同一图集。

如果这篇文章讨论的系统已经接近上线,我会要求团队给出三类证据。第一是功能证据,证明主流程确实可用;第二是边界证据,证明失败和异常路径不会把玩家卡死;第三是观测证据,证明线上再出问题时能定位。只有这三类证据都存在,才算不是靠运气发布。

结语

Phaser 的优势是轻、快、直接。它能让一个想法很快变成可以玩的东西,也正因为如此,项目很容易在“先跑起来”之后忽略工程边界。Arcade Physics 适合快速、明确、低成本的 2D 碰撞,但它需要被玩法时间轴约束。把这个原则落实到代码里,项目就不会因为功能增加而迅速失控。

真正可靠的 Phaser 游戏,不是每个模块都写得很重,而是关键链路有清楚的生命周期、明确的责任、可降级的失败路径和能解释问题的调试证据。做到这些,即使项目仍然保持轻量,也能承受上线后的真实流量和频繁改动。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页