====== ハイハイスクールアドベンチャー .NET MAUI版 ====== ===== あらすじ ===== 2019年神奈山県立ハイ高等学校は 地盤が弱く校舎の老朽化も進んだため、 とうとう廃校にする以外方法がなく なってしまった。 ところで大変な情報を手に入れた。 それは、 「ハイ高校にATOMIC BOMBが仕掛けられている。」 と、いうものだ。 どうやらハイ高が廃校になった時、 気が狂った理科の先生がATOMIC BOMBを 学校のどこかに仕掛けてしまったらしい。 お願いだ。我が母校のコナゴナになった 姿を見たくはない。 早くATOMIC BOMBを取り除いてくれ……!! 行動は英語で、“<動詞>” 或いは、“<動詞>”+“<目的語>“のように入れていただきたい。 例えば、”look room”と入れれば部屋の様子を見ることが出来るという訳だ。 それでは Good Luck!!!………… ===== 概要 ===== .NETのフレームワークは Xamarinから MAUIへと移行し、現在はMAUIが標準のフレームワークとなっています。 対応するプラットフォームは Windows, MacOS, iOS, そして Androidとなっており、ひとつのコードベースで様々なプラットフォームで動作させることが可能です。 AvaloniaUIもQtも似たようなところを目指いしていますが、リソースファイルの扱いなどが、.NET MAUIがよくできているように感じました。 以前、Xamarinでハイハイスクールアドベンチャーを動作させられるかどうか調べたとき、キモとなるビットマップを表示する機能が脆弱だった((ビットマップを直接表示はできず、BMPなどの画像ファイル形式に変換したのち、メモリストリームを介するなどしないと表示できなかった。多分今も標準のフレームワークではそう。))ため、断念したのですが、AIに聞くと「できます」っていうので、できるんならやったろうか、ということで、移植を開始したわけです。 確かに画像の表示ができるようになるまでは比較的早く、ゲームがただプレイできるだけなら二日もかからずに可能になったのですが、そこからあれこれやりだしたら壁にぶつかりまくりで大変でした。 {{::hhsadvmaui:windowsver.png?400|}}{{::hhsadvmaui:androidver.png?400|}} ===== .NET MAUIの利点 ===== ==== マルチプラットフォーム対応 ==== 比較的少ない労力で、Windows と Androidで動作するようになりました。 レイアウトの調整などもっと手間取るかと思ったんですが、そんなこともなく、それなりの見た目になりました。 MacOSやiOSではどうなるのか興味はありますが、テストする環境もないので特には確かめていません。 ===== .NET MAUIで苦労した話 ===== ==== UIのカスタマイズは制約がある ==== ==== Windows版のパッケージングは金がかかる ==== Windows版については、デフォルトでWiX Toolsetでインストーラまで作成してくれるので一見便利だ。 だが生成されたインストーラは、適切に署名されていないと、他のPCはおろか開発してるPCですらインストールできない。 そして適切な署名を得るには金がかかるのだ。 個人で、フリーソフトの開発して、それを再度ローディング用に配布するのに、なんで金払ってまでやらないといけないのさ? なので、WPFや AvaloniaUIでやったみたいに、publish(発行)して、パッケージングだけなしにしてもらって、Inno Setup 6かなにかでパッケージングすればいいんじゃなかろうかと思ってそのようにしようとしたら、今度は Visual Studio 2022からは発行できなくなりました。 とりあえずプロジェクトファイルに加える設定はつぎのよう。 None true 最初は真面目に、ターゲットフレームワークが Windowsの時だけこれが設定されるようにとかしようと思ったんですが、なぜかうまくきかなくて、普通に共通の設定に混ぜちゃいました。 よくよく考えてみれば、WindowsPackageTypeだのWindowsAppSDKSelfContainedだのはWindows以外では無視されるので、分ける必要は皆無だってことに後から気づきました。 とはいえ、この設定をして、ターゲットを Windows Machineにすると、「発行」メニューがグレーアウトして選べないのです。 {{::hhsadvmaui:publish_disabled.png?400|}} なので、とりあえずはコマンドラインからやるしかないのです。 C:\Users\foo\source\HHSAdvMAUI> dotnet publish -c Release -f net9.0-windows10.0.19041.0 --self-contained true -fでフレームワークを指定する必要があるなど、少々癖が強い((おまけにWindowsだけ net9.0-windowsではなく、後ろにリビジョン番号がくっついてて面倒くさい。))けれど、これでpublishはできるので、あとはそれをパッケージングしてやればいい。 ==== Android版のパッケージングは鍵がいる ==== Android版も、ストアアプリにする予定じゃなくても、できるパッケージは黙ってれば aabになってしまうのです。 これではサイドローディングできません。 これもプロジェクトファイルに出力タイプを指定して apkにすることはできます。 apk ただ、これで生成されるAPKはインストールできません。 無署名だからです。 Androidもパッケージに署名がいるのですが、別にこちらは金を払って公式な署名をする必要はありません。 オレオレ署名で問題なしです。 まずはキーストアをつくります。 $ keytool -genkey -v -keystore keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias keyalias keystore.jksとか keyaliasは適切なものにしてください。 パスワード一回しか聞かれませんが、キーとストアの両方が一度にできます。 キーができたら、プロジェクトファイルに署名のための設定を追加します。 但し、csproj本体にやっちゃうと、プロジェクトをgitなどで公開したくなったときに困るので、csproj.userの方に書いて、.gitignoreなどで公開除外することをお勧めします。 true keystore.jks keyalias xxxxxxxx xxxxxxxx ご覧のようにパスワードべた書きになるので、間違って公開されないようにご注意ください。 あとは普通にpublishしてやれば APKが得られます。 ==== Matrial You対応 ==== Android Studioでは、基本的にMaterial You対応のランチャーアイコンを生成してくれません。 自分で手作業で対応を行う必要があります。 何もしなければ、ランチャーアイコンはカスタムアイコンになります。 ところが、.NET MAUIは頼んでもないのにMatrial You対応のアイコンを生成します。 但し、モノクロアイコンにカスタムアイコンを持ってくるだけなので、カスタムアイコンがモノクロでない限りは何も表示されない間抜けな表示になってしまいます。 できないならやらない、という Android Studioの方針はある意味正しいですが、基本的にPixel Launcher以外はMaterial You対応していないものばかりなので、この問題は発覚することはないのかもしれません。 とはいえ、わたし自身がPixelユーザなのでこれは由々しき問題なのです。 ハイハイスクールアドベンチャーのアイコンはいさこちゃん。 ラスタ画像なので、mdpi, hdpi, xhdpi, xxhdpi, xxxhdpiに、元画像から適切なサイズのものを生成してくれます。 背景画像も適切に作ってくれます。 ただモノクロアイコンだけは作ってくれないのに、adaptive iconに定義を突っ込んでくれます。 これが問題。 まずは、モノクロアイコン画像ファイルを用意し、Resources/Imagesに置きます。 次に、ic_launcher.xml と ic_launcher_round.xml を用意し、Platforms/Android/Resources/mipmap-anyapi-v26の下に置きます。((MauiIconにpngファイルをしていしているとic_launcherではなくそのpngファイルの<ベース名>.xmlになります。)) ハイハイスクールアドベンチャーの場合はMauiIconにisako_1024.pngを指定しているので、生成され参照されるのがisako_1024.xmlとisako_1024_round.xmlになります。 isako_1024_round.xmlも内容は全く同じで基本的に問題はありません。 両方ないと、Pixel Launcherはモノクロアイコンを正しく扱えません。 ==== 画面遷移は戻れない ==== ==== Windows版の発行は自己内包型で ==== ===== 技術的なこと ===== ==== リソースファイル ==== プログラムが参照するファイルのうち、プログラムに同梱のものは、Windowsであればプログラム本体と同じ場所に、Linuxであれば、/usr/share の下かまたは /optの下にインストールされるタイプのアプリなら Windows同様、プログラム本体と同じ場所に、Androidであればリソースという扱いになるのが一般的でしょう。 ((MacOSやiOSがどうなっているのかは知らない。)) また、プログラムの動作中に生成されるユーザデータなどの置き場所も環境に依存するのが普通です。 これらの差分をフレームワークが吸収してくれると非常にプログラミングが楽になります。 .NET MAUIでは、プログラムと同根のファイルは Resources/Raw というフォルダにおいておけばよしなに配置してくれて、環境に依存しない形で読み出せるようにプログラミングできます。 おおこりゃ楽ちん。 と、思っていた時期もありました。 Windows版が一通り動作したので、Androidでも動くか確かめようと思ったら、いきなり例外。 fs.Length がサポートされていないと出てくるわけですよ。 なんですと? 調べてみると、Androidの場合、リソースファイルは圧縮されていたりなんだりで、普通のファイルと違って、Lengthをとったり、Seekしたりできない。 やりたかったら、全部メモリストリームにコピーしてからやるか、リソースファイルを一旦ユーザのデータフォルダーにコピーしてからそっちを使えっていうじゃないですか。 データファイルで最大のものは220KBのマップファイルで、まあ昨今のAndroid端末を考えればどうってこともないサイズ。 全部メモリ展開するっていうのもありかな、とちょっと思ったんですが、このゲームの基本的な構造は 500KB程度の M5 Stackや Raspberru Pi Picoなどでも動くように、けちけち使う分だけを展開するスタイル。 Android版は当初、全部メモリに展開してたんですが、ソースを整理するときに全面的にけちけち戦略に書き直した経緯もあって、今更全部展開っていうのもな、と思い、ファイルをユーザデータ領域にコピーして使う方針に。 private static async Task CopyAssetToAppDataAsync(string filename) { // アセットを開く using Stream input = await FileSystem.Current.OpenAppPackageFileAsync(filename); // 保存先パスを決定 string targetFile = Path.Combine(DataFolder, filename); // コピー using FileStream output = File.Create(targetFile); await input.CopyToAsync(output, bufferSize: 65536); await output.FlushAsync(); } アセットファイルの一覧は取れますか? とAIに聞いたら「できません」というのでファイル名はプログラムで決め打ちでコピーするしかないですが、まあ必要なファイルはプログラム側でわかっているので、面倒でも列挙しておけばいいだけなので、そのようにします。 なお、ご覧のようにこの操作は非同期操作です。 この関数内では awaitを入れて一応順次動作するようにしていますが、呼び出し元でもこの処理をきちんと awaitしないとコピーが半端に終わってしまったりします。 .NET自体が積極的に非同期を導入していて、そもそも非同期のサービスしかなかったりするものもあって、プログラム全体の流れをきちんと考えないとおかしな動作になることもあります。