Retro RPGの魔法の実装について検討します。
魔法を実装する上で面倒に感じるのが、個々の魔法の作用がバラエティーに富んでいる(ように感じる)点です。これを素直に条件分岐やハードコーディングでゴリゴリと実装はしたくないものです。
魔法をモデル化すると、対象(物(人や装備)や場(空間や時間))に対して永続的もしくは一時的に状態を変化させる事、と定義できます(その他に画面効果も発生しますが、後述とします)。
永続的な状態変化とは例えばHPの回復や逆に攻撃によるHPの減少などです。物や場への永続的な変化は扉の解錠、特定場所への移動などです。
一時的な状態変化とは「毒」や「石化」など状態を悪化させるもの、「祝福」など状態を向上させる物、後は一時的な状態変化を解除する物ぐらいでしょうか。
とりあえずここで抽出されたオブジェクトは以下の通りです。
魔法(Magic),対象(Target),状態(State),空間(World)、お約束として全体をコントロールする場として[Engine]も予め用意しておきます。
モデルの詳細化を順番に考えて行きたいと思います。
魔法と対象
Magicを行使するTarget(from)とかけられるTarget(to)がいます。fromもtoも自分なら自分自身に魔法をかけることになります。
後で考えることですが、魔法を発動出来る対象の制約(攻撃系は見方にはだめだとか)はプログラムで実装することになります(特定の状態の時は良いとかを考えると複雑になりそうですが、それも後回し)。
対象と状態
対象は永続的な状態と一時的な状態を持ちます。
また、対象には人、敵、道具および空間などがありますが、道具については割愛します。
人(プレイヤー)と敵は、抽象化してCreatureとしましょうか。
魔法
魔法の責務として、魔法を行使する「発動」と魔法の効果が継続している場合の「更新」がまず思いつきます。
まあ「更新」は対象に付与した一時的な状態に任せるので不要かもしれません。
[Magic||chant();update()]
また、この魔法は典型的なStrategyパターンでしょうか。特に複雑な処理が無ければ、
変化する状態を持ったテーブルだけで済むかもしれませんが、その判断は実装まで持ち越します。
状態
状態はHPやMP等のパラメータを持ちます。また一時的状態では状態の「発動」と「解除判定」、定期的な「更新」処理があります。
また、状態ごとに行動可能かの判定処理も必要そうです。
「発動」については状態を付与した時が発動のタイミングですが、解除条件はきっと複雑ですね(時限、戦闘終了、死亡・・・)。
[State|hp;mp;str;dex|enter();update();canAction()]
また、状態も魔法と同様にStrategyパターンか、Stateパターンを適用します(ここでは、Stateパターンとしておきますが大差ありません)。
場
概念モデルはだいたい出来たのでモデルを実装レベルへ詳細化していきます。
ここで必要なのが実際に魔法を呼び出したり、場を管理するためのコントローラーです。これは、既に抽出済みのEngineに担当させます。
ここでは戦闘シーンを想定します(ですのでEngineは戦闘用Engineです)。そこには自分のパーティーと敵のパーティがいます。
そして各ターンで戦術として攻撃や魔法が選択されますが、今は魔法の実装についての検討なので魔法が選択された後からの事を考えます。
ここで,PartyもTargetインターフェースを持つ必要が出てきました(魔法をかける対象として選択される可能性があります)。
[Target]^-.-[Party]
Engineは戦闘の参加者全員の一覧を保持しておきます。
永続的な魔法の呼び出し
例えば回復です。回復ストラテジーが永続状態のhpを増加させます。from側の能力に依存する場合は、fromの永続状態を取得します。
一時的な状態の管理
ターゲットが持つ複数の一時的状態の保持のためにChainオブジェクトを用意します。実装言語はJavaScriptなので配列から継承します。
また戦闘時に行動可能かの判定など、一次状態の総意を判断する必要があるのでor
メソッドとand
メソッドを
用意します(実用上、logicalAndを追加します、これは要素内を順番に走査してゆき判定がfalseになった時点で残りの要素を処理しません)。
一時的な状態を変化させる魔法には成功率があるものです。ですのでMagicのStrategyに判定メソッドを追加します。
例えば「毒」魔法呼び出しの流れですが、Magic.chant()
が呼び出されると成功判定の後、PoisonStorategy
によって状態Poison
が生成され、to
のChain
へ追加されます。
JavaScriptで書くとこんな感じ
{ if(magic.chant(from, to)) { // if(to.isDead()) { ... } } } Magic.prototype.chant = function(from, to) { if(!this._strategy.judge(from, to)) { return false; } this._strategy.chant(from, to); return true; } PosionStrategy.prototype.chant = function(from, to) { var poison = new PoisonState(to); to.getChain().push(poison); } PoisonState = function(target) { this._target = target; this.update(); } PoisonState.prototype.update = function() { var p = this._target.getParmernent(); p.hp = p.hp - this.hp <= 0 ? 0 : p.hp - this.hp; }
画面表示
最後に画面表示です。まず、画面表示をメッセージとビジュアル効果に分けます。メッセージの生成については、魔法オブジェクトに担当してもらいます。
ビジュアル効果については、画面オブジェクトに丸投げ(委譲)することにして終了します。とりあえずメッセージ表示部分だけ追記しておきます
{ if(magic.chant(from, to)) { // if(to.isDead()) { ... } } messageBox.push(magic.getText()); } Magic.prototype.chant = function(from, to) { if(!this._strategy.judge(from, to)) { this._text = this._strategy.getText(from, to, false); return false; } this._strategy.chant(from, to); this._text = this._strategy.getText(from, to, true); return true; } Magic.prototype.getText = function() { return this._text; } PosionStrategy.prototype.getText = function(from, to, success) { return success ? to.name + "は毒に侵されました。" : "効かなかった"; }
続きはそのうちに・・・