『RPGツクールMZ』マップの構成と更新
はじめに
『RPGツクールMZ』のマップ表示が JavaScript 上でどのように実装されているか調べていきます。
『RPGツクールMZ』非公式JavaScriptリファレンス
適宜このリファレンスのページにクラスなどリンクします。
用語
マップタイル…マップを構成する48×48ドットの画像で、単にタイルとも書かれます。
オートタイル…配置した場所により、自動でタイルの形が選ばれるタイプのタイルで、主に地面・壁といったものに使われます。
JavaScript… Webブラウザを中心に使われるプログラミング言語です。Java は別の言語の名前で JavaScript の略称ではありません。
コアスクリプト…『RPGツクールMZ』で作ったゲームに必要なクラスを定義しているスクリプトです。事実上、これを操作するのが『RPGツクールMZ』での JavaScript です。逆に一般の JavaScript プログラマは知らないので、質問しても「なにそれ」って言われる確率が高いです。プラグインは含みません。
プラグイン… ゲームの機能を拡張するプログラムです。付属しているもの、ユーザが公開しているものがあります。自作もできます。『RPGツクールMZ』 のプラグインは JavaScript で書かれています。
オブジェクト…関係する値(プロパティ)と機能(メソッド)をまとめたもの。JavaScript のプログラムはこれを組み合わせてできてます。
プロパティ…オブジェクトに付随した変数です。ただし変数と違ってオブジェクトの状態で値が変わるものもあります。逆に設定することでオブジェクトの他のプロパティなどの状態が変わる場合もあります。オブジェクトに .
をつけた後に指定します。例:object.property
メソッド…オブジェクトに付随した関数です。だいたいはプロパティを加工します。オブジェクトに .
をつけた後に指定し、()
の中で引数を指定します。例:object.method()
クラス…オブジェクトの設計図とか型とか説明される、プログラムのひとまとまり。
継承…クラスのプロパティ・メソッドを引き継いで、さらに機能を追加したクラスを作ることです。
大域変数…プログラムのどの領域からでも扱える変数が、大域(グローバル)変数です。
クラスツリー
RPGツクールMZ 非公式JavaScriptリファレンス - クラスツリーから、マップ関連のクラスを抜粋したものが次のリストです。
データベース
画像
その他オブジェクト
マップ関連だけで大変な量ですな。
クラスの包含関係を追ってみる
クラスの継承関係はだいたいわかったとして、稼働している状態でどのオブジェクトがどのオブジェクトを抱えているのか、その辺をざっと追っていきたいと思います。
調べ方としては、コアスクリプトのそれっぽいメソッドに次のようなコードを挿入して、実行してみるとスタックトレースができます。
console.log( new Error().stack );
スタックトレースはどのメソッドから呼ばれたかの履歴のことで、親子関係を類推できます。
Visual Studio Code のようなコードエディタにブレークポイントを入れても、スタックトレース(コールスタック)できます。
そういう調べ方をしなくても『『RPGツクールMZ』非公式JavaScriptリファレンス』の方ですでに多くのクラスは[主なパス]として包含関係を書いてるんですけど。
SceneManager
『RPGツクールMZ』はシーン(scene)というクラスで、タイトル・マップシーン・メニューなど、各場面が管理されています。
そしてシーンを管理している静的クラスがSceneManagerです。
テストプレイ中にF8キーで表示されるコンソールに次のように打ち込みます。
SceneManager._scene
するとマップ表示中だと次のような値が表示されます。
Scene_Map {_events: Events, _eventsCount: 0, tempDisplayObjectParent: TemporaryDisplayObject, transform: Transform, alpha: 1, …}
ここで帰ってきたScene_Map
がマップを管理するためのシーンというわけです。
このように_scene
プロパティに現在のシーンが入っているので、メニューなどのシーンにしてもSceneManager
を起点にたどり着けます。
Scene_Map、Spriteset_Map
リファレンスScene_Mapを見ると、_spriteset
プロパティにSpriteset_Map
クラスが格納されています。
リファレンスSpriteset_Mapを見ると、マップと[遠景]や[天候]、他に[アクター][イベント]の他、メッセージ・選択肢関連ウィンドウなんかも管理しています。
Tileset
さらにリファレンスを見ると、SceneManager._scene._spriteset._tileset
プロパティにタイルセットが入ってて、これは『RPGツクールMZ』エディタにある[タイルセット]がオブジェクトとして存在しているという感じ。
リファレンスTilesetを読むと、'data/Tilesets.json'の中身がそのまんまJavaScriptのオブジェクトになっています。
試しにF8で出るコンソールに次のように打ち込んでみます。
SceneManager._scene._spriteset._tileset
すると、現在のマップで使われているタイルセットの情報が出てきました。
{id: 3, flags: Array(8192), mode: 1, name: "内装", note: "", …}
このタイルセットに関しては大域変数$dataTilesetsに入ってるから、SceneManager._scene._spriteset._tileset
と延々辿らなくても構いません。
ただし、全タイルセットのデータが入っているので、添え字にマップIDを入れて次のように書きます。
たとえば、マップIDが3ならこうです。
$dataTilesets[3]
これで、先ほどと同じデータを取得できました。
{id: 3, flags: Array(8192), mode: 1, name: "内装", note: "", …}
Tilemap
現在のマップの状態を保持したオブジェクトはSceneManager._scene._spriteset._tilemap
プロパティに入っています。
例によってF8のコンソールに打ち込んでプロパティの内容を確かめてみましょう。
SceneManager._scene._spriteset._tilemap
Tilemap
オブジェクトが入っていることが確認できます。
Tilemap {_events: Events, _eventsCount: 0, tempDisplayObjectParent: null, transform: Transform, alpha: 1, …}
$dataMap
マップデータもタイルセットと同様に$dataMapという大域変数が用意されています。
これも JSON ファイルの中身がそのまま入っているのですが、マップが切り替わると同時に内容が入れ替わります。
なので『RPGツクールMZ』は別のマップのデータを知ることは基本的にはできません。
そのような処理が必要な場合、変数やスイッチ、セルフスイッチといったマップが切り替わってもアクセスできるデータを利用します。
$dataMap
の内容についてはリファレンスMapをご覧ください。
その中でも特にdata
プロパティの内容については『RPGツクールMZ』マップのデータ構造で詳しく解説しています。
Tilemap.CombinedLayer・Sprite
Tilemap
でやっとマップを描画しているオブジェクトにたどり着いたのですが、これがさらに細分化されてます。
中身はchildren
プロパティにあるので、F8コンソールで確かめてみます。
SceneManager._scene._spriteset._tilemap.children
children
と複数形なだけあって、大量に入ってます。
(11) [T…p.CombinedLayer, Sprite_Character, Sprite_Character, Sprite_Character, Sprite_Character, Sprite_Character, Sprite_Character, Sprite_Character, T…p.CombinedLayer, Sprite, Sprite_Destination]
この中身はマップに重なって表示されるオブジェクトが含まれています。
詳しくはリファレンスのTilemap.md#レイヤーの配置に書いてありますが、タイルマップそのものの他にキャラとか[イベント]も含まれています。
T…p.CombinedLayer
は途中省略されていますがTilemap.CombinedLayer
のことです。
これはタイルマップを表示するためのオブジェクトです。
Tilemap.CombinedLayer
は、SceneManager._scene._spriteset._tilemap._upperLayer
およびSceneManager._scene._spriteset._tilemap._lowerLayer
からも参照できます。
名前からわかるようにレイヤーの低層と高層で別れています。
豆知識
Tilemap.CombinedLayer
が導入されたのはバージョン1.7.0からなので、それ以前は多少構成が違いました。
その時の構成についてはとくに言及しません。
Sprite_Character
プレイヤーキャラなど、隊列メンバーや乗り物は表示されていなくてもオブジェクトとしては存在しているので[イベント]が置かれていないマップでもSprite_Character
は7つ存在します。
Sprite
は飛行船の影、Sprite_Destination
はクリックした時に出るマーカーです。
データと画像を繋ぐクラス
SceneManager
の下に画像を扱うクラスがあり、$dataTilesets
と$dataMap
に JSON データが格納されていますが、それらを繋ぐクラスがGame_Mapです。
これは大域変数の$gameMapに登録されているので、簡単にアクセスできます。
$gameMap._displayX // マップ表示の x座標(タイル数)
このようにスクロール状態など、マップデータと画面表示の仲介をする情報を持っていてリアルタイムに更新されます。
コマンドインタプリタ(イベントコマンドを実行するクラス)やコモンイベントも持っているのは、ちょっと意外ですが、静的な$dataMap
を活性化した$gameMap
がその辺のデータも持っているのは当然ではあります。
ここまでのまとめ
『RPGツクールMZ』のマップはまず、画像を扱うためのクラスが次のような包含関係になっていました。
SceneManager
→ Scene_Map
→ Spriteset_Map
→ Tilemap
→ Tilemap.CombinedLayer
・Sprite_Character
・Sprite
・Sprite_Destination
またデータとしてのタイルセットは$dataTilesets
、マップは$dataMap
に格納されていました。
そして、これら画像とデータを結びつけるクラスとして$gameMap
が存在しています。
マップの挙動を追う
構造がわかったので、今度はマップが描画される仕組みを追ってみます。
update()を辿る
『RPGツクールMZ』はupdate()
というメソッドが毎フレーム呼ばれます。
親オブジェクトのupdate()
が子オブジェクト(children
)のupdate()
を呼び出すことを繰り返して、全体のアップデートを行なっているという仕組みです。
個々のオブジェクトがupdate()
を起動していないので、子オブジェクトのupdate()
を呼び損なうと、アップデートが止まってしまいます。
豆知識
そもそもupdate()
が毎フレーム呼び出されるのはどうしてかという話が気になった人もいると思います。
流石にここまでシステムの根本を改造することもないだろうと思いますが、簡単に説明します。
PIXI.Application が持っているPIXI.Ticker
によって秒間60フレーム、Graphics._onTick()
メソッドが呼び出されています。
そして、Graphics._tickHandler に登録されているSceneManager.update()
が呼び出されるという流れです。
PIXI
というのは『RPGツクールMZ』が採用している JavaScript の描画ライブラリです。
詳細はPixiJS 公式サイトを見ていただくとして、『RPGツクールMZ』は毎フレームの呼び出し処理をライブラリに丸投げしてるということです。
SceneManager
SceneManager
の内部で次の経路を辿って、現在のシーンオブジェクトのupdate()
が起動されます。
update()
→updateMain()
→updateScene()
→
ここはまだシーン前の下準備なので、直接マップに関連する処理は行われていません。
豆知識
各メソッドの中身は次のような感じです。
update()
フレーム遅延を考慮して回数を調整してupdateMain()
呼び出します。
updateMain()
- フレーム数のカウント
- 入力機器からの入力チェック
- Effekseerの更新
- シーンの変更中であれば変更処理
updateScene()
シーンの状態に応じて、シーンのupdate()
を呼んでいます。
Scene_Map
SceneManager
はさまざまなシーンを管理していますが、現在調べているのはマップの挙動なのでScene_Map
を読みます。
その親クラスであるScene_Message
、Scene_Base
は直接マップとは関係ないので無視。
Scene_Map
のupdate()
を見ると、次の経路を辿って$gameMap
つまりGame_Map
オブジェクトのupdate()
が起動されます。
update()
→updateMainMultiply()
→updateMain()
→
update()
では次の更新が行われます。
- クリック・タッチのマーカー
- メニューボタン(の表示判定)
- マップ名表示ウィンドウ(を閉じるか判定)
- タイマーのカウント
updateMainMultiply()
では高速実行状態(決定ボタン押しっぱなし)の場合にupdateMain()
を2回呼び出す処理が行われています。
updateMain()
では次の処理が行われます。
- プレイヤーと隊列メンバー($gamePlayer)
- タイマー($gameTimer)
- 画面効果($gameScreen)
Game_Map
Game_Map
のupdate()
では次の処理が行われます。
- [イベント]のスイッチ・変数などの変化によるEVページの切り替え
- 実行内容の処理
- スクロールの更新
- [イベント]の更新
- [乗り物]の更新
- [背景]の更新
ここまで追ってわかったのはupdate()
ではマップの描画自体はやっていない、ということ。
やっているのはマップ周りの画像処理・[イベント]およびコマンドの処理。
updateTransform()を辿る
update()
と似たメソッドでupdateTransform()
というのが描画準備のためのメソッドです。
※これ以降の処理は『RPGツクールMV』から大きく変わっているので、同じノリで『RPGツクールMZ』のスクリプトを書くことはできません。
Tilemap.updateTransform()
から_addAllSpots()
が呼ばれタイルひとつごとに_addSpot()
が呼ばれます。
タイル毎に設定された順にレイヤー2タイル、影ペン、レイヤー2タイルと描画されていきます。
データ的には先にあるものから順に重ねて描かれます。[☆]設定の場合は高層レイヤーに描かれます。
描画対象 | メソッド | 説明 |
---|---|---|
タイル | _addSpotTile() |
低層と高層に振り分けます |
影ペン | _addShadow() |
|
テーブル端 | _addTableEdge() |
|
タイル | _addTile() |
オートとノーマルを振り分けます |
オートタイル | _addAutotile() |
|
ノーマルタイル | _addNormalTile() |
この辺りのどのルートを辿るにせよTilemap.Layer.addRect()
でタイル番号に対応した_elements
プロパティにマップに書き込む情報が記録されます。
豆知識
updateTransform()
もupdate()
と同じくGraphics._onTick()
から呼ばれ、次のような経路でTilemap
まで辿り着きます。
PIXI.Application.render()
→PIXI.Renderer.render()
→Scene_Map.updateTransform()
→Spriteset_Map.updateTransform()
→Sprite.updateTransform()
→Tilemap.updateTransform()
途中のScene_Map``Spriteset_Map``Sprite
はすべてPIXI.Container
の子孫オブジェクトで、メソッドとしてはPIXI.Container.updateTransform()
が実行されます。
PIXI.Container.updateTransform()
は描画と同時にchildren
プロパティに登録されたオブジェクトのupdateTransform()
を呼び出す仕組みになっています。
Tilemap.updateTransform()
以降は上記の通りです。
render()を辿る
update()
やupdateTransform()
と似たメソッドでrender()
というのがレンダリング(画像の描画)のためのメソッドです。
マップの描画を担当するのはTilemap.Layer.render()
メソッドです。
_images
プロパティにタイルセットの画像が保持されているので、それを直前にupdateTransform()
で_elements
プロパティに記録されたデータにもとづき描画します。
描画は PixiJS(WebGL) にお任せです。
具体的におまかせしている部分のコードは次の部分です。
renderer.geometry.draw(gl.TRIANGLES, numElements * 6, 0);
つまり…render()
は_elements
プロパティに基づいて実行されるので、ほとんどの場合触れる必要がないコアスクリプトの中でもコアに近い部分ということですね。
豆知識
render()
もやはりGraphics._onTick()
から呼ばれ、次のような経路でTilemap
まで辿り着きます。
PIXI.Application.render()
→PIXI.Renderer.render()
→Scene_Map.render()
→Spriteset_Map.render()
→Sprite.render()
→Tilemap.render()
→Tilemap.Layer.render()
途中のScene_Map``Spriteset_Map``Sprite
はすべてPIXI.Container
の子孫オブジェクトで、render()
とrenderAdvanced()
セットで実行されますがrenderAdvanced()
は省略しています。
PIXI.Container.updateTransform()
は描画と同時にchildren
プロパティに登録されたオブジェクトのrender()
を呼び出す仕組みになっています。
render()
の中ではupdateTextures()
、_updateIndexBuffer()
、_updateVertexBuffer()
といったメソッドで更新する画像の下準備を行なっていますが、この辺の処理はもう WebGL の領域なので WebGL の勉強の必要があります。
僕はあんまり勉強してないのでわかりません(笑)
最後に
マップは『RPGツクールMV』から『RPGツクールMZ』で大きく変わった部分で、ちょっとほったらかしにしていたのですが、TF_LayeredMap.js というマップ関連プラグインを移植したのを機に調べ直したのがこの記事です。
あんまり役に立たない記事な気がしますが、自分の脳内はだいぶ整理できました。
最近はメソッドを選択して GitHub Copilot(とかのAI)に「このコードの説明してください」と聞くと、割といい感じのことを答えてくれるので、こういう技術記事の役割も無くなるんじゃないかと思ったりします。
ただ、その説明が理解できるかというとスッとはわからなかったりするので、自分の理解を深める意味では有用ですね。
それとあまりに技術記事がなくなると、AIが手本にする記事がなくなって質が落ちちゃうなんてこともあるかもしれません。
レッツエンジョイ ツクールライフ!
Discussion