はじめまして。
ランカースのプログラマーの尾関です。
弊社は「世界樹の迷宮」「ロストヒーローズ」など、ターン制RPGの開発を得意としています。そこで、今回はターン制RPGを作る際に、知っておくと開発が楽になるプログラムの技術を紹介します。
■ターン制ゲームに共通すること
これはターン制RPGに限らないことですが、ターン制のゲームはたいてい、入力した結果が「遅れて」発生します。例えば、一般的なRPGでは、プレイヤー側のキャラクター全員のコマンド入力が完了した後、それに対する結果が再生されます。
1 2 3 4 5 |
■味方側のコマンド 戦士: 敵Aを攻撃 狩人: スキル > 攻撃力アップ 魔法使い: 敵Bに炎の魔法 僧侶: 戦士に回復魔法 |
このようなコマンドを入力した後の結果として、
1 2 3 4 5 6 7 |
> 狩人は攻撃力アップのスキルを発動した 狩人は攻撃力が20上昇した > 敵Aの攻撃 戦士は12のダメージを受けた > 戦士の攻撃 敵Aに35のダメージを与えた > ……(以下省略) |
というような行動結果が再生されることになります。ユーザはこの結果を見て、次のターンどうするかを考えるのが、ターン制RPGの醍醐味となります。
ここで、ゲームにおける狩人の処理の流れを見てみます。
- 狩人の行動開始演出の再生
- 攻撃力アップスキル発動演出の再生
- 攻撃力アップ演出の再生
- 狩人の攻撃力を20上昇する
通常のゲームであれば、「攻撃力がアップした」というメッセージを表示するだけではなく、誰が行動し、そのキャラが何をしているのかをわかりやすく表現するために「演出」を行います。この「演出」は、エフェクトに限らず、キャラクターのモーションやサウンドの再生を含みます。
ただ、それら演出を除いた部分で、ゲームルールに本当に必要な処理は「4. 狩人の攻撃力を20上昇する」だけです。この部分を「ロジック」と呼び、これを「演出」から分離を行うことで効率的に開発を行うことができる、というのが今回の主題となります。
■なぜ「演出」と「ロジック」を分離するべきなのか?
先ほどの狩人の処理を、そのままswitch文で実装すると以下のようなコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// バトル更新 void Battle::Update() { switch(m_Step) { case 0: // 行動開始演出再生 m_Step++; break; case 1: // 演出完了待ち if(/*演出完了*/) { m_Step++; } break; case 2: // 攻撃力アップスキル演出再生 m_Step++; break; case 3: // 演出完了待ち if(/*演出完了*/) { m_Step++; } break; case 4: // 攻撃力アップ演出再生 // 攻撃力を20アップする m_Step++; break; case 5: // 演出完了待ち if(/*演出完了*/) { // 終了 } break; } } |
なぜこのように書く必要があるのかというと、演出は「再生の完了を待つ」必要があります。そのため演出の完了を待つタイミングが存在し、状態用の変数(ここではm_Step)を保持する必要があります。
この記述方法は処理の流れを把握しやすいのですが、変更に弱いという欠点があります。
例えば、ゲームデザイナーから以下の修正要望が発生したとします。
1 2 3 |
> すべてのキャラクタに覚醒を実装し、 覚醒した後にスキルを使用すると特殊な演出が再生され スキルの効果を2倍にして欲しい |
このような変更が発生した場合、覚醒後スキルの演出が追加となり、先ほどのswitch文の途中にその処理を挟み込む必要があります。
このようなswitch文は、挟み込む場所を探すのが面倒ですし、前後の処理が不自然にならないよう注意して追加する必要があり、なかなか大変です。
そこで、「演出」と「ロジック」を分離すると修正処理が簡単になります。
■「演出」と「ロジック」の分離
「演出」と「ロジック」の分離は以下のように行います。
キャラをActorクラスとします。
- Actorのパラメータを一時的なActorにコピー
- テンポラリで計算を行い演出データを生成
- 演出キューを上から順に実行 (Actorへのパラメータ反映も同時に行う)
先ほどのswitch文で修正が困難な理由は、「演出の完了待ち」がところどころに挟まるためです。
そのため、「演出」と「ロジック」を最初にまとめて計算し、「演出の完了待ち」を後で行う、というアプローチに変更しています。
このように修正することで、以下のように書くことができます。
1 2 3 4 5 6 7 8 9 |
// バトル演出キュー BtlEffects efts; // 開始演出の生成 efts.enqueue(new BtlEffectBegin(actor.id)); // スキル演出の生成 efts.enqueue(new BtlEffectBegin(actor.id, skill.id); // 攻撃力アップ actor.str += 20; |
switch文や、演出完了待ちがなくなり、コードの見通しが良くなりました。
これで、もし演出を追加することがあっても、簡単に修正を行うことができるようになります。
ただ、実際には、演出データクラスの定義や、生成した演出キューを再生する処理が必要になったりするため、全体のコード量は増えます。しかし、一度仕組みを作ってしまえば、処理の追加や入れ替えがとても楽にできるようになります。
■最後に
このやり方は、正確には「演出」と「ロジック」を分離できていませんが、基本的な考え方は演出の再生タイミングに依存せずにロジックの計算を行う、というものです。
なお、この考え方はすべてのターン制ゲームに応用することができます。
例えばローグライクを作るときでも、プレイヤーがアイテムを投げた際に「投げたアイテムがどの敵に当たるのか」「敵に当たって何ダメージ入るのか」「その後の敵はどのように行動するのか」ということをすべて事前計算して演出データを生成し、あとは演出データを再生するだけ、という作りにしておくと処理の入れ替えや演出の待ち時間の調整などがとてもやりやすくなります。