メインコンテンツまでスキップ

バトルの内部詳解

このガイドは、MCE の C# ソースコード全体にアクセスできる Source / Studio エディションのユーザー向けです。

バトルモジュール

15 個のモジュールと 1 つのオーケストレーター。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、Struggle は 4)が効果のパイプライン全体を実行し、ForcePerformMove は他の技を呼び出す技に使われます。ダメージはここではなく、技自身(DamageMove.CalculateMoveDamage)が、DamageMath の純粋なヘルパーを通じて計算します。

ダメージのパイプライン(DamageMath、第 5 世代以降):

// 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 が 0 になったバトラーを検出します。
  • ProcessFaintedBattlers() -- ひんしのアニメーションを再生して解決します(経験値、努力値、交代)。

BattleManagerStatusesModule​

ファイル:BattleManagerStatusesModule.cs

4 つの状態のファミリーを管理します。それぞれ拡張可能な ScriptableObject です。

ファミリークラス存在する場所
状態異常Status(Burn、Poison、BadlyPoison、Paralysis、Freeze、Sleep)バトラーごとに 1 つ
揮発性VolatileStatusバトラー
サイドSideStatusフィールドの片側
グローバルGlobalStatusフィールド全体

天候と地形は、このモジュールではなくバトルのシナリオ(BattleManagerScenariosModule)の一部です。Status は仮想フックの集合です。たとえば Burn は、OnStatusAddedInBattle、OnStatusTickInBattle(ターン終了時のダメージ)、OnCalculateMoveDamage(物理技に対して 0.5)、PlayAnimation をオーバーライドします。ほかに利用できるのは CanAddStatus、OnStatusRemovedInBattle、OnAboutToPerformMove、OnCalculateStat、AboutToBeHitByMove、AfterHitByMove です。

モジュールの API は、AddStatus(...) と RemoveStatus(...)(ファミリーごとに 1 つのオーバーロード)、HasStatus(...)、そしてステートマシンがターンごとに 1 回呼び出す 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() で開始します。状態の enum ではありません。while (!BattleManager.IsBattleOver) ループの 1 回の周回が 1 ターンです。

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));

ベストプラクティス​

  1. モジュールは焦点を絞る。各モジュールは 1 つの責務を扱うべきです。
  2. 同じ階層のモジュールには、シーン検索ではなく基底クラスのアクセサーでアクセスする。
  3. ルールはループではなく ScriptableObject に置く(状態、特性、アイテム、技)。テスト可能で再利用できます。
  4. ダメージと捕獲の計算を、既知の入力と期待される出力で、純粋なヘルパーを通じてテストする。
  5. Battler の状態を直接変更しない -- その関心事を所有するモジュールを通します(HP は ChangeLife、状態は状態モジュール)。
  6. ターンの順序付けにはステートマシンを使う。場当たり的なコルーチンを追加しないでください。