RPGツクールMV

隊列の変更ショートカットキープラグイン【RPGツクールMV】

なにこれ

マップ画面でQ/W(PageUp/PageDown)に相当するキーを入力すると先頭キャラを入れ替えてくれるプラグインです。既存のものとしてはボタンひとつでキャラチェンジできるプラグインがあるようです。私が気づいた時にはダウンロード不能になっていたのでソースコードは確認できていません。

ソースコード

dice2000_changeLeader.js
例によってGistにぶん投げました。煮るなり焼くなり好きにしろライセンスなので好き勝手してください。

解剖

以上で本題はすべて終わってしまっているのですが、せっかくのQiitaなんで復習がてら中身を解説していきます。だいたいMITライセンスプラグインの実装を覗き見てコピペしてすげ替えているだけなんで、自分が何やったかすぐ忘れますしね。

操作する対象の特定

隊列を変更するプラグインを書きたいのですから、まずはメニューの「並べ替え」が何やっているかを見ます。

rpg_scenes.js
Scene_Menu.prototype.onFormationOk = function() {
    var index = this._statusWindow.index();
    var actor = $gameParty.members()[index];
    var pendingIndex = this._statusWindow.pendingIndex();
    if (pendingIndex >= 0) {
        $gameParty.swapOrder(index, pendingIndex);
        this._statusWindow.setPendingIndex(-1);
        this._statusWindow.redrawItem(index);
    } else {
        this._statusWindow.setPendingIndex(index);
    }
    this._statusWindow.activate();
};

で、$gameParty.swapOrderが入れ替えの本体らしいなというので、そっちに飛びます。

rpg_objects.js
Game_Party.prototype.swapOrder = function(index1, index2) {
    var temp = this._actors[index1];
    this._actors[index1] = this._actors[index2];
    this._actors[index2] = temp;
    $gamePlayer.refresh();
};

ここから分かるのは、

  • 隊列の情報はGame_Party._actorsというプロパティ(配列)に入っている。Game_Party._actorsの要素は「アクターID(Game_Actor._actorId)」。
  • 順序を入れ替えた後に$gamePlayer.refresh()で更新する必要がある

以上の二点です。

さて、キーを押したら先頭を入れ替える場合、どの順番にキャラが出てくるのが良いでしょうか。隊列歩行(メニュー画面で確認できる隊列)の順番が一番分かりやすいでしょう。
というわけでGame_Party._actorsの配列の要素を順繰りに入れ替える関数が欲しいですね。こういうのを「循環シフト」とか「巡回シフト」と言うらしいです。

循環シフトを作ろう

var a = [1, 2, 3, 4];

//最後尾を前に押し出す
var b = a.pop();//4
a.unshift(b);//[4,1,2,3]

//次の要素を前に押し出す
b = a.shift();//4
a.push(b);//[1,2,3,4]

pop/push/shift/unshiftを使えばこの通り(参考:unshift, shift, pop, pushが混乱するので、絵で整理した)。Rubyにはrotateという便利なメソッドがありまして、JavaScriptでも環境によっては実装されているようなんですが、MVで動かなかったので深追いしてません。仮にrotateが動いた所で2行が1行になるだけですし、別に構わないでしょう。

//ずらす方向
// +1か-1かを入れる
// -1 …後ろが前にくる
// +1 …前が後ろに行く
var temp_ary = this._actors;
var temp_e = 0;
//次の要素を前に
if(direction > 0){
    temp_e = temp_ary.shift();
    temp_ary.push(temp_e);                                
//最後尾を前に
}else{
    temp_e = temp_ary.pop();
    temp_ary.unshift(temp_e);
}
this._actors = temp_ary;
$gamePlayer.refresh();

ちなみに、for/forEachで作った循環シフトもあります。
//ずらす方向
// +1か-1かを入れる
// -1 …後ろが前にくる
// +1 …前が後ろに行く
var last_index = this.size() - 1;
var temp = 1;
if (direction > 0){
    temp = this._actors[last_index];
}else{
    temp = this.leader().actorId();
}
if(direction > 0){
    for(var i = last_index; i >= 0; i--){
        this._actors[i] = this._actors[i - 1];
    }            
}else{
    this._actors.forEach(function(value, index, actors) {
        if(index != 0){
            actors[index - 1] = actors[index];
        }
    });            
}
if (direction > 0){
    this._actors[0] = temp;
}else{
    this._actors[last_index] = temp;
}

先頭/末尾を変数で保護してからループ処理で要素を移動して、保護した要素をもう一回代入する、ごくありきたりなやり方です。

戦闘不能キャラを先頭にしない

既存のものを調べていたらそういう要望が出ていたので、オプション設定として考えてみました。
循環シフトを終えた後、先頭キャラが死亡しているようならばもう一回シフトをすることになりますね。ということは、循環シフトの処理自体をループで囲ってしまわなくてはいけません。

//ずらす方向
// +1か-1かを入れる
// -1 …後ろが前にくる
// +1 …前が後ろに行く
//while文用の真理値を用意
var loopcheck = true;
var temp_ary = this._actors;
var temp_e = 0;
while (loopcheck) {
    //console.log(temp_ary);
    //次の要素を前に
    if(direction > 0){
        temp_e = temp_ary.shift();
        temp_ary.push(temp_e);                                
    //最後尾を前に
    }else{
        temp_e = temp_ary.pop();
        temp_ary.unshift(temp_e);
    }
    //プラグインパラメータの指定で「死亡したキャラをスキップする」を予め設定しておく
    //入れ替えた結果を見て必要があればループ
    if(!(ParamSkipDeadMembers && $gameActors.actor(temp_ary[0]).isDead())){
        loopcheck = false;
    }            
}
this._actors = temp_ary;
$gamePlayer.refresh();

$gameActorsはアクターのデータベースで、actorメソッドにアクター番号を入れると対応するGame_Actorオブジェクトを返します。で、Game_Actorが死亡しているか否かを調べるメソッドを使って判定します。
こんなwhile文の使い方をしていいのか?と思ったんですが、JavaScriptにはRubyで言うloopはないんですね……という程度の人です。はい。

戦闘に参加するメンバーのみを入れ替える

ショートカットキーで入れ替える場合は、控えメンバーを出したくないこともあるかもしれません。控えメンバーがいる(=戦闘参加人数よりパーティの人数が多い)場合には、戦闘参加者の配列を取り出して、そこで循環シフトをするようにしましょう。

//ずらす方向
// +1か-1かを入れる
// -1 …後ろが前にくる
// +1 …前が後ろに行く
//while文用の真理値を用意
var loopcheck = true;
//プラグインパラメータの指定で「戦闘参加者のみを切替対象とする」を予め設定しておく
//パーティー人数が戦闘参加者よりも多いかどうかをチェック
var splitMembers = ParamLoopBattleMembers && (this.size() > $gameParty.maxBattleMembers());
var temp_ary = this._actors;
var temp_e = 0;
//条件を満たした場合、配列を切り出す
if(splitMembers){
    //部分配列の取り出し
    temp_ary = this._actors.slice(0, $gameParty.maxBattleMembers());
    //削除しておく
    this._actors.splice(0, $gameParty.maxBattleMembers());
}
while (loopcheck) {
    //次の要素を前に
    if(direction > 0){
        temp_e = temp_ary.shift();
        temp_ary.push(temp_e);                                
    //最後尾を前に
    }else{
        temp_e = temp_ary.pop();
        temp_ary.unshift(temp_e);
    }
    //プラグインパラメータの指定で「死亡したキャラをスキップする」を予め設定しておく
    //入れ替えた結果を見て必要があればループ
    if(!(ParamSkipDeadMembers && $gameActors.actor(temp_ary[0]).isDead())){
        loopcheck = false;
    }            
}
//切り出した配列を結合する破壊的メソッド
if(splitMembers){
    Array.prototype.push.apply(temp_ary, this._actors);
}
this._actors = temp_ary;
$gamePlayer.refresh();

「戦闘不能キャラを先頭にしない」+「戦闘に参加するメンバーのみを入れ替える」は併用できます。

その他

隊列の変更ショートカットキーを押した時の効果音が個別に設定できるようにしてありますが、別段面白いものじゃないので解説はカットします。

だいたい書き終えたところで、マップ画面で追加のキー入力処理を拾う実装の解説も必要だろうかと思い始めましたが……いろんな方面に応用がきいてしまう話題なので、解説を書くことにある程度の自信が持ててからにします。