search
LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

RPGツクールMVでclass記法によって書いたクラスをセーブデータに含める方法

こんな情報使う人わずかしかいないでしょ…

クラス構文の対応状況

まず、ツクールMVではES2015(ES6)から登場したclass構文によるクラスが使えます。

リンク先は私がgithubで公開しているプラグインですが、Spriteを継承してclass構文で書いています。
数字を画面に出すプラグイン
'use strict'が必須ですが、ちゃんと動きます。

デバッグ効率も上がってとても便利です。
なお、セーブデータに含めるのためには以下のような記述が必要です。

class Game_Quests{
//中略
}
window[Game_Quests.name] = Game_Quests;

この方法であれば、自作のクラスをセーブデータに含めることができます。

なぜ、これが必要なのか?

ここから先はJavaScriptの文法や実行形式がどうなっているかを知っていれば、なんとなく考え付く方法です。
普通にツクールでゲームを作る上ではこの知識が必要になるのはプラグイン作者のみですので、知らなくていいです。

JavaScriptでクラス(ここでは旧来のprototypeを用いたものを含む)のメソッドを呼び出す場合、型定義であるprototypeが必要です。

prototypeというのはC++であれば仮想関数テーブルに相当します。
actor.setName("ああああ")といった感じでアクターの名前を「ああああ」に変更すると仮定します。
すると、内部ではこんな感じの処理が走ります。
(大雑把な解説ですので、厳密には違う可能性があります)

function callFunction(_functionName,_this,_args){
  var proto = _this.__proto__
  var func = proto[_functionName];
  if(func){
    func.apply(_this,args);
  }else{
     thorw new Error("undefined error");
  }
}

大体こんな動きをします。
特にif文の下は見覚えがある方もいるでしょう。
クラスの初期化に使っているアレです。

セーブデータ読み込みの実態

ツクールMVでセーブデータを読み込む場合、JsonEx._decode()という関数があります。
丸ごと中身を引用します。

rpg_core.js
JsonEx._decode = function(value, circular, registry) {
    var type = Object.prototype.toString.call(value);
    if (type === '[object Object]' || type === '[object Array]') {
        registry[value['@c']] = value;

        if (value['@']) {
            var constructor = window[value['@']];
            if (constructor) {
                value = this._resetPrototype(value, constructor.prototype);
            }
        }
        for (var key in value) {
            if (value.hasOwnProperty(key)) {
                if(value[key] && value[key]['@a']){
                    //object is array wrapper
                    var body = value[key]['@a'];
                    body['@c'] = value[key]['@c'];
                    value[key] = body;
                }
                if(value[key] && value[key]['@r']){
                    //object is reference
                    circular.push([key, value, value[key]['@r']])
                }
                value[key] = this._decode(value[key], circular, registry);
            }
        }
    }
    return value;
};

これが何をしているかというと、文字列の状態のJSONを再帰的にたどってデータをクラスとして使える状態にしています。
関数の再起呼びだしにfor in文だとか、prototypeへthis._resetPrototype()で型情報を取り付けるなど上級者向けテクニックのオンパレードです。
ただ、セーブデータの復元方法として比較的オーソドックスな方法です。

いよいよ本題です。

class Game_Quests{
//中略
}
window[Game_Quests.name] = Game_Quests;

序盤で書いた、このおまじないの意味を説明します。
windowというのは、JavaScriptの実行においてrootにあたるオブジェクトです。
グローバル名前空間で関数を宣言すると、ここに追加されます。
しかし、class構文の使えるstrictモードは通常の処理と名前空間が異なり、strictモード内で宣言した変数は通常モードからは見えません。
そのため、strictでない空間に持っていく必要があるのです。

少し前の読み込みの内部実装でこんな場所がありましたよね?

rpg_core.js

        if (value['@']) {
            var constructor = window[value['@']];
            if (constructor) {
                value = this._resetPrototype(value, constructor.prototype);
            }
        }

これのwindowはwindow[Game_Quests.name] = Game_Quests;で何かを代入されているwindowと同じです。
これによって、グローバル名前空間から型情報に対応したprototypeを探しているのです。
(厳密に言えば、functionの下にぶら下がっているprototypeではないですが、重箱の隅をつついてもしょうがないので無視します)
JavaScriptの様々なオブジェクトは結局はどれも連想配列です。
それを複雑な使い方で仮想関数・メンバ変数・その他もろもろを実現しているというわけです。

終わりに

長かったですが、こんな話はごく一部のプラグイン作者が知っていればいいだけの話です。
普通にゲームを作る上では必要ないです。
ただ、知っているとたまに…役に立ちます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1