バトル周りを調べてみた

わりと、ややこしいです

はじめに

 RPGツクールMVを購入以来、延々マップ関連の部分を調べて修正してきたわけですが、ついに戦闘シーンに突入です。
 戦闘はRPGの華ですね。
 今回は戦闘シーンの流れを把握したいと思います。

追記: 星一さんのRPG ツクール MV の戦闘の実装 - Qiitaという記事がなかなか詳しく説明してあって、参考になります。
 例によって、一通り調べた後に参考資料に気づくパティーン。

追記: フトコロさんのRPGツクールMVの戦闘システムの解析という記事。
…前読んだ気がしますが、メモがてら追記しておきます。

概要

 ざっくり SceneManager を基点に Scene_Battle が呼び出され、Scene_Battle から BattleManager を呼んでるという仕組みです。
 RPGツクールMVはとにかく SceneManager から辿っていけば良い! とこれまでの経験でわかっております。

 そして、Scene_Battle がウィンドウや敵味方の画像を持ってる。
 というのが概要です。

戦闘の流れをみる

 地味にコードを追って戦闘の流れを確認してみます。
 まず基本的なこととして、BattleManager はフェーズ(_phase )という現在の行動状態を記録している変数を持っていて、処理の分岐などしています。
 この状態を軸に見ていくと理解しやすそうです。

nullフェーズ

 まず、戦闘シーンではない状態から戦闘シーンへの移行から。
 ここで処理がBattleManagerSceneManager の二手に分かれます。

  • Game_Player.executeEncounter()Game_Interpreter.command301()([戦闘の処理]イベントコマンド) から BattleManager.setup() が呼ばれる('init'フェーズへ)
  • 同時に SceneManager.push( Scene_Battle ) を実行しシーンの遷移を要求
  • SceneManager が戦闘開始までの画面切り替えを行う
  • 切り替えが終わったところで、Scene_Battle.start() が呼ばれる('start'フェーズへ)

'init'フェーズ

 初期化のフェーズで、画面切り替えの頭で行われます。
 戦闘に入ったらもうこのフェーズは通過しません。

  • BattleManager.setup() から BattleManager.initMembers() が呼ばれる
  • フェーズを 'init' に変更
  • 戦闘に必要な変数の初期化など行われる
  • 準備が整ったらSceneManager から Scene_Battle.start() が呼ばれる

'start'フェーズ

 戦闘ターン開始の処理。
 'start' フェーズは戦闘突入エフェクトなどで時間がかかっているので、即切り替わっている 'init' フェーズの後に実行されます。
 BattleManager.update() から呼ばれるタイプのフェーズですが、戦闘に入ったらもう通過しません。

  • Scene_Battle.start() から BattleManager.startBattle() が呼ばれる
  • フェーズを 'start' に変更
  • Scene_Battle.update() から BattleManager.update() が毎フレーム呼ばれるようになる
  • BattleManager.isBusy() の場合は何もしない(戦闘開始メッセージがスキップされるのを待つ状態)
  • BattleManager.update() でフェーズが 'start' であることを確認して、BattleManager.startInput()が呼ばれる
  • $gameParty.makeActions()$gameTroop.makeActions() を呼んで、アクションを定義
  • [不意打ち]なら、BattleManager.startTurn() を呼ぶ('turn'フェーズへ)
  • フェーズを 'input' に変更('input'フェーズへ)

 多少実際の動作は異なったりしていますが、だいたいこんな感じ

'input'フェーズ

 Scene_Battle.update() から呼んだ BattleManager.update() から帰ってきて、フェーズが 'input' に変わってたらパーティかアクターのコマンド選択を開始します。

  • Scene_Battle.actor()true
    • Scene_Battle.startActorCommandSelection() でアクターコマンドウィンドウを開く
  • Scene_Battle.actor()false
    • Scene_Battle.startPartyCommandSelection()でパーティコマンドウィンドウを開く
  • コマンドウィンドウがアクティブになるので、BattleManager.update() が呼ばれなくなる
  • ウィンドウは常に、updateChildren() によって update() が送られている
  • アクティブになったことを確認したウィンドウは入力に対する処理(コマンド選択)を行う
  • コマンド選択結果が BattleManager.inputtingAction() に設定される
  • BattleManager.selectNextCommand() で全アクターの行動入力が完了した場合にBattleManager.startTurn() が呼ばれる('turn'フェーズへ)
  • ウィンドウが非アクティブになるので、Scene_Battle.update() から BattleManager.update() が毎フレーム呼ばれるようになる

 この入力周りはもっとややこしいことやってるんですが、戦闘の仕組みとは直接関係ないので割愛。
 ちなみに、入力に対する処理(ハンドラ)は Scene_Battle の中にあります。

'turn'フェーズ

 入力の結果 BattleManager.selectNextCommand() が呼ばれ、全アクターの行動入力が完了した場合、あるいは前述の[不意打ち]の場合で、'turn'フェーズになります。

  • BattleManager.startTurn() が呼ばれる
  • フェーズを 'turn' に変更
  • BattleManager.makeActionOrders() で戦闘の順番を定義
  • BattleManager.update() から BattleManager.updateTurn() が呼ばれる
  • 次の行動対象(BattleManager._subject)を設定して BattleManager.processTurn() を呼ぶ
  • 行動対象がアクション可能なら BattleManager.startAction() が呼ばれる('action'フェーズへ)
  • 行動対象がなくなったらBattleManager.endTurn() を呼ぶ('turnEnd'フェーズへ)

'action'フェーズ

 戦闘のアクションを処理するフェーズです。
 このフェーズと'turn'フェーズを行動可能なバトラーの分だけ繰り返していきます。

  • BattleManager.startTurn() が呼ばれる
  • フェーズを 'action' に変更
  • BattleManager.update() から BattleManager.updateAction() が呼ばれる
  • BattleManager.invokeAction()Game_Action に従って行動を処理
  • 行動が終わったらBattleManager.endAction() が呼ばれる('turn'フェーズへ)

 Game_Action の内容と処理については、結構なボリュームがあるので、今回は割愛。
 戦闘シーンで一番大事な部分を飛ばしてしまうのもナンですが。

'turnEnd'フェーズ

 ターンを終了して'input'フェーズに戻す処理です。

  • BattleManager.endTurn() が呼ばれる
  • フェーズを 'turnEnd' に変更
  • BattleManager.update() から BattleManager.updateTurnEnd() が呼ばれる
  • BattleManager.startInput() が呼ばれる('input'フェーズへ)

'battleEnd'フェーズ

 勝利、中断、敗北のいずれかの状態になった時に BattleManager.endBattle() が呼ばれます。
 そして戦闘終了の処理が行われます。

  • BattleManager.endBattle() が呼ばれる
  • フェーズを 'battleEnd' に変更
  • BattleManager.update() から BattleManager.updateBattleEnd() が呼ばれる
  • 戦闘の終了状況に応じてシーンを呼ぶ
  • フェーズを null に変更

 とまぁ、こんな感じで戦闘の処理が行われています。
 SceneManagerScene_BattleBattleManagerupdate() メソッドが呼ばれて進行することと、BattleManager._phase で進行状況が制御されることが分かれば、どうにか追っていけます。
 後、コマンド選択周りが SceneManagerScene_BattleScene_Battle.childlen()update() メソッド呼び出しになっているのは他のシーンと同じなので、特に戦闘シーンに特有の動きではない、ということを理解すれば。
 後それから…って結構、色々わかってないとコード追えませんね(笑)

Scene_Battle

 Scene_Battle.update()Scene_Battle.updateBattleProcess() を呼んで、Scene_Battle.isAnyInputWindowActive() をチェックしてから BattleManager.update() 送るようになってます。
 アクティブなコマンドウィンドウがあって何か入力する時は、戦闘の進行を停止するということですね。
 アクティブタイムバトルを実装したい場合は、この辺を変更する必要がありそうです。

 ウィンドウとスプライトはシーンに addChild() されているので、 updateChildren() によって update() が送られてます。
 コマンドウィンドウについては、Windowオブジェクト側でアクティブな状態をチェックして、操作の実行してます。

Windowについて

 Scene_Battle のプロパティはSpriteset_Battleを除くと、以下の表の通り、ウィンドウで構成されています。

プロパティ名 クラス 説明
_statusWindow Window_BattleStatus [ステータス]ウィンドウ
_partyCommandWindow Window_PartyCommand [パーティ]コマンドウィンドウ
_actorCommandWindow Window_ActorCommand [アクター]コマンドウィンドウ
_skillWindow Window_BattleSkill [スキル]ウィンドウ
_itemWindow Window_BattleItem [アイテム]ウィンドウ
_actorWindow Window_BattleActor [アクター]選択ウィンドウ
_enemyWindow Window_BattleEnemy [敵キャラ]選択ウィンドウ
_logWindow Window_BattleLog ログウィンドウ
_helpWindow Window_Help ヘルプウィンドウ
_messageWindow Window_Message メッセージウィンドウ
_scrollTextWindow Window_ScrollText スクロールテキストウィンドウ

 コマンドが選択された後の処理はだいたい、commandXxx の形でScene_Battleが持っています。

BattleManager

 Scene_Battle から呼び出されて、戦闘のあれこれを管理します。

 Manager関連は全部そうですが、大域変数的に戦闘全体に関わる変数を保持していると考えると良いと思います。
 そして、その変数に関わる処理も一緒に持ってる、という感じですね。

まとめ

 ちょっと長くなったので、今回はここまでにしておきます。

 そこで結論。

流石はRPGの華! ややこい!