プラグイン コアスクリプトのアーキテクチャ解説

fspace

ユーザー
ツクールのプラグインやスクリプトを眺めているとちょくちょくこんなコード片を見かけたりします。

JavaScript:
SceneManager._scene._spriteset

これを見て「このコードちょっとヤバそうだな」と思える人はいいんですが、「何のこっちゃわからない」「カプセル化の問題かな?」という人も少なくないんじゃないかと思います。

コアスクリプトを含め、ある程度の規模のソフトウェアには、コードを整理するための設計意図とルールが存在します。この設計意図やルールのことを「(ソフトウェア)アーキテクチャ」と呼び、これに違反してしまうと挙動が壊れやすくなり、不具合や競合の大きな原因となります。

アーキテクチャについては開発者による説明文書が用意されるのが一般的ですが、ツクールでは提供されていません。そのため、実際の構造や命名から推測するしかなく、プラグイン開発で初めてプログラミングを学んだような人には読み取るのが困難というのが実情です。

そこで、初心者向けにコアスクリプトのアーキテクチャについて簡単に説明しようと思います。

……いつか書こう書こうと思いつつ、今更感のある時期になってしまってますが、多分次回作が出ても設計はそんなに変わらない……はず。

1. 対象読者​


初心者向けには書きますが、説明できる範囲にも限界があるので、次のことが理解できている程度の読者を想定します。

  • JavaScriptの基本文法
    • 変数や関数、オブジェクトやプロパティなどの基本概念
    • if文、for文、while文などの制御構文
    • 各種データ型や演算子等
    • その他入門書レベルのすべての内容
  • オブジェクト指向プログラミングの基礎
    • 継承、ポリモーフィズム(多相性、サブタイピング)、カプセル化の概要
    • クラス、フィールド、メソッド、コンストラクタ、インスタンスなどの基礎用語
    • プロトタイプベースの継承の仕組み(プロトタイプチェーン)
    • その他コアスクリプトのオブジェクト指向的構造を理解できるだけの知識

もし上記がわからないようであれば入門書を一冊読むことをおすすめします。

2. 準備​


アーキテクチャの話をする前に、設計の基礎として「依存」について少し説明します。

設計というのはざっくりと言えば、コードを意味のあるまとまりに分割して、そのまとまりの間の依存を整理する作業です。そして、アーキテクチャはその分割と依存に関する方針やルールの集合にあたります。

依存とは何か、依存によって何が引き起こされるのかを理解することは、設計を理解するための第一歩です。

2.1. 「依存」とは​


プログラミングにおいて「依存」とは、何らかの対象が別の何らかの対象の存在を前提としている状態のことをいいます。

例えば、クラスAがそのメソッドaにおいて、クラスBのメソッドbを呼び出しているとします。このとき、「メソッドaはメソッドbに依存している」「クラスAはクラスBに依存している」といいます。対象はメソッドやクラスでなくてもよく、特定の役割を果たす複数クラスから成る集合や、クラスの一部の性質をまとめたインターフェース、あるいはもっと小さく、変数や値なんかの場合もあります。

なぜ依存なんてものを考えるかというと、プログラムを書き換えたとき、どこに影響が波及する可能性があるのかを把握したいからです。

例えば、クラスAがクラスBに依存しているとします。このとき、クラスBの挙動を変更すると、クラスAはクラスBの挙動を前提としているため、クラスAの挙動もまた変わってしまう可能性があります。その結果、クラスAは想定通りに動かなくなってしまうかもしれません。そのため、クラスBを変更する際には、クラスAの挙動にも注意しなければなりません。

一方、クラスBがクラスAに依存していなければ、クラスAの挙動を変更してもクラスBの挙動が壊れることはありません。そのため、クラスAの変更は、クラスBのことを気にせずに行うことができます。

プログラムの規模が大きくなってくると、一箇所変更する度に別の場所で問題が発生しないかどうか、ソースコード全体を確認するなんてことはやってられなくなります。そのため、ソースコードを細かなパーツに分解して、適切に依存関係を設定してやることで、確認が必要な部分を小さく保つ設計作業が必要不可欠となります。

2.2. 良い依存と良くない依存​


依存はもちろん存在しない方が挙動を変更しやすいのですが、プログラムを書く以上、依存は絶対に発生します。何からも依存されていないコードというのは、プログラムの開始点(エントリーポイント)でもない限り、単に実行されないコード(デッドコード)です。そのため、性質の悪い依存を避けて、性質の良い依存でパーツ同士を組み合わせていくことが重要となります。

じゃあ依存の性質の良し悪しって何さ、というのが次の問題ですが、実はこれに明確な基準はありません。様々な形とその性質を知って、ケースバイケースで目的に合ったものを選択していく必要があります。

例えば、一般にあまり良くないと言われている形として循環依存があります。これは依存を辿っていくと開始点に戻ってくるような依存関係のことで、最小構成としてはクラスAがクラスBに依存して、かつクラスBがクラスAに依存しているような状態です。どちらのクラスを変更しても、もう片方のクラスが影響を受ける可能性があり、実質的にひとつの大きなクラスを管理しているのと同じような状態となってしまう場合があります。

といっても、限られた小さな範囲では循環依存もあまり問題にはなりませんし、循環依存であっても依存方法によって十分に制限がかかっていれば問題のないケースも多く、あくまでもひとつの指標に過ぎません。

2.3. 参照と変更​


ここまで依存依存と書いてきましたが、実際の設計では単なる依存と捉えるよりも、より具体的な関係を定義して整理することの方が多くなっています。

代表的なものにその依存が「参照」か「変更」かという観点があります(※1)。

参照とは対象のデータや状態の読み取りのみを行うことを指し、変更とは対象のデータや状態の書き込みも行うことを指します。例えば、変数の値を読んだり、何らかの値を計算するだけのメソッドを呼び出したりするのは参照です。一方、変数の値を書き換えたり、インスタンスフィールドの値が変化する可能性のあるメソッドを呼び出したりするのは変更です。

このように参照と変更を分けて扱うのは、変更と比較して参照は非常に扱いやすいという特徴があるためです。

まずは変更が含まれるケースについてみてみましょう。クラスA、クラスB、クラスCがそれぞれ存在し、クラスAがクラスCを変更、クラスBがクラスCを参照しているものとします。このとき、クラスAの変更処理とクラスBの参照処理ではその順序が重要となります。クラスAが変更した後にクラスBが参照するのと、クラスBが参照した後にクラスAが変更するのでは結果が変わってしまうためです。そのため、クラスAとクラスBの間に直接的な依存が存在しなくとも、プログラム全体としてはクラスAとクラスBの処理の順序を制御しなければならなくなります。これはクラスAとクラスBが両方クラスCを変更している場合でも同様です(むしろこの場合は順序がより重要となります)。

一方、クラスAとクラスBがともにクラスCを参照のみしている場合には、この順序を気にする必要がなくなります。クラスAが参照した後にクラスBが参照しても、クラスBが参照した後にクラスAが参照しても結果は変わらないからです。これによりクラスAとクラスBはほぼ独立に扱えるようになります。

また、より自明な性質として、参照は依存対象の状態を壊さないということがあります。プログラム中の何らかの状態が壊れるバグに遭遇した時、その原因として依存関係にある対象を疑うことになりますが、参照のみの関係にある対象はすぐに容疑者から外すことができます。



※1 ここでは「参照」と「変更」としていますが、あまり決まりきった呼び方はありません。「読み取り(read)」と「書き込み(write)」だったり、「クエリ(query)」と「コマンド(command)」だったり、参照のみに注目して「読み取り専用(readonly)」といったり、もう少し広い意味で「副作用がない」といったりします。

2.4. ドメインロジックとプレゼンテーションロジック​


依存の方向、そして参照や変更を意識した代表的な例に、「ドメインロジック」と「プレゼンテーションロジック」の区別があります。

ドメインロジックとは、プログラムの解決したい問題そのものを扱う処理のことを指します。「ドメイン」というのは「問題領域」のことで、一般的にソフトウェアはビジネスの課題解決を目的とすることが多いので「ビジネスロジック」とも呼ばれます。ゲーム分野ではゲーム性に関する部分に当たるため「ゲームロジック」と呼ばれることもあります。例えば、キャラクターの体力を表現するHPを用意する、敵の攻撃を受けるとHPが減少する、といった処理はドメインロジックに当たります。

一方、プレゼンテーションロジックとは、ドメインロジックによる結果をユーザーに対して示す、あるいはユーザーの指示をドメインロジックに伝えるといった、ユーザーとドメインロジックの橋渡しをする処理のことを指します。ざっくりとUIを表示する処理のことと理解してもよいかもしれません。例えば、HPゲージを表示する、HPが減少したときにHPゲージの幅を変化させる、といった処理はプレゼンテーションロジックに当たります。知覚に関するものは基本的に含まれるため、効果音の再生なんかも同様です。

ドメインロジックとプレゼンテーションロジックを分離する設計をしたとき、重要となるのはプレゼンテーションロジックが一方的にドメインロジックに依存するように処理を組み立てることです。つまり、ドメインロジックがプレゼンテーションロジックに依存してはいけません

このようにするのはドメインロジックと比較してプレゼンテーションロジックが非常に変更されやすいためです。問題そのものを扱う方法にはそれほど多くのバリエーションがありませんが、ユーザーの使い勝手には正解がないためいくらでも改善の余地があります。ユーザーへの見え方を調整する度に、問題の扱い方まで変えたくはないのです。ドメインロジックからプレゼンテーションロジックへの依存が存在しなければこれが実現できます。

また、ドメインロジックとプレゼンテーションロジックの間の参照と変更のフローは明確に分けて規定するのが一般的です。特に参照と変更で担当やタイミングを完全に分けてしまうことも多くなっています。

ドメインロジックの要素とプレゼンテーションロジックの要素は一対多で結びつくことがあり、その際にそれぞれの表示要素を独立に扱いたい、というのがその理由の一つです。例えば、ドメインロジックの管理するHPという要素に対して、プレゼンテーションロジックにはHPゲージや残HPの数字表示、瀕死のキャラクター画像など複数の要素が対応する可能性があります。このとき、ある表示要素がHPに関する情報を勝手に操作してしまうと、処理順序等によっては他の表示要素がその変化をうまく反映できないままゲーム画面が表示されてしまうかもしれません。これを防ぐには、変更と参照のタイミングを明確に分けるか、変更の度に必ず再参照する仕組みが必要で、いずれにしても各表示要素が情報源の変更を伴わずに表示更新できなければなりません。
 

3. コアスクリプトのアーキテクチャ​


前節冒頭でも書いた通り、アーキテクチャは分割と依存に関する方針やルールの集合です。

コアスクリプトにはアーキテクチャに関する公式の解説がなく、実際のコードからの推測に過ぎないため完全なものではありませんが、コアスクリプトにおける基本的な分割と依存について説明します。

3.1. 分割と用語定義​


分割については、ファイル分割や命名規則等で示されているためさほど難しくありません。

本記事中の用語定義(※2)と併せて以下に整理します。

用語​
意味​
コアレイヤー​
基盤となるクラス群。rmmz_core.js/rpg_core.js内の定義クラス全体を含む。​
Managerレイヤー​
特定要素の管理クラス群。rmmz_managers.js/rpg_managers.js内の定義クラス全体を含む。​
Gameレイヤー​
ゲーム性に関するクラス群。rmmz_objects.js/rpg_objects.js内の定義クラス全体を含む。​
Spriteレイヤー​
スプライトに関するクラス群。rmmz_sprites.js/rpg_sprites.js内の定義クラス全体を含む。​
Windowレイヤー​
ウィンドウに関するクラス群。rmmz_windows.js/rpg_windows.js内の定義クラス全体を含む。​
Sceneレイヤー​
シーンに関するクラス群。rmmz_scenes.js/rpg_scenes.js内の定義クラス全体を含む。​



※2 厳密には「レイヤー」という語はあまり構造に即していないかもしれません。用語としては「コンポーネント」あるいは「モジュール」の方が正確ですが、それぞれ多義語で別の意味との混同のおそれがあるため、本記事ではわかりやすさを優先して「レイヤー」を採用します(実を言うと「レイヤー」も多義語で、2Dグラフィックスでは前後関係等を規定するコンテナを指します。意味が離れているため比較的混同しづらいとは思いますが、コアスクリプトにはこちらの意味のWindowLayerというクラスも存在するので注意してください)。

3.1.1. コアレイヤー​


コアレイヤーには大別して二種類のクラスが含まれます。ひとつはユーティリティ、もうひとつは外部API(Web APIなど)(※3)やライブラリ(※4)のラッパーです。

ユーティリティというのは、汎用的に使える比較的小規模な便利機能のことをいいます。標準オブジェクトの拡張(JsExtensions)や名前の通りUtils、JSONの独自拡張であるJsonExなどがこれに当たります。

ラッパーというのは、何らかの機能を包み込んで(ラップして)隠してしまい、代わりにより制限されたあるいは整理された機能を公開するようなものを指します。設計的には包み込まれる機能に依存する対象をこのラッパーのみに限定し、他の対象は代わりにこのラッパーに依存させることで、包み込まれた機能による影響を最小限に留める効果があります。例えば、BitmapはCanvas APIをラップすることで、スプライトやウィンドウが直接Canvas APIに依存しないようにしています。

コアレイヤーの主な役割は後者のラッパーで、コアスクリプト外の機能への依存を限定することと思われます。しかし、一方でManagerレイヤー内でも外部APIやライブラリが直接利用されており、両者を分ける基準は明確ではありません。もっと言うと、実はSpriteレイヤーやSceneレイヤーでもおそらくうっかり使用してしまっているようで管理は緩い印象です。

なお、プラグインとして拡張する場合には、この外部依存に関するルールを遵守する必要はあまりないと思います。いくら守ったところでツクール開発部や他のプラグイン開発者がそれを考慮してくれるわけではないので。



※3 「API (Application Programming Interface)」とは、ソフトウェア同士がやり取りするための決め事のことを指しますが、ざっくりとプログラムから利用するために提供されている機能やその使い方くらいに捉えられることが多いです。初心者の多くがJavaScriptと認識しているものは、実際にはJavaScript (ECMAScript) の言語仕様と、Webを便利にするための機能群であるWeb APIに分けられます。JavaScript自体の担当は計算のみで、絵の表示や音の再生などの機能はすべてWeb APIによって提供されています。ちなみに、Webを介して利用するサービスのAPIのことも「Web API」と呼ばれるので混同に注意してください。

※4 「ライブラリ」とは、特定の目的のために用意されたプログラムの部品のことをいいます。特に本体プログラムと比較してより汎用的な部品であり、基本的には本体プログラムと関わりのない外部の開発者によって開発されたものを指します。ツクールではjs/lib/以下に配置されたPixiJS等がこれに当たります。

3.1.2. Managerレイヤー​


Managerレイヤーには様々な役割のクラスが雑多に含まれます。唯一の共通点は静的 (static) クラスであることです。

静的クラスとは、プログラムの実行中、インスタンスが常に1つだけ存在するクラスを指します。1つしか存在しないという特徴からどのインスタンスかを区別する必要がないため、プログラムの任意の場所から唯一のインスタンスに直接アクセスできるのが一般的で、コアスクリプト内でもそのようになっています。

プログラムの実行全体でインスタンス1つのため、各セーブデータごとに変化する情報を扱うのには向かず、セーブデータに依存しない内容を扱うクラスが多くなっています。Managerレイヤーに含まれる各クラスの状態もセーブデータには記録されません。

便宜上レイヤーと呼びますが、全体として統一された役割がないため、設計上まとめて扱うことは基本的にありません。

3.1.3. Gameレイヤー​


Gameレイヤーにはドメインロジックを主に扱うクラスが属します。一部の例外を除き、ドメインロジックはほぼこのクラス群に記述されています。加えて、ドメインロジック寄りのプレゼンテーションロジックも一部含まれます。

また、Game_Temp等の一部クラスを除き、各クラスのプロパティがセーブデータに記録されることも特徴です。逆にそれ以外のレイヤーに属するクラスの状態はセーブデータに記録されないため、セーブデータに記録が必要な情報はGameレイヤー内で管理する必要があります。

ロード時にはセーブデータから復元されることになるため、各クラスの状態は各セーブデータに依存します。そのため、セーブデータ決定前のタイミング(タイトル画面等)では操作してはいけません。

3.1.4. Spriteレイヤー​


Spriteレイヤーにはスプライトを扱うクラスが属します。表示に関する処理なのでプレゼンテーションロジックに当たります。

スプライトとは、2Dグラフィックスにおける画像を貼り付けて表示する板のような仕組みのことをいいます。ゲームでは画像を移動させたり回転させたり拡縮したりエフェクトをかけたりしながら合成するので、このような変換機能をもったスプライトに画像を載せて表示するのが一般的です。

Sprite_から始まるクラスとSpriteset_から始まるクラスがありますが、後者が前者をまとめる役割をもつというだけで、アーキテクチャ全体の中での役割に大きな違いはありません。

Spriteレイヤーに属するクラスはすべてコアレイヤーのSpriteを継承します。

3.1.5. Windowレイヤー​


Windowレイヤーにはウィンドウを扱うクラスが属します。表示に関する処理なので主にプレゼンテーションロジックに当たります。

Windowレイヤーに属するWindow_Base以外のクラスはすべてWindow_Baseを継承し、Window_BaseはコアレイヤーのWindowを継承します。

3.1.6. Sceneレイヤー​


Sceneレイヤーにはシーンを扱うクラスが属します。主にドメインロジックとプレゼンテーションロジックを繋ぐ役割を担います。

シーンとは、「○○画面」に対応する概念で、ゲームの文脈を切り替えるための管理単位です。ライフサイクルメソッドと呼ばれる、特定のタイミングで呼び出されるメソッドを定義することで、ゲーム画面の状態変化を管理します。

シーンクラスの主な役割は、スプライトおよびウィンドウのレイアウト、操作フローおよび演出フローの管理、ユーザー操作のドメインロジックへの反映などです。具体的な表示方法、演出、ゲーム状態の管理などは他のレイヤーに委譲することになります。

Sceneレイヤーに属するScene_Base以外のクラスはすべてScene_Baseを継承し、Scene_BaseはコアレイヤーのStageを継承します。

3.2. 基本依存ルール​


各レイヤー間および特定クラスとレイヤー間には互いにどのように依存していいかというルールがあります。

まずは、各レイヤー間の関係について、依存可能な対象の原則をレイヤーごとに確認していきます。

3.2.1. コアレイヤー​


コアレイヤーは他のすべてのレイヤーに対して一切依存してはいけません。依存していいのは外部APIやライブラリ、および同レイヤー内のクラスのみです。

「コア(core)」という名前の通り中核となる部分なので、依存するのではなく一方的に依存されるようになっています。これに関しては一切の例外もなく徹底されているようです。

3.2.2. Managerレイヤー​


Managerレイヤーは含まれるクラスの役割がバラバラすぎるため、全体としての依存に関するルールはありません。

しかし、自由に依存していいわけではなく、クラスごとにそれぞれルールが決まっています。他のレイヤーから依存される場合についても同様です。クラスごとのルールについては後述します。

3.2.3. Gameレイヤー​


Gameレイヤーは主にドメインロジックを扱うため、プレゼンテーションロジックには依存できません。そのため、SpriteレイヤーとWindowレイヤーには依存してはいけません

ドメインロジックの結果をスプライトやウィンドウに反映させるのはSpriteレイヤーやWindowレイヤーの役目であって、Gameレイヤーから直接反映させてはいけません。Gameレイヤーでは「どのような状態か」という情報の保持のみに留め、Sprite/Windowレイヤーがそれを読み取ることで画面に反映させます。このようにすることで、Sprite/Windowレイヤーが一方的にGameレイヤーに依存するようになっています。

なお、このときGameレイヤー側で保持する情報は極力ドメインロジックに属する内容に留め、知覚に関する内容へはSprite/Windowレイヤー側で変換します。例えば、キャラクターのHP自体はGameレイヤー側で管理しますが、瀕死状態のキャラクター画像を表示するかどうかや残りHPに応じてどのように描画色を変更するかなどは、Sprite/Windowレイヤー側でHPの値を基に決定します。

また、GameレイヤーはSceneレイヤーからは制御される関係にあるため、一方的に依存されることになります。そのため、原則としてSceneレイヤーにも同様に依存してはいけません

Sceneレイヤーはシーンの進行状況に応じてGameレイヤーのメソッドを呼び出してゲーム状態を更新します。Gameレイヤー側では予めそれを想定してメソッドを用意し、引数などによってSceneレイヤーから必要な情報を受け取ります。Gameレイヤーから直接Sceneレイヤーの情報を参照するのではなく、受け取るための仕組みを用意して、Sceneレイヤーから情報を渡してもらうことが重要です。

Gameレイヤーは同レイヤー内のクラスとコアレイヤーには依存が可能です。ただし、コアレイヤー内でもBitmapなどのプレゼンテーションロジックに関するクラスには依存すべきではありません。

3.2.4. Spriteレイヤー​


Spriteレイヤーはプレゼンテーションロジックを扱うため、ドメインロジックを扱うGameレイヤーに依存できます。ただし、原則として参照のみで変更してはいけません

コアスクリプトでは原則として、ユーザー入力をドメインロジックへと伝える役割はSceneレイヤーが担います(あるいは、Gameレイヤー内で直接ユーザー入力を処理します)。そのため、スプライトに対するユーザー操作に反応する場合には、Sprite_ButtonsetClickHandlerのような仕組みにより、具体的な反映処理をSceneレイヤーから登録された関数に任せ、スプライトはユーザー操作の検出のみに徹します。このようにすることで、Spriteレイヤーが直接Gameレイヤーを変更しないようになっています。

Gameレイヤーと同様に、Sceneレイヤーからは制御される関係にあるため、Sceneレイヤーに依存してはいけません

Sceneレイヤーの管理する他のスプライトやウィンドウと連携する場合には、情報を受け取るための仕組みだけを用意して、Sceneレイヤー側で結び付けや同期を行います。スプライトやウィンドウのインスタンスを直接受け取って操作する場合もあれば、共有情報を受け取ってそれを基に表示することで同期させる場合もあります。

Spriteレイヤーは同レイヤー内のクラス、Windowレイヤー、コアレイヤーには依存が可能です。

3.2.5. Windowレイヤー​


WindowレイヤーはSpriteレイヤーと基本的に変わりません。

原則としてGameレイヤーは参照のみ可能です。ウィンドウに対するユーザー操作の検出はしますが、基本的にsetHandlerなどによって具体的な処理はSceneレイヤーに任せ、Gameレイヤーの直接変更はしません。

Sceneレイヤーからは制御される関係にあるため、Sceneレイヤーに依存してはいけません。他のウィンドウやスプライトとの連携はSceneレイヤー主導で行います。

Windowレイヤーは同レイヤー内のクラス、Spriteレイヤー、コアレイヤーには依存が可能です。

3.2.6. Sceneレイヤー​


Sceneレイヤーは全体をまとめる役割を担うため、コアレイヤー、Gameレイヤー、Spriteレイヤー、Windowレイヤーのすべてに依存できます。依存関係における自由度が高いぶん肥大化しやすいため、他のレイヤーの役割を奪わないように、具体的な依存方法に注意が必要です。

Gameレイヤーに対しては、シーンの進行状況に応じたメソッド呼び出しやユーザー入力の反映を行います。Gameレイヤーには「シーンが現在どのような状態か」「何を意図したユーザー操作が発生したか」などを伝えるに留め、具体的な処理は極力Gameレイヤーに任せます。

Sprite/Windowレイヤーに対しては、そのクラスを利用してシーンに必要なスプライトやウィンドウの生成、レイアウト、管理を行います。初期状態の設定や表示要素間の関連付け、ユーザー操作の反映、進行状況に応じた状態更新などは行いますが、具体的な描画方法や演出方法などの知覚に強く関わる処理はSprite/Windowレイヤーに任せます。

Sceneレイヤーは同レイヤー内のクラスへの依存も可能と思われますが、シーンは同時に一つしか実行されないため機会はあまりないかもしれません。

3.2.7. まとめ​


依存に関する基本ルールをまとめると下表のようになります(Managerレイヤーは集合としてのルールがないため省いてあります)。各行が依存する側のレイヤー、各列が依存される側のレイヤーです。

主体​
コア​
Game​
Sprite​
Window​
Scene​
コア​
✅
Game​
✅
✅
Sprite​
✅
✅
参照のみ​
✅
✅
Window​
✅
✅
参照のみ​
✅
✅
Scene​
✅
✅
✅
✅
✅
 

3.3. Manager依存ルール​


Managerレイヤーに属するクラスは、どのレイヤーに依存できるか、どのレイヤーから依存できるかについて、レイヤー単位ではなくクラス単位で決まっています。

それぞれ順に確認していきます。

3.3.1. 依存可能レイヤー​


Managerレイヤーに属する各クラスが依存できるレイヤーは下表の通りです。

主体​
コア​
Game​
Sprite​
Window​
Scene​
DataManager
✅
✅
ConfigManager
✅
StorageManager
✅
FontManager
/ImageManager
/EffectManager
✅
AudioManager
/SoundManager
✅
TextManager
/ColorManager
✅
⚠️
SceneManager
✅
⚠️
BattleManager
✅
✅
⚠️
⚠️
PluginManager
✅

独立性の高いクラスが多いため、コアレイヤーにのみ依存可能なクラスが多くなっています。

DataManagerはゲーム状態のセットアップやセーブデータの管理のため、BattleManagerは戦闘に関するドメインロジックを扱うため、Gameレイヤーに依存可能となっています。ColorManagerhpColorがGameレイヤーに依存していますが、こちらは独立性を考えると依存しない方が望ましいようには思います。

SceneManagerBattleManagerには特殊な依存があるため注意が必要です。

SceneManagerは、SceneレイヤーではScene_Baseにのみ依存可能です。つまり、すべてのシーンに共通の仕様には依存できますが、具体的なシーン固有の仕様に依存してはいけません。

BattleManagerは、SpriteレイヤーではSpriteset_Battle、WindowレイヤーではWindow_BattleLogにのみ依存可能です。加えて、Spriteset_Battleに関してはisBusyで待機が必要かどうかを確認する以外のことをすべきではありません。この辺りは設計の脱出用ハッチのような部分なので、そういうものだと理解してください。

3.3.2. 被依存可能レイヤー​


Managerレイヤーに属する各クラスに依存できるレイヤーは下表の通りです(配置の都合上、これまでと行と列の依存関係が逆なので注意してください)。

客体​
コア​
Game​
Sprite​
Window​
Scene​
DataManager
✅
✅
✅
✅
ConfigManager
✅
✅
✅
✅
StorageManager
✅
FontManager
/ImageManager
/EffectManager
⚠️
✅
✅
✅
AudioManager
/SoundManager
✅
✅
✅
✅
TextManager
/ColorManager
⚠️
✅
✅
✅
SceneManager
✅
⚠️
⚠️
✅
BattleManager
✅
✅
✅
✅
PluginManager
⚠️

全体的にコアレイヤー以外のすべてのレイヤーから利用できるクラスが多くなっています。メソッド単位でみていくともう少し制限がかかる場合もありますが、巨大な表になってしまうのでここでは扱いません。

FontManagerImageManagerEffectManagerは視覚的なアセットを管理するクラスです。プレゼンテーションロジックに関連するため、Gameレイヤーでは極力用いるべきではありません。しかし、Gameレイヤーにはプレゼンテーションロジックもいくらか含まれており、それらに用いるためや事前読み込みのタイミング検知に都合がいいため、コアスクリプトではGameレイヤー内でこれらのクラスを利用している場合があります。方針としては他のレイヤーから利用すべきですが、ケースバイケースの判断が必要です。

AudioManagerSoundManagerは音を再生するためのクラスで、同じくプレゼンテーションロジックに関連するものですが、視覚的要素とは異なり特別に分離して扱う仕組みがアーキテクチャ内に存在しないため、Gameレイヤー内でもよく利用されています。ただし、純粋なドメインロジックと混ざらないようにする配慮は依然として必要です。

TextManagerColorManagerはそれぞれテキストと色を扱うクラスで、これらもプレゼンテーションロジックに関連するものです。ImageManager等と同様で基本的にはGameレイヤーから利用すべきではありませんが、諸々の事情で利用されることもあります。

SceneManagerは主にシーン遷移を扱うクラスで、ドメインロジックに関連する変更に当たるため、原則としてGameレイヤーおよびSceneレイヤー以外からは利用すべきではありません。ただし、戦闘画面やメニュー画面の背景を取得するbackgroundBitmapに限り、プレゼンテーションロジックに関連するため、SpriteレイヤーおよびWindowレイヤーから利用可能です。

PluginManagerはプラグインを扱うクラスで、基本的にゲーム中に利用するものではないため、どのレイヤーからも利用すべきではありません。ゲーム中では唯一プラグインコマンドを呼び出すためにGame_Interpreter内で利用されます。
 

3.4. 例外依存ルール​


ここまで大まかな依存に関するルールを説明してきましたが、実際のコアスクリプトではその通りになっていない箇所が多々あります。

最後にそれらがなぜそのようになっているのかを説明していきます。

3.4.1. シーンクラスと遷移​


GameレイヤーはSceneレイヤーに依存してはいけません。しかし、実際にGameレイヤーのコードを確認してみると、シーン遷移のためにSceneレイヤーのクラスを参照している箇所が見つかります。

JavaScript:
SceneManager.push(Scene_Battle);

このようにSceneManagergoto/pushにコンストラクタを渡す場合、および続けてprepareNextSceneを呼び出す場合に限り、GameレイヤーやBattleManagerはSceneレイヤーのクラスおよびその仕様に依存できます。

コアスクリプトの仕様上、シーンクラスのコンストラクタを渡す形とはなっていますが、この処理は次に遷移したいシーンを伝えるだけのもので、本来は次のようなイメージで分離できるコードです。

JavaScript:
// 次に遷移したいシーンIDとパラメータを渡す。
SceneManager.push({
    sceneId: 'battle',
    params: [],
});

JavaScript:
// SceneManager等の内部でシーンIDに対応したシーンをインスタンス化する。
const { sceneId, params } = nextScene;
let scene;
switch (sceneId) {
    case 'battle': scene = new Scene_Battle(); break;
    case 'shop': scene = new Scene_Shop(); break;
    // ...
}
if (params.length !== 0) {
    scene.prepare(...params);
}

要はシーンを一意に特定するIDとしてコンストラクタを渡しているのであって、シーンクラスの具体的な挙動に依存しているわけではないため、あまり問題にはならないわけです。

3.4.2. イベントの発行と消費​


SpriteレイヤーやWindowレイヤーはGameレイヤーの対象を変更してはいけません。そのため、基本的にGameレイヤーの対象を毎フレーム監視してその変化を反映するのですが、それだと困るケースがあります。何らかの一瞬の事象に対して一定の長さの演出を行う場合、例えば、スキルを発動した瞬間にアニメーションを表示するケースや、ダメージを受けた瞬間にポップアップを表示するケースです。

ドメインロジックの更新タイミングとプレゼンテーションロジックの更新タイミングは必ずしも一致しません(例えば、マップシーンでは早送りでドメインロジックのみ2倍速で更新されます)。ドメインロジック側で何かが起きたことを一瞬だけ記録したとしても、プレゼンテーションロジックの更新タイミングではすでに消されてしまっているかもしれません。どうにかして事象を確実に伝える必要があります。

コアスクリプトでは、その情報を用いる対象がただ一つの場合に限り、ドメインロジック側が発行した「何かが起きた」という情報(一般的に「イベント」と呼ばれます。ツクール用語の「イベント」と混同しないように注意)を、プレゼンテーションロジック側で消費(情報を取得と同時にリセット)可能とすることでこれに対応しているようです。

情報の消費は変更に当たる操作ですが、その情報を用いる対象が特定のスプライトまたはウィンドウただ一つに限られるのであれば、実行順序の問題は発生しません。

3.4.3. ユーザー操作の記録​


コアスクリプトではGameレイヤーの一部のクラスの状態以外はセーブデータに記録されません。しかし、そうなるとプレゼンテーションロジックに関する状態をセーブデータに記録したい場合に、Gameレイヤー側に含めるしかなくなってしまいます。プレゼンテーションロジックの状態を記録したいケースというのはあまり多くはありませんが、例えば、ユーザーが直近に選択したコマンド等を記録して次回選択時にカーソルをその位置に初期化したい場合などがあります。

実際にこのようなケースでは、Gameレイヤー側で状態を持ち、Windowレイヤー等から直接変更操作を行っているようです。もちろん分離の観点では好ましくありませんが、レアケースのために特別な仕組みを作り込むよりは、例外として認めてしまった方が楽ということかもしれません。

3.4.4. 変数・スイッチを操作するウィンドウ​


コアスクリプトには結果を変数やスイッチに記憶するウィンドウがいくつか存在し、これらのウィンドウは変数やスイッチを直接変更します。

これについてはそうすべき合理的な理由が見当たらないため、例外ルールというよりは単なるルール違反かもしれません。

3.4.5. Window_BattleLog​


Windowレイヤーのクラスの中でもひときわ異質なのがWindow_BattleLogです。主にドメインロジックを扱うBattleManagerから指示を受け、ログのみならず戦闘演出全般の制御を行い、Gameレイヤーのクラスに演出の指示を出すとともに、自身もログの表示を行います。プレゼンテーションロジックを扱うはずにも関わらずドメインロジックに依存されていたり、ウィンドウのひとつに過ぎないのに戦闘演出全体のタイミングを制御していたり、Windowレイヤーにも関わらずGameレイヤーを変更していたりと、このクラスを中心としてアーキテクチャの崩壊が見られます。見方によっては、アーキテクチャのうまくいかなかった部分を一手に引き受けたクラスとも言えるかもしれません。

理想的なアーキテクチャからはやや外れた存在ですが、クラス独自のルールは存在しており、基本的に次のようになっているようです。

  • BattleManagerからのみ演出開始の指示を受け、pushにより指示に応じた演出手順を構成する。
  • 各演出手順では、Gameレイヤーのクラスに対する演出の指示、および自身のログ状態の変更のみを行う。このとき呼び出されるGameレイヤーのメソッド(Game_Battlerperformから始まるメソッド等)にはドメインロジックを含めてはならない。
  • 演出の完了を待機する場合を除いて、他のウィンドウやスプライトに依存してはならない。他のウィンドウやスプライトによる演出は、Gameレイヤーのメソッドを介して間接的に開始する。

4. 冒頭のコードの何が問題か​


アーキテクチャのルールを確認したところで、改めて冒頭のコード片の問題点について考えてみます。

JavaScript:
SceneManager._scene._spriteset

注目すべきはSceneManager._sceneでSceneレイヤーのクラスを参照している点です。

Sceneレイヤーに依存していいのは、同じくSceneレイヤーのクラスとSceneManagerだけでした。つまり、このコード片はこれらの場所以外からは実行してはいけません。また、処理が実行されるシーンのインスタンスは常に一つしか存在しないため、Sceneレイヤーから参照するのであればthisのみでいいはずです。もちろんSceneManagerから参照する場合にはthis._sceneで十分です。これらからSceneManager._sceneと書かれている時点でどうも怪しいぞとなるわけです。

加えて、_spritesetはSpriteレイヤーのため、こちらに依存可能かどうかも重要です。シーンクラス以外からスプライトを参照しなければならない場面というのは限られており、その場合の参照方法も基本的に決まっています。例外的状況に陥った場合には大抵何かを間違えています。そのため、これもかなり怪しいポイントです。

ちなみにもっと言えば、カプセル化を無視してプライベートメンバーにアクセスしていることも、Scene_Baseとして抽象化されている_sceneを勝手にScene_MapScene_Battleと仮定して_spritesetというフィールドが存在している前提でアクセスしていることも、問題といえば問題です。上記の依存の問題と比較すれば些事ではありますが……。

4.1. どう修正すればいいか​


修正方法はこのコード片が何を目的としてどこで実行されているかによりますが、よく見られるのは次の2つのパターンかなと思います。

  • プラグインコマンドからスプライトを操作するケース(Gameレイヤーで実行)
  • スプライトやウィンドウから別のスプライトを操作するケース(SpriteレイヤーやWindowレイヤーで実行)

前者の場合、スプライトを直接操作しようとしていること自体が間違いです。スプライトの挙動をどのように変えたいかを考えて、まずはそれを表現する状態をGameレイヤー内に用意します。スプライトは毎フレームの更新処理でその状態を参照して、自らの表示を更新します。

この辺りの流れはピクチャ系のイベントコマンドの実装を参考にするとわかりやすいと思います。これらのコマンドはSprite_Pictureの表示方法を変更するためのものですが、コマンド自体が変更するのはGame_Pictureの方です。Game_InterpreterからGame_Pictureを変更するのは問題ありませんし、Sprite_PictureGame_Pictureを参照するのも問題ありませんので、これはアーキテクチャに沿っています。

後者の場合、まず考えるべきは本当にそのスプライトやウィンドウから別のスプライトを操作すべきかどうかです。表示要素間に特別に強い結びつきがないのであれば、その操作はシーンを介して行うべきです。setHandlerのようなメソッドを用意することで予めシーンから関数を登録しておき、別の表示要素に変化を与えたいタイミングでその関数を呼び出すようにします。そして、シーン側では登録した関数内で、その変化を対象の表示要素に伝えます。このようにするとシーンが一方的に依存する形を保ったまま、表示要素間で変化を伝達できます。

あるいは、もし直接操作すべきだと考えるのであれば、その対象をシーンから渡してもらうべきです。setXxxSpriteのようなメソッドを用意して、シーンの初期化時などにそのメソッドを介してインスタンスを共有します。コアスクリプトではヘルプウィンドウなどがこの方法を用いています。

上記以外のパターンでも、大抵は処理の流れを間違えているだけなので、よく考えて整理すればアーキテクチャに沿った形に修正できるはずです。コアスクリプト内に似たようなケースがないかどうかを探してみると、実装の助けとなるかもしれません。

5. おわりに​


プログラミング初心者向けに設計の基礎とコアスクリプトのアーキテクチャについて説明しました。

記事中では主にコアスクリプトではこうなっているという感じで書きましたが、プラグインを開発する際にもそれに倣って書くことで不具合や競合が起きづらくなります。コアスクリプトのアーキテクチャは素晴らしいと言えるほどのものではありませんが、少なくとも基礎には則っており、従って書けば初心者が好き勝手にやるよりはだいぶマシになるはずです。そして、何よりもすべてのプラグイン開発者がある程度同じルールに従って書くことは、競合を防ぐ上で非常に大きな効果があります。

もしこの記事を読んで設計に興味が湧いたらいろいろと調べてみてください。オブジェクト指向プログラミングには、少し古いですが、GoF (Gang of Four) と呼ばれる4人がまとめた有名なデザインパターン(よく「GoFデザインパターン」と呼ばれます)があるので、そこから始めてみるとよいかもしれません。より一般化された考え方を知りたい場合には、同じく「SOLID」という有名な原則があるので調べてみるとよいでしょう。アーキテクチャについてもっと知りたい場合には、古典的な「MVCアーキテクチャ」やそこから派生した「MVPアーキテクチャ」「MVVMアーキテクチャ」、より広範なシステムを扱った「多層アーキテクチャ」「クリーンアーキテクチャ」などがあります。より現代的な考え方を知りたければ、オブジェクト指向プログラミングを離れて関数型プログラミングを学んでみると最近の流行がわかります。「TEA (The Elm Architecture)」あるいは「MVUアーキテクチャ」は、この関数型プログラミングを基礎とする堅牢なアーキテクチャです。

正直、当初の想定より数倍長い記事になってしまったので、最後まで真面目に読む人どれくらいいるんだろうという感じですが、ツクール全体のプラグインの質が上がって、この競合地獄が少しでも改善されることを願っています。
 
読みました。

記事の内容は至極ごもっともで、再確認ぐらいの内容でした。でも誰かが書くことは大事ですね。

正直なところ、ツクールMZのコアスクリプトは問題が多すぎて正面から向き合うのは骨折り損だと思ってます。
prototypeを好き放題書き換えたり、寿命を無視してグローバル変数を触るプラグイン作者が多いのは、コアの設計に歪みがあるからだと考えています。
諸事情あってツクールMZのイベントコマンド用のテストコードを多数書きましたが、グローバル依存が多すぎて地獄でした。

私はScenaManager._scene._spritesetのようなコードは一度も書いたことが無いですが、これがシンプルな解になってしまう設計に問題があると思います。
「シンプルで多機能」がツクールのコンセプトらしいですが、私から見れば「機能不足でカオス」です。
TypeScriptで書こうにも色々と苦痛なのですが、慣れたか他所のエンジンに引っ越したかで地獄は続きそうです。
 
正直なところ、ツクールMZのコアスクリプトは問題が多すぎて正面から向き合うのは骨折り損だと思ってます。
prototypeを好き放題書き換えたり、寿命を無視してグローバル変数を触るプラグイン作者が多いのは、コアの設計に歪みがあるからだと考えています。
諸事情あってツクールMZのイベントコマンド用のテストコードを多数書きましたが、グローバル依存が多すぎて地獄でした。

コアスクリプトに問題が多いというのは、まあ……そうですね。自分もつい先日、ナゾ設計に刺されました。

ただプラグインを書く限りコアスクリプトは避けようがないので、その中で可能な範囲で整えていくしかないかな、と。避けようとして独自の仕組みを作り込み過ぎると、他の人が書いたプラグインと併用できなくなるので……。



私はScenaManager._scene._spritesetのようなコードは一度も書いたことが無いですが、これがシンプルな解になってしまう設計に問題があると思います。

記事の趣旨としては「コアスクリプトの設計に則ればこんなコードにはならない」なので、そもそもこれは解ではないという立場です。

「インターフェースを限定して変なことができないようにすべき」という話かなとも思いますが、それをやるためにはツクールはまずモンキーパッチ方式の拡張をやめないとですね。明文化されていないとはいえ今も設計がある中で、それを逸脱してこういうコードを書いてしまっているわけで、うまくやるための仕組みを用意しても、抜け道がある限り仕組みを理解していない人は同じことをすると思います。
 
Back
トップ