年末年始に作成しているRPGツクール用プラグインの管理リポジトリをいい感じにしよう!と思い、いろいろ作業をしていました。
ひとまず一段落したので、何を思っていたのか、何をしたのかについてまとめてみることにしました。
- 前提:ツクールのプラグイン
- プラグインを作る上でのなやみ
- ビルド環境を整備して、なやみを解消する
- そんなわけはなかった
- モノレポ化による改善を試みる
- 何が嬉しくなったの?
- アノテーション用パラメータの問題も解消する
- おわり
前提:ツクールのプラグイン
僕はRPGツクールMV/MZ用のプラグインを制作しています。
RPGツクールMV/MZのプラグインは、単一の JavaScript ファイルとして生成する必要があります。この JavaScript ファイルは大きく分けて以下の2つが含まれる必要があります。
- RPGツクールのエディタ向けのアノテーション付きコメント
- プラグイン本体のコード部分
- 既存のツクールの class の prototype を上書きしたりする
プラグインというかMODだよね
プラグインの詳しい仕様については 公式サイトのプラグイン講座 に書かれています。
プラグインを作る上でのなやみ
2015年にRPGツクールMVが発売されたころは、ベタにアノテーション+コード部分が含まれた単一の JavaScript ファイルを普通にガリガリ書いていました。
もちろん書けることは書けるのですが、いくつか悩みがありました。
(1) アノテーションコメント書くのがしんどい
こういうのを手書きしていくのしんどい!
/*: * @plugindesc コモンイベントの注釈で実績システムさん * @author ru_shalm * * @param ■ 基本 * * @param Common Event ID * @type common_event * @desc 実績の注釈を記述するコモンイベントのID * @default 1 * * @param Storage Key * @type string * @desc 【Web公開用】保存キー名。1つのWebサイトで複数のゲームを公開する場合は、それぞれ別の名前にしてください。 * @default Achievement: Game * .... */
そもそもアノテーションを覚えてないため、都度調べながら書く必要があります。また、あくまでコメントの中なので VSCode や IntelliJ IDEA などの補完に頼ることも難しいです。
そのため、アノテーションの書き方を間違えて「動かね~」と困ることがよくありました。
(2) コードの分割ができないので長くなりがち
単一の JavaScript ファイルの中にゴリゴリ書いていくため、コードの分割という概念がありません。
もちろんプラグイン自体を分割してしまうという方法もありますが、その場合はプラグインを使う人がたくさんのプラグインを導入する必要があり、それはそれで不便です。
また、複数のプラグインで共通の処理をまとめることもできないため、似たようなコードが様々なプラグインでコピペのように増えていくといった問題もありました。特にRPGツクールから渡されるパラメータの整形処理などはほぼ全プラグインで使う頻出処理ですが、各プラグインにちょっとずつ違う実装が生まれていき地獄のようになっていました……
(3) (ツクールMV固有)古いブラウザのために新しい記法は使えない
RPGツクールMV/MZ はエディタに NW.js が付属しており、テストプレイ実行時には NW.js 上でゲームが動くようになっています。
しかし、RPGツクールMVに付属している NW.js は結構バージョンが古いため、最近のECMAScriptの記法を使うと動かない場合があります。そのため、RPGツクールMVで使えるプラグインを作る際には、新しい記法を極力避けるように人間が注意する必要があります。
まぁ、そもそもRPGツクールMVが発売されたときは iOS8 が現役だったので、ES6も使えなかったんですけどね。 iOS 8 は const
とか let
とか書くだけで死にます。
ビルド環境を整備して、なやみを解消する
ある程度、作成したプラグインが増えてきたところで、これ以上は厳しいだろうと考え、これらの問題を解消する方法を検討し始めました。
そこで僕が考えたのが以下のような方法です。
(1) アノテーション問題 → YAMLで書いて自動生成する
アノテーションコメントを手で書くのは厳しいため、YAML形式で表現できるようにしました。以下のようなイメージです。
Torigoya_Achievement2: target: 'MV' body: ja: | 実績・トロフィー的なシステムを定義します。(ry parameter: achievementMenuCancelMessage: parent: 'achievementMenu' name: '閉じるボタンのテキスト' desc: | 実績画面を閉じるボタンのテキスト 空欄の場合は閉じるボタンを表示しません default: '閉じる' # 以下略
コメントに比べれば、シンタックスハイライトも効くし、だいぶ見やすさと書きやすさが向上します。
このYAMLファイルを読み込んで、自動的にアノテーションコメントに変換するスクリプトを作成するようにすることで、アノテーション問題を解消しようとしました。
(2) コードの分割問題 → Rollup でビルドするように
バンドラーとして Rollup を使用するようにしました。
JavaScriptのバンドラーはいろいろありますが、Rollup はバンドル後の出力コードにバンドラー固有のコードがほぼ含まれず、非常にキレイな出力をしてくれるため採用することにしました。これによって、コードを分割し、複数のプラグインで使用する共通処理を別の場所にくくりだすことができるようになりました。
また、共通処理をくくりだせるようになったおかげで、 (1) で用意したYAMLをもとにRPGツクールから渡されるパラメータを読み取る処理を自動生成することも容易になりました。
(3) 古いブラウザ問題 → Babel を通す
Babel を通せばなんでもできる!(雑)
なお、最初のころは Babel を入れていたのですが、途中で入れた理由を忘れてしまい「これいらなくね?」といって抜いて、あとでぶっ壊れることに気づいて戻すといったおバカなムーブをしています。
これですべての悩みが解決し、僕は快適なプラグイン開発生活を送れるようになりました!(*^_^*)
そんなわけはなかった
嘘です。送れませんでした。
最初の頃は快適だなぁと思っていたのですが、さらにたくさんのプラグインを作っていく中で新しい悩みが生まれていきました。
新なやみ(1):ビルドにかかる時間が遅い(重い)
https://github.com/rutan/torigoya-rpg-maker-plugin/blob/old-repo/rollup.config.js
Rollup でビルドするため、僕は「全プラグインのエントリーポイントのファイルを洗い出し、全部一気に Rollup に渡す」といった方法を取っていました。結果、60個くらいのエントリーポイントを Rollup に渡すことになり、ビルドが非常に遅くなってしまいました。
特に僕の開発環境の都合(WSL1 + Windows側のドライブ上 + HDD)でディスクアクセスが非常に遅いという問題もあり、プラグインが増えるごとにどんどん開発体験が悪化していきました。
新なやみ(2):これ、YAMLになっても別に嬉しくねぇな…?
気づいてしまった。
シンタックスハイライトがついて最高~と思っていましたが、逆に言うとシンタックスハイライトくらいしか最高な点がありません。
ちゃんと真面目に考えると、以下の2つの問題をカバーする術が欲しいなぁと思うようになりました。
- 書いた結果が正しいかどうかの判定がちゃんとできる
- そもそも書いてる最中に僕が間違わないように補完とか効きまくって欲しい
僕の作成した変換スクリプトも作りが雑だったためエラー時のアレコレなども整備できておらず、(1) の判定でさえもうまくできておらず、やっぱり僕が気をつけて書かないといけない状態はまだ残っていました。
新なやみ(3):プラグインパラメータの受け取り処理の信頼性が低い
RPGツクールは、プラグインに書かれたアノテーションコメントをもとに、GUI上でプラグインにパラメータを設定して渡すことができます。
このパラメータがプラグインに渡ってくる際に、様々な部分がエスケープされていたり、JSON文字列になっていたりと、かなり複雑な形式になっています。
// 例 {"base":"","baseAchievementData":"[\"{\\\"key\\\":\\\"welcome\\\",\\\"title\\\":\\\"よく来たな、我が相棒よ!\\\",\\\"description\\\":\\\"気がつくと、そこは見知らぬ街だった。\\\\nあなたはアテもなく歩いていると、\\\\n前から少女が駆け寄ってきた。\\\\n\\\\n\\\\\\\\c[6]「お前が来るのをずっと待っていたぞ!\\\\n さぁ、我と共にこの街を支配しよう!」\\\\\\\\c[0]\\\\n\\\",\\\"icon\\\":\\\"359\\\",\\\"hint\\\":\\\"ゲームをプレイする\\\",\\\"isSecret\\\":\\\"false\\\",\\\"note\\\":\\\"\\\"}\"\"]","baseSaveSlot":"achievement","popup":"","popupEnable":"true","popupPosition":"rightUp","popupTopY":"10","popupAnimationType":"tween","popupWait":"1.25","popupWidth":"380","popupPadding":"10","popupTitleFontSize":"20","popupTitleColor":"1","popupMessage":"実績を獲得しました","popupMessageFontSize":"16","popupSound":"{\"soundName\":\"Saint5\",\"soundVolume\":\"90\"}","popupWindowImage":"Window","popupOpacity":"-1","titleMenu":"","titleMenuUseInTitle":"false","titleMenuUseInMenu":"true","titleMenuText":"実績","achievementMenu":"","achievementMenuHiddenTitle":"?????","achievementMenuHiddenIcon":"0","advanced":"","advancedFontFace":"","advancedOverwritable":"false"}
\
の群れに押しつぶされて泣きそう。
このパラメータをキレイに受け取る処理をYAMLをもとに自動生成するようにしていましたが、そもそも自動生成されたコードが正しいのかがよくわかっていません。このあたりはちゃんとツクールの挙動を調べた上で、テスト書いたりして安心できるようにしたいです。
モノレポ化による改善を試みる
これらの新しい悩みを解消する方法として、モノレポ化を行うことにしました。
今までは1つのプロジェクトの中に大量のプラグインが含まれているというリポジトリ構造になっていましたが、共通機構や各プラグインごとにプロジェクトを分割し、それらを1つのリポジトリの中で扱うようにします。
いままで(モノレポ以前)
├── extensions/ # Rollup の拡張置き場 ├── scripts/ # YAML生成などのスクリプト類 ├── src/ │ ├── common/ # プラグインの共通処理を入れる場所 │ ├── entries/ # 各プラグインのエントリーポイントを入れる場所 │ │ ├── base │ │ │ └── tween │ │ │ ├── TorigoyaMZ_FrameTween.js │ │ │ ├── Torigoya_FrameTween.js │ │ │ └── config.yml │ │ └── ... │ └── templates/ ├── babel.config.mjs ├── package.json └── rollup.config.js # 全プラグインのビルド設定
リポジトリが1つのプロジェクトになっており、プラグインのコードは src
以下に大量に置かれています。それをルートにある rollup.config.js
の設定に従って一気にビルドする形を取っていました。
新しいの(モノレポ化)
├── packages/ # 共通パッケージ置き場 │ ├── rpgmaker-plugin-annotation/ # アノテーション │ │ ├── src/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── torigoya-plugin-common/ # プラグインの共通処理 │ └── torigoya-plugin-config/ # ビルド設定・ツール類 ├── plugins/ # 各プラグインを入れる場所 │ ├── achievement/ │ │ ├── config.ts │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── src/ │ │ ├── TorigoyaMZ_Achievement.js │ │ └── Torigoya_Achievement.js │ ├── ... │ └── template.ejs ├── babel.config.mjs ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json └── vitest.config.ts
packages/
と plugins/
という2つのディレクトリに別れました。
packages/
以下には、アノテーションコメントを扱うツールや、プラグインの共通処理(汎用関数)、共通のビルド設定・Rollup拡張などのパッケージが含まれています。これらはそれぞれが別々のパッケージのため、それぞれに package.json があります。
plugins/
以下にはプラグインのコードが入っています。これらもプラグインごとに別パッケージ扱いになっているため、各プラグインが package.json やビルド用の rollup.config.js を持っています。ただし、 rollup.config.js の中身は packages/
のほうに置かれている共通設定を呼び出しているだけで、中身はほぼ空っぽです。
何が嬉しくなったの?
一見なにが嬉しくなったのかわかりづらいですが、以下のような問題が解決できるようになりました。
各プラグインごとにビルドができるため、重くない
各プラグインが単独のパッケージになっているため「今日は◯◯プラグインだけいじるぞ!」というときは、そのプラグインだけ watch モードでビルドするといったことができます。今までは60個以上のプラグインを一気にビルドするしかありませんでしたが、その悩みが解消されました。
今回はTurborepo を使っているため、各パッケージごとの依存の順序に従ったビルドも簡単にできます。また、コードに変更がない場合はキャッシュを利用して一瞬でビルドが完了します(キャッシュからの復元が行われ、ビルド処理は skip される)。
共通処理もパッケージとして独立しているので、ビルドやテスト周りをいじりやすい
僕はツクールのプラグイン自体は TypeScript を使わずにベタに JavaScript で書いています。
これはそもそもツクール本体がTypeScriptでないことや、プラグイン利用者の使っているツクールのバージョンが違う場合があることなどを考慮すると、完璧&安全なツクールの型定義を用意することはできないだろうといったところから、そういった判断をしています(IntelliJ IDEAは JavaScript でも IDE サポート効くしネ)。
しかし、共通処理については話は別です。アノテーションをいい感じにする処理などは複雑怪奇なため、TypeScriptのパワーを借りたり、テストツールを使ったりしたいです。
今までは単一のプロジェクトにそういった事情が違うコードが混ざっていたため対応が難しかったですが、モノレポ化してパッケージが別になったことでビルドツールやテストツールの導入が容易になりました。
今まで信用がおけなかったパラメータの受け取り処理についても、テストコードを書いて Vitest でテスト実行できるようになったため、安心感がだいぶあります。
アノテーション用パラメータの問題も解消する
ビルドに関する問題は解消しましたが、アノテーションコメントもいい感じに倒します。
パラメータのチェックは zod で
RPGツクールのドキュメントと真面目ににらめっこしながら zod でスキーマ定義を作成しました。一部はツクール公式にないオレオレパラメータもありますが……。
このzodの定義があるため、アノテーション用のパラメータを渡して間違いがあれば即エラーにすることができます。また、スキーマ定義から TypeScript の型が取り出せるため、IDEの補完を受けることもできます。
なお、このコードはもともと別リポジトリでやってたのですが、どうせ僕しか使わないし別リポジトリにあるの普通に面倒くさいな……ということでアーカイブしてしまいました。なので npm に上がってるやつはちょっと古かったりします。そのうち更新するかもしれません。
パラメータはYAMLではなくTypeScriptで書く
今まで YAML で書いていたアノテーションコメント用の定義は TypeScript のコードで書けるようにしました。
export const TorigoyaMZ_NotifyMessage: Partial<TorigoyaPluginConfigSchema> = { target: ['MZ'], version: '1.3.0', title: { ja: '通知メッセージプラグイン', }, params: [ ...createParamGroup('base', { text: { ja: '■ 基本設定', }, children: [ createNumberParam('baseAppearTime', { ...defineLabel({ ja: { text: '登場/退場時間', description: dd` 通知が画面にスクロールイン/アウトする時間(フレーム数/60=1秒)を指定します。 `, }, }), min: 0, default: 15, }), ...
一見すると記述量が増えていることから劣化しているように見えますが、TypeScript化したことによって以下のようなメリットがあります。
型の恩恵で記述を間違えると赤線が出る
例えば text
と書くべき場所をタイプミスしたりすると、その部分が赤くなります。存在しないパラメータを書いたらその場でエラーが出ますし、足りない場合もエラーが出ます。書いてるその場でエラーが出るため、「実際に実行してみて確認する」なんて必要はありません。
Struct についても型の恩恵を受けられる
文字列とか数値とかはツクール標準なので問題ないですが、自分で定義できる Struct についてはどうでしょうか?
これについても以下のように記述することで、型の恩恵が受けられるようになっています。
// 独自の Struct を関数を使って定義する const structHuman = createStruct('Human', [ createStringParam('name', { text: '名前', }), createNumberParam('age', { text: '年齢', min: 0, default: 20, }) ]); const MyPlugin = { params: [ createStructParam('human', { struct: structHuman, text: 'プレイヤー', default: { // ← ここの中身も間違えるとちゃんとエラーが出る name: '太郎', age: 20, sage: 'さげぽよ~' // ← こんなパラメータは無いので赤線が出る }, }) ], structs: [structHuman] };
これができるようになったときは勝ったな、という感じがしましたね。
Struct の定義変えたのにパラメータのデフォルト値変えるの忘れた!などはやりがちですが、そういったミスもなくなります。便利~。
なお、便利です~と言いながら、まだ全プラグインのパラメータ定義を TypeScript 化はしておらず、多くのプラグインはYAMLのままになっています。書き換えるの普通に大変だからね!
おわり
そんなわけで、ここ2年くらい悩んでいた問題をやっと解消できた……かなぁ?
まだこの構成にして新しいプラグインを作ったりはしていないため、実際に運用をしていく中で、また新たな悩みなどが生まれる可能性はあると思います。銀の弾丸はないですから。
実際、まだ未解決の問題や悩みとしては以下のようなものがあります。
- パラメータ定義の抜け漏れ判定が甘い
- Struct の定義したのにプラグインの設定に入れ忘れた!みたいなミスは回避できていません
- 使ってるはずなのに無かったらエラー吐く、みたいな仕組みは作りたい
- ツクールMZのプラグインコマンド周りのサポート強化
- もともとMVから始めているため、あんまりMZのプラグインコマンドの気持ちが考えられてない
- このあたりも自動生成できると便利そう
- そもそもプラグインがツクール上でちゃんと動くかのテスト
- できたら嬉しいけど、どうやって……?
- ブラウザ立ち上げてテストするような感じになりそうだけど、ゲーム上でのパターン網羅するの無理がある
- プラグインのリリースをいい感じにやる(?)
- 今は gh-pages にめちゃくちゃな置き方してるヤベーやつ><;
- そもそも僕の中に理想図が描けていない
ひとまず目先の苦しい部分は排除できたので、あとはできるようになり次第いろいろやっていきたいですね。
実績プラグインをいい感じにしたいと思いながら1年以上経ってしまったので、新しいリポジトリ運用で頑張っていくぞ~!