Map関連クラスを調べてみた
Mapについて調べてよう
なんかユーチューバーの動画タイトルみたいですが、RPGツクールMV(以下MV)のマップ関連のJavaScriptがどうなってるか調べてみました。
ryiwamotoさんのRPGツクールMVのランタイムコードを読む - Sceneを理解するが、非常に参考になりました。
どーやって調べるの?
MVはコードが公開されているので、それ読んでいけば全部わかる…わけはないですね。
読めればわかるのかもしれませんが、きちんと読める人はなかなかいません。
しょーがないので泥縄的に、動いているのを監視したり、ちょっと書き換えたりしながら調べていきます。
MVはテストプレイした時にF8を押すと、デベロッパーツールが出てきます。
このF8で出てくるウィンドウのConsoleタブにはJavaScriptを直接入力・実行できる機能があるわけです。
それ使えば、ゲームのテストプレイ実行中に、脇からスクリプトを実行して諸々試すことができます。
トリアコンタンさんの DevToolsManage プラグインを導入すると、起動したら自動的にコンソールが開くのでF8キーで開くことを忘れても安心。
基本テストプレイの時にはコンソール開いておくんで、入れといた方がいいでしょう。
どっから呼ばれてるんだか調べたい関数の頭に console.log( new Error().stack );
を書いておくと、スタックトレースができます。
実行された関数の履歴をスタック(積み上げ)してるデータを出力して呼び出し関係を追うのを、スタックトレースといいます。
エラーが発生した時に、コンソールにバラバラバラっと関数が列挙されることがありますが、アレを意図的に起こしてるわけです。
なお、サンシロさんの「Visual Studio Code」+「Chrome」で「ツクールMV」のローカルでデバッグなテクニックの紹介 を参考にブラウザでの実行環境を用意すれば、ブレークポイントを置いておくだけでスタックトレースできますし、その他諸々便利な機能が使えます。
できればブラウザでの実行を基本にした方が精神衛生上良いかと思います。付属のコンソールは正直「無いよりマシ」レベルなので。
加えて残念ながら、公式のJavaScriptリファレンスはものすごい中途半端なものしかないです。
幸い有志が作った RPGMV-CoreScript-Reference があるので、登場したクラスはこのリファレンスにリンクしていくことにします。
クラスを並べてみる
リファレンスからMap関連のクラスを取り出して、クラス継承関係をつけてガーッと並べてみることにします。
データベース
画像
- EventEmitter
- PIXI.extras.PictureTilingSprite
ユーティリティ
まとめ
マップ関連だけ取り出したつもりですが、多いっスね…。
抜けがあったり、不要なものまで引っ張ってきてたりしそうです。
もうやる気がなくなってきます(笑) 実際に使うときは、必要そうなところだけ読めばいいと思います。
割と綺麗にMVCに分かれてる感じで、やはり画像(View)が一番量が多いですね。
マップまでの包含関係を追ってみる
クラスの継承関係はだいたいわかったとして、稼働している状態でどのオブジェクトがどのオブジェクトを抱えているのか、その辺をざっと追っていきたいと思います。
SceneManager
MVはシーン(scene)というオブジェクトで、タイトル・マップシーン・メニューなど、各場面が管理されているようです。
そのシーンを制御しているのがClass: SceneManager という静的クラス。
なのでいきなりコンソールにクラス名を打ち込んでreturnキーを押すと、
SceneManager
ƒ SceneManager() { throw new Error('This is a static class'); }
という感じでコンストラクタが出てきて「静的クラスだからnewしないでね」的なこと書いてある。
MVのJavaScriptの根っこをバッチリ掴んだ感じです。
また、コンソールのSceneManagerに続けて . を打つとオートコンプリートが起動して、プロパティやメソッドの一覧が出てきます。
これも便利に使っていくと良いと思います。
Scene_Map・Spriteset_Map
直接 JavaScript のコードを読むのも良いですが、手を抜いて SceneManager のリファレンスを見ると _scene プロパティに現在のシーンが入っているらしい。
今回はマップを調べたいので、マップが出る画面までゲームを進めて以下をコンソールに打ち込みます。
SceneManager._scene
すると次のような結果が返ってくるので、マップ表示中は Class: Scene_Map のインスタンスが入っていることが分かります。
Scene_Map {_events: Events, _eventsCount: 0, tempDisplayObjectParent: null, transform: TransformStatic, alpha: 1, …}
例によってリファレンスで、Scene_Mapのプロパティを見ていくと、_spriteset にマップ本体のClass: Spriteset_Mapがあるみたい。
Spriteset_Mapは、 マップと[遠景]や[天候]、他に[アクター][イベント]なんかも管理してるオブジェクトです。
Tileset
さらにを見ると、SceneManager._scene._spriteset._tileset プロパティにタイルセットが入ってて、これはMVのエディタにある[タイルセット]がオブジェクトとして存在しているという感じ。
Class: Tileset を読むと、'data/Tilesets.json'の中身がそのまんまJavaScriptのオブジェクトになっているという解釈でいいようです。
SceneManager._scene._spriteset._tileset
{id: 2, flags: Array(8192), mode: 1, name: "外観", note: "", …}
なるほど、今マップに設定しているid 2のタイルセットの内容が出てきた。
このタイルセットに関しては大域変数$dataTilesetsに入ってるから、SceneManager._scene._spriteset._tileset と延々辿らなくても、一発で読めるみたい。
どれどれ…
$dataTilesets
(8) [null, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
あれ、配列の形が違う…あ、_tilesetで出てきたのはマップで設定しているタイルセットで、$dataTilesetsで出てきたのはタイルセット全体か!
sが付いてる複数形だもんね。
コンソールの左っ側の▶︎押したら出てくる詳しい内容を見ると、確かにMVのエディタで設定したタイルの内容が書かれてるみたい。
最初がnullなのは、タイルセットのIDが1から始まるので、0は使ってないというだけ…だと思う。
Tilemap・ShaderTilemap
んで、SceneManager._scene._spriteset._tilemapにタイルマップが入ってる。
で、MVには、Class: TilemapとClass: ShaderTilemapの二つのタイルマップのオブジェクトがあって、どちらかが使われてます。
通常は WebGL で描画する ShaderTilemap が使われてますが、古いブラウザなどでは Canvas で描画する Tilemap が使われる、ということのようです。
最初に並べたクラスの継承図を見ると、Tilemap を継承して ShaderTilemap が作られているので、基本的な使い方は一緒ということですね。
SceneManager._scene._spriteset._tilemap
ShaderTilemap {_events: Events, _eventsCount: 0, tempDisplayObjectParent: null, transform: TransformStatic, alpha: 1, …}
手持ちのパソコンは流石に前世紀の代物とかではないので、ちゃんとShaderTilemapが返ってきました。
ちなみにコンソールで調べなくても、F2キーで現在利用している描画モードが表示されます。
大体はこのTilemap・ShaderTilemapのメソッドを(インタラプトするなりして)使えば、マップ表示関連の操作はできそうです。
Sprite・ZLayer
ShaderTilemap でやっとマップを描画しているオブジェクトにたどり着いたのですが、これがさらに細分化されてます。
中身は childrenプロパティにあるみたいです。
SceneManager._scene._spriteset._tilemap.children
(12) [ZLayer, Sprite_Character, Sprite_Character, Sprite_Character, …]
一種類ではなくて、いろんなオブジェクトが出てきました。
ZLayer, Sprite,Sprite_Character, Sprite_Destination なんかがあります。
ざっくり言うと、マップの低層レイヤーがあって、影とかキャラとかイベントとかが続いて、マップの高層レイヤー[☆]があって、その上に、飛行船の影、飛行船、吹き出し、アニメーション、タッチ位置表示なんかが乗ってるというレイヤー構成のようです。
マップ低層・高層の表示を担当している ZLayer は、PIXI.tilemap ってライブラリに含まれるオブジェクトです。
なので残念ながらRPGMV-CoreScript-Referenceに解説はありません。
Githubに上がってるTyperScriptで書かれたソースpixi-tilemap/ZLayer.ts を見ると、PIXI.Container を継承していて比較的シンプルな拡張のようです。
PIXIに関してはリファレンスがあるので、主な機能は PIXI.Container を読めばわかるわけですね。
MVのコアスクリプトは PIXI.Container を継承しているものが多いので、似たノリで使えそうです。
そんで PIXI.tilemap.ZLayer の children には、さらに PIXI.tilemap.CompositeRectTileLayer が入っていて、 その中にまた PIXI.tilemap.RectTileLayer が入ってます。
この辺を活用すると多数のオブジェクトを管理できるようなんですが、MVでは 1個ずつしか使ってないようです。
ですから、ZLayer に CompositeRectTileLayer がひとつぶら下がり、そこに RectTileLayer がぶら下がってる、という構成です。
速度を稼ぎたかったので、少ないオブジェクトで実装したということなのかな、と思いますが、どーなんでしょ。
なお、PIXI.tilemap 関連のクラスは Tilemapクラスでは使われてなくて Spriteクラスに描画してます。
MVの[イベント]や[アクター]( Sprite_Character )は、このSceneManager._scene._spriteset._tilemap.children にどんどん追加されていきます。
また低層レイヤー( z: 0 )と高層レイヤー[☆]( z: 4 )に挟まれた階層に配置される( z: 3 )ので、マップタイルと細かい重ね合わせはできない仕様であることもわかります。
まとめ
SceneManager → Scene_Map → Spriteset_Map → Tilemap・ShaderTilemap → ZLayer・Sprite・ Sprite_Character・ Sprite_Destination
てな感じに、オブジェクトがぶら下がってます。
ZLayerに関してはさらに2種類のオブジェクトがぶら下がってました。
ZLayer → CompositeRectTileLayer → RectTileLayer
この辺を中心に探っていけば、マップの動作に関する理解は深まりそうです。
ただ、階層が深くてややこしいですね。
あまり全体の構成は気にせずに、触りたい箇所の前後程度を調べていけばいいんじゃないかと思います。
動作順を追う
オブジェクトの包含関係がわかったので、今度はどういう順番に起動されているか追っていきましょう。
くらむぼんさんのRPGツクールMVの構造を丸裸にするでは、Map関連が特に詳細に記述してあるので、参考にしました。
というか、だいたい調べ終わって、過去のアドベントカレンダーの記事を追ってたら見つけたとゆー。
なんというか「ぐはっ」って思いますよね、そういう時って。
事前に、同じところ調べてたので理解しやすくて、良かったと思います!!
update() を辿る
当然オブジェクトの根っこであるSceneManagerから追っていきます。
MVは update() というメソッドが毎フレーム呼ばれる仕組みになってます。
んで、親オブジェクトの update() が子オブジェクト( children )の update() を呼び出すことを繰り返して、全体のアップデートを行なっているという仕組みです。
個々のオブジェクトがタイマー持ってて update() を起動しているというわけではないので、子オブジェクトの update を呼び損なうと、アップデートが止まってしまいます。
調べてみたところ、update() をたどっていっても各オブジェクトの位置の更新とかはしてるんですが、具体的な描画は呼ばれてませんでした。
MVが update() ベースで動いているのは間違い無いので、他のコードを読むときには役立つとは思います。
updateTransform() を辿る
さて、update() と似たようなので updateTransform() ってのがあって、こいつがレンダリング(画像の描画)を行なっているようです。
そこで、ShaderTilemap.updateTransform() に前述のErrorを仕込むなりブレークポイントを置くなりして、スタックトレースを追うと、SceneManagerを起点にして諸々の経路を辿ってよばれてることが確認できました。
Tilemap(ShaderTilemap) の updateTransform() から_paintAllTiles() が呼ばれ、さらに _paintTiles() で低層・高層の振り分けが行われ、 _drawTile() が呼ばれます。
さらに _drawTile() の中から、通常タイルなら _drawNormalTile()、オートタイルなら _drawAutotile() が呼ばれます。
Tilemap の場合は、 bitmaps プロパティにタイルセットの画像が保持されているので、それを使って Bitmap.bltImage() で描画されます。
ShaderTilemap の場合は、bitmaps を PIXI.Texture に変換して、RectTileLayer.addRect() で描画されてます。
とにかく、Tilemap と ShaderTilemap の内部処理は大きく異なっていますし、環境によって使い分けられるものなので、プラグインを作る場合は極力その前の段階で決着させないと面倒臭そうです。
ほとんど使われてないんで、Tilemap は無視するという技もありかもしれません。
まとめ
アップデートを追っていくと、マップに関しては2ルートあるみたいです。
ひとつはupdateメソッドのチェーン。フレーム毎に情報をアップデート。
SceneManager → Scene_Map → Spriteset_Map → Tilemap・ShaderTilemap
もうひとつはrenderを通してのupdateTransformメソッドのチェーン。フレーム毎に画像をアップデート。
SceneManager → Game_Map → Graphics→ Scene_Map → Spriteset_Map → Sprite → Tilemap・ShaderTilemap → Sprite・ZLayer
アップデートひとつにまとめちゃいけないの? とか思いますが、PIXIのレンダラ使ってる都合もあって、分けたほうがむしろスッキリする、ということなんでしょう。
ファイル処理
次に気になるのは、データはどういうタイミングで読み込まれて、オブジェクトを生成しているか。
読み込まれたタイミングでデータを加工すれば、その後の処理に負荷をかけずに色々できそうじゃないですか。
マップデータの読み込み
データの読み込みを管理しているのは、Class: DataManager。
マップの読み込みに関してはズバリ loadMapData() というメソッドがあります。
マップデータは他のJSONデータと違って、ファイル名が一意ではなく'Map001.json'みたいにナンバーが振ってあるので、ちょっと特別扱いになってます。
で、読み込みが完了するとonLoad()が呼ばれます。
仮引数 object に読み込んだデータが渡されるんですが、これは$で始まる大域変数がそのまま渡されます。
マップの場合は $dataMap なので、object === $dataMap
で判定できるというわけです。
もしマップデータを表示前にいじっておきたいなら、ここで適当に前処理を入れればいいみたい。
タイル設定の読み込み
タイル設定は'Tilesets.json'に入っていて、マップデータと同様に DataManager で読み込んでます。
読み込みに使うメソッドは loadDataFile() で、読み込みが完了すると onLoad() が呼ばれるのはマップデータと同じ。
タイル設定は $dataTilesets なので、object === $dataTilesets
で判定できるわけです。
まとめ
DataManager の onLoad() で Map データを事前に処理しておくと、画像処理部分の変更を行う必要がないので速度や安定性を犠牲にせずに、色々と変更が加えられるので、とても良い感じです。
タイルセットのデータ構造
タイル設定の調べ方
だいたい調べ終わった時に、すでに調べてる人の記事を見つけました(また!)
F_さんの【RPGツクールMV】 タイル仕様 - エフアンダーバーです。
ほぼ内容かぶってますが、せっかく書いたので続けます。
タイル設定は大域変数 $dataTilesets でアクセスできます。とりあえず中身を確認したいなら、Tilesets.json ファイルを直接見れば簡単に見れます。
基本的なタイル情報(A1〜A5、B、C、D、E)は flags プロパティの配列の添字(flags[この番号])に書く「タイルID」で決まっています。
柔軟性に欠ける設計でモニョりますが、歴史のあるプロダクトにありがちな負の遺産というやつですかねー。
具体的な設定は Tilemap の定数に設定してあり、以下のような番号になっています。
定数名 | 番号 |
---|---|
TILE_ID_A1 | 2048 |
TILE_ID_A2 | 2816 |
TILE_ID_A3 | 4352 |
TILE_ID_A4 | 5888 |
TILE_ID_A5 | 1536 |
TILE_ID_B | 0 |
TILE_ID_C | 256 |
TILE_ID_D | 512 |
TILE_ID_E | 768 |
TILE_ID_MAX | 8192 |
…んーTILE_ID_E は 256パターンあるので、次のTILE_ID_A5との間に512個も使ってない配列があることになりませんか?
将来の拡張用としてももったいないというか、はっきり言って無駄というか…。
よく分からない設計ですね。これも歴史のあるプロダクト(以下略)。
さらにオートタイルがあるA1〜A4は、オートタイルのパターンごとに番号が使われてるんですけど、オートタイルの性質(flag)ってパターンが変わっても全部一緒なのに、ちょっと富豪的プログラミングにすぎる気が…。
さて、そのオートタイルなんですけど、配置される場所が決まってて、1床オートタイルにつき48、1壁オートタイルにつき16、1滝オートタイルにつき4つの番号が使われてます。
オートタイルのパターン番号は、Tilemap.getAutotileShape() にタイルIDを渡すと返してくれます。
組み合わせはTilemap.FLOOR_AUTOTILE_TABLE、Tilemap.WALL_AUTOTILE_TABLE、Tilemap.WATERFALL_AUTOTILE_TABLEに設定されてます。
タイルを4分割した[左上, 右上, 左下, 右下]の順に、それぞれオートタイル用の画像を24ピクセルで分割した座標[x, y]が入ってます。
床オートタイルなら、以下の通り。
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 0,0 | 1,0 | 2,0 | 3,0 |
1 | 0,1 | 1,1 | 2,1 | 3,1 |
2 | 0,2 | 1,2 | 2,2 | 3,2 |
3 | 0,3 | 1,3 | 2,3 | 3,3 |
4 | 0,4 | 1,4 | 2,4 | 3,4 |
5 | 0,5 | 1,5 | 2,5 | 3,5 |
で、これを組み合わせた[[2,4],[1,4],[2,3],[1,3]]
みたいなのが、48パターン入っています。
最後の47番には左上のアイコン部分[[0,0],[1,0],[0,1],[1,1]]が入ってますが、マップエディタでは使用できません。
ちなみに直接マップファイルのJSONを書き換えると表示されます。
でまーこれが、A1だったらflags[2048]から48パターンぶん同じflagの数字が並ぶ…1つあればいいのに、なんたる無駄。
なお、3コマのアニメーションするオートタイルだからといって3倍インデックスが使われているわけではなく、48パターンなのは救いです。
床は48パターンなんですけど、壁は16パターンで番号消費が少ない…と思いきや48個使ってるんですよ、正確にいうと使っているのは16個で、残りの32個は何にも使ってなくて指定するとエラーが出る。
滝に至っては4パターンしか必要ないのに、44個無駄にしてる。
無駄ラッシュでこの滝のパターンの無駄具合を書けば…。
「滝!滝!滝!滝!無駄ッ!!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄!無駄ッ!! この ジョルノ・ジョバァーナには 夢があるッ! 」
ま、夢はともかく無駄ですよねぇ…。
[通行]などの各種タイル設定は、flags にビット情報として格納されていて、読むにはビット操作が必要です。
詳しくは、Class: Tileset を参照ください。
また、ビット演算について詳しいことは、ビット演算 - Wikipediaあたりを、JavaScriptで使えるビット演算子についてはビット演算子 - JavaScript | MDNを読むと良いかと。
まとめ
とにかく無駄が多く柔軟性のない設計で、ちょっと理解に苦しみます。
そもそも、このflagsって使ってようがいまいが、使えようが使えなかろうが、8192まで全部詰まってるんですよ。
加えて特に謎なのが 4桁の数字が詰まってることで、使ってない箇所に1桁の数字を詰めていくだけで、ファイルサイズが半分以下になりそうな気がします。
とりとめもなく、調べた事柄を開陳しただけで、イマイチ面白みもまとまりよもくない記事ですが、ないよりマシだろの精神で公開しました。
調べた結果を元に作ったプラグイン(TF_LayeredMap.js)を tonbijp/RPGMakerMV: Plugins for RPG Maker MV に置いていますので、MVユーザの方はお気軽にご利用ください。MITライセンスですのでー。
屋根や壁の後ろや、柵の前後を動けるようになるので、マップの面積を無駄にせず、動作も自然に表現できます。
そこで結論。
公式の方から、もーちょっとまとまった資料出してくださいっ!!