⚔️

『RPGツクールMZ』[ピクチャの表示]を調べてみた

に公開

『RPGツクールMZ』関連記事 目次

『RPGツクールMZ』の[ピクチャの表示][ピクチャの移動][ピクチャの回転][ピクチャの色調変更][ピクチャの消去]といったピクチャ関連イベントコマンドがコアスクリプト(JavaScript)でどのように実装されているかを調べます。

ピクチャは利用要求が高い割におざなりな作りのコマンドで、その辺もーちょっと便利になるように改造したプラグイン作れないものかな、と思いまして。
というか⋯すでにいくつもあるような気もしますが。

例によって『RPGツクールMZ』非公式JavaScriptリファレンス のページにクラスなどリンクします。
難しい単語は『RPGツクールMZ』用語集にまとめましたので、参考にどうぞ。
なお、引用しているコアスクリプトには適宜コメントを加えています。

ピクチャ関連クラス

ピクチャは、ゲーム側が操作できるGame_Picture、それを読み取って画像を表示するSprite_Picture、そしてピクチャが扱う画像データを保持しておくBitmapというクラスで構成されています。
Game_PictureGame_Screenが、BitmapImageManagerが管理しています。
また、Sprite_PictureSpriteset_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>タグにあたるオブジェクトで、画面に表示しなくても内部的にデータのみの存在として生成できます。

参考 : HTMLImageElement - MDN

暗号化している場合の処理は今回の範囲外なので追わないでおきます。

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_PictureSpriteset_BattleSpriteset_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_BattleScene_Battleに、Spriteset_MapScene_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