Help us understand the problem. What is going on with this article?

RPGツクールMVにおけるパッド入力、Inputクラスの理解と改造 (特別編)

More than 1 year has passed since last update.

RPGツクール系のコードは読みやすく、拡張しやすく設計・開発されていて、学習教材としても有用だとおもいます。全部を読むのは大変なので、何かお題を決めて、じっくりと眺めていきましょう。

今回、対象としているのは RPGツクールMV ver1.5.2 です。

と、このタイトルで 前編後編 を公開しましたが「ちょっとサンプルの実用性が低すぎるんじゃない?」とのご意見をいただき。確かにそうだな、と。あの後で個人的には更に改造をしたので、そちらの説明も追加で公開しておきましょう。

実用的な改造?

後編説明したサンプルのプラグインですが、右スティックを有効化してみる は、わりと便利だと思うんですよ。なので残しましょう。

あまり役立たないのは 左右トリガーを有効化してみる のほうですね。なにせ、すぐ上にあるボタンと同じ動作をするだけですから… 今回はこれを改善してみましょう。

個人的に欲しいのは、アイテムやスキルへのダイレクトな移動ボタンです。戦闘後にちょっとヒットポイントを回復したり、解毒をしたりしたい時って多いんですが、毎回メニューを開いてから移動、選択ってちょっと面倒ですよね。ワンボタンでスキルを使用できるメニューを開けたら楽だと思うんです。

直接メニューを開くにはどうしたらいいか?

RPGツクールMV でゲーム全体の骨子となるのは、Sceneクラスです。以下は主なSceneの子クラスを、ゲーム進行にあわせて並べてみた図です。

image.png

今回、私たちのターゲットとなるのは、赤枠で囲った Scece_Item と Scece_Skill クラスです。これをうまく呼び出せれば、アイテムやスキルの利用画面を表示できます。

まずはその前段階、マップを移動中(Scece_Item)に、メニュー画面を開く(Scece_Menu)部分の処理をみてみましょう。

メニュー画面を開くロジック

rpg_scenes.js ファイルを開き、まずは Scene 実行の中心となる updateScene 関数をみていくと、メニューに関係しそうな更新ロジックがあることがわかります。

Scene_Map.prototype.updateScene = function() {
    this.checkGameover();
    if (!SceneManager.isSceneChanging()) {
        this.updateTransferPlayer();
    }
    if (!SceneManager.isSceneChanging()) {
        this.updateEncounter();
    }
    if (!SceneManager.isSceneChanging()) {
        this.updateCallMenu();  // ココ
    }
    if (!SceneManager.isSceneChanging()) {
        this.updateCallDebug();
    }
};

見つけた関数を更に見ていきます。

Scene_Map.prototype.updateCallMenu = function() {
    if (this.isMenuEnabled()) {
        if (this.isMenuCalled()) {
            this.menuCalling = true;
        }
        if (this.menuCalling && !$gamePlayer.isMoving()) {
            this.callMenu();
        }
    } else {
        this.menuCalling = false;
    }
};

Scene_Map.prototype.isMenuEnabled = function() {
    return $gameSystem.isMenuEnabled() && !$gameMap.isEventRunning();
};

Scene_Map.prototype.isMenuCalled = function() {
    return Input.isTriggered('menu') || TouchInput.isCancelled();
};

isMenuEnabled 関数で、いまメニューが使用可能かどうかを確認しています。$gameSystem.isMenuEnabled() はイベントコマンドの以下で設定・変更した値をみていますね。またイベントの実行中はメニューが開けないこともわかります。

image.png

isMenuCalled 関数が定期的に呼び出されることで、メニューを開くボタン(キー)が押されたかどうかを判断していることがわかります。Input.isTriggered('menu') は見慣れた処理ですね。

さてこれらのチェックを経て、メニューが開かれることが確定した場合に呼び出されるのが以下の関数です。

Scene_Map.prototype.callMenu = function() {
    SoundManager.playOk();
    SceneManager.push(Scene_Menu);  // ココ
    Window_MenuCommand.initCommandPosition();
    $gameTemp.clearDestination();
    this._mapNameWindow.hide();
    this._waitCount = 2;
};

SceneManager.push(Scene_Menu) という部分が実際にメニュー画面に遷移する指示です。SceneManager.goto ではなく SceneManager.push を使用しているのは、メニューが終了したら現在のマップに戻ってくるから、ですね。

アイテム画面を開くロジック

さてメニュー画面を開いたところで、そこから更にアイテム画面を開くロジックを確認してみましょう。

メニュー系のロジックを確認する際、まず見るべきはメニュー項目を設定している以下の関数です。

Scene_Menu.prototype.createCommandWindow = function() {
    this._commandWindow = new Window_MenuCommand(0, 0);
    this._commandWindow.setHandler('item',      this.commandItem.bind(this));      // ココ
    this._commandWindow.setHandler('skill',     this.commandPersonal.bind(this));  // ココ
    this._commandWindow.setHandler('equip',     this.commandPersonal.bind(this));
    this._commandWindow.setHandler('status',    this.commandPersonal.bind(this));
    this._commandWindow.setHandler('formation', this.commandFormation.bind(this));
    this._commandWindow.setHandler('options',   this.commandOptions.bind(this));
    this._commandWindow.setHandler('save',      this.commandSave.bind(this));
    this._commandWindow.setHandler('gameEnd',   this.commandGameEnd.bind(this));
    this._commandWindow.setHandler('cancel',    this.popScene.bind(this));
    this.addWindow(this._commandWindow);
};

まずはアイテムのほうに登録された commandItem 関数をみていきましょう。

Scene_Menu.prototype.commandItem = function() {
    SceneManager.push(Scene_Item);
};

むむっ、なんとシンプルな。まあアイテム画面も Scene なのだから、それに遷移すればアイテム画面の表示になりますよね。そして終了したら、このメニュー用の Scene に戻ってくるわけで。

ではスキル画面のほうはどうでしょうか。同様に登録された commandPersonal 関数をみていきましょう。

Scene_Menu.prototype.commandPersonal = function() {
    this._statusWindow.setFormationMode(false);
    this._statusWindow.selectLast();
    this._statusWindow.activate();
    this._statusWindow.setHandler('ok',     this.onPersonalOk.bind(this));  // ココ
    this._statusWindow.setHandler('cancel', this.onPersonalCancel.bind(this));
};

アイテム画面と異なり、こちらは Scene は遷移していません。画面はそのままで、_statusWindow のほうに制御を移しています。実際のゲーム画面で試してみると、そうでした、スキルの場合には以下のようにキャラタを選択する状態になりますね。

image.png

ここでキャラクタを選択すると、commandPersonal 関数で登録された onPersonalOk 関数が呼び出されます。そのコードを見てみると、

Scene_Menu.prototype.onPersonalOk = function() {
    switch (this._commandWindow.currentSymbol()) {
    case 'skill':
        SceneManager.push(Scene_Skill);  // ココ
        break;
    case 'equip':
        SceneManager.push(Scene_Equip);
        break;
    case 'status':
        SceneManager.push(Scene_Status);
        break;
    }
};

ここでやっとスキル用の画面(Scene_Skill)に遷移することがわかります。

まずは基本的な部分を

まず最初に、後編の 左右トリガーを有効化してみる で実施した変更を、少しアレンジしてみましょう。

    Input.gamepadMapper[6] = 'pageup';
    Input.gamepadMapper[7] = 'pagedown';

としていたところを、以下のようにメニューに変更します。

    Input.gamepadMapper[6] = 'menu-skill';
    Input.gamepadMapper[7] = 'menu-item';

ゲームを実行して試すと、左右のトリガーは動作しません。新しく追加した値に対応するロジックがないので、この時点ではこれで正しい動作です。

そして機能を実装しよう

さーて、ここからが本番です。新しく定義した値に対応するロジックは、どこに追加するのが最も簡単でしょうか?

いろいろな方法が考えられますが、マップ画面でメニューを実際に呼び出している、以下の関数を修正するのは悪くない気がします。

Scene_Map.prototype.updateCallMenu = function() {
    if (this.isMenuEnabled()) {
        if (this.isMenuCalled()) {
            this.menuCalling = true;
        }
        if (this.menuCalling && !$gamePlayer.isMoving()) {
            this.callMenu();
        }
    } else {
        this.menuCalling = false;
    }
};

ここで callMenu 関数が呼ばれるとメニュー画面に遷移するのですから、まずは入力を確認している isMenuCalled 関数を以下のように修正してみます。

Scene_Map.prototype.isMenuCalled = function() {
    return Input.isTriggered('menu') || Input.isTriggered('menu-item') || Input.isTriggered('menu-skill') || TouchInput.isCancelled();
};

これでゲームを実行してみると、再び左右のトリガーでメニューが開くようになったとおもいます。更に修正していきましょう。

Scene_Map.prototype.updateCallMenu = function() {
    if (this.isMenuEnabled()) {
        if (this.isMenuCalled()) {
            this.menuCalling = true;
        }
        if (this.menuCalling && !$gamePlayer.isMoving()) {
            if (Input.isTriggered('menu-item')) {          // 追加
                this.callMenuItem();                       // 追加
            } else if (Input.isTriggered('menu-skill')) {  // 追加
                this.callMenuSkill();                      // 追加
            } else {                                       // 追加
                this.callMenu();
            }                                              // 追加
        }
    } else {
        this.menuCalling = false;
    }
};

ちょっと追加した行が多いですが、落ち着いて眺めてみてください。これまで callMenu 関数を呼んでいた部分を、右トリガーなら callMenuItem 関数を呼びし、左トリガーなら callMenuSkill 関数を呼び出すように条件を追加しただけ、です。

このままでは callMenuItem 関数と callMenuSkill 関数が定義されていなくてエラーになりますので、callMenu 関数を真似して定義しておきましょう。

Scene_Map.prototype.callMenuItem = function() {
    SoundManager.playOk();
    SceneManager.push(Scene_Item);  // ココ
    Window_MenuCommand.initCommandPosition();
    $gameTemp.clearDestination();
    this._mapNameWindow.hide();
    this._waitCount = 2;
};
Scene_Map.prototype.callMenuSkill = function() {
    SoundManager.playOk();
    SceneManager.push(Scene_Skill);  // ココ
    Window_MenuCommand.initCommandPosition();
    $gameTemp.clearDestination();
    this._mapNameWindow.hide();
    this._waitCount = 2;
};

これで完成です!ゲームをプレイしてみて、右トリガーでアイテム画面が、左トリガーでスキル画面が出てくるのを確認してみてください。

image.png

この先の拡張

今回の改造は Input の定義のところで "menu-item" や "menu-skill" など新しいアクションを定義し、マップ画面で実際に入力を処理する部分に、それに対応するロジックを追加しました。

今回の拡張はわりと便利だとおもいます。同じようなやり方で、余っているボタンにいろいろな機能を設定してみてください。

前編 キーボードからの入力値 を参考に、Input.keyMapper に定義を追加して、iキーでアイテム画面、sキーでスキル画面を表示させても便利かも?

今回のプラグインのソース

今回の改造に加え、後編の右スティック有効化を加えたプラグインのソースを全て掲載しておきます。いろいろ改造して楽しんでみてください!

//=============================================================================
// RTK_Test.js  2016/07/30
// The MIT License (MIT)
//=============================================================================

/*:
 * @plugindesc テスト用プラグイン
 * @author Toshio Yamashita (yamachan)
 *
 * @help このプラグインにはプラグインコマンドはありません。
 * テスト用に作成したものなので、実際に利用する場合には適当にリネームしてください
 */

(function(_global) {
    // ここにプラグイン処理を記載

// ----- 前回の右スティックの有効化 -----

Input._updateGamepadState = function(gamepad) {
    var lastState = this._gamepadStates[gamepad.index] || [];
    var newState = [];
    var buttons = gamepad.buttons;
    var axes = gamepad.axes;
    var threshold = 0.5;
    newState[12] = false;
    newState[13] = false;
    newState[14] = false;
    newState[15] = false;
    for (var i = 0; i < buttons.length; i++) {
        newState[i] = buttons[i].pressed;
    }
    if (axes[1] < -threshold) {
        newState[12] = true;    // up
    } else if (axes[1] > threshold) {
        newState[13] = true;    // down
    }
    if (axes[0] < -threshold) {
        newState[14] = true;    // left
    } else if (axes[0] > threshold) {
        newState[15] = true;    // right
    }
    if (axes[3] < -threshold) {
        newState[12] = true;    // up
    } else if (axes[3] > threshold) {
        newState[13] = true;    // down
    }
    if (axes[2] < -threshold) {
        newState[14] = true;    // left
    } else if (axes[2] > threshold) {
        newState[15] = true;    // right
    }
    for (var j = 0; j < newState.length; j++) {
        if (newState[j] !== lastState[j]) {
            var buttonName = this.gamepadMapper[j];
            //if (buttonName == 'shift') {console.log(JSON.stringify(gamepad))}  // テスト用
            if (buttonName) {
                this._currentState[buttonName] = newState[j];
            }
        }
    }
    this._gamepadStates[gamepad.index] = newState;
};

// ----- ここから先が今回の実装 -----

    Input.gamepadMapper[6] = 'menu-skill';
    Input.gamepadMapper[7] = 'menu-item';

Scene_Map.prototype.isMenuCalled = function() {
    return Input.isTriggered('menu') || Input.isTriggered('menu-item') || Input.isTriggered('menu-skill') || TouchInput.isCancelled();
};

Scene_Map.prototype.updateCallMenu = function() {
    if (this.isMenuEnabled()) {
        if (this.isMenuCalled()) {
            this.menuCalling = true;
        }
        if (this.menuCalling && !$gamePlayer.isMoving()) {
            if (Input.isTriggered('menu-item')) {          // 追加
                this.callMenuItem();                       // 追加
            } else if (Input.isTriggered('menu-skill')) {  // 追加
                this.callMenuSkill();                      // 追加
            } else {                                       // 追加
                this.callMenu();
            }                                              // 追加
        }
    } else {
        this.menuCalling = false;
    }
};

Scene_Map.prototype.callMenuItem = function() {
    SoundManager.playOk();
    SceneManager.push(Scene_Item);  // ココ
    Window_MenuCommand.initCommandPosition();
    $gameTemp.clearDestination();
    this._mapNameWindow.hide();
    this._waitCount = 2;
};

Scene_Map.prototype.callMenuSkill = function() {
    SoundManager.playOk();
    SceneManager.push(Scene_Skill);  // ココ
    Window_MenuCommand.initCommandPosition();
    $gameTemp.clearDestination();
    this._mapNameWindow.hide();
    this._waitCount = 2;
};

})(this);

おわりに

以上、RPGツクールMVでキーボード、そしてゲームパッドからの入力を処理しているコードを順に見てきました。またそれを改造する簡単なプラグインを作成してみました。おまけの改造もしてみました。

楽しんでいただけましたでしょうか?

RPGツクールMVという楽しいツールと、学ぶ価値のあるすてきなライブラリを制作した開発者の皆さんに感謝します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away