战斗内部机制深入解析
本指南面向可以访问完整 MCE C# 源码的 Source 和 Studio 版用户。
十五个模块,一个协调者。BattleManager 自己不包含战斗逻辑 —— 它负责询问,
而状态机决定接下来询问什么。
本文档详细介绍 BattleManager 的架构、它的 15 个模块、BattleStateMachine、战斗所用的公式,以及如何添加新的战斗功能。公共成员请参见 BattleManager API。
BattleManager 架构
BattleManager 遵循模块化组合模式。它不是一个有数千行的庞大类,而是将战斗逻辑拆分到专注的模块中,这些模块位于同一个 GameObject 上,并通过 BattleManager 这个枢纽通信。管理器用 [RequireComponent] 声明它们,因此给 GameObject 添加 BattleManager 就会添加整套模块:
[RequireComponent(typeof(BattleStateMachine))]
[RequireComponent(typeof(BattleManagerBattlersModule))]
[RequireComponent(typeof(BattleManagerBattlerSwitchModule))]
[RequireComponent(typeof(BattleManagerBattlerStatsModule))]
[RequireComponent(typeof(BattleManagerHealthModule))]
[RequireComponent(typeof(BattleManagerMovesModule))]
[RequireComponent(typeof(BattleManagerItemsModule))]
[RequireComponent(typeof(BattleManagerCaptureModule))]
[RequireComponent(typeof(BattleManagerRostersModule))]
[RequireComponent(typeof(BattleManagerAIModule))]
[RequireComponent(typeof(BattleManagerScenariosModule))]
[RequireComponent(typeof(BattleManagerStatusesModule))]
[RequireComponent(typeof(BattleManagerCharactersModule))]
[RequireComponent(typeof(BattleManagerAnimationModule))]
[RequireComponent(typeof(BattleManagerAudioModule))]
[RequireComponent(typeof(BattleManagerMegaModule))]
public class BattleManager : OpenMonBehaviour<BattleManager>, IBattleManager, IPlayerDataReceiver
{
// Module accessors, resolved from sibling components:
internal BattleManagerBattlersModule Battlers => GetCachedComponent<BattleManagerBattlersModule>();
internal BattleManagerHealthModule BattlerHealth => GetCachedComponent<BattleManagerHealthModule>();
internal BattleManagerMovesModule Moves => GetCachedComponent<BattleManagerMovesModule>();
// ...BattleManagerBattlerSwitch, BattlerStats, Items, Capture, Rosters, AI,
// Scenario, Statuses, Characters, Animation, Audio and Megas.
}
BattleManagerModule 基类
所有模块都继承自泛型的 BattleManagerModule<T>:
public abstract class BattleManagerModule<T> : OpenMonBehaviour<T>
where T : BattleManagerModule<T>
{
protected BattleManager BattleManager => GetCachedComponent<BattleManager>();
// The same accessors the manager has, so modules can call into each other:
protected BattleManagerBattlersModule Battlers => BattleManager.Battlers;
protected BattleManagerHealthModule BattlerHealth => BattleManager.BattlerHealth;
protected BattleManagerMovesModule Moves => BattleManager.Moves;
// ...
}
因此,模块既不需要注册代码,也不需要 Initialize 调用:它是战斗 GameObject 上的一个 MonoBehaviour,通过继承的访问器访问其他模块。GetCachedComponent<T>()(来自 OpenMonBehaviour)可以找到任何同级组件。
模块深入解析
BattleManagerBattlersModule
文件:BattleManagerBattlersModule.cs
把战斗者与其战斗索引和精灵相互解析,处理形态变化和逃跑。Battler(位于 Battler.cs)是在 MonsterInstance 之上加了战斗中状态的类:
public class Battler : MonsterInstance
{
public SerializableDictionary<Stat, short> StatStage;
public SerializableDictionary<BattleStat, short> BattleStatStage; // accuracy, evasion...
public byte CriticalStage;
public SubstituteData Substitute;
public Dictionary<VolatileStatus, int> VolatileStatuses;
public List<Battler> BattlersFought;
public List<Move> MovesUsedSinceEnteringBattle;
public bool EnteredBattleThisTurn;
// ...hooks such as OnCalculateStat, OnStatChange, CanSwitch, GetTypes, GetAbility
}
关键方法:GetBattlerFromBattleIndex(type, index)、GetTypeAndIndexOfBattler(battler)、GetBattlersFighting(type)、IsBattlerFighting、ChangeForm(...)、RunAway(...)。
BattleManagerMovesModule
文件:BattleManagerMovesModule.cs
运行一个招式:PerformMove(type, index, targets, moveIndex)(槽位 0-3,4 表示 Struggle)执行整个效果流程,ForcePerformMove 供调用其他招式的招式使用。伤害不是在这里计算,而是由招式自己(DamageMove.CalculateMoveDamage)通过 DamageMath 的纯辅助方法计算。
伤害流程(DamageMath,第五世代及以后):
// Base: (2 * level / 5 + 2) * power * attack/defense * externalPowerMultiplier / 50 + 2
float baseMultiplier = DamageMath.ComputeBaseMultiplier(level, power, attackDefense, externalPowerMultiplier);
// Every factor is computed by the move and multiplied in one place:
float damage = DamageMath.ComputeDamage(baseMultiplier,
targets, // 0.75 (SpreadMultiplier) when hitting several targets
weather, // from the active Weather
terrain, // from the active Terrain
critical, // 1.5 (CriticalMultiplier) on a critical hit
random, // between 0.85 (MinRandom) and 1
stab, // 1.5 (StabMultiplier) x ability changes
typeEffectiveness, // product over the defender's types
status, // e.g. Burn returns 0.5 for physical moves
additional); // abilities, held items, side statuses, allies
int final = DamageMath.RoundDamage(damage); // rounded, never below 1
additional 系数通过钩子累积:使用者的 OnCalculateMoveDamageWhenUsing、盟友的钩子、目标的 OnCalculateMoveDamageWhenTargeted 以及目标方的 SideStatus 钩子,每个钩子接收当前的乘积并返回新值。
BattleManagerHealthModule
文件:BattleManagerHealthModule.cs
所有 HP 变化都经过这个模块:
ChangeLife(battler, user, amount, ...)-- 对战斗者造成伤害或治疗的标准方式:运行受击动画和音频,触发特性,并报告(appliedAmount, fainted)。CheckFaintedBattlers()-- 检测 HP 归零的战斗者。ProcessFaintedBattlers()-- 播放濒死动画并处理它们(经验值、努力值、替换)。
BattleManagerStatusesModule
文件:BattleManagerStatusesModule.cs
管理四类状态,每一类都是你可以扩展的 ScriptableObject:
| 类别 | 类 | 所在位置 |
|---|---|---|
| 异常状态 | Status(Burn、Poison、BadlyPoison、Paralysis、Freeze、Sleep) | 每个战斗者一个 |
| 易变状态 | VolatileStatus | 某个战斗者 |
| 己方场地状态 | SideStatus | 场地的一侧 |
| 全场状态 | GlobalStatus | 整个场地 |
天气和地形属于战斗的场景设定(BattleManagerScenariosModule),而不是这个模块的一部分。Status 是一组虚拟钩子。例如 Burn 重写了 OnStatusAddedInBattle、OnStatusTickInBattle(回合结束时的伤害)、OnCalculateMoveDamage(物理招式为 0.5)和 PlayAnimation;其他可用的有 CanAddStatus、OnStatusRemovedInBattle、OnAboutToPerformMove、OnCalculateStat、AboutToBeHitByMove 和 AfterHitByMove。
该模块的 API 是 AddStatus(...) 和 RemoveStatus(...)(每个类别一个重载)、HasStatus(...) 以及 TriggerStatusesCountdown(),后者由状态机每回合调用一次。
BattleManagerCaptureModule
文件:BattleManagerCaptureModule.cs,公式位于 BattleUtils.cs
TryCaptureMonster(index, ball) 对野生敌人执行捕捉,包含投球和摇晃动画。概率来自 BattleUtils.CalculateShakeProbability:
// The modified catch rate:
float catchRate = ((3 * maxHP - 2 * currentHP) * rate * ballMultiplier + ballAddition)
/ (3 * maxHP)
* statusBonus // Status.CatchMultiplier
* Mathf.Max((30 - level) * 0.1f, 1); // low-level bonus
// One shake succeeds with this probability (x the game's catch-difficulty multiplier):
float shake = Mathf.Floor(65536 / Mathf.Pow(255 / catchRate, 0.1875f)) / 65535f;
被标记为 NeverFails 的球总是会摇晃成功。在线时,成功摇晃的次数由服务器决定(参见战斗权威)。
BattleUtils.RunChance(allySpeed, enemySpeed, attempts) 是从野生战斗中逃跑的对应公式。
BattleStateMachine
文件:BattleStateMachine.cs
状态机是一个简单的协程循环(BattleLoop),由 BattleManager 通过 StartBattleLoop() 启动。它不是状态枚举:while (!BattleManager.IsBattleOver) 循环的每一次迭代就是一个回合。
while (!BattleManager.IsBattleOver)
{
Battlers.UpdateBattlersFought();
yield return RequestActions(result => actions = result); // player menu + AIs
// Server-authoritative battle: the server resolves the turn and the loop shows the result.
// Otherwise:
yield return DetermineActionOrder(actions, ...); // priority, then speed
yield return PerformActions();
yield return AfterTurnPreStatusCallbacks(BattlerType.Ally);
yield return AfterTurnPreStatusCallbacks(BattlerType.Enemy);
yield return Statuses.TriggerStatusesCountdown();
yield return AfterTurnPostStatusCallbacks(BattlerType.Ally);
yield return AfterTurnPostStatusCallbacks(BattlerType.Enemy);
yield return ResetBattlersTurnFlags(BattlerType.Ally);
yield return ResetBattlersTurnFlags(BattlerType.Enemy);
yield return BattlerHealth.CheckFaintedBattlers();
yield return BattlerHealth.ProcessFaintedBattlers();
// ...end of the turn
}
yield return Animation.BattleEndAnimation();
yield return Characters.GivePlayerPriceMoney();
yield return Characters.TakePlayerDefeatMoney();
yield return BattleEndCallbacks();
(该清单经过了删节;要查看包含服务器权威分支的完整循环,请阅读该文件。)
添加新的战斗模块
要向战斗系统添加模块:
步骤 1:创建模块类
using System.Collections;
using OpenMon.MCE.Battle;
using UnityEngine;
public class BattleTurnLogModule : BattleManagerModule<BattleTurnLogModule>
{
private int turnsLogged;
public IEnumerator LogLeads()
{
Battler ally = Battlers.GetBattlerFromBattleIndex(BattlerType.Ally, 0);
Battler enemy = Battlers.GetBattlerFromBattleIndex(BattlerType.Enemy, 0);
turnsLogged++;
Debug.Log($"Turn {turnsLogged}: {ally.Species.name} vs {enemy.Species.name}");
yield break;
}
}
步骤 2:把它放在 BattleManager 旁边
把组件添加到战斗场景中 BattleManager 的 GameObject 上。(如果你希望它随每个 BattleManager 一起出现,还要在管理器上添加 [RequireComponent(typeof(BattleTurnLogModule))],并在其他访问器旁边添加一个带类型的访问器。)
步骤 3:调用它
从状态机、另一个模块或某个钩子 中调用:
yield return BattleManager.GetCachedComponent<BattleTurnLogModule>().LogLeads();
不使用事件接入战斗
战斗不会在模块之间发布 C# 事件。扩展点有:
| 你想要…… | 扩展 |
|---|---|
| 在游戏中对战斗的开始和结束做出反应 | IBattleSystem(OnBattleStarted、OnBattleEnded)或 BattleManager.SubscribeToBattleInitialized / SubscribeToBattleFinished |
| 改变训练师的行为 | 一个 BattleAI ScriptableObject(战斗 AI) |
| 改变伤害、能力值或回合效果 | 一个 Status、Weather、Terrain、Ability、Item 或 Move ScriptableObject:它们都公开了循环所调用的 OnCalculate...、OnStatusTick...、AfterHitByMove 风格的钩子 |
| 为回合添加一个全新的阶段 | 一个由 BattleStateMachine 调用的新模块 |
测试战斗逻辑
战斗的纯逻辑部分是简单的静态函数,因此无需场景就能测试。仓库的编辑模式测试套件(位于 Assets/OpenMon/Core/Tests/Editor/ 的 MCE.Editor.Tests)就在 DamageFormulaTests.cs 等文件中这样做:
[Test]
public void ComputeDamage_MultipliesEveryFactor()
{
// Base 100, hitting one target, no weather or terrain, no crit, max roll,
// STAB (x1.5) and super effective (x2), no status, nothing extra.
float damage = DamageMath.ComputeDamage(100f, 1f, 1f, 1f, 1f, 1f,
DamageMath.StabMultiplier, 2f, 1f, 1f);
Assert.AreEqual(300f, damage, 0.001f);
}
[Test]
public void RoundDamage_NeverBelowOne() => Assert.AreEqual(1, DamageMath.RoundDamage(0.2f));
最佳实践
- 保持模块专注。每个模块应只处理一项职责。
- 通过基类访问器访问同级模块,而不是通过场景查找。
- 把规则放在 ScriptableObject 中(状态、特性、道具、招式),而不是放在循环里:它们可测试且可复用。
- 用已知输入和预期输出测试伤害与捕捉的计算,通过纯辅助方法进行。
- 不要直接修改
Battler的状态 —— 通过负责该事务的模块(HP 用ChangeLife,状态用状态模块)。 - 使用状态机来安排回合顺序。不要添加临时的协程。