一、战斗系统构成
- 通常游戏的战斗框架分为这么几个部分,战斗实体(角色怪物等),能力或者技能系统,buff系统,事件系统
- 战斗实体是战斗系统的主体,战斗实体上通常会挂载技能组件(能力组件),一些基本的数据血量之类的基础数据组件,buff列表,角色动画组件等。战斗实体可以接收技能事件,并交付给对应组件去执行。
- 能力或者技能组件,代表角色持有的一种能力,比如受到伤害,发出火球攻击之类的。
二、战棋游戏的战斗公式设计
最近在做一个火焰纹章式的 SRPG(《归义军传说》),核心战斗公式参考 GBA《烈火之剑》(FE7),做了一比一的公式还原。这里把几个关键机制和实现记录下来。
战棋游戏和 ARPG 的战斗不同——每次出手的结果对战术决策影响极大,公式必须在前端算得明明白白(而不是依赖服务器算完下发)。玩家的容错率很低,所以公式设计有两个核心原则:
- 概率不要"骗"玩家:显示 90% 命中,就应该是直觉上"基本能命中"的感觉,而不是真的有 10% 概率 Miss
- 确定性预览 > 随机结算:玩家在确认攻击前应该能看到准确的伤害/命中/必杀数值(不引入随机数),帮助决策
1. 2RN 真命中机制(True Hit)
GBA 火焰纹章最经典的设计之一就是 2RN(2 Random Numbers):命中判定不是单次 0~99 随机,而是掷两个随机数取平均值,再和显示命中率比较。
这样做出的实际效果:
| 显示命中 | 50 | 75 | 90 | 95 | 99 | 25 | 10 |
|---|---|---|---|---|---|---|---|
| 实际命中 | 50% | 87.5% | 98% | 99.5% | ~99.99% | 12.5% | 2% |
公式:显示 ≤ 50 时实际 = x²/50,显示 ≥ 50 时实际 = 100 − (100−x)²/50。
设计意图很聪明:高命中率(≥75%)变得"更可靠",低命中率(≤25%)变得更不可靠。玩家对概率的直觉是非线性的——70% 在直觉上"大概率中",但如果是线性概率,70% 就是真的 30% 概率 Miss,这在玩家长线体验中会造成很大的挫败感。2RN 把 70% 拉到了实际的 98%,极大减少了"连续 Miss"的体验。
实际代码中我用了一个小技巧:战斗预览用的是 2RN 换算后的真实命中概率,不引入随机数,但实际结算时仍掷两个骰子。这样玩家的决策信息量足够,但结果仍然有随机性(只是这个随机性是经过 2RN 矫正的)。
// 2RN 真命中换算
public static float TrueHitPercent(int displayedHit)
{
displayedHit = Mathf.Clamp(displayedHit, 0, 100);
if (displayedHit <= 50) return displayedHit * displayedHit / 50f;
int miss = 100 - displayedHit;
return 100f - miss * miss / 50f;
}
// AI 期望伤害(确定性,不做随机)
public static float EstimateExpectedDamage(BattleUnit attacker, BattleUnit defender)
{
int hitRate = CalculateHitRate(attacker, defender);
int critRate = CalculateCritRate(attacker, defender);
int normalDamage = CalculateDamage(attacker, defender, false);
float trueHit = TrueHitPercent(hitRate) / 100f;
return trueHit * normalDamage * (1f + 2f * critRate / 100f);
}2RN 对必杀不生效(必杀用 1RN),因为必杀率通常很低(多数情况 0~15%),如果也用 2RN 会让低必杀率的期望影响极低,破坏体系平衡。
2. 攻速与追击(AS 制)
GBA 火焰纹章的追击判定不是简单的"速度比对面快",而是引入了体格(Constitution)和武器重量的概念:
攻速 AS = 速度 − max(0, 武器重量 − 体格)
追击条件:AS(攻方) − AS(守方) ≥ 4也就是说,就算你速度很高,如果体格不够、拿一把重武器,攻速也会被拖慢。这个设计直接影响装备选择——是拿轻剑稳定追击,还是拿重斧打一刀高伤害?没有绝对最优解。
实际代码:
public static int GetAttackSpeed(BattleUnit unit)
{
int weight = unit.equippedWeapon?.weight ?? 0;
int penalty = Mathf.Max(0, weight - unit.currentStats.constitution);
return unit.EffectiveSpeed - penalty;
}回避公式也复用 AS:回避 = AS×2 + 运 + 支援加成 + 地形加成。攻速快的人不仅容易追击,也更容易闪避——这个一致性让速度属性成为战棋游戏中最重要的属性之一。
3. 伤害公式与武器相克
FE7 伤害公式本身很简单(力量 + 武器威力 + 三角加成 − 防御 − 地形),但有几个值得一提的细节:
- 武器相克:剑克斧、斧克枪、枪克剑。克制方命中 +15、伤害 +1。注意命中的 ±15 才是大头——武器相克更多影响的是能不能打中而非打多少。
- 特效系数制:弓对飞行单位不是固定加伤害,而是武器威力 ×3(日版)。这意味着越是高攻击的弓,对飞行的压制越明显。
- 飞行单位不受地形加成:回避和防御的地形加成对飞行都是 0——虽然飞行无视地形移动,但也无法利用地形保护自己。
4. 战斗预览面板
和一般游戏"拼完再告诉你结果"不同,火焰纹章在玩家确认攻击前会弹出一个双列对阵预览面板:
左列(攻击方) | 右列(防御方)
HP 30 → 22 | HP 25 → 8
伤害 8 ×1 | 伤害 17 ×1
命中 85% | 命中 60%
必杀 3% | 必杀 0%这个面板的数据全部来自 CombatCalculator 的公开方法(CalculateHitRate、CalculateCritRate、CalculateDamage),和实际结算完全同源但不引入随机数。这样做的好处是——玩家看到的面板数据和实际战斗公式一致,不会出现"面板说能秒,打出来没秒"的情况。如果有偏差,只能来自随机数(命中/必杀掷骰),而不是来自公式不一致。
三、经验/升级系统设计
火焰纹章的升级系统是这套游戏最让人上瘾的机制之一——每次升级,每项属性独立根据成长率掷骰,可能全 +1 也可能零成长。这种随机升级带来的"抽卡感"和角色个性化的代价,是一个很巧妙的设计。
但因为首版我的目标是"能玩",暂时没有做完整的成长率系统,只实现了一个简化的经验公式:
伤害经验 = max(1, (31 + 敌Lv − 己Lv) / 3)
击破经验 = 伤害经验 + max(0, (敌Lv − 己Lv)×3 + 20)效果:Lv1 打 Lv1 → 30 EXP(打三次升一级),Lv10 打 Lv1 → 7 EXP(高端局碾压初级兵基本没经验)。这保证了越级挑战有高回报,而碾压无益。
参考文献
- Serenes Forest — FE7 Calculations
- arXiv:1808.10750 — Victory Probability in the Fire Emblem Arena
- GitHub — 归义军传说 SRPG(个人项目,本文相关内容来自此仓库)
- 本项目设计文档 [Docs/fire-emblem-mechanics.md]
- 本项目设计文档 [Docs/combat-flow.md]