RPGツクール系のコードは読みやすく、拡張しやすく設計・開発されていて、学習教材としても有用だとおもいます。全部を読むのは大変なので、何かお題を決めて、じっくりと眺めていきましょう。後編 や 特別編 もあります。
今回、対象としているのは RPGツクールMV ver1.5.2 です。
パッド入力について
RPGツクールMVで作成したゲームをUSB有線接続のXBOX360用コントローラーなどで操作すると、非常に快適にプレイすることができます。
なにも設定しなくても、フィールド上では左スティックでもDパッドでもキャラを上下左右に移動させることができます。またAボタンで決定、Bボタンでキャンセル、Xボタンでダッシュ移動、Yボタンでメニューが開きます。メニューからステータス画面を開くと、LボタンとRボタンでキャラページを変更できます。
少しだけ残念なのは、それ以外の入力手段、つまり右スティック、Backボタン、Startボタン、左右のトリガーが認識しないことです。
この仕組みはどんなふうに実現されており、どうすれば拡張できるのでしょうか。コードをのぞいて少し学んでみたいと思います。
Inputクラスの初期化
RPGツクールMVにおいて、入力を司るのは rpg_core.js で定義されている Input クラスです。
それが初期化される部分を探してみると、rpg_managers.js で定義されている SceneManager の初期化時に実行される SceneManager.initialize 関数にあることがわかります。この関数は最初に実行される main.js に記載された SceneManager.run から最初に呼び出される関数のため、実行初期に初期化されるのがわかります。
SceneManager.initialize = function() {
this.initGraphics();
this.checkFileAccess();
this.initAudio();
this.initInput(); // ココ
this.initNwjs();
this.checkPluginErrors();
this.setupErrorHandlers();
};
SceneManager.initInput = function() {
Input.initialize();
TouchInput.initialize();
};
あ、ちなみにタッチパネル関係は今回はスルーしますので、ご了承ください。思ったより長くなりそうなので…
Inputクラスの初期化コード
Inputクラスの初期化コードが以下になります。
Input.initialize = function() {
this.clear();
this._wrapNwjsAlert();
this._setupEventHandlers();
};
_wrapNwjsAlert は NW.js 環境におけるアラート表示を抑制するコードなので、今回はスルーします。それ以外で呼ばれている初期化コードの中身が以下です。
Input.clear = function() {
this._currentState = {};
this._previousState = {};
this._gamepadStates = [];
this._latestButton = null;
this._pressedTime = 0;
this._dir4 = 0;
this._dir8 = 0;
this._preferredAxis = '';
this._date = 0;
};
Input._setupEventHandlers = function() {
document.addEventListener('keydown', this._onKeyDown.bind(this));
document.addEventListener('keyup', this._onKeyUp.bind(this));
window.addEventListener('blur', this._onLostFocus.bind(this));
};
clear関数で初期化されている内部変数が、入力の状態を保持する主要なデータなようですね。名前からだいたいの機能は想像できますが、これから順に見ていきましょう。
と、その前に初期化時に設定される、2つの設定オブジェクトについて先に見ておきます。
ゲームパッドから入力値
コードを見ていくと、gamepadMapper オブジェクトが定義されており、これがゲームパッドからの入力の一覧のようです。A,B,X,Y,LB,RBの6ボタンと、4方向の入力がひとつあるのがわかります。
Input.gamepadMapper = {
0: 'ok', // A
1: 'cancel', // B
2: 'shift', // X (ダッシュ)
3: 'menu', // Y
4: 'pageup', // LB
5: 'pagedown', // RB
12: 'up', // D-pad up
13: 'down', // D-pad down
14: 'left', // D-pad left
15: 'right', // D-pad right
};
キーボードからの入力値
同様に keyMapper オブジェクトには、キーボードからの入力の一覧が定義されているのがわかります。9 などの数値はキーコードで、'tab' などの値は機能を示す文字列ですね。
Input.keyMapper = {
9: 'tab', // tab
13: 'ok', // enter
16: 'shift', // shift
17: 'control', // control
18: 'control', // alt
27: 'escape', // escape
32: 'ok', // space
33: 'pageup', // pageup
34: 'pagedown', // pagedown
37: 'left', // left arrow
38: 'up', // up arrow
39: 'right', // right arrow
40: 'down', // down arrow
45: 'escape', // insert
81: 'pageup', // Q
87: 'pagedown', // W
88: 'escape', // X
90: 'ok', // Z
96: 'escape', // numpad 0
98: 'down', // numpad 2
100: 'left', // numpad 4
102: 'right', // numpad 6
104: 'up', // numpad 8
120: 'debug' // F9
};
'tab' や 'debug' などゲームパッドに無い操作が可能なことがわかります。また 'escape' はゲームパッドにおける 'cancel' と 'menu' を兼ねた動作をするようです。
キーボードからの入力イベント
さてそれでは、初期化時に設定されたイベントハンドラを見ていきましょう。まずはキーが押された時。
Input._onKeyDown = function(event) {
if (this._shouldPreventDefault(event.keyCode)) {
event.preventDefault();
}
if (event.keyCode === 144) { // Numlock
this.clear();
}
var buttonName = this.keyMapper[event.keyCode];
if (ResourceHandler.exists() && buttonName === 'ok') {
ResourceHandler.retry();
} else if (buttonName) {
this._currentState[buttonName] = true;
}
};
イベントの preventDefault は、その後のイベント処理をキャンセルします。キャンセルする対象は以下の関数を見るとわかりますね。
Input._shouldPreventDefault = function(keyCode) {
switch (keyCode) {
case 8: // backspace
case 33: // pageup
case 34: // pagedown
case 37: // left arrow
case 38: // up arrow
case 39: // right arrow
case 40: // down arrow
return true;
}
return false;
};
'page up' や 'down arrow' など画面に影響を与えそうなキーの本来の機能を無効化しているようにみえますね。特に'backspace' はブラウザ環境では「戻る」のアクションが割り当てられていることが多いので、無効化しておかないと面倒な気がします。
その後の Numlock は特殊なキーで、キーボードの配置が変わるので Input クラスを初期化しています。これはなるほど、という感じですね。
さてその後に、さきほど見た keyMapper オブジェクトが出てきました。基本的には、得られた機能を示す文字列(buttonName)を使って
var buttonName = this.keyMapper[event.keyCode];
this._currentState[buttonName] = true;
と押されたことを記録するだけ、なんですが… その前の ResourceHandler が気になりますね。'ok'ボタンを押した時に、何らかの条件があるとそちらの retry() を実施する、ようなのですが…
ResourceHandler とは何か?
ResourceHandler は同じ rpg_core.js で定義されている、ネットワークからリソース(音楽や画像データ)をロードするための補助クラスです。ローカルの場合にはファイルシステムからの読み込みになります。
ResourceHandler.createLoader 関数で、特定の URL からデータをダウンロードすることができ、同時に複数のデータをダウンロードできます。ダウンロード中のものは内部で _reloaders 配列に保存されています。
ですので以下の関数は、現在ダウンロード中のリソースの数を返します。
ResourceHandler.exists = function() {
return this._reloaders.length > 0;
};
リソースのダウンロード中は、ゲーム画面は停止した状態になります。先ほどの Input._onKeyDown で 'ok' ボタンが押されたとき、ResourceHandler.exists 関数をチェックしていたのは「もし画面が制止したダウンロード中の場合にokキーが押されたら、ダウンロード処理をリトライする」という例外的な処理を実行するため、でした。
キーボードからの入力処理の続き
さて、Input._onKeyDown 関数の動きが理解できたところで、イベントハンドラとして指定された残りの2つの関数についても見ていきましょう。
Input._onKeyUp = function(event) {
var buttonName = this.keyMapper[event.keyCode];
if (buttonName) {
this._currentState[buttonName] = false;
}
if (event.keyCode === 0) { // For QtWebEngine on OS X
this.clear();
}
};
Input._onLostFocus = function() {
this.clear();
};
と、これらは簡単でしたね。
_onKeyUp 関数は離したキーに対応する機能のステータスを false に戻しているだけですし、フォーカスを失った場合の _onLostFocus 関数は Numlock キーの時と同様にステータスをクリアしているだけです。
でも、これでキーボードの入力を処理するロジックを理解できました。
もうひとつあったキーボード入力
と、これで安心してはいけません。念のために addEventListener で検索してみると、同じ rpg_core.js ファイル内にもうひとつハンドラが定義されています。
Graphics._setupEventHandlers = function() {
window.addEventListener('resize', this._onWindowResize.bind(this));
document.addEventListener('keydown', this._onKeyDown.bind(this)); // コレ
document.addEventListener('keydown', this._onTouchEnd.bind(this));
document.addEventListener('mousedown', this._onTouchEnd.bind(this));
document.addEventListener('touchend', this._onTouchEnd.bind(this));
};
Graphics._onKeyDown = function(event) {
if (!event.ctrlKey && !event.altKey) {
switch (event.keyCode) {
case 113: // F2
event.preventDefault();
this._switchFPSMeter();
break;
case 114: // F3
event.preventDefault();
this._switchStretchMode();
break;
case 115: // F4
event.preventDefault();
this._switchFullScreen();
break;
}
}
};
というわけでこれは、画面表示周りの特殊な設定キーですね。例えば実行中に F4 キーを押すと、全画面表示になる、といった機能がここで実現されています。
かなりレアだとおもいますし、画面周りはまた別の機会に見ていきたいので、こちらに関しての説明はこれだけに留めておきますね。
パッド入力のハンドラは?
キーボード入力はイベントをハンドラで受け取り、処理してきました。でも不思議なことに、パッド入力用のハンドラを定義した部分がありません。どうして?
実は残念なことに、パッド入力に対応したイベント発生はなく、よってハンドラの登録もできません。イベント発生が無い場合はどうすれば良いか?それはポーリングです。
ま、砕いて言えば「入力をそちらから教えてくれないのであれば、こちらから頻繁に見に行って値を確認する」という感じになります。イベントの仕組みが実装される前から使われてきた、わりと古典的なやり方なのです。
さて、RPGツクールMVのコードの中で、定期的に実行される関数の名前はほぼ決まっています。これです!
Input.update = function() {
this._pollGamepads();
if (this._currentState[this._latestButton]) {
this._pressedTime++;
} else {
this._latestButton = null;
}
for (var name in this._currentState) {
if (this._currentState[name] && !this._previousState[name]) {
this._latestButton = name;
this._pressedTime = 0;
this._date = Date.now();
}
this._previousState[name] = this._currentState[name];
}
this._updateDirection();
};
そしてこの関数を定期的に呼び出してくれるのは、rpg_managers.js ファイルで定義されているこちら
SceneManager.update = function() {
try {
this.tickStart();
if (Utils.isMobileSafari()) {
this.updateInputData();
}
this.updateManagers();
this.updateMain();
this.tickEnd();
} catch (e) {
this.catchException(e);
}
};
SceneManager.updateInputData = function() {
Input.update();
TouchInput.update();
};
ちなみに SceneManager.update 関数は requestAnimationFrame に登録されるので、各フレームの表示ごと(秒間に60回とか30回とか)に呼び出されることになります。格闘ゲーム以外なら入力状態のチェックには十分な間隔ですね。
SceneManager.requestUpdate = function() {
if (!this._stopped) {
requestAnimationFrame(this.update.bind(this));
}
};
Input._pollGamepads 関数のしくみ
Input.update 関数がパッド入力を司っているようで、まあ、今回のメインターゲットという感じでしょうか。じっくり見ていきましょう。
まず最初に呼んでいるのは _pollGamepads 関数です。先ほど説明したポーリング動作用をするための関数なので、この名前。関数名の付け方も適切で読んでいて嬉しくなりますね。
Input._pollGamepads = function() {
if (navigator.getGamepads) {
var gamepads = navigator.getGamepads();
if (gamepads) {
for (var i = 0; i < gamepads.length; i++) {
var gamepad = gamepads[i];
if (gamepad && gamepad.connected) {
this._updateGamepadState(gamepad);
}
}
}
}
};
まずイキナリですがこの関数、navigator.getGamepads 機能に依存していることがわかります。リンク先をみてもらうとわかるのですが、IE/Safari では未サポートになっているのが気になります。
そして私も今知ったのですが、この関数では繋がっているゲームパッドを全部見にいくようになっている模様… 複数のゲームパッドを繋げていると、どれでも操作できるのかもしれません。(注:後編でもう少し詳しく調べます)
さて、それぞれのゲームパッドの状態に対して実行される _updateGamepadState 関数を見てみましょう。
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
}
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;
};
まずは引数の gamepad の値を知りたいので、上記のコードにテスト用の機能を足しています。ダッシュ(X)ボタンの状態が変化したときに限り、コンソールに値を出力するだけのシンプルな1行。Aボタンと上を押しながら実行した結果は次のような感じ。
buttons は16個の配列で、Dパッド(十字キー)は12~15の要素が対応することがわかります。まあこのあたりの詳細は、最初のほうで出てきた Input.gamepadMapper オブジェクトをもう一度見てもらうと、わかりやすいと思います。
ちなみに定義のないbuttonsの6~11だが、これはゲームパッドによって異なるとおもわれます。例えば私のXBOX360有線コントローラーだと、6がLトリガー、7がRトリガー、8がback、9がStart、10が左アナログスティックの押下げ、11が右アナログスティックの押下げ、が該当しました。
axes の配列の4つの値は左右のアナログスティックの状態で、axes[0]が左スティックの上下、axes[1]が左スティックの左右を示している模様。右スティックの値は使用されていないです。
ここのif文二つの処理があるおかげで、左アナログスティックがDパッドと同様にゲームで利用できることがわかりますね。
例えばこの2つのif文をコピペして増やし、axes[3]とaxes[2]にすれば、右アナログスティックも同様に利用できるようになるはず。飲み食いしながら右手だけでプレイしたい人には喜ばれる改造かも?
また閾値である threshold が 0.5 であるので、アナログスティックは半分以上倒さないと反応しない。固めのスティックだとちと大変かもしれないので、この値を変更できるようなプラグインがあると喜ぶ人も居るかもしれない?
Input.update 関数のしくみ
_pollGamepads 関数で時間をとられましたが、残りの部分も見ていきましょう。
その後の処理で面白いのは、最後に状態の変わったボタン(に関連付けられた機能ストリング)が _latestButton に保持されることです。その際に _pressedTime と _date もセットされます。これらの値の使われ方は、以下の関数を見ると理解し易いでしょう。
Input.isLongPressed = function(keyName) {
if (this._isEscapeCompatible(keyName) && this.isLongPressed('escape')) {
return true;
} else {
return (this._latestButton === keyName &&
this._pressedTime >= this.keyRepeatWait);
}
};
これは、あるボタン(機能)が長押しされたかどうかを確認する関数ですが、ここで記録しておいた _latestButton や _pressedTime を使って長押しかどうかを確認しています。
またここでも使われている _isEscapeCompatible は面白くて、'escape' 機能を便利キーにしているのはこの関数のおかげです。最初のほうで「キーボードのESCキーはゲームパッドのBとYの両方の機能があるね」と言っていたアレです。
Input._isEscapeCompatible = function(keyName) {
return keyName === 'cancel' || keyName === 'menu';
};
ね、そうでしょう。
とりあえず前編は完了
以上、RPGツクールMVでキーボード、そしてゲームパッドからの入力を処理しているコードを順に見てきました。どこで何をやっているか、だいたい理解できたでしょうか?
理解しただけではつまらないので、後編 では、この知識を生かして何か改造、もしくはプラグインの作成をやってみたいとおもいます。
以前のコード読みのメモは jgss-hack や RPGツクールMV JGSS 技術メモ にもあります。だいぶ前のものもあるので、時間があったら書き直した版を Qiita で公開できると良いのですが。