ユーザ用ツール

サイト用ツール


ハイハイスクールアドベンチャー_.net_maui版

差分

このページの2つのバージョン間の差分を表示します。

この比較画面へのリンク

両方とも前のリビジョン前のリビジョン
次のリビジョン
前のリビジョン
ハイハイスクールアドベンチャー_.net_maui版 [2025/10/26 07:23] arakiハイハイスクールアドベンチャー_.net_maui版 [2025/11/04 00:33] (現在) – [リソースファイル] araki
行 27: 行 27:
 確かに画像の表示ができるようになるまでは比較的早く、ゲームがただプレイできるだけなら二日もかからずに可能になったのですが、そこからあれこれやりだしたら壁にぶつかりまくりで大変でした。 確かに画像の表示ができるようになるまでは比較的早く、ゲームがただプレイできるだけなら二日もかからずに可能になったのですが、そこからあれこれやりだしたら壁にぶつかりまくりで大変でした。
  
 +{{::hhsadvmaui:windowsver.png?400|}}{{::hhsadvmaui:androidver.png?400|}}
 ===== .NET MAUIの利点 ===== ===== .NET MAUIの利点 =====
  
行 35: 行 36:
  
 MacOSやiOSではどうなるのか興味はありますが、テストする環境もないので特には確かめていません。 MacOSやiOSではどうなるのか興味はありますが、テストする環境もないので特には確かめていません。
 +
 +==== 選択ダイアログ ====
 +
 +ハイハイスクールアドベンチャーでは選択肢が必要な場合、オリジナル版ではコマンドラインから入力させていましたが、Palm版を作ったときからダイアログで選択する方式に改変しています。
 +
 +なので、選択ダイアログは必須なのですが、システムがいい塩梅のを提供してくれてなければ自分でダイアログを作っています。
 +
 +.NET MAUIにはDisplayActionSheet()という選択肢から選ぶダイアログが用意されているのでこれを丸っと使うだけで済みました。
 +
 +<code csharp>
 +string gender = await DisplayActionSheet("あなたの性別を教えて下さい", null, null, "男子", "女子");
 +</code>
 +
 +のようにすれば、下のようなダイアログが表示されて、genderには選ばれた方の選択肢の文字列がそのまま返ってきます。
 +ダイアログは当然非同期なので、適宜awaitします。
 +
 +{{::hhsadvmaui:displayactionsheet.png?400 |}}
 +
 +
 +==== マルチプラットフォーム対応 ====
 +
 +.NET MAUIは、Windows/Android/MacOS/iOS のマルチプラットフォーム対応です。
 +なので単一のソースから複数のプラットフォームに対応したパッケージを得ることができます。
 +もちろん、完全にすべてが同一というわけではなく、一部は、その機種のための設定やコードなどを書く必要がありますが、最小限度です。
 +
 +フレームワークとしてマルチプラットフォームをカバーするように作られいてるので、基本的に、ほとんどの処理の機種依存を隠ぺいしてくれています。
 +
 +このあたりは AvaloniaUIのマルチプラットフォーム対応とは大きく違っている部分だと思います。
 +AvaloniaUIも Windows/Linux/Web/Android/iOSをサポートしていますが、AvaloniaUI の名前の通り、こちらは基本的にUIに関する部分を中心にマルチプラットフォーム対応をしています。
 +
 +なので、ちょっと、ファイル操作を統一的にやろうとしたら、それはUIではないので、自分でインターフェイス書いて、クラスを機種ごとに実装して、それをDI注入しろとかいう話になって案外面倒くさいです。
 +
 +.NET MAUIはこのあたりも基本的にきれいに隠ぺいしているので、リソースファイルがプラットフォームによってはSeekできないなどの制約があったとしても、比較的楽にマルチプラットフォーム対応ができるようになっています。
 +これは秀逸でした。
 +
  
  
行 41: 行 77:
 ==== UIのカスタマイズは制約がある ==== ==== UIのカスタマイズは制約がある ====
  
 +UIはモダンなものになると、例えばチェックボックスではなくスライドスイッチが、ラジオボタンではなく押しボタンが用いられている。
 +当然そういったものが提供されていればいいのであるが、実際にはそうではない場合が多い。
 +
 +意外と、ウィジェットは古いものが多いのだ。
 +
 +しかし、モダンなライブラリともなると、XMLや CSSなどを用いて外観をカスタマイズできる機能を提供していることが多い。
 +WPFもQtもそうだし、.NET MAUIもそういった機能をもっている。
 +
 +但し、なんでもできると思ったら大きな間違いで、限界はライブラリの実装やデザインなどから存在している。
 +
 +今回、スライドスイッチは、おそらくAndroidやiOSが提供しているからだろう。
 +Windowsでも <switch>タグを用いればこれはそのまま使える。
 +但し右端にやたらと大きな余白があって、これをつぶそうとしてもつぶせないのはご愛敬だが。
 +AIに聞いたら「右端に余白を大きくとっているのは指でタップしても十分な応答領域をとっているから」とかどこ情報なのか定かではない適当な返事をしてくるが、Androidではむしろ余白はなく、Windowsの余白をクリックしてもボタンは反応しないので、正しくない情報なのは明らかだが、そんなわけで、この余白をつぶすのはなかなかに困難そうなので放置している。
 +
 +ラジオボタンを押しボタン風の外観にするのは、まあ、そんなに難しくもなかった。
 +
 +ラジオボタンはマウスオーバー(ホバー)すると、色が変わるギミックを持っていたので、この押しボタン風のものもそのようにしたかったのだけれど、これが結論から言うと、できない相談だったようだ。
 +
 +まあ、マウスがのっかるなんて言うのは、基本的にはAndroidやiOSではあまり想定しなくていい機能だし、そういった事情も鑑みると、カスタム機能としては提供されていない、あるいは想定外だったとしても仕方がないといえばそうなのだが。
 +
 +問題は、ラジオボタンの持っている((そしてカスタム可能な))状態の定義にある。
 +
 +ラジオボタンは、大まかに分けて、CommonStateとCheckStateの二種類の状態がある。
 +
 +CheckStateで、ボタンがチェックされた(Checked)とチェックが外された(Unchecked)という状態の変化が管理されていて、それぞれに遷移したときに外観をどうするのかをカスタムできる。
 +
 +もう一方のCommonStateについては、通常状態(Normal)と、マウスオーバー(PointerOver)の状態が定義されていて、それぞれに遷移したときの外観がどうように定義できる。
 +
 +問題は、CommonStateがマウスオーバーと通常状態の二種類しかないので、押されているボタンの上にマウスカーソルが来て、そしてボタン外へ移動すると、通常状態の色に戻されてしまい、押された状態が消されてしまうのだ。
 +
 +要するに、Checked かつ PointeOvertとか、Checkedかつ Normalとかの複合状態が持てないので、この種のボタンにホバーによる色変化をつけようとすると使い物にならないボタンになってしまうのだ。
 +
 +複合状態を提供してくれりゃいいじゃんっていうのが率直なところだが、先にも書いたように、モバイルデバイスでは比較的どうでもいい機能なので、まあ、そういうもんだと飲み込んであきらめるしかなさそうなのが現状である。
 +==== Windows版のパッケージングは金がかかる ====
 +
 +Windows版については、デフォルトでWiX Toolsetでインストーラまで作成してくれるので一見便利だ。
 +だが生成されたインストーラは、適切に署名されていないと、他のPCはおろか開発してるPCですらインストールできない。
 +そして適切な署名を得るには金がかかるのだ。
 +
 +個人で、フリーソフトの開発して、それを再度ローディング用に配布するのに、なんで金払ってまでやらないといけないのさ?
 +
 +なので、WPFや AvaloniaUIでやったみたいに、publish(発行)して、パッケージングだけなしにしてもらって、Inno Setup 6かなにかでパッケージングすればいいんじゃなかろうかと思ってそのようにしようとしたら、今度は Visual Studio 2022からは発行できなくなりました。
 +
 +とりあえずプロジェクトファイルに加える設定はつぎのよう。
 +
 +<code xml>
 +<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
 + <WindowsPackageType>None</WindowsPackageType>
 + <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
 +</PropertyGroup>
 +</code>
 +
 +最初は真面目に、ターゲットフレームワークが Windowsの時だけこれが設定されるようにとかしようと思ったんですが、なぜかうまくきかなくて、普通に共通の設定に混ぜちゃいました。
 +よくよく考えてみれば、WindowsPackageTypeだのWindowsAppSDKSelfContainedだのはWindows以外では無視されるので、分ける必要は皆無だってことに後から気づきました。
 +なお、Configuration == Debugの時もNoneにしてしまうと「配置ができない」といってデバッグ自体できなくなってしまうので、あくまでもリリース用の設定として、Release|AnyCPU のときだけの設定にしてあります。
 +
 +なお、この設定をして、ターゲットを Windows Machineにすると、「発行」メニューがグレーアウトして選べなくなります。
 +
 +{{::hhsadvmaui:publish_disabled.png?400|}}
 +
 +なので、発行自体はコマンドラインからやるしかないのです。
 +
 +<code powershell>
 +C:\Users\foo\source\HHSAdvMAUI> dotnet publish -c Release -f net9.0-windows10.0.19041.0 --self-contained true
 +</code>
 +
 +-fでフレームワークを指定する必要があるなど、少々癖が強い((おまけにWindowsだけ net9.0-windowsではなく、後ろにリビジョン番号がくっついてて面倒くさい。))けれど、これでpublishはできるので、あとはそれをパッケージングしてやればいい。
 +
 +
 +
 +==== Android版のパッケージングは鍵がいる ====
 +
 +Android版も、ストアアプリにする予定じゃなくても、できるパッケージは黙ってれば aabになってしまうのです。
 +これではサイドローディングできません。
 +
 +これもプロジェクトファイルに出力タイプを指定して apkにすることはできます。
 +
 +<code xml>
 +<AndroidPackageFormat>apk</AndroidPackageFormat>
 +</code>
 +
 +ただ、これで生成されるAPKはインストールできません。
 +無署名だからです。
 +
 +Androidもパッケージに署名がいるのですが、別にこちらは金を払って公式な署名をする必要はありません。
 +オレオレ署名で問題なしです。
 +
 +まずはキーストアをつくります。
 +
 +<code bash>
 +$ keytool -genkey -v -keystore keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias keyalias
 +</code>
 +
 +keystore.jksとか keyaliasは適切なものにしてください。
 +パスワード一回しか聞かれませんが、キーとストアの両方が一度にできます。
 +
 +キーができたら、プロジェクトファイルに署名のための設定を追加します。
 +但し、csproj本体にやっちゃうと、プロジェクトをgitなどで公開したくなったときに困るので、csproj.userの方に書いて、.gitignoreなどで公開除外することをお勧めします。
 +
 +<code xml>
 +  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
 +    <AndroidKeyStore>true</AndroidKeyStore>
 +    <AndroidSigningKeyStore>keystore.jks</AndroidSigningKeyStore>
 +    <AndroidSigningKeyAlias>keyalias</AndroidSigningKeyAlias>
 +    <AndroidSigningKeyPass>xxxxxxxx</AndroidSigningKeyPass>
 +    <AndroidSigningStorePass>xxxxxxxx</AndroidSigningStorePass>
 +  </PropertyGroup>
 +</code>
 +
 +ご覧のようにパスワードべた書きになるので、間違って公開されないようにご注意ください。
 +
 +あとは普通に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になります。
 +
 +<file xml isako_1024.xml>
 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
 + <background android:drawable="@mipmap/isako_1024_background" />
 + <foreground android:drawable="@mipmap/isako_1024_foreground" />
 + <monochrome android:drawable="@drawable/isako_1024_monochrome" />
 +</adaptive-icon>
 +</file>
 +
 +isako_1024_round.xmlも内容は全く同じで基本的に問題はありません。
 +両方ないと、Pixel Launcherはモノクロアイコンを正しく扱えません。
 +==== 画面遷移は戻れない ====
 +
 +.NET MAUIのアプリケーションは、App → AppShell → ContentPage という階層構造を持っています。
 +
 +アプリケーション本体の画面は、普通にプロジェクトを作れば MainPageというContentPageになります。
 +ここに設定画面やこのプログラムについて(About)などの画面を追加するとき、それぞれは別のContentPageになります。
 +
 +ページ遷移のための情報は AppShellのコンストラクタで定義をします。
 +ハイハイスクールアドベンチャーはMainPageのほかに SettingsPage, AboutPageを持っているので以下のようになっています。
 +
 +<code csharp>
 +    public partial class AppShell : Shell
 +    {
 +        public AppShell()
 +        {
 +            InitializeComponent();
 +            Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));
 +            Routing.RegisterRoute(nameof(AboutPage), typeof(AboutPage));
 +        }
 +    }
 +</code>
 +
 +ページを開く処理をするときは
 +
 +<code csharp>
 +await Shell.Current.GotToAsync(nameof(SettingsPage));
 +</code>
 +
 +などとして、ページ遷移を行います。
 +
 +呼び出された画面には、明示的に「閉じる」のようなボタンを付けないでも、タイトルバーに「←」ボタンが出てくるので、これを押せば元のページに戻ります。
 +
 +が、戻ったときに、MainPageは新しく作り直されてしまい、要するにゲーム途中で設定を呼んだとしても、タイトル画面に強制的に戻されてしまうのです。
 +
 +GoBackComandを GoToAsync("..")のようにすれば元の状態に戻る(かもしれない)といわれて試してみましたが、戻れませんでした。
 +((何かほかの要因があるのかもしれないので、うまくいく場合もあるかもしれませんが。))
 +
 +要は、MainPageが初期化されても、元の状態に復帰できればいいわけなので、初期化時に元に戻れるように細工をしました。
 +
 +もともと、ハイハイスクールアドベンチャーはゲームのセーブとロードに対応しているので、ゲーム中であればメモリ上にデータをセーブして、初期化時にデータがあればそれをロードするだけでゲームの状態は元通りです。
 +
 +あとは、システムの状態(Title, Playing, GameOver)がどれなのかで、どの画面に戻せばいいのかを MainPageの初期化の中で切り分けるだけです。
 +
 +ログエリアの内容は、MVVM化したときに、MainPageではなく MainPageModelに分離してあったので、MainPageModelをシングルトンにして、画面遷移で MainPage自体が初期化されても、継承されるようにしておきます。
 +
 +ゲームデータや、システム状態も、モデル側に保存するようにして、画面遷移問題は解決しました。
 +
 +この辺がうまくデザインできていないと、設定画面をContentPageではなく、オーバレイで作り直しみたいなことになったかもしれません。
 +
 +MVVMなどのデザインにはこういう面でも意味があるんだと思いました。
 +
 +
 +==== Windows版の発行は自己内包型で ====
 +
 +publishするときに --self-contained trueをつけろという話です。
 +これをつけないと、.NET のラインタイムのリビジョンに強い縛りが発生して、結果実行できないバイナリが出来上がってしまいます。
 +((実行すると、延々 .NET Runtime 9.0.0をインストールしろと言われ続けるが、ダウンロードできるのが9.0.14だったりして結果要件を満たせない。))
 +
 +
 +===== 技術的なこと =====
 +
 +==== リソースファイル ====
 +
 +プログラムが参照するファイルのうち、プログラムに同梱のものは、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版]]は当初、全部メモリに展開してたんですが、ソースを整理するときに全面的にけちけち戦略に書き直した経緯もあって、.NET MAUI版とはいえ今更全部展開っていうのもな、と思い、ファイルをユーザデータ領域にコピーして使う方針に。
  
 +<code csharp>
 +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();
 +}
 +</code>
  
 +アセットファイルの一覧は取れますか?
 +とAIに聞いたら「できません」というのでファイル名はプログラムで決め打ちでコピーするしかないですが、まあ必要なファイルはプログラム側でわかっているので、面倒でも列挙しておけばいいだけなので、そのようにします。
  
 +なお、ご覧のようにこの操作は非同期操作です。
 +この関数内では awaitを入れて一応順次動作するようにしていますが、呼び出し元でもこの処理をきちんと awaitしないとコピーが半端に終わってしまったりします。
  
 +.NET自体が積極的に非同期を導入していて、そもそも非同期のサービスしかなかったりするものもあって、プログラム全体の流れをきちんと考えないとおかしな動作になることもあります。
ハイハイスクールアドベンチャー_.net_maui版.1761463394.txt.gz · 最終更新: by araki