伤害公式不是越复杂越专业
Phaser 做 RPG、动作冒险、塔防或割草游戏时,伤害公式很快会从 damage = attack - defense 变成一团公式:攻击、技能倍率、元素克制、护甲减免、暴击、等级压制、随机浮动、Buff、减伤、护盾、穿透。公式复杂不一定坏,但如果团队解释不了“为什么这一刀是 137”,玩家和策划都会失去信心。平衡系统最重要的品质是可解释和可调。
伤害计算应该独立于 Phaser Sprite。Sprite 负责播放攻击和飘字,DamageResolver 负责从攻击上下文和防御上下文计算结果。结果里不仅有最终数字,还应包含拆解:基础伤害、倍率、护甲减免、暴击、元素修正、随机因子。UI、调试面板和战斗日志都使用同一个结果。这样数值问题能被讨论,而不是靠感觉猜。
公式要从设计目标反推
在写公式前,先定目标:同等级普通攻击打普通怪需要几下?高等级玩家打低等级怪是否碾压?护甲应该线性收益还是递减收益?暴击是稳定增益还是高波动?元素克制差距多大?如果这些目标不清楚,公式只是数学装饰。建议先做一张目标表:等级 1、10、20、50 的玩家攻击力和怪物血量,期望击杀时间,技能爆发窗口。再选公式拟合目标。
护甲减免常见做法是 reduction = armor / (armor + k)。这种公式有递减收益,适合避免护甲堆到无敌。k 决定曲线陡峭程度。简单减法公式容易出现低伤害变 0 或高攻完全无视护甲,需要额外夹取。没有绝对正确,关键是公式曲线要被团队看见。
flowchart TD
A["AttackContext:攻击者属性、技能、Buff"] --> B["BaseDamage:攻击力 * 技能倍率"]
C["DefenseContext:护甲、抗性、护盾"] --> D["Mitigation:减伤曲线"]
B --> E["ElementModifier:元素克制/抗性"]
D --> E
E --> F["CriticalResolver:暴击和暴伤"]
F --> G["RandomVariance:小幅随机"]
G --> H["DamageResult:最终伤害 + 拆解"]
H --> I["Phaser 表现:飘字、音效、血条"]
H --> J["BalanceLog / 调试面板"]
随机波动要有限制
随机伤害能让战斗不那么机械,但波动太大会破坏策略。普通攻击上下浮动 5% 到 10% 通常足够;暴击已经提供波动,就不需要再给暴击伤害过大随机。关键技能如果用于打断或斩杀,随机波动可能让玩家计划失败。可以让核心控制技能固定伤害,普通输出技能有小波动。
随机数还要可复现。需要回放、排行榜或自动测试时,DamageResolver 应注入随机源,而不是直接 Math.random()。同样输入和随机种子应得到同样结果。平衡测试也依赖这一点。
暴击要考虑体验稳定性
暴击率和暴击伤害是最容易让数值爆炸的组合。50% 暴击率、200% 暴伤看起来是 50% 期望增益,但短时间战斗里波动很大。玩家可能连续不暴击觉得倒霉,也可能连续暴击秒 Boss。可以使用伪随机分布让暴击更均匀,或者通过保底计数降低极端情况。是否需要取决于游戏类型。Roguelike 可以接受高波动,竞技或硬核动作可能需要稳定。
UI 要展示期望,不要只展示暴击率。比如装备比较时显示平均 DPS 变化,同时保留暴击率和暴伤细节。否则玩家很难判断 5% 暴击率和 20 攻击力哪个更好。
一个可拆解的伤害计算器
下面的代码展示如何返回详细拆解。真实项目会加入更多标签和抗性。
interface DamageInput {
attack: number;
skillMultiplier: number;
armor: number;
critRate: number;
critDamage: number;
variance: number;
}
interface DamageBreakdown {
base: number;
afterArmor: number;
critical: boolean;
varianceFactor: number;
final: number;
}
export function resolveDamage(input: DamageInput, random: () => number): DamageBreakdown {
const base = input.attack * input.skillMultiplier;
const armorReduction = input.armor / (input.armor + 120);
const afterArmor = base * (1 - armorReduction);
const critical = random() < input.critRate;
const critApplied = critical ? afterArmor * input.critDamage : afterArmor;
const varianceFactor = 1 + (random() * 2 - 1) * input.variance;
const final = Math.max(1, Math.floor(critApplied * varianceFactor));
return { base, afterArmor, critical, varianceFactor, final };
}
这段代码故意返回中间值。战斗系统可以只用 final 扣血,但调试面板能展示完整过程。策划看到护甲减免太强,可以调整常数 120,而不是猜哪一层出了问题。
平衡工具要贴近游戏
不一定要做复杂后台,简单的本地表格和曲线图就很有价值。输入等级、攻击、护甲、技能倍率,输出平均伤害、暴击分布、击杀时间。再画出护甲减免曲线和等级成长曲线。很多公式问题在图上一眼就能看出:某个等级后伤害突然陡增,护甲收益过早封顶,暴击期望远超其他属性。
Phaser 项目可以内置一个开发面板,在战斗中点选玩家和敌人,显示当前属性和下一次技能预估伤害。也可以把 DamageResolver 导出给 Node 脚本跑批量模拟。关键是公式只实现一份,工具和游戏共用。不要让表格里一套公式,游戏里另一套公式。
护盾、免疫和最小伤害
护盾通常在生命前吸收伤害,但护盾是否享受护甲减免要明确。有些游戏护盾先吃原始伤害,有些护盾也受抗性影响。免疫和格挡也要定义顺序:先判断闪避,再护甲,还是先护甲再格挡?顺序不同,结果差异很大。建议写成清晰管线,并在 DamageResult 中记录每步。
最小伤害也很重要。完全打 0 会让玩家困惑,除非明确显示“免疫”。普通减伤后可以保留至少 1 点,护盾完全吸收则显示护盾数字。所有特殊结果都要有飘字样式:免疫、格挡、吸收、闪避。数字反馈和公式语义要一致。
上线前检查清单
确认伤害计算独立于 Phaser 表现;确认公式从目标击杀时间和成长曲线反推;确认 DamageResult 包含拆解;确认护甲、抗性、暴击、随机波动顺序固定;确认随机源可注入;确认工具和游戏共用同一公式;确认 UI 能解释免疫、格挡、护盾和暴击;确认极端属性有上下限;确认数值改动有版本记录;确认战斗日志能还原一次伤害。
伤害公式的价值不在于数学多漂亮,而在于它能稳定支撑玩家决策。Phaser 能让伤害表现很直观,但数字必须先有逻辑。让每一次伤害都能拆开解释,平衡才有讨论基础。
属性来源和快照时机
伤害计算还需要明确属性快照时机。攻击者按下技能时锁定攻击力,还是命中瞬间读取攻击力?投射物飞行途中玩家换装备,伤害是否变化?持续伤害每跳读取当前 Buff,还是按施加时快照?不同游戏可以选择不同规则,但必须一致。常见做法是即时命中技能在命中时读取,投射物在发射时快照,持续伤害在施加时快照主要倍率但每跳读取目标抗性。
快照规则影响玩家理解和性能。若所有东西都实时读取,Buff 变化会影响已飞出的子弹,可能让玩家觉得奇怪;若全部快照,临时增益结束后持续伤害仍然很高,也需要说明。DamageContext 应保存必要快照字段,战斗日志记录快照来源。这样出现争议时能解释。
等级压制和新手保护
等级差公式很危险。让高等级玩家打低等级怪更爽是合理的,但等级压制过强会让装备和操作失去意义。低等级玩家挑战高等级怪时,完全打不动也会挫败。可以用温和倍率,比如等级差每级影响 2% 到 4%,并设置上下限。新手保护则可以在早期关卡降低怪物暴击或保留最低生存时间,避免玩家被随机暴击秒杀。
这些保护不应藏得太深。新手期的特殊规则要有明确结束点,否则后期平衡会被污染。比如前 3 关玩家受到致命伤时保留 1 点血一次,可以作为教学保险;进入正式关卡后关闭。所有特殊规则都应进入 DamageResult 标记,调试面板显示“newbie_guard_applied”,避免团队误以为公式本身有问题。
自动化平衡回归
每次改公式、成长表或装备属性,都应跑一组模拟。模拟不同等级、不同装备质量、不同敌人类型下的击杀时间和承伤时间,和目标区间比较。若某个等级段突然偏离,就在开发阶段发现。Phaser 游戏虽然运行在浏览器,但 DamageResolver 是纯逻辑,完全可以用 Node 脚本批量测试。
平衡回归不要求取代试玩。它的价值是拦住明显事故,比如某次把护甲常数从 120 改成 12,导致所有怪物近乎无敌;或者暴击伤害从 1.5 写成 15。数值系统越长线,越需要这种低成本保险。
UI 展示和真实计算
角色面板上的攻击力、防御、暴击和 DPS 必须和真实公式同源。很多游戏面板显示“攻击 1000”,实际技能伤害还受隐藏等级压制和元素抗性影响,玩家就会觉得被欺骗。可以区分基础属性和预估输出:面板显示攻击力,技能详情显示对当前目标的预估伤害范围。若目标不同,预估结果也不同。
装备比较也要基于构筑。加 10 攻击对高攻速角色收益大,加 5% 暴击对高暴伤角色收益大。简单绿箭头有时会误导。至少在开发和高级详情中展示期望伤害变化,普通玩家界面保持简洁。数值系统越复杂,越需要不同层级的信息展示。
伤害日志的采样策略
完整记录每次伤害会产生大量日志。开发模式可以全量记录,正式模式只采样关键事件:玩家死亡前若干秒、Boss 战、异常高伤害、异常低伤害、排行榜成绩。日志记录 DamageResult 拆解、配置版本和随机种子片段。出现玩家投诉时,这些日志能快速判断是公式问题、配置问题还是显示问题。
如果没有服务端日志,客户端也可以保留最近 N 次战斗的本地环形日志,玩家反馈时导出。Phaser Web 游戏调试环境复杂,能拿到一段真实设备日志非常有价值。
平衡变更要能灰度
数值改动不一定要一次推给所有玩家。可以按配置版本灰度,让少量玩家先体验新的护甲曲线或技能倍率。客户端显示当前 balanceVersion,战斗日志也记录它。若新版本造成异常,可以快速回滚。对于离线或单机内容,灰度意义较小,但版本记录仍然重要,因为玩家存档中的装备和怪物可能来自不同配置时期。
改动公式时也要考虑补偿。某件装备因为公式调整变弱,是否退还材料?某个技能倍率下降,是否重置技能点?这些不是 Phaser 技术问题,但客户端要准备展示补偿邮件、重置按钮或版本说明。数值系统越透明,玩家越能接受必要调整。
版本说明不要只写“优化平衡”,应列出受影响属性和大致方向,减少误解。
同时保留旧版本测试入口,方便设计师对比改动前后的实战差异。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。