読者です 読者をやめる 読者になる 読者になる

エフアンダーバー

個人でのゲーム開発

ツクールMVでローグライク-ライクなゲームをツクった話

ふと「ツクールMV買ったのにゲームひとつもツクってないな」と気づき、衝動的にゲーム制作。 一週間くらいでできるかな、とか考えていたのですが、どうやらRPGの設定項目の多さを甘く見過ぎていたようです・・・。 結局、倍の二週間をかけ、とりあえず動いて、とりあえず遊べるくらいのものをRPGアツマールにアップしました。

今回はそんな感じでつくったものの紹介と製作途中のあれこれについてです。

制作物

ミニローグライク(RPGアツマールに飛びます)

ランダムダンジョンかつ死んだら終わりというローグのシステムを採用したゲームです。

f:id:fspace:20170404204124p:plain:w300

「不思議のダンジョン」シリーズのようなローグのシステムをまるまる受け継いだものを『Roguelike(ローグライク)』と呼ぶのに対し、 システムの一部だけを採用したゲームを『Roguelike-like』や『Rogue-Lite』、『PDL』と呼ぶそうです。 自分がやったことあるのだとSpelunkyとかですね。 今回つくったのは単なる妥協版ローグライクなのですが、一応ローグ的な性質を持っているのでローグライクライクゲームということに。

ゲームの目的は最上階にいるボスを倒すことです。 途中の階層では、障害物・敵・アイテム・罠がランダムに配置されます。 敵を倒してレベルをあげたり、アイテムを拾ったりして、 できるだけ有利な状況で最上階にたどり着くことで、ボスを撃破できる確率が上がります。

ローグライクゲームでは普通、敵がマップ上で様々な行動をとりますが、 このあたりのシステムをつくっていると時間がかかるので、このゲームでは単に徘徊するだけです。 敵と接触するとツクール標準のバトルが始まります。 戦闘中は一対一ですが、戦闘後にマップでアイテムやスキルを使うとターンが消費されるので、囲まれると連戦になります (とはいえ初期配置が悪くない限りはほとんどそうなりませんが)。

またローグライクゲームのダンジョンというと、部屋と道に分かれていてどこに何があるかわからないというのが普通ですが、 実装が面倒だったので、見通しのいい一画面にボンバーマンよろしく破壊可能な障害物をちりばめただけのダンジョンになっています。 一応、破壊に1ターンかかるので、うまく逃げられないような状況は作り出せているかな、と。

未識別なアイテムや攻撃系のアイテムは実装するつもりだったのですが、これ以上時間を割きたくなかったので断念しました。 バグっぽい挙動も修正が面倒そうなものは仕様ということにして目を瞑ってます。 実のところ、ろくにテストプレイもしていないのでゲームバランスはてきとーです。

ここまで読んでなお興味を持ってくれる人がいたならば、RPGアツマールにて遊んでみてください。

発想

このゲームシステムに至るまでの思考の垂れ流し。

もともと開発開始の時点で期間を一週間程度と決めていたので、まず短期間で面白いものをつくるためにどうするか考えました。 ツクールの本分ではないRPG以外のゲームをつくるのは時間がかかりますし、 かといってまともにRPGのマップやストーリーを作るような時間はありません。 そこで、ランダム生成ダンジョンをひたすら探索するゲームにすることに決めました。

以前のプラグイン開発(過去記事 #1 #2 参照) でまともにローグライクのシステムを組み込むのが面倒なことはわかっていました。 そこで、まず真っ先にマップ上での特殊行動(行動順の制御)と敵の追加発生(マップデータの動的変更)を切ることに決めました。 しかし、この二つがなくなるとマップの探索はただただ歩き回るだけで極端につまらなくなります。

どんなダンジョンにすれば探索が面白くなるのか考えた結果、ふとボンバーマンを思い出して、障害物の下にアイテムを隠すことにしました。 障害物を置くだけのマップ生成は非常に簡単なので一石二鳥です。 マップの大きさをどうするかは悩んだのですが、無駄に広くして壮大な感じを出すよりはわかりやすい一画面にしました。 クソゲーはクソゲーとしての身分をわきまえていた方が遊ぶ側としても気楽に手を出せると思ったので。

敵との戦闘は結局普通のツクールの戦闘を採用しました。 下手に複雑なシステムを組むよりわかりやすいですし、なにより実装の手間が省けるので。 ツクールには敵の群れを設定する機能がありますが、マップ上の敵キャラ表示からイメージがずれないように敵は一体ずつにしました。 同時に複数の敵と接触したら同時に戦闘、というような仕様も考えましたが実装が面倒なのでボツに。

そんなこんなで基本的なゲームシステムは決まりました。

絵や音を作ったり探したりしている時間はないだろうということで、これらは最初からデフォルト素材を使うことに決めていました。 とりあえず素材を一通り眺めたところ、パッケージキャラクターのデザインがいい感じだったのでこれらを使うことに。 主人公っぽいやつ一人だけだと少し面白みに欠けるので、アイテムを使ってキャラを切り替えることにしました。 「今のキャラの方がいいから切り替えなくてもいいや」となるとつまらないので、 切り替えアイテム(鏡)には、先制かつ状態異常含め全回復という特殊効果をつけました。

あとは各パッケージキャラクターの特徴をイラストから推測して能力に落とし込み、 それぞれが生きそうな状況を考えつつ、状態異常やスキル、敵キャラの能力なんかを設定してゲームが完成しました。

小技・改造

制作中の小技とか改造とかの話です。

ツクールでのゲーム制作の参考になれば。

TypeScript

ツクールMVは基本的にES5なのでJavaScriptでコードを書くのは結構苦痛だったりします。 そのため、今回のコードはすべてTypeScriptで書きました。

ツクールMVとTypeScriptについては過去記事を参照してください。

www.f-sp.com

ただし、過去記事ではモジュールを用いていたのに対し、今回はすべて名前空間で済ませてしまっています。 他のプラグインとの併用を考えないくてよいのであれば、名前空間で十分なので。

ダメージ計算式

ツクールMVでは各スキルごとにダメージの計算式を記述できます。 a.atk * 4 - b.def * 2のような感じで書くのですが、すべてのスキルに同じような計算式を書くのは結構面倒だったりします。 特に途中で計算式を変更しようものなら、全スキルの記述をいちいち直さなければなりません。

しかしこの計算式、実は内部的にはevalしているだけなのでプラグインで定義した関数呼び出しができます。 そこで、$dmgのようなグローバル関数を定義して$dmg(a,b)とだけ書いておくと、 プラグイン側の定義変更で全スキルの計算式が一括設定できます。 地味に便利。

取得経験値

各敵キャラには撃破時の取得経験値を設定できます。 最初、取得経験値は手動でそれっぽい値を入れていたのですが、 非常に面倒だったので最終的にプラグイン側でおおまかな値を計算してそれに係数をかける方式にしました。

const AUTOEXP_TAG = 'autoexp';

const _Game_Enemy_exp = Game_Enemy.prototype.exp;
Game_Enemy.prototype.exp = function (this: Game_Enemy): number {
    const meta = this.enemy().meta;
    if (AUTOEXP_TAG in meta) {
        const value = meta[AUTOEXP_TAG];
        const number = (value !== true ? parseFloat(value) : NaN);
        const rate = (isFinite(number) ? number : 1);

        return Math.ceil((this.mhp * 3 + this.atk + this.def + this.mat + this.mdf + this.agi) * rate);
    } else {
        return _Game_Enemy_exp.apply(this, arguments);
    }
}

あとは敵キャラのメモ欄に<autoexp:2>のようなタグをつけるだけです。

スキルのレーティング

各敵キャラの使用スキルにはレーティング(使用度合)が設定できます。 しかし、マニュアルをみるとわかるように結構わかりづらいアルゴリズムで割合が決まります。

そのままだと調整がしづらかったため 2^{x-1}で重みを設定する簡単な方式に変えました。 ただし、最大値が設定された場合には条件を満たす限り必ずそのスキルを使用するようにしています。

コードはこんな感じ。

Game_Enemy.prototype.selectAction = function (this: Game_Enemy, actionList: Action[], ratingZero: number): Action | null {
    if (actionList.length !== 0) {
        const MAX_RATING = 9;
        const max = actionList.filter(action => action.rating === MAX_RATING);
        if (max.length === 0) {
            const weights = actionList.map(action => 1 << (action.rating - 1));
            const total = weights.reduce((sum, weight) => sum + weight, 0);
            const value = Math.randomInt(total);
            for (let i = 0, sum = 0; i < weights.length; i++) {
                if (value < (sum += weights[i])) return actionList[i];
            }
        } else {
            return max[0];
        }
    }

    return null;
}

バトルイベントのスパン

ボス戦にて被ダメによるステート解除を検出する必要があったのですが、どう実装すべきか結構悩みました。 そもそもツクールにはステート変更に関するコールバックはありませんし、 戦闘中はコモンイベントの並列処理も働かないようで監視できません。

そこで、バトルイベントのスパン「モーメント」で監視することを考えたのですが、 この「モーメント」というのは通常イベントの「自動実行」と同じで、 条件を満たさなくなるまで他の処理より優先的に実行され続けます。 つまり、内部で条件を無効にするような処理を書かない限り、無限ループとなって戦闘が進みません。 一方で、条件を無効にする処理を書いてしまうと、再度有効にしない限り処理が実行されないので監視ができません。

仕方がないので、プラグイン側で「モーメント」の仕様を「並列実行」に近いものに書き換えて使用することにしました。 バトルイベントはボス戦でしか使わないので大丈夫だろうという妥協策です。

そもそも条件式としてスクリプトを書ければそれで済んだのだけど、 使ってみると案外ツクールのエディタからスクリプト差し込める場所って少ないんですよね・・・。

苦労話

開発期間が延びた原因とかハマったところの話。

名前・説明文

正直、今回のゲーム開発で一番苦労したのが名前や説明文といった文章問題だったり。

今まであまり王道RPGをやってこなかったことや、RPG開発経験がほぼなかったため、とにかくかっこいい言葉が出てこない・・・。 あんまりベタな英語なんかを使うとかっこ悪いし、かといってよく意味を知りもしない言葉を使うと誤用が怖いので、 なんとか日本語でそれっぽい感じにできないかといろいろ考えました。

しかしまあ、こうして記事を書きながら改めて見直してみると結構恥ずかしいこと書いてますね。 こういうのって案外、羞恥心との戦いなのかもしれません。

Edgeとアツマール

実はゲーム自体は投稿日より一日早く完成していたのですが、RPGアツマールにアップする過程でいろいろあって投稿が遅れてます。

そんな問題のひとつめがEdgeとアツマールの問題。

私は基本的にOSのデフォルトブラウザを使う人間なので、ブラウザはEdgeを使ってます。 しかし、Edgeからアツマールにゲーム情報を入力して投稿ボタンを押すとなぜか何度やってもエラー。 試しにFirefoxでやってみたらすんなり投稿できました。

Edgeが悪いのか、アツマールが悪いのか、はたまた私のブラウザ設定が悪いのかはわかりませんが、 とりあえずEdgeとアツマールの相性はよくない模様。 アツマールでゲームをプレイするときもEdgeだとキー入力がハンドルされなかったり、画面がぶれたりするんですよね・・・。

『未使用』ファイルを含まない

アツマールにどうにか投稿できたということでとりあえずテストプレイしてみたところ、敵を攻撃した瞬間に画面がフリーズ。 ブラウザのデバッグ機能を使ってみたところ、実際には動いているけれど、どこかで無限ループに近い状態に陥り進行不能となっているようでした。

ステップ実行で処理を追っていった結果、ロードに失敗した画像のロード待ちを延々としている状態のようでした。 ローカルでは動いていたことを考えると、おそらくデプロイ時の「未使用ファイルを含まない」というオプションで使用しているファイルが削除されたんだろうな、と。 そこで、出力されたデータのアニメーション画像を確認してみると、通常攻撃時の画像が含まれていませんでした。

データベースを確認して通常攻撃のアニメーション設定を見てみると「通常攻撃」になっているのを発見。 これは通常攻撃に使用するアニメーションと同じものを使用、という意味だと思われるのですが、 通常攻撃のスキル自体に設定した場合にどうなるのかわからなかったためコアスクリプトを確認。 すると、どうも武器を装備している場合には武器のアニメーションが使われ、そうでない場合には1番のアニメーションが自動的に選択されるようでした。

1番のアニメーションはデータベース上のスキルや武器などから明示的に参照されていないため、 未使用ファイルとみなされデプロイ時に削除、結果としてロードエラーで止まっていたようです。 通常攻撃スキルのアニメーションに明示的に1番のアニメーションを設定したところ、削除されなくなり正しく動作しました。

ありがちなバグだとは思いますが、正直勘弁してほしい。

セーブデータとアツマール

アツマールでは一人あたり90ブロックのセーブデータ領域が与えられます。 アツマールのAPIの説明を読むと、1ブロックあたり1KBと書いてあるのですが、 一人分の領域が90KBしかないなんてことはないだろうと勝手に書き間違いだと思い込んでいました。

が、実際にテストプレイしてみると、セーブデータひとつあたり4,5ブロック持っていかれます。 この時点で、このゲームは階層ごとに別ファイルにオートセーブする仕様だったため、 これはまずいということで急遽セーブ方法とデータ容量を見直すことにしました。

セーブ方法はこれまでの方式をオプションとして残しつつ、 基本的にひとつのファイルを上書き保存していく形に変えました。 データ容量は自動生成しているマップと使っていない仲間の情報を捨てることで軽量化しました。 それでも1ブロックにはなりませんでしたが、 これ以上軽量化しようと思うとかなり細かく制御する必要があり、 バグを生みそうなので諦めました。

おそらくビジネス上の戦略としてこの仕様なのだと思うのですが、 プログラマが1KB単位でセーブデータ容量を調整するのは非常に労力の無駄なので、 1ブロックの上限10KBで30ブロックくらいにしてほしいところ。

やりのこしたことメモ

現在特にアップデートの予定はありませんが、いつか気が向いたときにでもやるべきことのメモ。

  • 接触時の戦闘開始を移動と床イベントの発生後まで遅延orキャンセル
  • 足元コマンドの実装
  • 未識別状態の実装
  • 攻撃系アイテムの追加
  • マイナス効果アイテムの追加
  • マップ上で機能するアイテムの拡充
  • アイテム所持可能数の制限
  • 逃走時の敵体力の引継ぎ
  • 各種メニュー画面の簡素化
  • 戦闘画面の簡素化
  • 罠回避手段の考案
  • お金の取得とその利用法の考案
  • バランス調整

逆にやる必要がないこと、または迷っていること。

  • 攻撃による罠の発見
    • 現状、ターンを消費するリスクが少なく、単にテンポを悪くする
    • プレイヤーの感覚とずれるためなんとなく不完全感がある
  • 空腹度の実装
    • 歩数を意識させると障害物の破壊の作業感が増す
    • 階層数が少ないため、現状ひとつの階層の持つ役割が大きく、急かすと詰む確率が格段に上がる
    • 敵をアイテムから遠ざけるためにでたらめに歩き回ることを抑止できる
    • プレイヤーの生命値として体力以外の要素を組み込むこと自体は面白い
  • 階層ごとの行動回数制限
    • 現状、特に階層に留まるメリットがなく制限の必要がない

おわりに

ゲームの紹介だけだとあまり書くことないな、と思い、 とりあえずやったことや考えたことを片っ端から書いてみました。 わりと勢いだけで書いたので、間違いとか不快な部分とかあったらごめんなさい。

前回、Unityについていろいろ書くとか言いつつ、ツクールの記事で申し訳ない。
次こそUnity関係で記事書こうと思います。



執筆時のRPGツクールMVのバージョン:1.3.5