MZ+TypeScript+Electron
【2023-06-17追記3】
一旦断念した GitHub Actions によるCI/CD以外はやろうとしていた環境整備が全部できたので、(仮)がとれました。
例によってわたしのnote記事は思い浮かんだ順番に書いてるので本題は後半です。適宜目次等でジャンプしてください。また、本記事は途中でも公開しながら試行錯誤の様子も含めて追記していく予定です。もしかしたら諦めるかもしれません。そこはあらかじめごりょうしょうください。
MZ+Electronだけならできている
先日「macOS/Steam版 #RPGツクールMZ を脱NW.jsして Electron で開発、テスト、デプロイする手順」という記事を公開しました。
で、その体制で実際に制作して、体験版リリースまでこぎつけました。こういうの公開するのは初めてだったんですが、夢現さんとふりーむさん合計で 200 DL くらいしていだきまして、これだけでももうほっくほくであります。
ただしひとつ問題があるので対策したい
ただ、前述の「Electron化」の記事では見送ったそこそこ大きな問題がありました。それは「macOSのFinder上で見えるファイルの書き込みは一旦全部放棄してlocalforageによるローカルストレージへの書き込みですませていたこと」です。もちろんこれでもゲームプレイに支障はないようにはなっています。しかし実際に作品を公開してみると、「バグらしき現象に遭遇したので確認してほしい、状況再現のためにセーブデータを送……あれ、
セーブデータどこ?」ということが実際ありました。
また、不要なセーブデータを削除する機能も体験版時点では入れていませんし(MZにもともとあってもいい気はするけど)、(ツクール製としては)ちょっと特殊なゲームで、Weapons.json とか Armors.json とか Actors.json にあたるデータ(これもローカルストレージにいる)がゲームをプレイするほどにどんどんデカくなるんですね。ランダム生成してるので。いまはそこまでパフォーマンス上の問題が出たという話は聞いてませんが、作者以上にやり込んでる人は実際ほんとにいたのでやっぱり可能性は無視できないし、このデータが過大になるとおそらくゲーム中のすべての動作に影響が出ます。そのあたり、いまから見据えて対策を取っておきたいと考えました。つまり、Electron でもちゃんとmacOS上のファイルの読み書きができるような体制を整えようというわけです。
TypeScriptによるかいはつを少し経験した
話は変わるんですが、MZ云々とは別件でいまさらではありますがちょっと TypeScript を触る機会がありまして、これかなーり便利だなと。実際に触るまでは「JavaScriptでも動いてるし型がどうのっつっても JavaScript でそういうふうにちゃんと書けばいいじゃん」と思ってましたが、それが如何に無茶な発想であるかということを思い知りました。実際、MZのコアスクリプトの設計って(ああなってる理由はわからんではないものの)決してわかりやすくもないし安全でもないんですよね。それがなぜか安定して動くといういわば曲芸というか芸術というかそういうたぐいのもので、やっぱり実際に複雑なプラグインを書いてたりしてるとけっこう苦労はさせられます。
そこで、まぁ自作のかいはつも途中ではあるけど体験版リリースということで一段落はしているし、TypeScript という言語自体のチカラとそれをフルサポートしている VS Code のチカラをフル活用して、もっとさくさくMZでもかいはつできる体制を整えた上で続きのかいはつ=完成版のかいはつに臨もう……と考えました。
意外とみんな我流?
そこで、先人に学ぼうとぐぐってみたんですが……。
まず「MZを TypeScript でなどと考える開発ガチ勢」自体が少数派なのと、みんな少しずつ目指す環境が違うんですよね。MV時代からゴリゴリやっててMV向けにいまもやってる人とか、ざっとみた感じみなさん webpack だけどわたしは esbuild 使いたいなとか、そして何より流石に Electron 対応してる人はたぶん誰もいません。じゃあ、まぁ、自分でやってみるか、というわけです。
方針の設定
ということで、どんな環境がいいかなと改めて考えてみました。
MZ本体のコアスクリプトは一切改造しない(本体更新のたびに手直しするのがヤ)
Electronでテストプレイもデプロイもする(NW.jsはもはや使い物にならない)
esbuild をつかう(はやい)
jest もちゃんと通す(あとあとキいてきそう)
GitHub Actions でCI/CDする(実際やったらちょう便利だったので)
まぁあくまでいまのところの「理想」です。どれか諦めたり全部やめたりするかもしれませんが、上記5点を同時に実現することを目指します。
で、いきなり自作に適用するのもコワいので、まずいわゆる Project1 をやります。MZで新規プロジェクトを立てて、それについて上記5点を実現して効率の良い開発体制を整備したうえで、上記 #DonutMachineMZ の既存コードを移植していきます。まぁ我ながらなんとも気長な話ですが、しゅみなので時間無制限。手元でいろいろやりながら、キリのいいところとか何か成果が出たところで本記事に追記していきたいと思います。
本題
まっさらなものを動かす
【2023-06-13追記】
MZ本体から新規プロジェクトを作成したものについて、Electron のメインプロセスを TypeScript で書いて、それを esbuild でビルドして Electron でテストプレイ起動までまずはもってくることができました。
まだIPCは実装してないので、レンダラープロセスについては1行も書いてません。メインプロセスからMZ本体の index.html をロードしただけです。途中まではまた例によって(苦笑) import は使うなだの export なんて知らんだのと Electron さんに怒られ倒していたのですが、electron-esbuild なるnpmモジュールを偶然発見したので導入してみたところ瞬殺でした。すげー。これで上記5点のうち1番と3番は完了、2番もテストプレイは動いたという状況です。
リポジトリはこちら。
ただ、このままだとほんとうにまっさらなプロジェクトしか動かないので、何かプラグインを TypeScript で書いて、ビルドしたものをゲーム側に読ませたいですね。そこでIPCも仕込めればモアベター。
プラグインの読み込み
【2023-06-14追記】
やはりプラグインが使えないと話にならない、でもまっさらなプロジェクトが動いていた時点の「メインプロセスからは index.html をロードするだけ」というシンプルな構成は生かしたい(※)ということでいろいろ試行錯誤の末、srcディレクトリに入れたプラグインをレンダラープロセスの部品としてそのままMZプロジェクト側のjs/pluginsディレクトリにコピーすることに成功しました。
※index.html から読んでる main.js がさらに plugins.js を読んでいて、その plugins.js はMZ本体の側でプラグインマネージャで設定した内容の反映先になってることを考えると、これを活かさないとそのプラグイン同士の関係性を再定義しないといけなくなるのでは…? と思ったのでここはこだわりました。ダミーファイルで electron-esbuild.config.yaml を「騙す」みたいな若干小賢しいこともやってますが、上記構成の維持が最優先と判断しました。
ただ、じゃあ何かプラグインを TypeScript で書きましょうというときに、そこで登場するコア内の要素とかアテにするほかのプラグインとかについて型定義をしないといけないっぽいですね。なるほどここで必要になるか、という感じ。
人様のプラグインをそのまま使うだけでこちらで特に何も利用したりしない場合はそのままsrcディレクトリに入れておけばMZプロジェクト側のjs/pluginsにコピーされるので、それでふつうに使えるでしょう、たぶん。困るのはあくまで TypeScript で自分でプラグインを書く場合だけ。なんでわざわざ TypeScript にして「困る」なんて言ってるのかという気もしてきましたが、複雑なことをやればやるほど恩恵があるはずと信じていまは進んでみるしかなさそうです。
型定義とプラグインによるIPC
【2023-06-16追記】
最初に TypeScript で書いたプラグインの導入を試すにはやはり、以前 JavaScript で書いた Electron 環境用のデバッグ支援プラグイン"CSVN_electronDebugHelper.js"が最適だろうということで、これを書きました。
ちょうどこれが"PluginCommonBase.js"を前提にしているプラグインだったので、これも含めて型定義を進めていたんですが、関連しそうなところを全部やろうとして試行錯誤してるうちにビルドも通らなくなっていることに気づかずだいぶ時間を無駄にしてしまったので、型定義はあくまで補助と割り切って上記2つのプラグインで使う部分のみに作業を範囲を絞ってどうにかこうにか、
TypeScript で書いたプラグインを動作させる
既存のプラグインを前提とする場合でに前提側にも補完を効かせたい場合はそれも TypeScript でリライト(今回の場合PluginCommonBase.tsができた)
前提とする側される側両方のとりあえずエラーにならない範囲の型定義
メインプロセス側にもIPC関連について追記
以上をがんばって、IPC経由でテストプレイのリロードをMZの元の仕様と同様にF5でできるようにしました。まぁやってるうちにこの環境だとCtrl/Cmd+Rでもできることに気づいちゃったんですが。
なお型定義をまじめにやっておくと、VS Code 上でこういうことができます。
微妙な問題発生
概ねいい感じなんですが、微妙な問題もあります。
これはいろいろ調べてもよくわからなかったんですが、VS Code で作業しているとき、該当の型定義ファイルを開いてないと補完が効かずにエラーになります。別にビルド自体はこれでも通ってテストプレイでも期待どおりの動作はするし、ESLint の設定が厳しすぎるのかもしれませんが、まぁせっかくいろいろ補完で見やすくするために試してたことでもあるので、作業中のファイルのぶんだけは開いておくという運用で行くことにしました。微妙。
さて例の方針については現状こんな感じです。
MZ本体のコアスクリプトは一切改造しない(本体更新のたびに手直しするのがヤ)
→ OK維持Electronでテストプレイもデプロイもする(NW.jsはもはや使い物にならない)
→ OK維持、IPCにも成功。esbuild をつかう(はやい)
→ OK維持。jest もちゃんと通す(あとあとキいてきそう)
→ まだ。CSVN_electronDebugHelper は題材としてちょっと向いてないのでそういうのが出てきたときに。GitHub Actions でCI/CDする(実際やったらちょう便利だったので)
→ 断念。ちょっとやろうとしたけどチュートリアルどおりにいかず。
ここまでで感じた恩恵
ツクールのプラグイン開発って「コアが何をしているのかいちいち読み直さないとわからない」ことでとにっかく無駄に時間がかかる印象でしたが、ここをだいぶカットできそうだなと思われました。
あと毎回IIFEにしなくていいのも地味に効いてきそうです。これで1ファイルにクッソ長く書いたり、仕方なくコンストラクタだけIIFEの外に出したりする必要もなくなりそう。
ただ、プラグインはレンダラープロセス側なので、ここを変更しても即リロードがかからないのは微妙ですね。メインプロセス側を書いて消して保存ってやると即リロードが走ってくれはするんですが。
まぁ一応、最低限の内容「Project1」これで完成でもいいかもしないですね。
次なにするか
CSVN_electronDebugHelper.ts にまだ実装してない機能があるのでそれを次にやって、その次はノロワレ式というか、毎回使うだろうと思われるプラグインを TypeScript でリライトしてこうと思っています。これが一式できあがったら本来目指していた「Project1」ですね。
Jest が通った
(2023-06-17追記)
もともと JavaScript で書いていた自分用共通関数群"CSVN_base.js"のリライトにかかりました。ちょうどそこにMathの拡張として戦闘中の処理の中だけではなくてどこでも数値に分散を入れられるようにしたやつとかがあったので、それを TypeScript で改めて書いて、それを ts-jest でテストする形をとった……んですが、今回は実験的機能に甘えてしまったので、そこだけどうにかしたいところです。また、ああいう静的なメソッドはそのままテストコードに書いてもビルドできないようで、import/exportのためにプロダクションコード側がちょっといびつな書き方になってるのもやや痛いところ。
とはいえ一応その実験的ナントカがいきなりなくなったり変わったりするとかしなければ(可能性はあるけど)、このままいけんくないかなと。アプリ側のビルドも通っていまのところ動いてるし。ということで、この実験的ナントカをナントカするのだけもう少し試してみて、ダメそうならもう続きを書いていこうと思います。
(2023-06-17追記2)
その後、tsconfig.json の内容を見直して実験的機能を取り除くことができました。これでOKですね。ts-jest で UT を回していく環境ができました。あとは自分的に必須な機能をゴリゴリ「 TypeScript で」書いていくだけです。babel いらなかったな……。
electron-esbuild が tsconfig.json ナシでも動いていたので、なんとなく electron-esbuild の設定ファイルたちが tsconfig.json の代替になってるのかなと思い込んでたんですが、別にそれらとは別に tsconfig.json を書いても効くので、それで解決です。
なお、最低限の機能だけに絞ったMZETS-1とは別にMZETS-2をたてました。
コアスクリプト既存のクラスに新しいものを生やす
(2023-06-18追記)
前述の CSVN_base.ts の中に、 DataManager クラスに新メソッドを生やすものがあるんですが、型を定義して宣言しておいても、ts-jest したときには当然ながらコアスクリプトの中身は読まれてないので DataManager クラスの中身がないって怒られるんですよね。しかし、ビルド後のアプリの側では DataManager クラスに生やした状態でないと使えません。
それでどうしたもんかなというのでしばらくアタマをひねった結果がこれです。
if (typeof window === "object") {
DataManager.isHiddenItem = isHiddenItem;
}
まぁなかなか哀愁があっていいんじゃないでしょうか。
これで一応、いままで実際にゲーム上で使ってみないとこの関数大丈夫なのかなというのがわかんなかったところが、ゲーム上で使う前にしっかり書けていることを(ある程度は)保証がある状態で使えるようになってきてるかなと思います。そして何より型安全なので、たとえば「これは Game_Actor なの $dataActors の要素なの」みたいなことでウッとなることがなくなる……はずです。まぁまだもう少し複雑なケースをやってみないと何があるかわかりませんが。
IPC経由のローカルファイルの読み書きに成功
【2023-06-20追記】
StorageManagerクラスが Web API に同名のクラスがあるために型定義ができず改名する羽目になるという悲劇もあり、また、Promise だの async だの await だのも含めた型定義もなかなか大変でした。それでも、もともとの MZ のコアスクリプト、特にファイルのロードまわりの「なぜか動いている(たまに動かない)」を可能な限り排除して、処理順が保証された形で、Electron の IPC を経由したローカルファイルの読み書きに成功しました。
これで #DonutMachineMZ では一旦諦めてローカルストレージでやっていたファイルの読み書きも、ふつうにOSのローカルファイルシステム上でできるようになり、完成版ではその形でリリースできるでしょう。状況再現のためのセーブファイルを送……あれ、セーブデータどこ? ってプレイヤーさんがなることもおそらくなくなります。
ただ、別途かいはつしていた "CSVN_baseElectron.ts" (CSVN_electronDebugHelper.ts から改称) でやっているタイトル画面カットの一部が動かなくなりました。まぁテストプレイでしか使わない機能だし、できるようになったことに比べればはるかにどうでもいいですね。一応直すけど。
いちばん懸念していた部分ができたので、あとは #DonutMachineMZ でつくった機能のなかであの作品特有の部分と他の作品でも使えそうな部分とにより分けながら、より汎用性と有用性が高そうなプラグインを TypeScript でリライトしていく感じになるでしょう。
ところでプラグインの公開どうすんの
ただ、先日ひとつ思い当たったのが、「書いたプラグインの公開どうしよう」というところ。TypeScript で書いているので、そのまま TypeScript で公開すると、使えるのは TypeScript でMZのプラグイン開発ができる環境がある人だけになるでしょう。
それなら transpile 後にできた JavaScript を公開するかとも考えましたが、そのままだと source map? (よくわかってない) 部分のバイナリみたいな記述がびゃーっと入ってます。まぁ削除して JavaScript のみのプロジェクトに放り込んでも動かないことはないとは思うんですが、 transpile 後に書いてある内容の詳しいところは正直わかりませんし、ほんらいそこを気にしなくていいのが良さであるはずですし、JavaScript のほうを使って仮に何か起きた人がいても TypeScript に書いてある内容は正しくて TypeScript コミの環境なら動いてますとしか言えないのかなぁ、と。
我ながらだいぶマニアックなことをしている自覚はあるので、これを以て「あなたも MZ+TypeScript+Electronやろうぜ」とはよう言わんのですが、まぁ既存資産を活かす必要がなくて最初から作り込むのでええよという場合は、今後もいろいろできるようになっていくであろう MZETS-2 をcloneして中身を見ていただくのも一興かもしれません。
import の罠
【2023-06-23追記】
JavaScript で書いていた頃は、それぞれのプラグインで同名の定数/変数を宣言しても、それはあくまでそれぞれのプラグインの IIFE の中だけの話で衝突することはありませんでした。たとえば、PluginCommonBase.js をつかってプラグインパラメータをさくっと読み込みたいときに、
const params = PluginManagerEx.createParameter(document.currentScript);
なんてやるわけですが、この params とかは JavaScript で書いていた頃はプラグインごとのIIFEの中の定義だったので重複することはありませんでした。
今回は TypeScript のファイルを分けておけば、ビルドしたときにそれぞれ IIFE の形にくるんで js にしてくれるので、最初にいちいち IIFE でくるまなくていいなぁと自由を謳歌(?)していたんですが、このときプラグインAに書いた定数XとプラグインBに書いた定数Xは「重複」します。また、実際に共通の定数をまとめた CSVN_base.ts を別のプラグインで import するときにこういう問題が発生しました。
PluginManager がプラグインAを読んで、さらにプラグインBの中でプラグインAを import しているとまたそこで読んでしまうわけですね。そうすると、プラグインBで読まれたときに、プラグインBの中でプラグインAの内容が処理されることになり(実際transpileされたプラグインBを見てみるとプラグインAの内容が全部コピーされています)、プラグインAのプラグインパラメータがプラグインパラメータBのものに入れ替わってしまいます。
これはマズいので、document.currentScript が自身と一致している場合のみプラグインのなかみを処理するということが必要になって、結局処理をだーっとくるむことになってしまいました。まぁしゃーないかぁ……という感じです。
これまでに、前述の全体で使う共通定数や Manager まわりの拡張、Electron の IPC 経由のローカルファイルシステム上のファイルの読み書きを実装しており、現在はゲームデータを自動で変数に同期する処理と、それを画面上にHUD的にだすものを書いていて、概ねできています。
これも、 JavaScript で書いていた頃よりだいぶ実装が整理されていい感じです。
あとは別途こういうのも試しています。
通常はテストプレイ中にF9を押すとゲーム画面の中にデバッグウィンドウとしてスイッチと変数の一覧が出ますが、そこをゴニョゴニョしてコンソールに一覧が出るようにしてみました。これは変数の内容は操作できませんが、まず見やすいのがひとつと、これだとゲーム側を止めずにスイッチや変数の内容を確認できますね(いま気づいた)。
こういう、つくってて不便だなとかもっと便利できそうだなと思ってたやつをどんどん追加していこうと思っています。
ロード直後の挙動
【2023-06-26追記】
まえから気がついているのは画像の読み込みが間に合わないケースが無視できない頻度であるということで、これはまぁストレージまわりでやったような Promise/await/async をどうにか使いこなして ImageManager まわりの処理順を保証していく作業をしないといけないんだろうなとは思ってました。
で、今回起動時に $gameVariables.value(id) を $v.get(id) と書けるショートハンドを定義したりしてたんですが、どうもこれも定義のタイミングに問題があったのか、きちんと値を読めていないケースがあったので一旦全廃しました。ロード直後はあれこれ動きがおかしい部分がありそうというのを念頭に置いてかいはつを進めていったほうがよさそうだなぁと思っています。たぶんマップのロード前後にやるのがいけないんだろうなと思うので、もっと前のタイミングならワンチャン……?
新規開発のシーンのウィンドウが透明に
今回 TypeScript であれこれつくろうとしてるわけですが、新しいシーンで新しいウィンドウを配置しようとすると上の画像のようになってしまいました。現時点では原因不明なので、ひとまずworkaround だけかまして逃げてる状態です。
これは後日、不透明度の設定をどこでどうやるのがこの環境で正解なのかを詰めないといけないかなと考えています。
【2023-06-26夕刻追記】
諦めきれずに再挑戦していたところ解決。
ざっくりいうと、「やっぱり本家コアスクリプトは処理順が全然保証されてねぇ」ということにつきるかと思います。
【2023-06-26夕刻追記了】
一応これで、なんどかつくったことがあるファストトラベルの機能に目処がついてきて、それが済んだらBGMの動的変更(=スイッチ/変数/リージョン/地形タグが条件を満たしたら)とかをやってみようかと思っています。
【つづく】