レベルアップまでの適切な戦闘回数とは?~ドラクエ3の戦闘回数とレベルアップ調査および考察~
背景と目的(なんでこんな記事書いたの?)
ゲームを作る上で頭を悩ませるのが「ゲームバランス」というものである。特に「プレイヤーにとって快適なバランスとは何か」というものを考えるとき,製作者は往々にして悩み,考え果て,そして答えが見つからず悶々とする。
今回はそんなバランスを考えなければならない項目の一つである戦闘とレベルアップについて,一つの調査と考察を行った。具体的には「レベルアップするまでに必要となる戦闘回数は何回であるか」を調査・考察した。
調査の対象は「ドラクエ3」とした。これは本ゲームが最もバランスが秀逸であると個人的に思うためである。
結論
- ドラクエ3を対象にレベルアップまでの戦闘回数を調査した。
- 序盤は5回程度の戦闘でレベルアップすることがわかった。
- 中盤(イシスあたり)でレベルアップがしにくいレベル帯があることがわかった。
- 終盤(アレフガルド以降)は再度レベルアップしやすくなり,20回程度の戦闘でレベルアップすることがわかった。
- 「5回程度の戦闘でレベルアップ」はサクサクあがる,「10回程度」はレベリング作業感がある,「20回以上」となると根気良く作業しなければならないイメージであると結論づけた。
方法(どういうことを調べたの?)
ドラクエ3を対象に以下の条件で戦闘を行った場合にレベルアップするまでの戦闘回数を調査した。
- 勇者のレベルを対象とする。
- パーティーメンバーは4人とする。
- モンスターの平均出現数は3~7体それぞれの場合を計算する。
- 攻略の場所とそのタイミングでのレベルを設定する。
- 攻略の場所付近のモンスターと戦うこととする。
- 特別に経験値の高いモンスター(メタルスライムやはぐれメタルなど)とは戦わない。
つまり,「攻略適正レベルぐらいのときに拠点の周りをうろちょろして適当に戦った場合のレベルアップまでの戦闘回数」と考えていただければよいかと思う。
結果と考察(何がわかったの?)
攻略の場所と出現モンスターについてを表1にまとめる。また,レベルアップまでの平均戦闘回数を表2にまとめる。
表1 場所・出現モンスター
表2 レベルアップまでの平均戦闘回数
表1については見たままであるので,特に説明は不要と考える。表2は表1の場所・レベルをそのままに,「表1で示したモンスターが平均何体出現したならば,レベルアップまでの戦闘が何回になるか」を示している。
例えば,アリアハン周辺では,モンスターとして「スライム,おおがらす,いっかくうさぎ,おおありくい」が出現可能であり,モンスターが平均5体出現したならば,4回の戦闘で1レベルから2レベルへレベルアップする,となる。
表1・2から以下のことが言える。
- 序盤は5回程度の戦闘でレベルアップする。
ゲームの最序盤はサクサクレベルアップでき,5回程度の戦闘でレベルアップしていく。これは当時プレイした思い出とも合致する気がする。
- 中盤でレベルアップしにくく,終盤では再度レベルアップしやすくなる。
中盤で一度レベルアップしにくくなる。シャンパーニの塔やイシスでは「平均7体のモンスターが出現してもレベルアップに30回以上の戦闘が必要となる」という結果になった。
中盤でレベルアップしにくく,終盤で再度レベルアップしやすくなるという結果については記憶のとおりである。しかし,1レベルアップに30以上の戦闘ということはやっていなかったように思える。このあたりは装備が良くなってくる時期なので,装備の拡充を優先し,レベルアップは行わずにガンガン進んでいったというところか(そして,バハラタでメタルスライムを狩ってたのかと思う)。
- 終盤は20回程度の戦闘でレベルアップする
終盤については「この程度であったかな」といった感覚である。結局のところレベルアップははぐれメタルに頼るわけだが,それ以外の状況(例:ダンジョンの攻略中,はぐれメタルがなかなか出ないなど)でのレベルアップは20回程度と考えて差し支えないかと思う。20回程度の戦闘でレベルアップとなると,ある程度根気良くレベリング作業をしていかなければならないイメージである。
提言(自作ゲームではどう設計するか?)
得られた結果から,自作ゲームにおいてレベルアップまでの戦闘回数を何回とすべきか考える。
ドラクエ3の序盤の「5回程度の戦闘でレベルアップ」はサクサクあがるという感覚である。一方で,「10回程度の戦闘でレベルアップ」はレベリング作業感があると思うし,「20回以上」となると根気良く作業しなければならないイメージである。
作成するゲームがドラクエ3並の超大作であれば,ドラクエ3のバランスに則ってしまうのが手っ取り早い。戦闘回数を表2のように設計し,かつメタルスライムなどの経験値の大きな美味しいモンスターも作ってあげると良いだろう。
もっと短い中短編のRPGであればどうだろうか?
序盤についてはドラクエ3と同じく5回程度の戦闘でサクサクレベルアップするのが良いだろう。しかし,中短編のレベルアップに30回以上の戦闘が必要,もしくはメタルスライムなどの美味しいモンスターを作る,というのは適切な設計でないと考える。
理由は,中短編のRPGは全体を通してサクサク遊べる設計とすべきと考えるためだ。であれば,ドラクエ3のように中盤でレベルアップしにくく,終盤で再度レベルアップしやすくという形ではなく,終始序盤並のサクサク感を出してはどうだろうか。
または,中盤・終盤のレベルアップしやすさの変化はあっても良いが,序盤は5回程度の戦闘,中盤で10回程度,終盤で5回程度と,レベルアップしにくいレベル帯でも戦闘回数の増加は控えめにしてはどうかと考える。
結論
- ドラクエ3を対象にレベルアップまでの戦闘回数を調査した。
- 序盤は5回程度の戦闘でレベルアップすることがわかった。
- 中盤(イシスあたり)でレベルアップがしにくいレベル帯があることがわかった。
- 終盤(アレフガルド以降)は再度レベルアップしやすくなり,20回程度の戦闘でレベルアップすることがわかった。
- 「5回程度の戦闘でレベルアップ」はサクサクあがる,「10回程度」はレベリング作業感がある,「20回以上」となると根気良く作業しなければならないイメージであると結論づけた。
【ツクールMVプラグイン】ダンジョン半自動生成のアルゴリズム
この記事の背景・目的
ツイッターにて「ダンジョンの半自動生成」プラグインを公開したところ,思いのほか多くの方に「いいね」をいただきました。ありがとうございました。
【ダンジョン半自動生成】ダンジョン生成プラグインを公開しました。https://t.co/jJ4LpwUkNz
— くらげや (@kurageya0307) 2018年1月15日
通路を繋げた迷路を作っておけば,自然な感じのダンジョンに仕上げてくれます!皆使って下さい!!#ツクールMV #プラグイン pic.twitter.com/dmSIHpIaPS
このブログにて上記プラグインのアルゴリズムを解説したいと思います。次のツクール・または別のゲーム製作ツールで応用されたのならこれ以上の喜びはありません。
ダンジョン半自動生成アルゴリズム概要
- MapInfo.jsonおよびMapXXX.jsonをロードする。
- 素材マップのタイルを読み込み,準備する。
- テンプレマップのデータを読み込み,通路の位置を把握する。
- 通路の位置から「角」の位置を割り出し,角が丸くなるように修正する。
- 通路を拡げ,ガタガタした感じにする。
- 通行可能なオブジェ・通行不可能なオブジェ・壁の飾りをランダムに配置する。
- 影をつける。
- 3.~7.で作成したデータを元にダンジョンを生成し,保存する。
以降の章でこれらの詳細を説明していきます。
1.と2. マップのロードとタイルの読み込み
MapInfo.jsonとMapXXX.jsonの読み込みはGame_Mapオブジェクトのupdate関数に対しフックさせることで実現しています。データのロードそのものは,DataManagerクラスのloadDataFileを利用しています。プログラムの流れとしては以下のようになります。
- 毎フレーム(?)実行されるupdate関数が呼び出される。
- 条件分岐if(loadAtOnce)に到達する。テンポラリな変数であるloadAtOnceが初期状態ではtrueであるので,if文内の処理(ファイルの読み込み)が行われる。なお, if文内でloadAtOnceはfalseになるので,このif文は文字通りupdate関数内で1回しか実行されない。
- DataManagerクラスのloadDataFileに渡したロード先のグローバル変数が初期状態の「null」から,ファイルロード後のオブジェクトになるまで待つ。具体的には,if文の条件としてif(ロード先のグローバル変数)と記述する。nullであるうちはこのif文が真になることはなく,ファイルロードが完了し何かのオブジェクトとなった後にif文内の処理が行われる。
- 3,で示したとおりデータが読み込まれたのを待って,次の処理に進む。
このようにupdate関数内では「1回しか実行しないロード処理」と「ロードが完了するまで待つ処理」が上手く実装されています。このあたりはループやフックで実装するプログラミング特有の書き方になるかと思います。なかなか面白いので,詳しく知りたい方は「ループ フック 処理」などのワードで検索してみるといいでしょう。
3. データ読み込みおよび通路の把握
テンプレマップの読み込みが完了すると,そのマップデータを走査して,「何も置かれていないタイルは0」「何か置かれているタイルは1」とする0/1の二次元配列を作成します。イメージは下図のとおりです。
これにより,通路の位置を2次元配列として一時保存します。
4. 角の判定・角の修正
3.で得た通路の2次元配列に対し,角を見つけ・角を丸く修正します。
角を丸く修正するのはダンジョンをより自然な感じに仕上げるためです。下図に角が直角のままのダンジョン,角が丸く修正されたダンジョンの差異を示します。
なんとなくですが,角が丸まっている方が自然な感じに見えませんか?
角を修正するにはまず角がどこに存在するか見つけなければなりません。今,通路の位置のデータは0/1の二次元配列となっているため,プラグイン内では下図のように管理されている状態です。
これに対し,さらに下図のような範囲について0/1が並んでいるか調査し,角の外側・角の内側を探索します。
(角の外側については3×3マス+アルファを調査していますが,これは特に意味のない処理方法だったことに後で気づきました。)
上記方法にて,「ある地点が角である」と判定されると,次は角に対し2次元配列をマスクデータで上書きし,角の修正を行っていきます。この時,最終的に床タイルとなる予定地は-1,壁は-2,天井は-3の値を代入するようにしています(下図)。
判定用のデータ,マスク用のデータは定数として835行目から1036行目に記述しています。これらを変更することで,角の修正は自由に変更することができます。
5. 通路のガタガタ化
4.によって角が削れ丸くなったとして,通路がそのまま一直線上になっていては自然な感じがしません。ダンジョンをより自然な感じに近づけるためには,通路を幅の広い部分・狭い部分が入り乱れるようなガタガタした感じにした方が良いと考えました。通路のガタガタ化前後の比較を下図に示します。
通路のガタガタ化のためには二つの処理を行っています。
- ガタガタ化対象となる長い通路がどこに存在するか探索する。
- ガタガタ化させる。
前者の処理はマップデータの2次元配列上で0が連続して続く部分の探索により実現しました。例えば,ある角が修正された後の通路と何もない空間の境目は,以下のようなデータが並んでいます。
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 -3 -2 -2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2-2 -3 0 0 0 0
0 0 -2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -2 0 0 0 0
ここで重要なのは下線赤字で示した0の羅列です。ここが通路のガタガタ化の対象となる領域となります。これは「1と隣接し」かつ「0以外の数字で囲まれ」ているという条件で探索することができます。
探索した0の羅列は「開始点の座標」と「0の羅列の長さ」の情報をバッファに保存するようにしました。ここまでがプラグインのソースコード上における1136行目「MakeLineBuffer」の処理内容です。
続いて,後者の処理を行います。ガタガタ化は0の羅列の長さに依存します。すなわち,0の羅列が十分に長い場合は,複数回の広い通路・狭い通路の繰り返しが実現できますが,0の羅列が短い場合は多くはガタガタにできません。
さきほどの探索で「0の羅列の長さ」の情報があるので,これに合わせて適切な回数ガタガタの繰り返しが起きるようにプログラムを実装しました。
6. オブジェ等の配置
オブジェ等の配置はマップデータ上の床タイルとなる領域(壁の飾りは壁となる領域)にランダムに配置するように実装しました。ただし,完全にランダムにしてしまうと,配置に偏りがでて自然な感じがしません。そこで,マップ全体を小さなサブ領域にわけ,サブ領域内にいくつかのオブジェが配置されるように実装しました。サブ領域の概念図は下図のとおりです。
7. 影
影の付け方のアルゴリズムは単純です。マップデータに対し右側から走査し,壁にあたったら影をつける,という処理を実装しています。ちなみに,ツクールMVではMapXXX.jsonファイルのdataタグにマップタイル・影・リージョンのデータが格納されており,zのインデックスが4が影となっています。影のデータは影の形に対し数値が割り振られていて,下図のようになっています。
8. データの保存
マップデータが完成したらこれをjsonファイルに書き出します。ツクールMVが利用しているNW.jsはNode.jsというプログラムを利用していて,ファイルの書き出しを行う際にはFile System Moduleというものを利用する宣言をしなければなりません。まぁ,あまり難しく考えず
var fs = require('fs');
としたら使えます。このfsに対しwriteFileSyncというメソッドを呼び出し,書き出しファイル名と書き出すデータの内容を渡して上げればデータの保存ができます。
まとめ
ツイッターにて公開したダンジョンの半自動生成プラグインのアルゴリズムについて解説しました。わからないことがあったら私のツイッターまでご連絡いただければと思います。
このプラグインが皆さんのツクールライフの一助となれば幸いです。