メモ・メタデータについて調べてみた

プラグインについてしらべてみたシリーズも第3回、メモ・メタデータを調べていきます。

タグには情報をつけて渡せるよ

メモ・メタデータ

 イベントとか各種データベースに[メモ]って項目があって、その名の通り制作の備忘録的に使うのが本来想定されていると思います。
 ただこの項目、製作中だけ存在するのではなくて、実行中もデータが残ってて、JavaScriptからアクセスできちゃうのです。

 さらに、<識別子> または、<識別子:データ> というタグの形でメモに書いておくと、RPGツクールMVが自動的にメモの内容からタグの内容を読み取りデータを生成してくれます。
 例によって、僕のプラグインではTF_をタグ名の接頭辞として使用しています。

 RPG.MetaData の子孫クラスが noteプロパティ( [メモ]の内容 )と、metaプロパティ( タグを解析した結果 )を持っています。
 [マップ]と[イベント]、あとは[データベース]に含まれるものですね。

メモ欄の場所と大きさ

[イベント]のメモ欄

 [イベント]のメモ欄は非常に狭いので、すぐに入りきれなくなりますが、メモ欄の幅を超えた量の記述は可能です。
 表示されない部分は、ドラッグしたり矢印キーでカーソルを移動させたりして見ることができます。
 ただし改行はできず、入力できるのは一行だけです。
 画面に表示されないので、同じタグを二重に書いてしまったことに気づかない、などのミスが発生しがちです。
 同じタグが複数あっても処理されるタグはひとつだけです。
 そのため「ちゃんと記述しているのに反映されない」という状態になるのです。
 少々面倒ですが、メモ欄全体を選択(Cmd+A)してカット(Cmd+X)し、テキストエディタで編集してからまたペースト(Cmd+V)するのが無難です。
 あるいは、JSONファイルを直接編集する方がまだミスが少ないかもしれません。

[マップ]のメモ欄

 [イベント]に比べると広いですね。改行もできます。形は違いますが用途は一緒です。
 はみ出した部分があると、スクロールバーが表示されるのでミスに気付きやすいのが救いです。
 とはいえ[イベント]と同様の問題は発生します。

[データベース]に含まれるクラスのメモ欄はこんな感じ

 十分広いように感じるかもしれませんが、わりとあっという間に表示部分は使い切ってしまいます。
 当然[イベント]のメモ欄と同じように、画面に表示されない記述部分がでてきます。
 このメモ欄もスクロールバーが表示されます。

 ちなみに、メモ欄の上にマウスポインタを置いてF1キーを押すかコンテクスメニューを開くと[プラグインヘルプ]が見れます。
 大抵はヘルプにタグの記述例が書いてあるので、それをコピーして書き換えると単純なスペルミスなどが防げます。

メモ・メタデータの取得方法

 今度は書かれたタグのデータの取得方法です。
 オブジェクト内のメソッドに取得コードは書かれるものとします。
 次の表が、各データの取得方法です。

データベース オブジェクト 取得コード
RPG.Event Game_Event this.event().meta
RPG.Map Game_Map $dataMap.meta
RPG.Actor Game_Actor this.actor().meta
Game_Player $gameParty.leader().actor().meta
Game_Follower this.actor().meta
どこからでも $gameParty.member( メンバー番号 ).actor().meta
RPG.Class どこからでも $dataClasses[ 職業ID ].meta
RPG.Enemy Game_Enemy this.enemy()
RPG.State どこからでも $dataStates[ ステートID ].meta
RPG.Tileset どこからでも $dataTilesets[ タイルセットID ].meta
RPG.Item Game_Item this.object().meta
どこからでも $dataItems[ アイテムID ].meta
RPG.Skill Game_Item this.object().meta
どこからでも $dataSkills[ スキルID ].meta
RPG.Armor Game_Item this.object().meta
どこからでも $dataArmors[ 防具ID ].meta
RPG.Weapon Game_Item this.object().meta
どこからでも $dataWeapons[ 武器ID ].meta

 取得方法がバッラバラですね。
 せめてオブジェクトの生成元のデータベースを参照するのは this.object() に統一して欲しかったという気はします( object() というメソッド名が適切どうかは別として)
 ちなみに頭に $ がついているデータは大域(グローバル)変数なので、オブジェクトの中のメソッドからに限らず、どこからでもアクセスできますが、表に当てはめると、こうなっちゃうので悪しからず。

メモ・メタデータの存在確認

 さて今作ろうとしているプラグインは初期位置を変更したいので、なるだけ早くタグを調べたいということになります。
 そうなるとオブジェクトを生成するときに実行されるコンストラクタが良いと思われますが、RPGツクールMVの場合は大抵のクラスに initialize() メソッドが用意されていて、コンストラクタから呼ばれています。
 せっかく用意してあるのですから、ここに仕掛けておくのが良さそうです。

 それでは Game_Event.initialize() で試してみましょう。
 RPGツクールMVはイベントを重ねて配置することができないので、配置した場所から1タイル分右に移動する機能をつけます。
 タグは<slideX>にしておきます。
 タグ名は他のプラグインと衝突しないような名前にする必要があるので、僕は例によって TF_ の接頭辞をつけています。
 ただ、同じ名前のタグは同じか似たような機能を実現しようとしている可能性が高いので、タグが別であっても機能的に衝突(コンフリクト)して、片方あるいは両方の機能が動かない、という事になりそうですけど。
 あと、このタグ名は大文字小文字を区別します。

※このタグのデータの取得は DataManager.extractMetadata で行われています。
 ここを書き換えれば取得方法を変えることができるわけですが、他のプラグインへの影響が大きすぎるので、都度 noteプロパティから変換した方が無難です。
 とはいえ標準的な方法で解決できるならば、それがさらに無難と言えるでしょう。

 さて、initialize() を上書きしてしまうと、元々のコードが持っている処理が実行されなくなってしまいます。
 それを回避するには「一旦メソッドを定数に退避して、上書きしたメソッドから退避したメソッドを呼ぶ」という手法を使います。
 個人的には「ここは addEventListener() でイベントリスナを登録させるべきじゃないの?」と思うものの、提供されてないので定数退避方式を使うしかありません。

 なお、退避したメソッドを呼ぶのに _Game_Event_initialize( mapId, eventId ) とやらないのは、メソッド内の this の値が変わってしまうからです。
 call() メソッドの最初に this を渡していますが、これによって定数に退避しておいたメソッドの中でも、this は退避前と同じクラス( ここではGame_Event )になります。

 ちなみに call() ではなく apply() を使っていることもありますが、これは引数の渡し方が違うだけで、基本的な機能は同じです。
 apply( this, [ mapId, eventId ] ) というように第二引数に引数の配列を渡すのです。メソッドの引数は arguments に格納されているので、ここでは apply( this, arguments ) と書けます。
 僕は基本的に call( this ) を使い、引数がある場合は apply( this, arguments ) を使っていますが、特にこれといった明確な理屈はありません。ほぼ気分です!! 明日は違うことしてるかもしれません。

( function() {
    'use strict';
    const _Game_Event_initialize = Game_Event.prototype.initialize;
    Game_Event.prototype.initialize = function( mapId, eventId ) {
        _Game_Event_initialize.call( this, mapId, eventId );	// 退避したメソッドを呼び出す

        if( this.event().meta.hasOwnProperty( 'slideX' ) ) {
            this.setPosition( this.x + 1, this.y ); 	// タグに対応した処理
        }
    };
} )();

 タグが存在するかの判定を hasOwnProperty() メソッドで行なっています。これは引数(オプション)のあるなしを問いません。
 引数なしにタグを書いた場合、タグが存在すると (文字列ではなく真偽値の)true が値として設定されます。
 なので、if( this.event().meta.slideX === true ) という判定で、引数のないタグが設定されていることが判断できます。
 タグがあるかないかだけの判定なら、if( this.event().meta.slideX ) でも行けます。ただし引数が空文字列の場合、タグがないと判定します…引数が必要ないタグに引数つけた上で空文字設定するユーザとかレアケースだろって気がしますが、割と作者がレアケースだと思っていることを利用者はやりがちなので、あんまり手を抜かない方がいいです。

 処理内容は、特に説明するほどのことはないと思いますけど、元のX位置に+1して配置し直してます。

タグの引数を取得

 さて、さっきの処理だとマップの一番左(X=0)に重ねるのが無理とか、別のイベント横に並んでておけない場合などが考えられます。
 ならば引数をつけて、<slideX:1>と書くことでさっきと同じ処理になり、<slideX:-1> とか <slideX:4> とか自由に初期位置からX座標をずらす数値を書けるようにするといいと思います。

 先ほどは引数がなかったので true が入っていましたが、今度は引数を書いていますので : から > までに書かれた文字が文字列(string)として渡されます。
 あとは、前回の整数の変換方法を使っていけばOK!

( function() {
    'use strict';
    const _Game_Event_initialize = Game_Event.prototype.initialize;
    Game_Event.prototype.initialize = function( mapId, eventId ) {
        _Game_Event_initialize.call( this, mapId, eventId );

        if( this.event().meta.hasOwnProperty( 'slideX' ) ) {
            const dx = JSON.parse( this.event().meta.slideX );
            this.setPosition( this.x + dx, this.y );
        }
    };
} )();

 ここでは JSON.parse() を使いましたが、もちろん parseInt() でも構いません。
 他の値も前回紹介したの変換処理を使って変換して行けば良いでしょう。

複数の引数を取得

 ついでなので、X方向だけでなくY方向にも対応したい欲が出てきました。
 ところがRPGツクールMVのタグ解析は複数の値には対応していません。
 なので、自前でもらった文字列から分解する必要があります。

 とりあえずタグの名前も機能に合わせて slideX から slideXY に変更し <slideXY:[1,0]> みたいな形を考えてみます。
 引数部分がJSON文字列なので JSON.parse() で容易に分解して数値配列化できます。

( function() {
    'use strict';
    const _Game_Event_initialize = Game_Event.prototype.initialize;
    Game_Event.prototype.initialize = function( mapId, eventId ) {
        _Game_Event_initialize.call( this, mapId, eventId );

        if( this.event().meta.hasOwnProperty( 'slideXY' ) ) {
            const [ dx, dy ] = JSON.parse( this.event().meta.slideXY );	// JSONとして変換
            this.setPosition( this.x + dx, this.y + dy );
        }
    };
} )();

 という感じでしょうか。JSON文字列なので空白などが入っていても、いい感じに処理してくれます。
 ただ、利用者側からすると前後を角かっこで括っている意味がわかりません。
 <slideXY:1,0>という形が直感的だと思います。
 ひとつの方法としてはプラグイン側で前後に [ と ] を加えたのちに JSON.parse() するという方法。
 JSON.parse( `[${ this.event().meta.slideXY }]` ) という感じですね。
 あとは、split() メソッドを使って分解した後、配列の各文字列を数値に変換するという方法もあります。

( function() {
    'use strict';
    const _Game_Event_initialize = Game_Event.prototype.initialize;
    Game_Event.prototype.initialize = function( mapId, eventId ) {
        _Game_Event_initialize.call( this, mapId, eventId );

        if( this.event().meta.hasOwnProperty( 'slideXY' ) ) {
            const cordinate = this.event().meta.slideXY.split( ',' );
            const [ dx, dy ] = cordinate.map( e => parseInt( e, 10 ) );	// JavaScript は数値と文字列混在でも計算しますが一応
            this.setPosition( this.x + dx, this.y + dy );
        }
    };
} )();

 ちなみに、こちらも parseInt() が空白など入っていてもいい感じに変換してくれます。
 RPGツクールMVならば <slideXY:1 0> 空白区切りの方が方が利用者に馴染みがあるという気もします。
 その場合前後に空白があると、そこで区切られて空白文字が入っちゃうので trim() も入れておくといいでしょう。
 this.event().meta.slideXY.trim().split( ' ' ) って感じ?

複数行の引数を取得

 [イベント]のメモ欄は単一行で改行できませんが、他のメモ欄は複数行かけるので複数行のデータ渡したいですね。

<test:一行目
二行目
三行目>

 これで : から > の間に書いた文字列が meta に複数行入ります。JSONデータ書いて JSON.parse() で変換すれば何種類ものデータの取り扱いも容易いですね。

 HTMLXML みたいに、タグと閉じタグで囲っているプラグインを見かけたことがあります。
 こんな奴です。

<test>一行目
二行目
三行目</test>

 どのプラグインだったか忘れたので実際のコードは見てないんですが、自分ならこう書くかな、というのを書いておきます。
 引数のないタグを書いた際に metaに入る値は、true なのでまずそれでタグがあるかどうかチェックして、あれば note を解析するという流れだと思います。
 解析方法は正規表現で取り出すか、XML文字列とみなして DOM を生成するか、どっちかだと思います。
 Game_Map.initialize() では、まだマップデータが読み込まれていないので、Scene_Map.start()に仕掛けてみることにします。
 あと、機能考えるの面倒臭くなったので、コンソールに取り出した値を出力しておきます。

( function() {
    'use strict';
    const _Scene_Map_start = Scene_Map.prototype.start;
    Scene_Map.prototype.start = function() {
        _Scene_Map_start.call( this );
        if( $dataMap.meta.test !== true ) return;

        const tagList = $dataMap.note.match( /<test>(.+?)<\/test>/s ); // . を改行にも対応させる s オプション
        if( tagList === null ) return;

        const content = tagList[ 1 ];
        console.log( content );	// コンソールに出力
    };
} )();

 metaのプロパティをそのまま使う方式に比べると長いですね。
 正規表現の内容について説明すると長くなるので割愛しますが、XMLとして解析しているわけではないので、CDATAやら子要素なんかは使えません。
 あくまでもXMLっぽいテキストですね。

 あと、特に今説明しようとしていることと関係ないですが、途中で return で帰っちゃう手法を「アーリーリターン」などと言います。
 個人的にはネストが深くなるとコードが理解できなくなるので、多用しています。
 見やすいように return; の後は空行を入れる運用です。

 では、こんどはXMLとして解析するパターン。
 さらに長くなっています。

( function() {
    'use strict';
    const _Scene_Map_start = Scene_Map.prototype.start;
    Scene_Map.prototype.start = function() {
        _Scene_Map_start.call( this );
        if( $dataMap.meta.test !== true ) return;

        const parser = new DOMParser();
        const doc = parser.parseFromString( `${$dataMap.note}`, 'text/xml' );	// XML文字列にするためにrootノードをつけて解析
        const tagList = doc.getElementsByTagName( 'test' );
        if( tagList.length === 0 ) return;

        const content = tagList.item( 0 ).textContent;
        console.log( content );	// コンソールに出力
    };
} )();

 これまた詳細を解説するの大変なので割愛しますが、DOMParser を使って解析しているのでXMLで使える記法が一通り使えて便利です。
 解析した後の XMLDocument オブジェクトをそのままデータコンテナとして保持して使いまわしても良いですね。
 ただし、タグひとつ取り出すためには大袈裟で、XMLを解析して便利なのは最低数KBはあるテキストファイルに対して使うような時かと思います。

ファイルの削除防止

 タグの引数としてファイルを指定した時に、デプロイで[未使用ファイルを含まない]オプションにチェックを入れると、必要なファイルであっても削除されてしまいます。
 その回避方法は、プラグインのコメント部分に必要なパラメータを書き込むことで可能になります。
  MV.PluginSettings の ファイルを扱うメモタグの設定 に詳細がありますので参照ください。
 滅多にこの記述にお目にかかることありませんけどね!

 そこで結論。

タグはメモ欄に居候してるだけなので作りが荒っぽいけど便利だよ