Phaser 战斗反馈系统:Hit Stop、屏幕震动和伤害数字要服务手感

从 Phaser 动作游戏实战出发,拆解 Hit Stop、屏幕震动、受击闪烁、伤害数字和反馈优先级,避免把战斗反馈做成噪音。

战斗反馈不是把屏幕摇到玩家头晕

Phaser 动作游戏最容易在“能打”和“好打”之间卡住。攻击能扣血,敌人能死亡,动画也能播放,但玩家总觉得拳头像打在棉花上。团队通常第一反应是加特效:受击闪白、粒子爆炸、屏幕震动、伤害数字、音效叠加。加完之后,问题可能更糟:小怪被打一下像核爆,Boss 大招反而不突出,移动端帧率下降,玩家分不清自己是受伤还是命中敌人。战斗反馈的关键不是多,而是分层、限量和可解释。

好的反馈系统要回答三个问题:这次事件有多重要,应该影响哪些感官通道,持续多久。普通子弹命中可以只飘一个小数字;重击命中可以短暂停帧、相机轻震、敌人闪白、播放低频音效;玩家受伤则需要更高优先级的屏幕边缘提示和控制器震动。所有反馈都来自战斗事件,而不是每个武器、敌人、UI 自己临时播放。否则后期内容一多,反馈会互相抢戏。

先定义反馈事件

战斗系统应该产出语义事件,比如 enemyHitplayerDamagedcriticalHitshieldBreakbossStagger。反馈系统订阅这些事件,再根据事件强度和上下文决定表现。不要让武器代码直接调用 camera.shake(),因为它不知道当前是否已经在 Boss 转阶段、玩家是否刚受伤、设备是否开启低震动模式。语义事件保留了意图,反馈导演才能做取舍。

事件里至少要包含来源、目标、伤害、是否暴击、命中点、攻击类型和重要度。重要度可以由战斗层给出,也可以由反馈层按规则计算。比如玩家受到 5 点毒伤重要度低,受到 60 点重击重要度高;小怪死亡重要度中,Boss 破防重要度极高。反馈系统根据重要度分配 hit stop 时长、震动幅度、伤害数字样式和音效组。

flowchart TD
  A["CombatSystem 产生命中事件"] --> B["FeedbackDirector 评估重要度"]
  B --> C["HitStopController:短暂停帧"]
  B --> D["CameraShakeMixer:合并震动请求"]
  B --> E["DamageTextPool:飘字分层"]
  B --> F["FlashController:角色闪白/染色"]
  B --> G["AudioCueBus:选择音效强度"]
  C --> H["Scene update 节奏控制"]
  D --> H
  E --> H
  F --> H
  G --> H

Hit Stop 要短、准、有上限

Hit Stop 是动作游戏中非常有效的手感工具:命中的瞬间让攻击者和受击者停顿几帧,玩家会感到力量被“压实”。但它也是最容易滥用的工具。普通攻击每下停 80ms,连击时画面会像卡顿;多人或大量敌人同时命中,如果每个事件都停帧,游戏节奏会碎。建议把 Hit Stop 当作稀缺资源:普通命中 20 到 35ms,重击 50 到 80ms,Boss 破防 100ms 左右,并且全局有累计上限。

在 Phaser 里,Hit Stop 不一定要暂停整个 Scene。更稳的做法是让战斗对象进入局部时间缩放:攻击者、受击者、命中特效停顿,UI、输入缓冲、关键计时器继续运行。对于小型项目,也可以用一个 HitStopController 管理全局冻结,但要明确哪些系统不受影响。比如死亡结算、网络心跳、广告回调、音频淡出不应该被停帧影响。

屏幕震动需要混音器

屏幕震动看似简单,this.cameras.main.shake(120, 0.004) 就能动。但多个事件同时请求震动时会互相覆盖或叠得过猛。建议做一个 CameraShakeMixer:所有震动请求进入队列,按优先级、频率和当前状态合并。小命中只给轻微高频抖动,爆炸给低频大幅度,玩家受伤给短促方向性震动。若玩家开启减少动态效果,震动幅度统一降低或关闭。

震动还要考虑相机职责。Phaser Camera 可能同时承担跟随玩家、缩放、淡入淡出和震动。如果直接叠加太多效果,镜头会失控。Mixer 应该只输出最终 shake 参数,避免各系统直接改 Camera。Boss 演出、剧情对话、拍照模式等特殊状态可以暂时抑制普通战斗震动,保证画面可读。

伤害数字是信息,不是烟花

伤害数字的第一职责是告诉玩家发生了什么。普通伤害、暴击、治疗、护盾吸收、中毒、免疫、格挡都应该有不同样式,但不要每种都做成彩虹。数字池化很重要,高频战斗中每次创建 Text 对象会带来明显压力。可以使用 BitmapText 或预制 Text 池,按事件重要度决定是否显示。比如每帧毒伤可以合并显示,范围伤害可以只显示关键目标,召唤物造成的小伤害可以缩小或隐藏。

数字的位置也要设计。直接在命中点飘字,多个敌人重叠时会看不清。可以加入轻微水平偏移、同目标堆叠避让、屏幕边缘夹取。玩家受到伤害的数字应靠近玩家或 HUD,而不是藏在混乱特效里。Boss 伤害数字可以靠血条附近聚合,避免遮住 Boss 读招。

一个反馈导演骨架

下面的 TypeScript 片段展示如何把战斗事件映射到反馈请求。真实项目会有更多配置,但结构上应保持集中决策。

type CombatEventKind = "enemyHit" | "playerDamaged" | "criticalHit" | "shieldBreak" | "bossStagger";

interface CombatFeedbackEvent {
  kind: CombatEventKind;
  damage?: number;
  worldX: number;
  worldY: number;
  targetId: string;
  sourceId?: string;
  tags: string[];
}

export class FeedbackDirector {
  constructor(
    private readonly hitStop: HitStopController,
    private readonly shake: CameraShakeMixer,
    private readonly texts: DamageTextPool,
    private readonly flashes: FlashController,
  ) {}

  handle(event: CombatFeedbackEvent) {
    const critical = event.kind === "criticalHit" || event.tags.includes("heavy");
    const playerHurt = event.kind === "playerDamaged";
    const bossMoment = event.kind === "bossStagger";

    const level = bossMoment ? 4 : playerHurt ? 3 : critical ? 2 : 1;
    if (level >= 2) this.hitStop.request({ durationMs: level * 18, scope: "combat" });
    if (level >= 2) this.shake.request({ durationMs: 80 + level * 25, amplitude: 0.0015 * level });

    this.texts.spawn({
      x: event.worldX,
      y: event.worldY,
      value: event.damage ?? 0,
      style: playerHurt ? "playerDamage" : critical ? "critical" : "normal",
      priority: level,
    });

    this.flashes.flash(event.targetId, playerHurt ? "red" : "white", 70 + level * 20);
  }
}

这个骨架的重点是“集中判断”。武器只产出事件,不关心要停几毫秒。敌人只知道自己被命中,不直接摇相机。这样当玩家反馈“暴击太晃”时,你只改反馈配置,不用搜遍所有武器。

反馈优先级要保护玩家判断

战斗反馈越丰富,越需要优先级。玩家受伤通常高于敌人受伤,Boss 读招高于普通命中,关卡危险提示高于金币拾取。优先级不只是视觉层级,也包括音效、震动和屏幕空间。比如玩家被远程攻击命中时,屏幕边缘红色方向提示比敌人受击数字更重要;Boss 正在蓄力时,不应让大量小伤害数字遮挡手臂动作。

还要限制同类反馈频率。屏幕震动每秒最多触发几次,暴击音效需要冷却,同一目标的毒伤数字可以合并。限制不是削弱反馈,而是让关键事件更突出。没有节制的反馈会让玩家疲劳,最后什么都感受不到。

调试面板能救很多主观争论

手感调试很容易变成主观争论。有人说震动太重,有人说不够爽。开发模式可以显示最近 20 个反馈事件、Hit Stop 累计时长、当前震动请求、伤害数字数量和对象池占用。再加几个滑块调整 hit stop、震动幅度、数字寿命和闪白时间,设计师能在真实战斗里调参。调好的参数保存成配置,而不是写死在代码里。

还可以录制一段战斗反馈日志。相同战斗事件输入下,反馈系统输出应该稳定。改动数值后回放日志,可以比较屏幕震动次数、平均停帧时长和数字峰值数量。手感当然需要试玩,但可观测数据能帮助团队避免过度堆料。

移动端和可访问性

移动端设备差异大,屏幕震动和闪烁都要谨慎。低端机上大量 Text、粒子和后处理会掉帧,掉帧本身会破坏手感。可以提供低特效模式:减少伤害数字、关闭非关键闪白、降低震动频率。对于容易晕动的玩家,提供“减少屏幕震动”选项。对于光敏感玩家,避免高频全屏闪烁。战斗反馈是为了帮助玩家理解,不是为了挑战生理承受能力。

音效也要有分层。普通命中、重击、玩家受伤、护盾破裂使用不同频段,避免全都堆在高频。玩家关闭音乐时,关键战斗音效仍要清晰;玩家关闭音效时,视觉提示要能独立表达事件。多通道冗余能提高体验稳定性。

上线前检查清单

确认战斗层只发语义事件,反馈层集中决策;确认 Hit Stop 有全局上限,不会连续冻结;确认相机震动通过 Mixer 合并;确认伤害数字使用对象池并有显示优先级;确认玩家受伤反馈高于普通命中;确认 Boss 读招不会被数字和特效遮挡;确认低特效和减少动态效果可用;确认开发面板能显示反馈事件和参数;确认同一段战斗日志能复现反馈输出。

战斗反馈的目标是让玩家相信自己的输入产生了重量,同时清楚知道危险来自哪里。Phaser 给了我们足够直接的相机、动画、音频和文本能力,但这些能力需要被导演,而不是被每个对象随意调用。先建立反馈事件和优先级,再加震动和闪光,战斗才会有力量而不混乱。

与战斗判定的协作边界

反馈系统不能反过来改变判定。Hit Stop 期间,攻击是否已经命中、敌人是否已经死亡、玩家是否进入无敌帧,都应该由战斗模型先确定。反馈层只是延迟或强化表现。如果为了“看起来更爽”让反馈层临时延后扣血,就会产生大量边界问题:敌人死亡动画播放了但碰撞体还在,玩家看到暴击数字却没有实际伤害,网络同步时本地和服务端状态不一致。正确做法是战斗事件先落账,再由反馈层播放。

无敌帧也要进入反馈语义。玩家处于受伤无敌时,再次被攻击可能播放格挡或闪避提示,而不是普通受伤反馈。敌人护盾存在时,命中应该走护盾吸收样式,不能继续播放血肉受击音效。反馈样式和战斗状态一致,玩家才能通过画面学习规则。每一次飘字、震动、闪白都在教玩家系统怎么工作。

内容团队如何调反馈

反馈参数最好进入表格或 JSON 配置,而不是写死在代码里。每种事件可以配置 hit stop、震动、闪白、音效、数字样式和冷却。设计师调重击时,只需要改 heavy_hit 的反馈配置;程序只维护执行器。配置还可以按平台覆盖:桌面端震动强一点,移动端弱一点;低特效模式关闭部分粒子;Boss 战禁用普通小怪死亡震动。

发布前可以挑三段代表性战斗做反馈回归:新手小怪、普通精英、Boss 高压阶段。每次改反馈参数,都用同样场景录屏比较。这样团队讨论的是具体效果,而不是抽象地说“更爽一点”。战斗手感有主观成分,但主观也需要稳定样本。

继续阅读

探索更多技术文章

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

全部文章 返回首页