順番が前後しましたが、レトロRPGの戦闘シーンを設計してみたいと思います。
戦闘方式をレトロRPGに限って大別するとドラクエ方式とFF方式がメジャーです。今回はドラクエ方式について検討してみます。
分析
まず戦闘シーンの概要を決めて行きます。
- 戦闘はコマンド入力方式のターン制
- 画面表示は敵のビジュアルとメッセージ表示領域、自身のステータス、それからコマンド入力領域を用意する。
- 敵と見方はそれぞれ複数で順番にコマンドを実行する
とりあえず、これぐらいにしておきます。ここから抽出された概念は、「戦闘シーン」、「コマンド」、「敵のビジュアル」、「メッセージ表示領域」、「敵」、「見方」、「ステータス」等。ここから必要そうなオブジェクトを抽出します。
全体を統括するオブジェクトを用意すると後が楽なので戦闘シーン(Battle)をコントローラーとして定義してます。後、自分のこのみでBattleEngineと名づけます。
それからざっくりと画面表示部分(View)と戦闘のコアな処理部分(Model)に分けておきます。
パーティ
敵や見方は複数(Party)という前提ですが、このParty単位扱いたい場合がそこそこあります(Partyが全滅とかParty全体への作用とか)。
なのでPartyオブジェクトも抽出します。パーティではPlayerやEnemyを集約します。
戦闘準備
戦闘はフィールドなどでのエンカウントもしくは、イベントなどから発動します。戦闘が始まる前に画面の切り替え等の準備(setup)が必要です。戦闘の準備はBattleEngineに実装します。
[<<Control>>;BattleEngine||setup()]
戦闘の準備(setup)では対戦する敵見方の情報が引き渡されます。逆に言えばBattleEngineはプレイヤーの対戦相手の決定は行いません。
そして戦闘は参加者(cast)全員を素早さなどのパラメータを元に並び替えた状態で、順番にそれぞれの参加者(cast)が選択したコマンドを実行していく事にします。
そのため、EnemyとPlayerを抽象化してCreatureを用意し、BattleEngineはその一覧を保持します。
ターン
各ターンではCreature達が順番に戦術(Command)を決定します。この戦術には攻撃、魔法、道具、装備、防御、逃走などがあります。
装備は装備を使うのか装備をかえるのかですが、装備の交換に1ターン消費するかしないかはゲームデザインの範疇です。今回は消費しないこととし、さらに装備の使用は道具に集約します(つまり装備コマンドは装備の変更を指し、かつターンを消費しないためこの後の設計では特に触れません)。
[<<Control>>;BattleEngine||setup();turn()]
コマンドは各Creatureに問い合わせる事にします。少し細かい話をするとAIの場合は、問い合わせのあった時点でCommandを決定し、手動の場合はターンが開始される前にUIから入力済みであるとします。
よって戦闘は戦術(Command)の入力待ち状態とターン中の2つの状態を遷移することになります。戦術状態からターンへの移行条件は全ての手動入力Creatureの戦術が決定したタイミングです。
これはBattleEngineに戦術の登録メソッドを追加してその中で確認することにします。
[<<Control>>;BattleEngine||setup();turn();setCommand()]
BattleEngine.prototype.setCommand = function(name, command) { //creatureを特定 var target; for(var i = 0 ; i < this.cast.length ; i++ ) { } target.setCommand(command); this.allReady() && this.turn(); }
[BattleEngine]call-.-current>[Creature||getCommand()]
[Creature||getCommand()]- current 1[Command]
戦術(Command)
Commandについて掘り下げてみます。Commandは『by』を使って『to』に対して『what』をする。例えば、『武器』を使って『敵-1』に対して『攻撃』する、とします。
[Command]-to[Target]
[Command]-by[Item]
[Command]-what[Atack]
ただ、toはパーティや単体、byは武器だったり魔法だったりするので、それぞれ共通のインターフェースを出すべきでしょうか・・・
ここは好みとして、Whatの部分をStrategyとしてCommandからは隠蔽します。
ところで、ターン中に自分の番になっても行動不能(死亡や状態変化により)であれば、順番を飛ばされます。死亡と行動不能は分けて判定します。
[Creature||isDead();canAction()]
コマンドはCastから取り出されて発動(trigger)されます。
[Command||trigger()]
シーケンス図の代わりにコードで書くとこんな感じです。
BattleEngine.prototype.turn = function() { for(var i = 0 ; i < this.cast.length ; i++ ) { if(this.cast[i].isDead()) { continue; } if(this.cast[i].canAction()) { continue; } this.cast[i].getCommand().trigger(); } }
画面表示
各castのcommand.trigger()
を実行した結果を画面へ反映させる必要があります。
厳密には発動前の画面効果もありますが、それは見せ方の問題ですので画面効果担当のオブジェクトを用意しCommandを引き渡すこと、Commandに応じた表示をお願いすることにします。
[<<View>>;Visual||trigger()]uses-.->[Command]
Visualオブジェクトでは、以下の処理を行います。
- Commandに応じた画面効果を表示
- MessageBoxにCommandの内容を表示(例えば『fooがbarに攻撃した』)
- MessageBoxにCommandの結果を表示(例えば『fooはbarに○○のダメージを与えた』)
- Playerの状態表示を更新
Visualオブジェクトを抽出したので、モデルも更新しておきます。
Visual.prototype.trigger = function(command) { //戦術に応じた画面効果を実施 this.action(command); this.messageBox.push(command.getOpeningText()); this.messageBox.push(command.getResultText()); //ひもづけられたCreatureのステータス表示を更新 this.levelBox.update(); }
また、画面効果をそれぞれをひとつのオブジェクトとしてCommandの内容からVisulaオブジェクトが選択、実行します。
[Visual]-n>[Effect]
戦闘の開始と終了
先にも書きましたがターン戦闘の場合、戦術の決定→ターン→戦術の決定と二つの状態を繰り返します。最初は、「戦術の決定」から始まり、ターンへの移行はUIから発動されます。
戦闘は敵か見方が全滅するか逃亡するかで戦う相手がいなくなれば終了です。これらをBattleEngineに追加します。
[<<Control>>;BattleEngine||setup();turn();isEnd();end()]
BattleEngine.prototype.turn = function() { for(var i = 0 ; i < this.cast.length ; i++ ) { if(this.cast[i].isDead()) { continue; } if(this.cast[i].canAction()) { continue; } var cmd = this.cast[i].getCommand(); cmd.trigger(); this.visual.trigger(cmd); if(this.isEnd()) { this.end(); } } }
JavaScriptへのチューニング
JavaScriptでなければこれでも良いのですが前の記事でも書いたようにシングルスレッドでかつn秒待機などが出来ません。このままでは画面効果中が終わるのを待ったり、CastとCastのCommandの実行にウェイトを挿入したりできません。
そこでJavaScriptを意識した設計へ改修してひとまず設計編を終わらせます。
微調整は実装時に適宜行います。
まず、1/60秒等で定期的にupdate()を呼び出しならが、状態遷移を自身で管理します。
ターン状態へ移行した場合、都度castのupdate()を実行し、castが自分が行動可能な時間にならなければ行動しません(cast.getCommand()でCommandを返さない)。
コマンドが取得できたらコマンド実行モードへ移行します。
そしてコマンドを実行し結果をVisualへ渡します。この時点で画面効果表示モードへ移行します。
画面効果表示が終わるとターンモードへ復帰し、各castの時間を進めます。
整理すると下記の様になります。
Mode = { 'Turn' : 0, 'Command' : 1, 'Inquery' : 2, 'Visual' : 3 } BattleEngine.prototype.function turn(pos) { if (this.mode != Mode.Turn ) { return; } for (var i = pos ; i < this.cast.length ; i++ ) { if(this.cast[i].isDead()) { continue; } if (this.cast[i].canAction()) { continue; } this.cast[i].update(); var cmd = this.cast[i].getCommand(); if (cmd) { //コマンドモード cmd.trigger(); this.visual.trigger(cmd, function () { if (this.isEnd()) { this.end(); } engine.turn(i+1); //画面効果が終わったらターンモードへ復帰 }); break; } } }
次回は実装編