RPGツクールプラグイン制作過程紹介 第4回
前回の続き
前回は「プレイヤーの直前にイベントが存在するかどうかの判定関数」を探していました。今回もそれを続けていきます。
次の関数を見てみましょう。すぐ下にあるcheckEventTriggerThereですね。
Game_Player.prototype.checkEventTriggerThere = function(triggers) {
if (this.canStartLocalEvents()) {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
this.startMapEvent(x2, y2, triggers, true);
if (!$gameMap.isAnyEventStarting() && $gameMap.isCounter(x2, y2)) {
const x3 = $gameMap.roundXWithDirection(x2, direction);
const y3 = $gameMap.roundYWithDirection(y2, direction);
this.startMapEvent(x3, y3, triggers, true);
}
}
};
最初のif文は先ほどと同じですね。飛行船に乗っていないかどうかの判定です。
その後にconstによる定数の代入が続いています。上から見ていきましょう。
const direction = this.direction();
これはdirectionという名前の定数にthis(=プレイヤー)のdirection関数の戻り値を代入する、という処理ですね。この関数は今後とてもよく出てくるので、覚えてしまいましょう。これはキャラクターの「向き」を取得するための関数です。directionという英単語自体が「向き」という意味ですね(多義語なので「指示」などの意味もありますが、少なくともツクールでこの語が使用されるときは間違いなく「向き」の意味で使われていると考えてよいでしょう)。
ツクールではプレイヤーやイベントに上下左右の向きがありますよね。ですので、その値を保持する入れ物(プロパティ)が用意されています。そのプロパティの値を返すための関数がこのdirection()です。
なおこの関数の定義文までさかのぼって見れば分かりますが、そのプロパティの名前は_directionといいます。なぜアンダースコアがついているのか、とかなぜthis._directionという具合にプロパティを直接取得しないのか、といった疑問をお持ちの方もいらっしゃるでしょう。これこそが「オブジェクト指向プログラミング」の原理を体現している部分なのですが、これについてはあまりにも深すぎるので本記事では触れません。今後の回でその片鱗だけでもご紹介できたらいいな、と漠然と考えています。
本題に戻りますと、つまりこの処理はプレイヤーの向きをdirectionという名前の定数に代入している、ということですね。
続いて次の行です。
const x1 = this.x;
const y1 = this.y;
これは非常にシンプルですね。プレイヤーのX・Y座標をそれぞれx1・y1という名前の定数に代入しているだけです。ちなみに1とついているのはこの関数内にXY座標が他にも登場するので区別のためです。続けます。
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
これはx2・y2という名前の定数に代入するという、上とよく似た処理ですが、右辺が大きく異なりますね。これは$gameMapのroundXWithDirectionという関数に、これまでに用意してきた定数x1とdirectionを引数として呼び出し、その関数の戻り値をx2に代入する、という処理です(もちろん下はyバージョン)。
$gameMapというのはマップのことなので、Game_MapクラスにroundXWithDirectionという関数が用意されているはずです。ではおなじみの検索をしてみましょう。
一番上がこの関数の定義文のようですね。早速選択してみましょう。
thisはこの場合マップ($gameMap)のことですが、その後ろにやや複雑な構文が使用されています。解読用に、以下のようにしてみます。
return this.roundX(演算結果);
カッコの中身は一つの塊です。演算した結果を引数として、roundXという関数を呼び出している、ということなのです。ではどのような演算なのでしょうか。
x + (d === 6 ? 1 : d === 4 ? -1 : 0)
これは、xに右側のカッコの結果を足す、という処理ですね。ではカッコの中身はなんなのかというと、この解読のためには三項演算子について知っておく必要があります。
三項演算子とは平たくいうと「一行で書けるif文」です。…厳密には全く違うのですが、最初のうちはそう覚えてしまいましょう。以下のような構文を取ります。
a ? b : c
これは「aが真ならb、偽ならcを最終的な値とする」という意味です。例えば、次の二つは同じ結果になります。
const condition = true;
let value;
value = condition ? 1 : 0;
const condition = true;
let value;
if (condition) {
value = 1;
} else {
value = 0;
}
上も下も、valueという変数に代入される値は同じです。そう、1ですね。
conditionにはtrueが代入されています。conditionが真なら「 : 」の左側が最終的な値になるので、結果的にvalueに代入されるのは1、というわけです。
三項演算子はif文と同等の内容を一行にまとめることができるので非常にコードがスリムになる、という効果があります。そのためコアスクリプトでも多用されています。徐々に慣れていくとよいでしょう。
今回はこの三項演算子が入れ子になっていて分かりづらいので、以下のように変形してみます。
x + ①
① = (d === 6) ? 1 : ②
② = (d === 4) ? -1 : 0
xに①の演算結果を足すというのが、この演算の最終的な処理です。
①はdが6であれば1になります。それ以外の場合、②の演算結果になります。
②は、dが4であれば-1に、それ以外の場合0になります。
まとめると、以下のようになります。
dが6の場合、x + 1
dが4の場合、x - 1
それ以外の場合、 x + 0
この値を引数としてroundXという関数を呼び出す、というのがroundXWithDirectionという関数の処理です。
ではdとはなんでしょうか? 改めてこの関数を見てみましょう。
Game_Map.prototype.roundXWithDirection = function(x, d) {
return this.roundX(x + (d === 6 ? 1 : d === 4 ? -1 : 0));
};
ご覧の通りdはもともとこの関数の第2引数でしたね。では、ここには何が代入されて呼び出されていたでしょうか? もともとこの関数はGame_PlayerのcheckEventTriggerThere関数内で呼び出されていました。改めてその部分を見てみましょう。
// 〜中略〜
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
roundXWithDirectionの第2引数に代入されているのはdirection、つまりプレイヤーの向きです。つまりこの関数はプレイヤーの向きに応じてプレイヤーのX座標に1、-1、0のいずれかを足した値を引数としてroundX関数を呼び出している、という処理をしているようです。
ところでdirectionは先ほど確認したところ6の場合、4の場合、それ以外の場合の処理が用意されていました。この変数はプレイヤーの向きを表しているとのことでしたが、なぜ数字なのでしょうか?
これはもうそういう決めごとなのですが、キャラクターの向きを表すプロパティ_directionの値は以下の数字のいずれかをとります。
上:8
右:6
左:4
下:2
なぜこのような対応なのかというと、フルキーボードをお使いの方はテンキーをご覧ください。2、4、6、8のキーに矢印が印字されていませんか?(印字されていないこともあります)
そうです、電卓式に配置した際の上下左右方向に対応するキーの数字が_directionの値として使用されているのです。
ツクールのイベントコマンドでは「プレイヤーが上を向いているとき」などの、キャラクターの向きに関する条件分岐をよく使いますが、実際には以下のように判定されているのです。
if ($gamePlayer.direction() === 8)
ということでroundXWithDirectionがroundX関数に代入している値は、結局以下のような意味です。
キャラクターが右を向いているならそのキャラクターのX座標+1の値
キャラクターが左を向いているならそのキャラクターのX座標-1の値
それ以外の場合はそのキャラクターのX座標そのままの値
ツクールのマップ座標において、X座標を加算するということはその座標よりも右ということですよね。もちろん減算するということは左です。
これでもう大体お分かりの方もいらっしゃるかと思います。あとはroundX(roundY)という関数が何をやっているのか、を見てみましょう。検索で探してみます。
Game_Map.prototype.roundX = function(x) {
return this.isLoopHorizontal() ? x.mod(this.width()) : x;
};
Game_Map.prototype.roundY = function(y) {
return this.isLoopVertical() ? y.mod(this.height()) : y;
};
何やら複雑な処理をしています。
答えをいきなり言ってしまいますが、これはマップのループを考慮したX(Y)座標取得関数です。
ツクールではマップをループさせることができますよね。マップ右端からさらに右に移動すると左端に移動する、というあれです。
これは、マップが「横方向にループする」に設定されている場合それを考慮したX座標を、それ以外の場合X座標をそのまま返す、という関数なのです。
例えば横方向にループする、幅が17のマップがあるとします。
「マップ右端のX座標はマップ幅より1少ない値(この場合16)である」というツクールの仕様はご存知かと思います。
この座標からさらに右に1歩進んだ場合、X座標はいくつになるでしょうか? 17?
…正解は0です。マップの左端の座標は0ですよね? なのでループすると、0に戻るわけです。
roundXはマップがループしている場合はこのような値を返すのですが、ループしないマップの場合はxの値をそのまま返します。なのでこの関数は非ループマップの場合、無視して問題ありません。というよりもループマップの場合も特に気にする必要はありません。これについて解説しだすとあまりにも長くなってしまうので、ここでは割愛します。気になる方はぜひ調べてみてください。
これでroundXWithDirectionの処理内容については全て理解できたはずです。まとめてみます。
Game_Map.prototype.roundXWithDirection = function(x, d) {
return this.roundX(x + (d === 6 ? 1 : d === 4 ? -1 : 0));
};
Game_Map.prototype.roundYWithDirection = function(y, d) {
return this.roundY(y + (d === 2 ? 1 : d === 8 ? -1 : 0));
};
この関数はプレイヤーのX座標と向きをもとに、右向きの場合右に、左向きの場合左に1マス進んだX座標を返します。上向きか下向きの場合X座標をそのまま返します。
もちろんroundYWithDirectionはY座標版で、プレイヤーが下向きであれば下に、上向きであれば上に1マス進んだY座標を返します。右向きか左向きの場合Y座標をそのまま返します。
というわけで、「プレイヤーの直前の座標」を取得する関数はこれで発見できましたね。checkEventTriggerThere関数には続きがありますので見てみましょう。
this.startMapEvent(x2, y2, triggers, true);
if (!$gameMap.isAnyEventStarting() && $gameMap.isCounter(x2, y2)) {
const x3 = $gameMap.roundXWithDirection(x2, direction);
const y3 = $gameMap.roundYWithDirection(y2, direction);
this.startMapEvent(x3, y3, triggers, true);
}
前回見たstartMapEvent関数にx2とy2を代入して呼び出しています。つまりプレイヤーの直前の座標にイベントがあり、トリガーを満たしていればそのイベントを起動せよ、という意味です。これこそ求めている処理ですね!
ちなみにその下のif文の処理は、カウンター判定のために用意されているものです。マップタイルにカウンター属性が設定されている場合、カウンター越しにイベントに話しかけることができますよね。あの処理をしているのがこの部分で、x2とy2を引数にしてroundXWithDirection(roundYWithDirection)を呼び出してx3・y3という定数に代入しています。つまりプレイヤーの2マス先の座標を取得しているわけですね。これによってカウンター越しのイベントを参照できるというわけです。
さてお目当ての関数が見つかりましたが、厳密にはこれだけでは足りません。これはあくまでも決定ボタン使用時のイベント起動に関する関数なのですが、その先にイベントがあるかどうかの判定はこの関数の中ではなく、startMapEvent関数に任せています。もう一度この関数を見てみましょう。
Game_Player.prototype.startMapEvent = function(x, y, triggers, normal) {
if (!$gameMap.isEventRunning()) {
for (const event of $gameMap.eventsXy(x, y)) {
if (
event.isTriggerIn(triggers) &&
event.isNormalPriority() === normal
) {
event.start();
}
}
}
};
最初のif文は、$gameMap.isEventRunning()の戻り値を真偽反転させた結果が真なら中身を実行する、という処理です。ではこれはなんなのかというと、これも名前で大体推測できるのではないでしょうか。そうです、「イベントが起動しているかどうか」を判定しているGame_Mapの関数なのです(ちなみにrunという英単語には「走る」という意味のほかに「起動する」とか「実行する」という意味もあり、プログラミングではこの意味で使われることが非常に多いです)。つまりあるイベントが実行されている最中に別のイベントまで起動してしまうとこんがらがってしまうので「イベント実行中は何もしない」ということを実現するための処理です。
例えばプレイヤーの目の前にイベントがあって、決定ボタンで話しかけたとします。そのイベントにメッセージが設定されている場合、決定ボタンでメッセージを送りますよね? この決定ボタンを押したことでそのイベントのトリガーをもう一度引いてしまったとしたら、二重にイベントが起動してしまうのでは、と思いませんか? 実際にはそんなことは起きないわけですが、それはこのif文のおかげなのです。
続いてその中身を見てみましょう。
for (const event of $gameMap.eventsXy(x, y)) {
// 処理
}
これはfor文ですね。ofの後ろ(ここでは$gameMap.eventsXy(x, y))が配列(など)になっていて、それを一つずつ取り出してeventという定数に代入した上で処理を繰り返します。
それではeventsXyとはどのような関数なのでしょうか? 検索してみましょう。
似たような名前の関数が複数ありますが、一番上が定義箇所です。ジャンプしてみましょう。
Game_Map.prototype.eventsXy = function(x, y) {
return this.events().filter(event => event.pos(x, y));
};
Game_Mapのeventsという関数を呼び出し、それをフィルタリングしているようです。
eventsとは、そのマップ上に存在する全てのイベントが含まれた配列を返す関数です。その配列内のイベントにposという関数を実行させ、結果が真になったイベントだけを含む配列を新しく生成して返す、というのがこの処理です(filterという配列の関数を利用)。posとはXY座標を引数として、キャラクターがその座標に位置しているかどうかを返す関数です。
つまりeventsXyとは「引数であるXY座標に位置している全てのイベントの配列を返す」という関数なのです。なぜ配列なのかというと、同じ座標に複数のイベントが存在するというケースがありうるからです。すり抜け可能なイベントが移動によって重なった場合などですね!
ちなみに配列を返す関数の名前は、event"s"のように複数形を使うというのがよく用いられる慣習です。今後コードを書くときに意識することがあるかと思います。
さて、for文の中身、つまり繰り返すことになる処理とはどのようなものでしょうか。
if (
event.isTriggerIn(triggers) &&
event.isNormalPriority() === normal
) {
event.start();
}
これはイベントのトリガーとプライオリティを判定し、条件を満たしていればイベントを起動する、という処理です。その処理を同じ座標に位置している全てのイベントに対して行うわけですね。
とても長くなってしまいましたが、これで「プレイヤーの直前にイベントが存在するかどうかの判定」をツクール標準ではどのように実装しているのかがわかりました。以下にまとめます。
1. プレイヤーがマップ上で決定ボタンを押す
2. checkEventTriggerThere関数が呼び出される
3. 同関数内でroundXWithDirection(roundYWithDirection)が呼び出され、プレイヤーの直前のXY座標が取得される
4. 3を引数としてstartMapEvent関数が呼び出される
5. 3のXY座標に位置しているマップイベントを全て取得する
6. 5のイベントそれぞれに対しイベントの起動可能判定を行い、条件を満たしているならそのイベントを起動する
今回はあくまでも決定ボタンをトリガーとしてイベントを起動したいのではなく、スキル(およびそれに設定されているコモンイベント)をトリガーとして起動したいわけなので、上記には不要な処理もあります。なので必要な部分だけ抜き出して新しい関数を作ってあげれば、目的が果たせそうですね。
関数の作成
関数を作る前に、実際にどのような処理にするのかを考えてみましょう。
第2回では、以下のようなコモンイベントにしていました。
条件分岐の部分が煩雑なので、以下のようにしてみたいですね。
このようにすればマップごとや、イベントごとに条件分岐を組む必要がなくなるので、極めてスマートな処理になりますね。
ではその関数を実際に組んでいきましょう。
その前にFieldAction.jsに貼り付けてあったstartMapEvent関数ですが、これはもう使わないので削除ないしコメントアウトしてしまいましょう。
VSCでのコメントアウトは当該箇所を選択した状態でCtrl(command) + / ですね。
その下でも上でもいいのですが、新たに関数の定義文を用意します。
Game_Player.prototype.isFacingSkillTargets = function() {
};
プレイヤーに関連する関数であるので、Game_Playerのメソッドとします。
関数名はisFacingSkillTargetsとしました。フィールドアクションの対象となるイベントのことはskill targetと呼称することにします。そしてその対象に「直面」しているかどうかの判定なのでfacingとしました。先程のスクリーンショットでは、条件分岐のスクリプトにてこの関数を呼び出すようになっていました。ですので基本的には、この関数はブール値(true / false)を返すようにします。ブール値を返す関数の名前は、疑問文形式にするのが慣習です。ただし主語がこの関数の呼び出し元と等しい場合は省略します。
"Is $gamePlayer facing skill targets?" → isFacingSkillTargets
ちなみにコアスクリプトで一貫して用いられている、語頭を小文字で始めて単語ごとに頭文字を大文字にする命名規則はキャメルケースと呼ばれています。大文字の部分がラクダのコブに見えるから…とのことですが、そうは見えませんよね!?
さて関数の処理を書いていきましょう。まず、プレイヤーの直前にあるイベントを取得することから始めるので、checkEventTriggerThere関数と同様にGame_MapクラスのroundXWithDirection関数にてプレイヤーの1マス先の座標を取得します。
Game_Player.prototype.isFacingSkillTargets = function() {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
};
ここまではcheckEventTriggerThere関数と全く同じですね。
続いて、この座標にあるイベントを取得します。
Game_Player.prototype.isFacingSkillTargets = function() {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
const events = $gameMap.eventsXy(x2, y2);
};
こちらはstartMapEvent関数にもあった、Game_MapクラスのeventsXy関数ですね。これによってこの座標にあるイベントが全て取得できるはずです。
最後に、その座標にイベントが位置しているかどうかの判定を返しましょう。eventsという定数に代入したイベントの配列は、その座標にイベントが存在すればそのイベントが要素として入りますが、一つもイベントが存在しない場合は空の配列になります。なので配列に一つでも要素があればプレイヤーはイベントに直面していることになります。関数の戻り値となるreturn文を書いてみましょう。
Game_Player.prototype.isFacingSkillTargets = function() {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
const events = $gameMap.eventsXy(x2, y2);
return events.length > 0;
};
lengthというのは配列のプロパティであり、配列のサイズ(要素数)を表します。それが0よりも大きければ、直面しているイベントが一つ以上存在するということです。
これで関数ができました。では実際にコモンイベントに設定してみましょう。先程のスクリーンショットと同じですが、コモンイベント2番に設定します(1番は第2回の時に作ったものを念のためにそのまま残します)。
最初の条件分岐にてスクリプトを選択し、以下のコードを入力します。
$gamePlayer.isFacingSkillTargets()
そして《ファイアⅠ》のコモンイベントを2番にします。
ここまで設定できたらいよいよテスト開始です!
きちんとプレイヤーの前にイベントがある時にだけスキルが使用され、たきぎに火がつくようになっているのがお分かりかと思います(厳密には目の前にイベントがなくてもスキルは発動しますが、その場合MPが回復します)。
これで課題①は解決したと言えるでしょう!
まとめ
というわけで非常に長くなってしまいましたが、これにて第4回を締めくくりたいと思います。お疲れ様でした。
前回および今回は、プレイヤーの直前にイベントがあるかどうかを判定するための処理をイベントコマンドから関数に置き換えるための作業を見てきました。
その過程でコアスクリプトの関数をいくつか見てきたのですが、ある関数がそれ単独で完結することはめったになく、いくつもいくつも別の関数にジャンプする羽目になりました。
これはかなり煩雑に思えるかもしれませんが、このような過程を経ることでコアスクリプトに慣れるという効果は得られるかと思います。それを繰り返していけばやがて「あの関数はあそこに定義してあったはず!」という具合に、インスピレーションで目当ての場所を発見することもできるようになるでしょう。
さて次回は次の課題である②に移行しましょう…と言いたいところですが、実は今回の課題は完全に解決したわけではないのです。それが何なのか、どうすれば解決できるのかを見ていくことにします。
それではまた次回お会いしましょう!
今回までの最終コード
//=============================================================================
// RPG Maker MZ -
//=============================================================================
/*:
* @target MZ
* @plugindesc
* @author
*
* @help
*
*
*
*
*/
/*:ja
* @target MZ
* @plugindesc
* @author
*
* @help
*
*
*
*
*/
(() => {
'use strict';
Game_Player.prototype.isFacingSkillTargets = function() {
const direction = this.direction();
const x1 = this.x;
const y1 = this.y;
const x2 = $gameMap.roundXWithDirection(x1, direction);
const y2 = $gameMap.roundYWithDirection(y1, direction);
const events = $gameMap.eventsXy(x2, y2);
return events.length > 0;
};
})();
この記事が気に入ったらサポートをしてみませんか?