見出し画像

【ツクールMV・MZ】ゲームオブジェクトを追加してみよう

ゲームオブジェクトとは?
MVならrpg_objects.js、MZならrmmz_objects.jsに入ってるクラス群のことです。

たとえばGame_Actorなら文字通りアクターに関する処理などがまとめられていますし、Game_Mapならマップ関連です。
さて、今回はこのゲームオブジェクトを新たに追加してみようという話です。

なぜ追加するのか?
私自身はこれまでではよくGame_Talkというゲームオブジェクトを追加し、利用してきました。
これはキャラクターとの会話・話題を制御するクラスです。
『魔機人形』『異放生態』『百合籠』でも、なにか話題を持っているキャラクターには上に「…」とフキダシが出て、話しかければその話題の会話が出てきたと思います。

画像1

似たようなことはコモンイベントなどを駆使すれば実装はできますが、よりわかりやすく整理しやすいように、という目的でGame_Talkを実装しています。

・Game_Talkの実装

function Game_Talk() {
   this.initialize.apply(this, arguments);
}

Game_Talk.prototype.initialize = function() {
   this._talk = '';
   this._talked = {};
   this._roulette = [];
   this._testMode = false;
};

まずはこんな感じ。
ひとまずここまでできれば、

var gameTalk = new Game_Talk();

のようにして扱うことはできます。
ただ、上の内容ではGame_Talkを初期化して宣言したにすぎません。
さて、実装したい仕様は以下の通りです。

・新しい話題があるかどうかを判定
・新しい話題がない場合は既存の話題からランダムに話題を選ぶ

Game_Talk.prototype.talkCheck = function(speaker, test) {
   this.prepareTalk(test);
   const data = this.talkData()[speaker];
   let result = false;

   //条件に合う話題があるかどうかを探す
   data.forEach(function(talk) {
       if(talk.condition){
           if(this.setTalk(speaker, talk.id, talk.once)){
               result = true;
               return;
           }
       }
   },this);
   
   //テストモードなら値を返す
   if(this.isTest()) return result;
   
   // 初話題がすべて終わっている場合は既存の内容からランダム
   if(!this._talk) this.talkRoulette();
};

それがこの関数です。
この関数を呼び出せばthis._talkに上の条件を潜り抜けた話題が代入されるようになっています。

Game_Talk.prototype.prepareTalk = function(test) {
   this._talk = null;
   this._roulette = [];
   this._testMode = test || false;
};

一行目にあるthis.prepareTalk(test)はこうです。
要は、話題検索のための初期化関数です。
this._testModeはtrueのとき単にあるかどうかを検索するだけで、「話した」という扱いにはしない、という変数です。

Game_Talk.prototype.talkData = function() {
   return {
       partner:[
       {id:'初回', condition:this.chapter() == 1, once:false},
       {id:'キス未遂', condition:this.chapter() >= 2 && this.time() < 1, once:true},
       //(中略)
       {id:'アイテム', condition:this.chapter() >= 2 && $gameParty.hasAnyItem(), once:false},
       ],
   }
};

そしてthis.talkData()ですが、これは「話題が条件を満たしているか」どうか、などを判定するオブジェクトを格納している関数です。
partnerというのが話者を示すプロパティ。
その中に配列、そして話題ごとにオブジェクトが入ってます。
idが「話題」で、conditionが「条件」、onceはtrueのとき「一度話したら二度目以降は繰り返さない」というものです。

配列ですので、これをforEachで回して条件に合う話題があるかどうかをチェックします。
もしあれば、その話題がsetTalkされます。

Game_Talk.prototype.setTalk = function(person, talk, once) {
   if(!this._talked[person]) this._talked[person] = [];
   if(!this._talk && !this.isTalked(person,talk)){
       if(!this.isTest()){
           this._talked[person].push(talk);
           this._talk = talk;
       }
       return true;
   }else if(!once){
       this._roulette.push(talk);
   }
   return false;
};

ここでは「すでに話したことがあるかどうか」も記録しています。
this._talkedというのがそれです。
もしsetTalkされる話題がなければ既存の条件を満たした話題がthis._roulette(配列)にpushされていますので、this._roulette.lengthでランダム抽出すれば適当な話題がセットされるというわけです。

Game_Talk.prototype.talkRoulette = function() {
   this._talk = this._roulette[Math.floor(Math.random() * this._roulette.length)]
};

なにいってんだこいつ

いえ、JSのわかる人には伝わる内容だとは思いますが、そのへんよくわかってない人にも伝えたいなあという趣旨で書いてるはずなのに、必要な前提知識が多すぎて伝わる気がしない……。
ま、まあ、それでも役立つことはあるでしょう。続けます。

・使えるようにしたりセーブデータに含めたりする

さて、とりあえず基本的な要件は以上で満たせました。
実際にツクール上で利用するにはどうすればよいでしょうか。
まず、ゲームオブジェクトはふつう$gameSystem や$gameParty といったグローバル変数として宣言されています。
実際にそれを行っているのがrpg_managers.jsないしrmmz_managers.jsのなかにあるDataManager.createGameObjects()という関数です。

Game_Talkも同じように扱いたいので、同じように宣言します。

var $gameTalk           = null;
DataManager.createGameObjects = function() {
   $gameTemp          = new Game_Temp();
   $gameSystem        = new Game_System();
   $gameScreen        = new Game_Screen();
   $gameTimer         = new Game_Timer();
   $gameMessage       = new Game_Message();
   $gameSwitches      = new Game_Switches();
   $gameVariables     = new Game_Variables();
   $gameSelfSwitches  = new Game_SelfSwitches();
   $gameActors        = new Game_Actors();
   $gameParty         = new Game_Party();
   $gameTroop         = new Game_Troop();
   $gameMap           = new Game_Map();
   $gamePlayer        = new Game_Player();
   $gameTalk          = new Game_Talk();//@追加
};

こうです。
ちなみに、直接上書きするのではなくプラグインでオーバーライドする形にしましょう。

さて、実装するゲームオブジェクトの仕様にもよるのですが、これだけだとセーブデータには保存されません。
今回Game_Talkは「すでに話したことのある話題かどうか」を保存しておきたいので、セーブデータにも含めるようオーバーライドします。

DataManager.makeSaveContents = function() {
   // A save data does not contain $gameTemp, $gameMessage, and $gameTroop.
   const contents = {};
   contents.system = $gameSystem;
   contents.screen = $gameScreen;
   contents.timer = $gameTimer;
   contents.switches = $gameSwitches;
   contents.variables = $gameVariables;
   contents.selfSwitches = $gameSelfSwitches;
   contents.actors = $gameActors;
   contents.party = $gameParty;
   contents.map = $gameMap;
   contents.player = $gamePlayer;
   contents.talk = $gameTalk;//@追加
   return contents;
};

DataManager.extractSaveContents = function(contents) {
   $gameSystem = contents.system;
   $gameScreen = contents.screen;
   $gameTimer = contents.timer;
   $gameSwitches = contents.switches;
   $gameVariables = contents.variables;
   $gameSelfSwitches = contents.selfSwitches;
   $gameActors = contents.actors;
   $gameParty = contents.party;
   $gameMap = contents.map;
   $gamePlayer = contents.player;
   $gameTalk = contents.talk;//@追加
};

こうです。
これで実際に使う準備ができました。

・ツクール上で実際に使ってみる

画像2

『百合籠』ではこのように利用しています。
一行目で$gameTalk.talkCheck('partner');を呼ぶことで、条件に合う話題がセットされます。
セットされた話題は$gameTalk.talk()によって確認できます。

これをイベントコマンドだけで実装しようとすると、話題ごとに膨大な「条件分岐」が必要になります。
たとえば「アイテムを持ってる」だとか、「進行度が2以上」だとか、「プレイ時間が5分以上」だとか……。
ツクラーならわかると思いますが、もうわけのわからないネスト構造になってしまいます。
Game_Talkはその条件分岐を担ってネスト構造を回避することがまず大きな利便性になります。

・新しい話題がある場合はフキダシを出す

では次に、あの処理です。

const _Game_Event_update      = Game_Event.prototype.update;
Game_Event.prototype.update = function() {
  _Game_Event_update.apply(this, arguments);
  this.updateTalk();
};

Game_Event.prototype.updateTalk = function() {
   if(!this.needTalkWait && this.needTalk()){
       $gameTemp.requestBalloon(this, 8);
       this.needTalkWait = 180;
   }
   if(this.needTalkWait > 0){
       this.needTalkWait--;
   }
};

Game_Event.prototype.needTalk = function() {// add.
   if(!this.isTalkEvent()) return false;
   if($gameMap.isEventRunning()) return false;
   const name = this.event().meta.talk;
   return $gameTalk.talkCheck(name, true)
};

Game_Event.prototype.isTalkEvent = function() {
   return this.event() && this.event().meta.talk && this.isVisible();
};

こうです。
ちなみに先のtestModeはここで使います。
isTalkEvent ()は話しかけたときGame_Talkで制御された会話を返すイベントかどうか。例によってメモ欄のmetaを使います。
メモ欄に<talk:partner>と書くことで話者partnerとして処理します。

で、なにか新しい話題があればneedTalk ()はtrueを返します。
trueが返されればthis.needTalkWait に180という数値が代入されます。
180というのはフレーム数、すなわち3秒です。
つまり、3秒に一回「…」のフキダシが出る、という処理がupdateTalk ()では書かれています。


いかがでしたか?

今回は「ゲームオブジェクトを追加実装する」という話のため、実際に使っていて便利なGame_Talkの解説をしました。
『異放生態』では他にも商店の品揃えを制御するGame_Shops、通知を制御するGame_Signalなどのゲームオブジェクトを実装していたりします。
既存のツクールの機能だけでは不便だなあと感じる場合はなんらかのゲームオブジェクトを実装してみると便利になるかもしれません。

この記事が気に入ったらサポートをしてみませんか?