RPGツクールMVでローグライクゲームを作ってみた

はいどーもローグを目指したいニートです。
本日はRPGツクールMVでローグライクゲームもといローグライクプラグインを作った話をしてみようと思います。
どんなプラグインなのかちゃっちゃか知りたいという方は、Youtubeで動画をどうぞ!

Unityでも作った

たまに記事が浮上するのですが、その昔Unity公式チュートリアルでローグライクゲームを作りました
おそらくこの記事を読んだほとんどの方はがっくりして帰っていくのではないかなと思いますw
今回作ったものはツクールMV用のプラグインで、これよりはもう少しローグライクっぽい動きをします。
今までツクールのプラグインを70種類ほど作ってきて、その集大成(と言うとちょっと大げさですが)のような感じになっております。

どんなプラグインなのか

このプラグインの制作を依頼してきたふうりんさんは、おそらく風来のシレンを想像して「ローグライク」とおっしゃったのだと思います。
僕もローグライクと言えば風来のシレンを真っ先に思い浮かべましたので、お互いの共通認識に沿って「どこまで作るか」を話していけばよいということになります。

特徴は以下の通りです。

・マップで殴り合う
・ターン制
・8方向に攻撃・移動が可能
・空腹度システム
・HPゲージ
・遠距離攻撃可能

この特徴をそれぞれ見ていきます。

マップで殴り合う

ツクールMVの標準の戦闘は、マップから戦闘用の特別なシーンに切り替わっておこなわれます。
ですが風来のシレンはマップ上で殴り合うスタイルです。
なので、まずはこのマップ上で殴り合うシステムを作成する必要がありました。
とどのつまりはプレイヤーVSイベントということになりますね。
このシステムを作るにあたっては、みゆさんの依頼で作成した「MY_FollowerActionSystem」と、なうしきさんの依頼で作成した「TsumioCommandGameSystem」と、一般に公開している「TsumioActions」の作成経験が役立ちました。

注*MY_FollowerActionSystemはマップ上でフォロワーが自動で動いて敵を攻撃するシステムです。マップ上での戦闘システム自体はすでに組み込み済みでしたので、それに合わせてフォロワーも動くようにした感じです。

注*TsumioCommandGameSystemもマップ上でおこなうアクション系のシステムです。敵がトラップを踏むとダメージを食らう、吹っ飛ぶ、耐性値が上がる……といった感じです。

注*TsumioActionsはゼル伝を意識して作ったアクションプラグインです。

さてこの戦闘システムを作る際、なるべくツクールのシステムを流用した方が便利だろうと僕は考えました。
なので今回は各イベントの能力値に「敵キャラ」のデータベースをそのまま使っています。
ただし「敵グループ」は使えません。なぜなら、「マップのイベント」と「敵キャラ」を1対1で紐付けなければならなかったからです。
プレイヤーの能力も、もちろんそのまま参照しています。

能力値はまあそのまま使えばいいだけです。
問題はダメージ計算式でした。
計算式を僕が考えるのは難しかったため、またまたツクール標準の機能を流用することにしました。
すなわち「武器」ごとに参照する「スキル」を設定できるようにし、攻撃をおこなう際にこのスキルの計算式を適用するのです。
スキルの計算式をそのまま流用することによって、「会心率」や「分散度」等もそのまま適用することができました。
敵からプレイヤーへのステートの付加も可能です(プレイヤーから敵へは無理)。

敵がプレイヤーに攻撃してくる場合も同じです。敵ごとに能力を参照し、また必要であればスキルの参照も「攻撃」以外から変更します(例えばファイアとか)。
あとはツクール標準のバトルシーンで行われる戦闘計算をシミュレートし、結果をプレイヤー(ないしはイベント)の残りHPに反映させればOKとなります。

ここまででほぼほぼ完成で、あとはバトルログをマップ上に表示するシステムを作ればOKでした。
これについては、しなぱんさんの依頼で制作した「バトルログを改変するプラグイン」を参考に組み立てました。
ただローグライクプラグインの依頼者であるふうりんさんとバトルログの詳しい仕様を話し合っていなかったため、とりあえずはデフォルトの戦闘とほぼ同じ感じにしています。
デザインの問題で何かあれば改造してもいいかもしれませんね。

ダメージを食らったときにコモンイベントを実行

エネミーから攻撃があったとき、プレイヤーのHPが指定割合以下ならコモンイベントを実行したいという要望がありました。
要はダメージを食らった時、エッチな感じなイベントが発生する、アレですw
これについてはしなぱんさんの依頼で改変した「Mano_LifePoint」の経験が役立ちました(Mano_LifePointはしぐれんさんが制作したものです)。
このプラグインで実装した機能の一つに「プレイヤーが特定LP以下なら、指定(複数)のスキルからランダムに一つ選択」というものがあります。
今回の「ダメージを食らったときにコモンイベントを実行」にほぼそのまま使えます。
というわけで、多少の改修をほどこした以外はほぼまんま流用しました。
プラグインの説明を引用すると以下のような感じですね。

意味:
プレイヤーのHPが90%以下のとき、1番のコモンイベントを実行する。
プレイヤーのHPが30%以下のとき、4番のコモンイベントを実行する。
プレイヤーのHPが90%以下のとき、2番のコモンイベントを実行する。
プレイヤーのHPが60%以下のとき、3番のコモンイベントを実行する。

同じHPの指定が複数存在する場合、その中からランダムで一つのコモンイベントが選択されます。
上記の例で言えば、HPが90以下の場合は「1,2」番からランダムに一つ選ばれます。

ターン制システム

さてローグライクと言えば「プレイヤーと敵が交互に動く」感じのものが多いです。
風来のシレンもそうです。
ふうりんさんもこの機能を想定していたので、交互に動く機能を付け足す必要がありました。

このターン制のシステムを組み込むこと自体は特に問題はありませんでした(一部のメソッドを完全に上書きしてしまいましたが、コード的にはほんの2,30行くらい)。
問題は「交互の移動」と「攻撃」のタイミングでした。
プログラムの内部では

1.プレイヤーが動く
2.敵が順番に動く

となっていて、まさにターン制なのですが、見た目には

1.プレイヤーが動く
2.動き終わらないうちに敵が動く
3.攻撃してくる敵がいたら、全員同時にプレイヤーに攻撃してくる

のような感じでした。
これを「完全に順番に動く」ようにしたかったのです。

詳しい実装の話は省きますが、これを実装する上手い方法が思いつかず、結局は各イベントの状態をポーリングして「順番に動く」管理をすることにしました。
本当はチェーン状に動作を伝搬させていければよかったのですが、うまいこといきませんでしたね。

8方向に攻撃・移動が可能

8方向の移動ですが、これは自前で実装するのは僕の腕ではキツイと判断しました。
なので最初の段階でふうりんさんに「8方向の移動は既存のプラグインを利用するか、機能をオミットするしかない」と説明しました。
で、ふうりんさんとしては8方向機能が欲しいようでしたので、既存のプラグイン(具体的にはトリアコンタンさんのHalfMove)を使用することに決めました。
また、しおいぬさんのPD_8DirDashを併用することで、8方向の画像も設定できるようになります。

ここまではよかったのですが、どうもプレイ中に「プレイヤーに近づく、遠ざかる処理」が稀に動かないことがありました。
HalfMoveの仕様なのかな? とも思って自前で修正したのですが、やはりこれが仕様なわけはないだろうと思い、問題をトリアコンタンさんに報告してみました。
するとすぐに修正していただくことができ、8方向の移動に関しては完璧に問題をクリアしました。

攻撃については「周囲8方向を全て探索し、プレイヤーがいれば攻撃をおこなう」とかのプログラムは組みましたが、まあちょっとした拡張レベルですね。
プレイヤーの足踏み機能(HP回復で空腹度は減るやつ)やら、方向だけ転換機能を入れたりもしました。

なんにしても、この8方向に関連する部分はもっぱら他のプラグイン制作者の方のおかげですね。
ありがたいことです。

空腹度システム

風来のシレンで外せないのは空腹度システム(満腹度システム?)でしょう。
僕もこれは必須だと思ったので、組み込み方は初期の段階からいろいろと考えていました。
単純に考えると「移動するたびに空腹度が減る」のですが、空腹度は10ターンに1回減るようになっています。
つまりターン数をカウントする機能が必要です。
これは単にGame_Playerクラスに追加すればそれでよかったのですが、問題は「何を1とカウントするか?」ということでした。
プレイヤーの行動にはおおまかに「攻撃」「移動」がこの時点でありました。このどちらかが実行された場合、空腹度は減らなければなりません。
ですが将来他にも何かがトリガーとなって空腹度が減る、もといターンが経過する可能性があります(実際、遠距離攻撃が追加された)。
そこでObserverパターンを利用し、「攻撃」や「移動」をおこなったとき、プレイヤーのターンを1ターン進めることにしました。
で、プレイヤーのターンが1ターン進んんだとき、同時に他にも登録されている行動を起こすようにしたのです。
具体的には

・HPの回復(足踏みで回復できるやつのこと)
・空腹度をへらす
・敵の行動

あたりですね。
単にObserverを登録すればいいだけなので、あとから「ターン経過時にこうしてほしい」というものが増えたとしても容易に機能追加できます。
いやこれは便利です。

ちなみにですが、現在は「プレイヤーが1ターン動くと、敵も必ず1ターン動く」ようになっています。
これを「プレイヤーが1ターン動くと、敵は2ターン動く」に変更するのは非常に難しい作りになってしまっています。
ここは課題ですかね……。

注*理由は省略しますが、逆バージョンの「プレイヤーが2ターン動くと、敵は1ターン動く」はわりと簡単に実装できます。

マップ上のステータス画面

空腹度システムの見た目の表示にも使っているのですが、ローグライクプラグインではマップ上にステータス画面を表示させています。
このステータス画面ですが、当然ながら「現在の本当の状態」と同期しなければなりません。
最も簡単なのは、毎フレーム「現在の本当の状態」を再描画することです。
ですがWindow_Base系において、bitmapをclearして再描画するのは非常に重たい処理です。
それを毎フレームやるのは、さすがにちょっと重すぎます。
実際、僕が昔作ったプラグインは(当時重さとかあんまり気にしていなかったので)、何の考えもなしに毎フレームclearしていたので、使用する機器によっては見るに堪えないカクカク具合となっていましたw

マップ上にステータスを描画すると言えば、東横とこさんの依頼で制作した「TK_MapWindowSystem」も同じ系統のプラグインです。
このときはカクカク問題を解消するために、「何フレームに一度更新するか」を指定できるようにしていました。すなわち、状態が変わってからウィンドウに反映されるまでに時間差がありました。
これは特に問題がないとのことだったので、これはこれでいいのですが、ローグライクプラグインでは僕はどうにかして同時に描画を更新したいなと考えていました。

すぐに思い浮かぶのは、Game_Characterの内容が書き換えられたと同時にWindow側へと通知することですが、いわゆるデータバインディングのようなことは難しいです。
また、戦闘シーンでおこなわれているように「行動ごとに画面をリフレッシュ」ということもマップ画面では難しいです。
なぜなら、イベントに話しかけて能力値が変わるかもしれませんし、そのステータスの変更の仕方も色々とあるわけで、例えばイベントコマンドではなくスクリプトや別のプラグインを介して変動させるかもしれません。
あるいはGame_Characterにダーティフラグのようなものをつけてもいいかなと思ったのですが、「何が変わったのか」を把握するのが難しそうなのでやめておきました。

結局取った方法は「実際のデータ(Game_Character)」と「マップに表示しているデータのモデル」を用意し、この差を毎フレーム比較して、変更があったときだけ再描画(とモデルの更新)するようにしました。
なおステートの比較は単に「与えられているステートの数」です。これでほとんどの場合うまく行くはずなので、厳密に全てのステートの番号を比較しているわけではありません。

HPゲージ

こちらは風来のシレンにはない機能ですね。
ふうりんさんからの要望で導入しました。
単にHPゲージを表示しているだけですが、Game_CharacterからSprite_Characterは参照しない、という思想を大事にして作りました(このことを知らなかったとき、Game_CharacterにSprite_Characterへの参照を持たせてセーブデータを保存して大惨事になりました)。

さてゲージを表示する機能はツクール標準でもよく利用されています(ステータス画面とか)。
この機能はWindow_Base系についているのですが、ただゲージを表示したいだけでWindow_Baseのインスタンスを作るのは重いだろうと判断し、独自のSprite_MapGaugeなるクラスを作りました。
こちらはSpriteクラスを継承しているだけなので、Window_Base系よりはずいぶん軽いです(Window_Baseとのコードの重複は多少ありますが……まあそれは仕方ない)。

ゲージを表示できたあと、敵が死亡したときのエフェクトも作りたくなりました。
PIXI.Filterクラスを継承し、その名もズバリな「VanishFilter」なるものを作成し、Sprite_Characterに持たせましたよ。
シェーダがほとんどわからないので作成に非常に苦労しました。
あと多分ですけど、WebGLモードじゃないと動かないです。Canvasモードの人はエフェクトなしですね。

遠距離攻撃

ここまででローグライク風システムの基本形は完成しました。
お次は遠距離攻撃です。
これは「プレイヤーが遠距離攻撃をおこなう」場合と「敵が遠距離攻撃をおこなう」場合で少し事情が違います。
例えばプレイヤーは任意のタイミングで遠距離攻撃を撃てばそれでよいのですが、敵は「プレイヤーが対象範囲にいるかどうかを判断して撃つ」という作業が必要になります。
軽く考えると周囲8方向に対して「指定マス分の範囲内にプレイヤーがいるかどうか」を判断すればよい気がしたのですが、ふうりんさんが「敵キャラは別に遠距離攻撃使えなくていい」とのことでしたので、今回は実装を見送りました。

さてプレイヤーが遠距離攻撃をおこなう場合を考えてみます。

1.向いている方向の指定マス(風来のシレンなら10マス)先までを順番にチェック
2.チェックしている途中に障害物があればチェックを停止し、ヒットが失敗したことをログに表示
3.チェックしている途中に敵がいればチェックを停止し、攻撃のプロセスへ移行
4.チェックして最後まで何もなければ、ヒットが失敗したことをログに表示

こんな感じになります。
「障害物」については神奈月サスケさんの「多機能フックローププラグイン」を参考に、A3タイルとA4タイルを「障害物」と判定するようにしました。

注*TsumioActionsではリージョンで「障害物」を判定していましたが、神奈月サスケさんの仕様・実装の方が優れていると判断したためこのようにしました。また、TsumioActionsは「周りの敵もリアルタイムに動いていく」という性質上、そもそも「その時その時で毎フレーム自分の対象範囲をチェックしている」ので、ローグライクプラグインのように「最初にマスをチェックしておく」ということはやっていません。

遠距離攻撃の当たり判定システム自体はこれでOKです。
問題は「どういう条件で矢を打てるようになるのか?」ということです。
例えばプレイヤーがアイテムとして「木の矢」「鉄の矢」を持っていたとします。
遠距離攻撃をするたび、これらの一つが減っていくとします。
プレイヤーが遠距離攻撃を実行するとします。
で、「木の矢」「鉄の矢」のどちらを優先的に使えばよいのでしょう? アイテムを装備することはできないため、これは問題です。

「じゃあ、鉄の弓や木の弓という装備を用意しているときだけ矢を打てるようにしよう」とも考えましたが、そうするとプレイヤーは武器(あるいは防具として弓を用意しても)を他にも持たないといけなくなります。
ふうりんさんが作るゲームがどのようなものかわからないので、僕の判断で勝手に追加はできません。

別案として「MPを矢の弾数代わりにする」というのも考えましたが、こちらもやはりふうりんさんとの相談なしには作成できないでしょう。
というわけで、ここの仕様を先日ふうりんさんと詰めました。
結論としては以下のような仕様に落ち着きました。

・弓はデータベースとしては防具であり、特定の装備タイプに準ずる(デフォルトだと装飾品)
・矢は共通のアイテムを一つ指定し、矢を撃つたびにこれが一つ減る
・弓のヒット時、敵を中心にアニメーションを再生
・ダメージ計算に使用するスキルは通常の攻撃と同じく、防具内のメモタグで指定
・ヒット時にコモンイベントを呼ぶことができる(これは後で述べる杖機能のためのものです)
・MPを使用する場合もある

矢が飛んでいくような機能(シューティングゲームみたいに弾を飛ばすこと)は、ふうりんさん的にもどっちでもよいという感じだったので、諸々の事情でオミットしました。

杖的な機能

弓矢の仕様の中に「ヒット時にコモンイベントを呼ぶことができる」というものがあったと思います。
これはふうりんさん側から「弓矢とは別に、杖的な機能も欲しい」という要望があったからです。
こちらは実装コストが高く、また実際にふうりんさんに使ってもらう場合も「コモンイベントからスクリプトを組み立てる」という作業が発生するため、結局のところあまり使わないのではないか? とも考えました。
ですが杖の機能はあったらあったで絶対に面白いとも思ったので、今回実装に踏み切った感じです。
ただ僕が杖の機能を一つ一つ組み込むのは面倒ですから、ツクールのコモンイベントを利用することにしました(コモンイベントから「矢がヒットしたイベント」を取得できるようにする必要があったため、グローバル変数を泣く泣く一つ用意しました)。
つまりコモンイベントに「吹き飛ばしの杖」や「場所入れ替え」用のスクリプトを用意しておき、矢ヒット時にそれを呼ぶ感じです。

弓と違う点も挙げておきます。
それは「杖の使用にはMPを使用する」ことです。
ですが内部的には杖と弓は同一の機能を使っています。単にデータベースのスキルを参照し、そのとき消費MPが指定されていればそれだけ分使用するだけです。
この「MPを使用する」という仕様を入れるため、Game_Playerにドンドン書き足していた弓矢系の処理を別のクラスとして抽出しました(実はそれまで、消費MPを設定していてもMPは消費されなかった)。
このおかげで見通しがスッキリし、比較的楽に杖機能も組み込むことができました。リファクタリング万歳ですね。
時間があれば「移動系の処理」「通常の攻撃の処理」も別クラスに抽出したいなあと思います。

実際に使うかどうかは別として、「矢もMPも消費する」という豪華な特技も作れます。
戦略の幅が増えるといいなあ。

コモンイベントではダメな場合

ツクールMVに搭載されているコモンイベントは「実行可能になるまで実行しない」という性質があります。
このことが重要になるのは、例えば「場所の入れ替え」機能を杖の効果として実装したい場合です。
もしこれをコモンイベントとして実装すると、ゲームは以下のように実行されます。

1.プレイヤーの入れ替え攻撃
2.エネミーにヒット
3.エネミーのターンで攻撃
4.プレイヤーと場所を入れ替える

すなわち「場所を入れ替えたはずなのに、前回の場所で食らっていたであろう攻撃をもらってしまっている」という状況が発生します。
これを回避するために「プレイヤーが行動した瞬間に実行したいスクリプト」を弓のscriptタグで指定できるようにしました。
内部的にはevalで実行していて、あんまり綺麗じゃありません。
が、これ以外にやり方が思いつかなかったのでこうしています。

その他の入れたかった機能

「お金を投げる機能は入れられないか」という相談がありました。
いわゆる「ギタン投げ」ですね。
TsumioActionsのボム投げを参考にすれば組み込めないことはないと思うのですが、制作期間の関係でオミットしました。

「敵の遠距離攻撃も可能ならば入れてみたい」という話でしたが、これも先に話したようにオミットしました。
同じく「矢(のグラフィック)が飛んでいく機能」もナシにして、ヒットしたイベントを中心にアニメーションを再生するだけにしています。

「ダメージを食らったときに点滅する機能」も入れてみたいとのことでしたが、これは現在諸事情で組み込んでいません。
次のバージョンアップ時に組み込むかもですね。

依頼にはそもそもなかったもので言えば、

・超高速移動
・現在装備中の遠距離武器の表示

機能はあってもよかったのかな? と思いました。
超高速移動とは、特定のキーを押しているとき、壁の区切りのちょうどいい場所まで移動するアレです。
ダンジョンの奥までササッと行けて楽しそうだなあとは思ったのですが、ちょっと実装が大変そうですね。
「現在装備中の遠距離武器の表示」は、実際にプレイしていて「今何の効果の装備をしていたっけ?」とド忘れしてまうことが多かったので、あった方がよいのかなーと思いました。

試用してみて

プラグインの作成中に不具合は見つけ次第潰していくのですが、規模が大きくなると「見逃し」は増えていきます。
特にツクールMVはテストが作成しにくいので(と言うかぶっちゃけ僕はツクールMVのプラグインではテストはassertくらいしか入れてない)、これはしょうがないと思います。
で、実際にテストプロジェクトを作成してみると、やはり不具合が出てくるんですねw
僕が発見した不具合で一番大きかったのは、マップデータを再読込したとき、死んでいるエネミーが復活しちゃってたことです。
なんでこれを見落としていたんだ! という感じですが、テスト中ってマップ移動とか全然しないのですぐ見落としちゃうんですね。
あとは「素手の攻撃」を考慮していなくて、素手の攻撃時にエラーが発生した、とかもありました。
作成中は色々と視野が狭くなっているので見落としがちですから、やはり一度ちゃんと制作者自信が試用してみるのは大事だなあと思いました。
特にプラグインの利用者さんは「これは仕様なのか? それとも不具合なのか?」の見分けがつかない微妙な不具合も多いですしね。

終わりに

ふうりんさんがTwitterで「ローグライク風のゲーム作りたいなー」みたいな呟きをしていて、それを見た僕が「とりあえず交互に動くだけのプラグイン作ってみましたけど、よかったら使ってみてください」ということが事の発端でした。
当時作った「ローグライク風の動きをするだけのプラグイン」はほんの30行程度のコードでしたが、それから本格的に作ってみようということになり、現在に至ります(完成品は3000行近くあります)。
Game_Action系はあんまりいじったことがなかったので、ここに詳しくなったのはよかったですね。
あと『新装版リファクタリング』をちょうど読んでいたところだったので、得た知識でリファクタリングも積極的に行いました。
テストコードも書きたいんですが、ツクールだとどうやればいいのかイマイチわかりません……。

自分用のプラグインだとこの規模のものはちょこちょこ作っているのですが(あとUnityで作って、結局おしゃかになったゲームはプラグインとは比べ物にならないくらい時間をかけていたので規模もでかいですね……)、依頼品としては最大規模のものでした。
なのでちょっと緊張しましたが、ローグライク風のプラグインは前々から作ってみたかったものなので、なかなか楽しい制作でしたよ。
そんなこんなでもう今年も終わりですが、わたくしは相も変わらず無職です。
果たして来年はニートを脱しているのか!?
脱してたらいいなあ……。

という感じで長い長い記事を終えたいと思います。
ほなまた。

フォローする