ハイハイスクールアドベンチャー_.net_maui版
差分
このページの2つのバージョン間の差分を表示します。
| 両方とも前のリビジョン前のリビジョン次のリビジョン | 前のリビジョン | ||
| ハイハイスクールアドベンチャー_.net_maui版 [2025/10/26 08:33] – [Windows版の発行は自己内包型で] araki | ハイハイスクールアドベンチャー_.net_maui版 [2025/11/04 00:33] (現在) – [リソースファイル] araki | ||
|---|---|---|---|
| 行 36: | 行 36: | ||
| MacOSやiOSではどうなるのか興味はありますが、テストする環境もないので特には確かめていません。 | MacOSやiOSではどうなるのか興味はありますが、テストする環境もないので特には確かめていません。 | ||
| + | |||
| + | ==== 選択ダイアログ ==== | ||
| + | |||
| + | ハイハイスクールアドベンチャーでは選択肢が必要な場合、オリジナル版ではコマンドラインから入力させていましたが、Palm版を作ったときからダイアログで選択する方式に改変しています。 | ||
| + | |||
| + | なので、選択ダイアログは必須なのですが、システムがいい塩梅のを提供してくれてなければ自分でダイアログを作っています。 | ||
| + | |||
| + | .NET MAUIにはDisplayActionSheet()という選択肢から選ぶダイアログが用意されているのでこれを丸っと使うだけで済みました。 | ||
| + | |||
| + | <code csharp> | ||
| + | string gender = await DisplayActionSheet(" | ||
| + | </ | ||
| + | |||
| + | のようにすれば、下のようなダイアログが表示されて、genderには選ばれた方の選択肢の文字列がそのまま返ってきます。 | ||
| + | ダイアログは当然非同期なので、適宜awaitします。 | ||
| + | |||
| + | {{:: | ||
| + | |||
| + | |||
| + | ==== マルチプラットフォーム対応 ==== | ||
| + | |||
| + | .NET MAUIは、Windows/ | ||
| + | なので単一のソースから複数のプラットフォームに対応したパッケージを得ることができます。 | ||
| + | もちろん、完全にすべてが同一というわけではなく、一部は、その機種のための設定やコードなどを書く必要がありますが、最小限度です。 | ||
| + | |||
| + | フレームワークとしてマルチプラットフォームをカバーするように作られいてるので、基本的に、ほとんどの処理の機種依存を隠ぺいしてくれています。 | ||
| + | |||
| + | このあたりは AvaloniaUIのマルチプラットフォーム対応とは大きく違っている部分だと思います。 | ||
| + | AvaloniaUIも Windows/ | ||
| + | |||
| + | なので、ちょっと、ファイル操作を統一的にやろうとしたら、それはUIではないので、自分でインターフェイス書いて、クラスを機種ごとに実装して、それをDI注入しろとかいう話になって案外面倒くさいです。 | ||
| + | |||
| + | .NET MAUIはこのあたりも基本的にきれいに隠ぺいしているので、リソースファイルがプラットフォームによってはSeekできないなどの制約があったとしても、比較的楽にマルチプラットフォーム対応ができるようになっています。 | ||
| + | これは秀逸でした。 | ||
| + | |||
| 行 41: | 行 76: | ||
| ==== UIのカスタマイズは制約がある ==== | ==== UIのカスタマイズは制約がある ==== | ||
| + | |||
| + | UIはモダンなものになると、例えばチェックボックスではなくスライドスイッチが、ラジオボタンではなく押しボタンが用いられている。 | ||
| + | 当然そういったものが提供されていればいいのであるが、実際にはそうではない場合が多い。 | ||
| + | |||
| + | 意外と、ウィジェットは古いものが多いのだ。 | ||
| + | |||
| + | しかし、モダンなライブラリともなると、XMLや CSSなどを用いて外観をカスタマイズできる機能を提供していることが多い。 | ||
| + | WPFもQtもそうだし、.NET MAUIもそういった機能をもっている。 | ||
| + | |||
| + | 但し、なんでもできると思ったら大きな間違いで、限界はライブラリの実装やデザインなどから存在している。 | ||
| + | |||
| + | 今回、スライドスイッチは、おそらくAndroidやiOSが提供しているからだろう。 | ||
| + | Windowsでも < | ||
| + | 但し右端にやたらと大きな余白があって、これをつぶそうとしてもつぶせないのはご愛敬だが。 | ||
| + | AIに聞いたら「右端に余白を大きくとっているのは指でタップしても十分な応答領域をとっているから」とかどこ情報なのか定かではない適当な返事をしてくるが、Androidではむしろ余白はなく、Windowsの余白をクリックしてもボタンは反応しないので、正しくない情報なのは明らかだが、そんなわけで、この余白をつぶすのはなかなかに困難そうなので放置している。 | ||
| + | |||
| + | ラジオボタンを押しボタン風の外観にするのは、まあ、そんなに難しくもなかった。 | ||
| + | |||
| + | ラジオボタンはマウスオーバー(ホバー)すると、色が変わるギミックを持っていたので、この押しボタン風のものもそのようにしたかったのだけれど、これが結論から言うと、できない相談だったようだ。 | ||
| + | |||
| + | まあ、マウスがのっかるなんて言うのは、基本的にはAndroidやiOSではあまり想定しなくていい機能だし、そういった事情も鑑みると、カスタム機能としては提供されていない、あるいは想定外だったとしても仕方がないといえばそうなのだが。 | ||
| + | |||
| + | 問題は、ラジオボタンの持っている((そしてカスタム可能な))状態の定義にある。 | ||
| + | |||
| + | ラジオボタンは、大まかに分けて、CommonStateとCheckStateの二種類の状態がある。 | ||
| + | |||
| + | CheckStateで、ボタンがチェックされた(Checked)とチェックが外された(Unchecked)という状態の変化が管理されていて、それぞれに遷移したときに外観をどうするのかをカスタムできる。 | ||
| + | |||
| + | もう一方のCommonStateについては、通常状態(Normal)と、マウスオーバー(PointerOver)の状態が定義されていて、それぞれに遷移したときの外観がどうように定義できる。 | ||
| + | |||
| + | 問題は、CommonStateがマウスオーバーと通常状態の二種類しかないので、押されているボタンの上にマウスカーソルが来て、そしてボタン外へ移動すると、通常状態の色に戻されてしまい、押された状態が消されてしまうのだ。 | ||
| + | |||
| + | 要するに、Checked かつ PointeOvertとか、Checkedかつ Normalとかの複合状態が持てないので、この種のボタンにホバーによる色変化をつけようとすると使い物にならないボタンになってしまうのだ。 | ||
| + | |||
| + | 複合状態を提供してくれりゃいいじゃんっていうのが率直なところだが、先にも書いたように、モバイルデバイスでは比較的どうでもいい機能なので、まあ、そういうもんだと飲み込んであきらめるしかなさそうなのが現状である。 | ||
| ==== Windows版のパッケージングは金がかかる ==== | ==== Windows版のパッケージングは金がかかる ==== | ||
| 行 54: | 行 124: | ||
| <code xml> | <code xml> | ||
| - | < | + | < |
| < | < | ||
| < | < | ||
| 行 62: | 行 132: | ||
| 最初は真面目に、ターゲットフレームワークが Windowsの時だけこれが設定されるようにとかしようと思ったんですが、なぜかうまくきかなくて、普通に共通の設定に混ぜちゃいました。 | 最初は真面目に、ターゲットフレームワークが Windowsの時だけこれが設定されるようにとかしようと思ったんですが、なぜかうまくきかなくて、普通に共通の設定に混ぜちゃいました。 | ||
| よくよく考えてみれば、WindowsPackageTypeだのWindowsAppSDKSelfContainedだのはWindows以外では無視されるので、分ける必要は皆無だってことに後から気づきました。 | よくよく考えてみれば、WindowsPackageTypeだのWindowsAppSDKSelfContainedだのはWindows以外では無視されるので、分ける必要は皆無だってことに後から気づきました。 | ||
| + | なお、Configuration == Debugの時もNoneにしてしまうと「配置ができない」といってデバッグ自体できなくなってしまうので、あくまでもリリース用の設定として、Release|AnyCPU のときだけの設定にしてあります。 | ||
| - | とはいえ、この設定をして、ターゲットを Windows Machineにすると、「発行」メニューがグレーアウトして選べないのです。 | + | なお、この設定をして、ターゲットを Windows Machineにすると、「発行」メニューがグレーアウトして選べなくなります。 |
| {{:: | {{:: | ||
| - | なので、とりあえずはコマンドラインからやるしかないのです。 | + | なので、発行自体はコマンドラインからやるしかないのです。 |
| <code powershell> | <code powershell> | ||
| 行 120: | 行 191: | ||
| あとは普通にpublishしてやれば APKが得られます。 | あとは普通にpublishしてやれば APKが得られます。 | ||
| ==== Matrial You対応 ==== | ==== Matrial You対応 ==== | ||
| + | |||
| + | Android Studioでは、基本的にMaterial You対応のランチャーアイコンを生成してくれません。 | ||
| + | 自分で手作業で対応を行う必要があります。 | ||
| + | |||
| + | 何もしなければ、ランチャーアイコンはカスタムアイコンになります。 | ||
| + | |||
| + | ところが、.NET MAUIは頼んでもないのにMatrial You対応のアイコンを生成します。 | ||
| + | 但し、モノクロアイコンにカスタムアイコンを持ってくるだけなので、カスタムアイコンがモノクロでない限りは何も表示されない間抜けな表示になってしまいます。 | ||
| + | |||
| + | できないならやらない、という Android Studioの方針はある意味正しいですが、基本的にPixel Launcher以外はMaterial You対応していないものばかりなので、この問題は発覚することはないのかもしれません。 | ||
| + | |||
| + | とはいえ、わたし自身がPixelユーザなのでこれは由々しき問題なのです。 | ||
| + | |||
| + | ハイハイスクールアドベンチャーのアイコンはいさこちゃん。 | ||
| + | ラスタ画像なので、mdpi, | ||
| + | |||
| + | 背景画像も適切に作ってくれます。 | ||
| + | |||
| + | ただモノクロアイコンだけは作ってくれないのに、adaptive iconに定義を突っ込んでくれます。 | ||
| + | これが問題。 | ||
| + | |||
| + | まずは、モノクロアイコン画像ファイルを用意し、Resources/ | ||
| + | 次に、ic_launcher.xml と ic_launcher_round.xml を用意し、Platforms/ | ||
| + | |||
| + | ハイハイスクールアドベンチャーの場合はMauiIconにisako_1024.pngを指定しているので、生成され参照されるのがisako_1024.xmlとisako_1024_round.xmlになります。 | ||
| + | |||
| + | <file xml isako_1024.xml> | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | isako_1024_round.xmlも内容は全く同じで基本的に問題はありません。 | ||
| + | 両方ないと、Pixel Launcherはモノクロアイコンを正しく扱えません。 | ||
| ==== 画面遷移は戻れない ==== | ==== 画面遷移は戻れない ==== | ||
| + | |||
| + | .NET MAUIのアプリケーションは、App → AppShell → ContentPage という階層構造を持っています。 | ||
| + | |||
| + | アプリケーション本体の画面は、普通にプロジェクトを作れば MainPageというContentPageになります。 | ||
| + | ここに設定画面やこのプログラムについて(About)などの画面を追加するとき、それぞれは別のContentPageになります。 | ||
| + | |||
| + | ページ遷移のための情報は AppShellのコンストラクタで定義をします。 | ||
| + | ハイハイスクールアドベンチャーはMainPageのほかに SettingsPage, | ||
| + | |||
| + | <code csharp> | ||
| + | public partial class AppShell : Shell | ||
| + | { | ||
| + | public AppShell() | ||
| + | { | ||
| + | InitializeComponent(); | ||
| + | Routing.RegisterRoute(nameof(SettingsPage), | ||
| + | Routing.RegisterRoute(nameof(AboutPage), | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ページを開く処理をするときは | ||
| + | |||
| + | <code csharp> | ||
| + | await Shell.Current.GotToAsync(nameof(SettingsPage)); | ||
| + | </ | ||
| + | |||
| + | などとして、ページ遷移を行います。 | ||
| + | |||
| + | 呼び出された画面には、明示的に「閉じる」のようなボタンを付けないでも、タイトルバーに「←」ボタンが出てくるので、これを押せば元のページに戻ります。 | ||
| + | |||
| + | が、戻ったときに、MainPageは新しく作り直されてしまい、要するにゲーム途中で設定を呼んだとしても、タイトル画面に強制的に戻されてしまうのです。 | ||
| + | |||
| + | GoBackComandを GoToAsync(" | ||
| + | ((何かほかの要因があるのかもしれないので、うまくいく場合もあるかもしれませんが。)) | ||
| + | |||
| + | 要は、MainPageが初期化されても、元の状態に復帰できればいいわけなので、初期化時に元に戻れるように細工をしました。 | ||
| + | |||
| + | もともと、ハイハイスクールアドベンチャーはゲームのセーブとロードに対応しているので、ゲーム中であればメモリ上にデータをセーブして、初期化時にデータがあればそれをロードするだけでゲームの状態は元通りです。 | ||
| + | |||
| + | あとは、システムの状態(Title, | ||
| + | |||
| + | ログエリアの内容は、MVVM化したときに、MainPageではなく MainPageModelに分離してあったので、MainPageModelをシングルトンにして、画面遷移で MainPage自体が初期化されても、継承されるようにしておきます。 | ||
| + | |||
| + | ゲームデータや、システム状態も、モデル側に保存するようにして、画面遷移問題は解決しました。 | ||
| + | |||
| + | この辺がうまくデザインできていないと、設定画面をContentPageではなく、オーバレイで作り直しみたいなことになったかもしれません。 | ||
| + | |||
| + | MVVMなどのデザインにはこういう面でも意味があるんだと思いました。 | ||
| + | |||
| + | |||
| ==== Windows版の発行は自己内包型で ==== | ==== Windows版の発行は自己内包型で ==== | ||
| + | |||
| + | publishするときに --self-contained trueをつけろという話です。 | ||
| + | これをつけないと、.NET のラインタイムのリビジョンに強い縛りが発生して、結果実行できないバイナリが出来上がってしまいます。 | ||
| + | ((実行すると、延々 .NET Runtime 9.0.0をインストールしろと言われ続けるが、ダウンロードできるのが9.0.14だったりして結果要件を満たせない。)) | ||
| + | |||
| ===== 技術的なこと ===== | ===== 技術的なこと ===== | ||
| 行 150: | 行 313: | ||
| 全部メモリ展開するっていうのもありかな、とちょっと思ったんですが、このゲームの基本的な構造は 500KB程度の M5 Stackや Raspberru Pi Picoなどでも動くように、けちけち使う分だけを展開するスタイル。 | 全部メモリ展開するっていうのもありかな、とちょっと思ったんですが、このゲームの基本的な構造は 500KB程度の M5 Stackや Raspberru Pi Picoなどでも動くように、けちけち使う分だけを展開するスタイル。 | ||
| - | Android版は当初、全部メモリに展開してたんですが、ソースを整理するときに全面的にけちけち戦略に書き直した経緯もあって、今更全部展開っていうのもな、と思い、ファイルをユーザデータ領域にコピーして使う方針に。 | + | [[ハイハイスクールアドベンチャー |
| + | |||
| + | <code csharp> | ||
| + | private static async Task CopyAssetToAppDataAsync(string filename) | ||
| + | { | ||
| + | // アセットを開く | ||
| + | using Stream input = await FileSystem.Current.OpenAppPackageFileAsync(filename); | ||
| + | // 保存先パスを決定 | ||
| + | string targetFile = Path.Combine(DataFolder, | ||
| + | // コピー | ||
| + | using FileStream output = File.Create(targetFile); | ||
| + | await input.CopyToAsync(output, | ||
| + | await output.FlushAsync(); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | アセットファイルの一覧は取れますか? | ||
| + | とAIに聞いたら「できません」というのでファイル名はプログラムで決め打ちでコピーするしかないですが、まあ必要なファイルはプログラム側でわかっているので、面倒でも列挙しておけばいいだけなので、そのようにします。 | ||
| + | なお、ご覧のようにこの操作は非同期操作です。 | ||
| + | この関数内では awaitを入れて一応順次動作するようにしていますが、呼び出し元でもこの処理をきちんと awaitしないとコピーが半端に終わってしまったりします。 | ||
| + | .NET自体が積極的に非同期を導入していて、そもそも非同期のサービスしかなかったりするものもあって、プログラム全体の流れをきちんと考えないとおかしな動作になることもあります。 | ||
ハイハイスクールアドベンチャー_.net_maui版.1761467629.txt.gz · 最終更新: by araki
