Skip to content

一、战斗系统构成

  • 通常游戏的战斗框架分为这么几个部分,战斗实体(角色怪物等),能力或者技能系统,buff系统,事件系统
  • 战斗实体是战斗系统的主体,战斗实体上通常会挂载技能组件(能力组件),一些基本的数据血量之类的基础数据组件,buff列表,角色动画组件等。战斗实体可以接收技能事件,并交付给对应组件去执行。
  • 能力或者技能组件,代表角色持有的一种能力,比如受到伤害,发出火球攻击之类的。

二、战棋游戏的战斗公式设计

最近在做一个火焰纹章式的 SRPG(《归义军传说》),核心战斗公式参考 GBA《烈火之剑》(FE7),做了一比一的公式还原。这里把几个关键机制和实现记录下来。

战棋游戏和 ARPG 的战斗不同——每次出手的结果对战术决策影响极大,公式必须在前端算得明明白白(而不是依赖服务器算完下发)。玩家的容错率很低,所以公式设计有两个核心原则:

  1. 概率不要"骗"玩家:显示 90% 命中,就应该是直觉上"基本能命中"的感觉,而不是真的有 10% 概率 Miss
  2. 确定性预览 > 随机结算:玩家在确认攻击前应该能看到准确的伤害/命中/必杀数值(不引入随机数),帮助决策

1. 2RN 真命中机制(True Hit)

GBA 火焰纹章最经典的设计之一就是 2RN(2 Random Numbers):命中判定不是单次 0~99 随机,而是掷两个随机数取平均值,再和显示命中率比较。

这样做出的实际效果:

显示命中50759095992510
实际命中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 矫正的)。

csharp
// 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

也就是说,就算你速度很高,如果体格不够、拿一把重武器,攻速也会被拖慢。这个设计直接影响装备选择——是拿轻剑稳定追击,还是拿重斧打一刀高伤害?没有绝对最优解。

实际代码:

csharp
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 的公开方法(CalculateHitRateCalculateCritRateCalculateDamage),和实际结算完全同源但不引入随机数。这样做的好处是——玩家看到的面板数据和实际战斗公式一致,不会出现"面板说能秒,打出来没秒"的情况。如果有偏差,只能来自随机数(命中/必杀掷骰),而不是来自公式不一致。

三、经验/升级系统设计

火焰纹章的升级系统是这套游戏最让人上瘾的机制之一——每次升级,每项属性独立根据成长率掷骰,可能全 +1 也可能零成长。这种随机升级带来的"抽卡感"和角色个性化的代价,是一个很巧妙的设计。

但因为首版我的目标是"能玩",暂时没有做完整的成长率系统,只实现了一个简化的经验公式:

伤害经验 = max(1, (31 + 敌Lv − 己Lv) / 3)
击破经验 = 伤害经验 + max(0, (敌Lv − 己Lv)×3 + 20)

效果:Lv1 打 Lv1 → 30 EXP(打三次升一级),Lv10 打 Lv1 → 7 EXP(高端局碾压初级兵基本没经验)。这保证了越级挑战有高回报,而碾压无益。

参考文献