『RPGツクールMZ』[ピクチャの表示]を調べてみた
『RPGツクールMZ』の[ピクチャの表示][ピクチャの移動][ピクチャの回転][ピクチャの色調変更][ピクチャの消去]といったピクチャ関連イベントコマンドがコアスクリプト(JavaScript)でどのように実装されているかを調べます。
ピクチャは利用要求が高い割におざなりな作りのコマンドで、その辺もーちょっと便利になるように改造したプラグイン作れないものかな、と思いまして。
というか⋯すでにいくつもあるような気もしますが。
例によって『RPGツクールMZ』非公式JavaScriptリファレンス のページにクラスなどリンクします。
難しい単語は『RPGツクールMZ』用語集にまとめましたので、参考にどうぞ。
なお、引用しているコアスクリプトには適宜コメントを加えています。
ピクチャ関連クラス
ピクチャは、ゲーム側が操作できるGame_Picture
、それを読み取って画像を表示するSprite_Picture
、そしてピクチャが扱う画像データを保持しておくBitmap
というクラスで構成されています。
Game_Picture
はGame_Screen
が、Bitmap
はImageManager
が管理しています。
また、Sprite_Picture
はSpriteset_Base
というコンテナ(入れ物)で管理されています。
この辺のクラスが複雑に絡み合って運用されているので、なかなかに理解するのが大変です。
イベントコマンドから探る
非公式リファレンスのイベントコマンドのメソッド一覧 - Game_Interpreterを見ると、[ピクチャの表示]のコマンド番号が231
とわかるのでGame_Interpreter
の次のメソッドを見ます。
// Show Picture
Game_Interpreter.prototype.command231 = function(params) {
const point = this.picturePoint(params);
// prettier-ignore
$gameScreen.showPicture(
params[0], params[1], params[2], point.x, point.y,
params[6], params[7], params[8], params[9]
);
return true;
};
引数(params
)からピクチャの座標を取り出し、その他の引数を$gameScreen.showPicture()
に送っているだけです。
Game_Screen.prototype.showPicture = function(
pictureId, name, origin, x, y, scaleX, scaleY, opacity, blendMode
) {
const realPictureId = this.realPictureId(pictureId); // 戦闘中とそれ以外でピクチャIDを使い分け
const picture = new Game_Picture();
picture.show(name, origin, x, y, scaleX, scaleY, opacity, blendMode);
this._pictures[realPictureId] = picture;
};
ここで、新しくGame_Picture
オブジェクトを生成してshow()
メソッドを呼んでいます。
Game_Picture.prototype.show = function(
name, origin, x, y, scaleX, scaleY, opacity, blendMode
) {
this._name = name;
this._origin = origin;
this._x = x;
this._y = y;
this._scaleX = scaleX;
this._scaleY = scaleY;
this._opacity = opacity;
this._blendMode = blendMode;
this.initTarget();
this.initTone();
this.initRotation();
};
show()
メソッドは引数で渡した値を、オブジェクトのプロパティにコピーして、初期化しています。
具体的に表示は行いません。
表示を担当するのはSprite_Picture
です。
Sprite_Picture
で結局イベントコマンドは、画像のプロパティを設定しただけで、具体的に画像ファイルを読んだり、画像を表示したりはしていません。
じゃあ、どうして『RPGツクールMZ』は画像が表示されるのでしょうか。
それはSprite_Pictureが担っています。
『RPGツクールMZ』の多くのオブジェクトは毎フレームupdate()
メソッドが呼ばれるという作りをしています。Sprite_Picture
も例に漏れずupdate()
メソッドが呼ばれます。
update()
→updateBitmap()
と呼ばれていて、ここで画像の読み込みが行われます。
Sprite_Picture.prototype.updateBitmap = function() {
const picture = this.picture(); // 同じピクチャIDの Game_Picture を取得
if (picture) { // ピクチャデータがある場合
const pictureName = picture.name();
if (this._pictureName !== pictureName) { // 新しい画像ファイルか
this._pictureName = pictureName; // 画像名を更新
this.loadBitmap(); // 画像ファイルを読み込む
}
this.visible = true;
} else { // ピクチャデータがない場合
this._pictureName = "";
this.bitmap = null;
this.visible = false;
}
};
つまり画像表示担当のSprite_Picture
からデータ担当のGame_Picture
を毎フレーム監視して、変化があったら読み込みを行うという仕組みです。
Sprite_Picture.prototype.loadBitmap = function() {
this.bitmap = ImageManager.loadPicture(this._pictureName);
};
これは単純に、ImageManager.loadPicture()
メソッドを読んでいるだけで、画像ファイルの読み込みについてはImageManager
が請け負っています。
画像表示に関しては、if (this.visible) {
ブロックで各種プロパティを反映させているので、毎フレームの画像変化ができます。
Sprite_Picture.prototype.update = function() {
Sprite_Clickable.prototype.update.call(this);
this.updateBitmap();
if (this.visible) {
this.updateOrigin();
this.updatePosition();
this.updateScale();
this.updateTone();
this.updateOther();
}
};
ImageManager
先ほど呼び出していた、ImageManagerクラスのloadPicture()
メソッドは次の通り。
ImageManager.loadPicture = function(filename) {
return this.loadBitmap("img/pictures/", filename);
};
ImageManager
のコードを見ると、このメソッドのフォルダ指定だけ変えたメソッドが大量に並んでいます。
ImageManager.loadBitmap = function(folder, filename) {
if (filename) {
const url = folder + Utils.encodeURI(filename) + ".png"; // 拡張子を追加
return this.loadBitmapFromUrl(url); // 画像(Bitmapオブジェクト)を返す
} else {
return this._emptyBitmap;
}
};
これを見るとリテラル(直接プログラムに書いた値)で".png"が与えられていますが、実は読もうとすれば、『RPGツクールMZ』はjpegファイルなども普通に読めます。
この辺、もうちょっと柔軟に設計して標準でいろんなフォーマットが読めるとありがたかったんですが。
閑話休題、結局ここもファイルにフォルダのパスをくっつけてloadBitmapFromUrl()
メソッドを読んでいるだけのシンプルなメソッドです。
ImageManager.loadBitmapFromUrl = function(url) {
const cache = url.includes("/system/") ? this._system : this._cache;
if (!cache[url]) {
cache[url] = Bitmap.load(url); // urlをキーにしてBitmapオブジェクトを格納
}
return cache[url]; // キャッシュされてると読み込みを行わず即座に返せる
};
ここではImageManager
が持っているキャッシュ(データを都度読まなくていいように保持している変数)に設定しています。
このおかげで、同じ画像ファイルを使う場合は、一瞬で表示できます。
画像読み込みの処理を行うBitmap.load()
に関しては後で解説します。
で、読み込み終わったかどうかは、isReady()
メソッドで判定されています。
このisReady()
をチェックすることで、読み込み完了を待って表示という動作が可能になります。
ImageManager.isReady = function() {
for (const cache of [this._cache, this._system]) { // ふたつのキャッシュを順番に
for (const url in cache) {
const bitmap = cache[url]; // キャッシュ中のBitmapオブジェクトを取得
if (bitmap.isError()) {
this.throwLoadError(bitmap);
}
if (!bitmap.isReady()) { // 読み込み中がひとつでもあれば false
return false;
}
}
}
return true;
};
シーン側の詳しい仕組みは『RPGツクールMZ』シーンの生成から廃棄までをお読みください。
Bitmap
先ほど呼び出されていたBitmapクラスのload()
メソッドは次の通り。
※次のメソッドはコアスクリプトにコメントを加えています。
Bitmap.load = function(url) {
const bitmap = Object.create(Bitmap.prototype); // Bitmapオブジェクトを生成
bitmap.initialize(); // new Bitmap()ではないので、自前で初期化
bitmap._url = url; // URLを設定
bitmap._startLoading(); // ロード開始メソッドを呼ぶ
return bitmap;
};
見ての通りBitmap
オブジェクトを生成して返すメソッドですが、注目するのは_startLoading()
メソッドです。
※次のメソッドはコアスクリプトにコメントを加えています。
Bitmap.prototype._startLoading = function() {
this._image = new Image(); // 画像(<img>タグ)オブジェクトを生成
this._image.onload = this._onLoad.bind(this); // 読み終わりのハンドラ
this._image.onerror = this._onError.bind(this);
this._destroyCanvas();
this._loadingState = "loading";
if (Utils.hasEncryptedImages()) { // 暗号化されたデータの処理
this._startDecrypting();
} else {
this._image.src = this._url; // 読み込み開始
if (this._image.width > 0) {
this._image.onload = null; // onload ハンドラをキャンセル
this._onLoad(); // 直接、読み終わりの処理を呼ぶ
}
}
};
Image
クラスは HTML の<img>タグにあたるオブジェクトで、画面に表示しなくても内部的にデータのみの存在として生成できます。
暗号化している場合の処理は今回の範囲外なので追わないでおきます。
else
節でやってることは若干自信がないですが、
ここで_image.width
が0より大きい場合、すでに_url
で指定した画像が読み込まれてキャッシュされていたと判断しているようです…たぶん。
Bitmap.prototype._onLoad = function() {
if (Utils.hasEncryptedImages()) { // 暗号化されたデータの処理
URL.revokeObjectURL(this._image.src);
}
this._loadingState = "loaded"; // ステートを読み込み完了
this._createBaseTexture(this._image); // テクスチャを作成
this._callLoadListeners();
};
_callLoadListeners()
メソッドは前にaddLoadListener()
を使ってリスナを登録していた場合に、読み込み完了を知らせます。
Bitmap.prototype._callLoadListeners = function() {
while (this._loadListeners.length > 0) {
const listener = this._loadListeners.shift();
listener(this);
}
};
逆にいえば、登録されていなければなにもしません。
今回の調査では登録している部分はなかったので、とりあえず無視してもいいのですが、新たに画像を表示するクラスを設計する場合には気にする必要があるでしょう。
Spriteset_Base
そして、Sprite_Picture
はSpriteset_Battle
とSpriteset_Map
に含まれています。
その親クラスはSpriteset_Baseです。
初期化の順番はinitialize()
→createUpperLayer()
→createPictures()
となっていて、次のようにSprite_Picture
が生成・追加されています。
Spriteset_Base.prototype.createPictures = function() {
const rect = this.pictureContainerRect();
this._pictureContainer = new Sprite();
this._pictureContainer.setFrame(rect.x, rect.y, rect.width, rect.height);
for (let i = 1; i <= $gameScreen.maxPictures(); i++) {
this._pictureContainer.addChild(new Sprite_Picture(i));
}
this.addChild(this._pictureContainer);
};
このコードを見ると、Sprite_Picture
が上限の$gameScreen.maxPictures()
(規定値:100)まで一気に追加されていることがわかります。
なおSpriteset_Battle
はScene_Battle
に、Spriteset_Map
はScene_Map
で生成・追加されていてSceneManager._scene._spriteset
で指定できます。
Game_Interpreter
実はGame_Interpreter
の方で[実行内容]にあるイベントコマンドを200行まで読んで、そこに画像表示用のイベントコマンドがある場合に、先読みを自動で行っています。
Game_Interpreter.prototype.loadImages = function() {
// [Note] 『RPGツクールMV』では複雑なプリロードスキームがありました。
// しかし、通常は顔と画像のプリロードで十分と思われます。
const list = this._list.slice(0, 200);
for (const command of list) {
switch (command.code) {
case 101: // [文章の表示]Show Text
ImageManager.loadFace(command.parameters[0]); // 顔グラフィックのプリロード
break;
case 231: // [ピクチャの表示]Show Picture
ImageManager.loadPicture(command.parameters[1]); // ピクチャのプリロード
break;
}
}
};
まとめ
[ピクチャの表示]イベントコマンドについてだけ調べようと思ったら、Game_Interpreter
から芋蔓式にGame_Picture``Sprite_Picture``Bitmap``Game_Screen``ImageManager``Spriteset_Base
を調べることになってしまいました。
Picture が付いてないクラスはピクチャ以外でも使われるので、独立した記事があった方が望ましいようにも思いますが、ひとまず勢いで書いておきました。
[ピクチャの移動][ピクチャの回転][ピクチャの色調変更][ピクチャの消去]イベントコマンドについては、同様にGame_Interpreter
から辿っていけばわかるはずです、行けばわかるさ。あやぶぶなかれ。
自前で立ち絵用のクラスなど設計・実装したら、より理解が深まると思うので、頑張ってみようと思います。
レッツエンジョイ ツクールライフ!
Discussion