====== ハイハイスクールアドベンチャー WIndows版 ======
===== あらすじ =====
2019年神奈山県立ハイ高等学校は 地盤が弱く校舎の老朽化も進んだため、 とうとう廃校にする以外方法がなく なってしまった。
ところで大変な情報を手に入れた。 それは、
「ハイ高校にATOMIC BOMBが仕掛けられている。」
と、いうものだ。 どうやらハイ高が廃校になった時、 気が狂った理科の先生がATOMIC BOMBを 学校のどこかに仕掛けてしまったらしい。
お願いだ。我が母校のコナゴナになった 姿を見たくはない。 早くATOMIC BOMBを取り除いてくれ……!!
行動は英語で、“<動詞>” 或いは、“<動詞>”+“<目的語>“のように入れていただきたい。 例えば、”look room”と入れれば部屋の様子を見ることが出来るという訳だ。
それでは Good Luck!!!…………
===== 概要 =====
様々なプラットフォームに移植し続けているハイハイスクールアドベンチャーですが、待望の(?)Windows版の登場です。
[[ハイハイスクールアドベンチャー SDL版]]を作った勢いをかって、Windowsネイティブ版を一気に作り上げました。
開発は Visual Studio 2022 Community Editionを使い WPF版として実装してあります。
{{ ::hhsadvwin:hhsadvwin.png?400 |}}
===== 遊び方 =====
インストーラを作ってないので、[[https://github.com/wildtree/HHSAdvWin.git|GitHub]]から一式を取り出したら、VIsual Studioでビルドし、AppData\Local\HHSAdvWin を $env:LOCALAPPDATA\HHSAdvWin にまるっとコピーしてから実行してください。
そのうち気が向いたらインストーラを作るかもしれません。
[[https://www.wildtree.jp/~araki/HHSAdvWin110.zip|インストーラ]]をダウンロードして展開したら、Setup.exe を実行してください。
もし古いバージョンがインストールされていましたら、**お手数ですが、必ず、まず古いバージョンをアンインストールしてください**。
勝手インストーラで無署名のため、どうもアップデートがうまくいかず、古いものが残ったままのように見えてしまいます。((実際には新しいもので上書きされているのに。))
インストール情報だけだと思っていますが、副作用があっても困るので、よろしくお願いします。
ユーザーデータは削除されませんのでセーブデータなどは失われません。
初回実行時に、データフォルダを勝手に作って勝手にデータを放り込みます。
あとは High High School Adventure for Windowsをメニューから起動してください。
===== もろもろのこと =====
==== 移植 ====
Windows版は、ほぼ[[ハイハイスクールアドベンチャー SDL版]]からの流用でできています。
このゲームの実装に当たっては、ダブルバッファを使って、任意のピクセルに点を打ったり、打たれた点の色を読み出したりできるかどうかが鍵なのです。
SDL2版を作ったときに、もうこれでWindows版っていっていいんじゃないのか?と思ったのですが、SDL2-CS.dll, SDL2.dll, SDL2_ttf.dll, SDL2_image.ddl, そして SDL2_mixer.dll ととにかく依存するライブラリの多いこと。
遊ぶための手順が多いのと、そこら辺からDLLを拾ってくるリスクを考えると、やっぱりネイティブ版を作った方がいいのではないか?
そう思ったので、Windowsのビットマップイメージに対して、ダブルバッファでの読み書きが可能かどうか AIに尋ねたら、できるよ、というので、実験コードを作って、思ったように動くことを確認し、一気に移植しました。
そもそも、SDL2版も C#で開発しており、SDL2にべったり依存している部分以外はそのまま流用可能なので、移植にかかった時間は非常に短かったといえます。
SDL2に依存していた描画、サウンド機能、そしてイベントループはすべてWPFのそれで置き換わっています。
さらに、SDL2版では実装がめんどくさすぎてさぼっていた、設定画面を含む、ダイアログなどの様々なウィジェットを利用可能。
実装難易度はSDL2比でずっと低かったです。
最初からこっち作っとけよって感じでした。
==== DPI問題 ====
オープニングとエンディングでスクロール画面でメッセージを流すのですが、思いもよらぬ落とし穴がありました。
そもそも、この部分に関してはあれこれと問題があったのですが、ようやく想定通りの動作になったと思って喜んでいたところ、まだ罠があったのです。
開発は、自作のデスクトップPC上で行っています。
理由は、Visual Studio入れてるとばかばかWindows SDK関連がインストールされて、あっという間にCドライブを食いつぶすからです。
自作デスクトップにはぜいたくに2TBのSSDをCドライブに割り当ててあります。
まあ、それはいいのです。
このマシンは1920x1080の96DPIの環境なのですが、リビングにある Surface Pro7からRDPして操作することもあります。
こっちのマシンは144DPIなのです。
ことは、デバッグの続きとばかりに、RDPした状態から起動したところ、流れるメッセージの右1/3ほどが欠落、さらに下1/3ほども欠落。
文字のサイズなどは変わってないのに、2/3くらいの領域を残して、残りが黒くなってしまっているのです。
何もいじってないのにどうした?
で、デバッガで潜っていくと、メインウィンドウからDPI情報を引き抜いてビットマップを作るところで**144DPI**って出てる。
96/144 = 2/3 ですね。
はい、犯人確保!
で、どうしたらいいか AIに聞いたら、いろいろ御託を並べた後に、最終的に、
**WPFは96DPIが基本だから96決め打ちにすればいいんじゃない?**
とか言い放ってきましたよ!
そもそもメインウィンドウからDPI引き抜いてくるやり方提案したのもあんたじゃんか!
とりあえず、その辺を全部96にしたらあっさり動きましたよ。
DPI指定する意味ってなんなのさ?!
==== インストーラ関連 ====
インストーラのなかった時分、遊ぶための手順が多すぎてめんどくさかったわけです。
まず、ビルドしないといけないですしね。
そもそも、SDL2版についてもインストーラを検討したんですが、あれ、SDL2のDLLへの依存性があるじゃないですか。
SDL2-CS.dllについては、nugetで持ってくるのですが、他のは手で放り込んでいるので、再配布とかするときにめんどくさいじゃないですか。
なので、そういう余計な依存性がないバージョンが欲しいなと思ったのも、WPF版を作った理由の一つなので、ここでインストーラを作らないっていうのはなあって思っていました。
めんどくささがわからなかったので「作るかも」みたいな濁し方してましたが、一応朝ごはん食べながら完成させることができる程度のめんどくささでした。
とはいえ、そこそこめんどくさかったので、ここに諸々を記しておきます。
=== データの扱い ===
このゲームのデータは、%LOCALAPPDATA%\HHSAdvWin においてある想定です。
なので、インストーラにはここにデータを展開してほしいなあって思ったんです。
で、やり方はありますか~、ってAIに聞いたら「管理者権限でユーザデータ書くとあとあとめんどくさいよ。とりあえずアプリケーションフォルダに突っ込んどいて、プログラム自身でコピーしな。」というようなご宣託。
なるほど。
どうして、Windowsのアプリってば、データフォルダがアプリケーションフォルダにいるのかと思っていましたが、そんな罠があったんですね。
とりあえず、データ用のフォルダがなかったら、作ってデータを放り込んでから動作開始する方式にしました。
=== EXEがパッケージされない ===
インストーラ作るの簡単だよ。
そんな噂をAIから聞いて、Visual Studio 2022にセットアップ用の拡張突っ込んで、プロジェクトを追加したところ、ぼろぼろ問題噴出。
多くは些細な問題だったんですが、致命的なのはEXEがパッケージされない問題。
DLLはいくのに。
AIに聞いたら、「ライブラリプロジェクトじゃないの?」とかいうわけですが、そんなことない。
ちゃんとWPFのアプリケーションプロジェクトになってる。
そもそもEXEは生成されているのだ。
最終的には、それはWPFのNETプロジェクトに関するあるある問題で、理由はよくわかりませんがEXEを生成物として認識できないらしいのです。
じゃあどうするかというと「発行」という機能を使って、ビルドしたものを「発行」してやって、発行されたものをインストーラに突っ込むというやや迂遠な処理をすればいいらしい、ということがわかりました。
そもそも、ちゃんと認識しろよって思うんですけれどね。
=== バージョンアップと署名問題 ===
正しくインストーラを配布するには、電子署名をしなくてはならないらしい。
そしてそれには、年数万以上かけて、認証団体から取得しないとならないらしい。
やってられるか!
というわけで無署名でやっているんですが、バージョンアップしようとしたら問題が発生しました。
「知らないアプリだからポリシーでインストールさせない」
といわれました。
どうする?
結論としては、インストーラを作成するときにプロダクトコードを毎回新しくしろということでした。
アップグレードコードは変えるな。
ということだったので、アップグレードしてくれるのかな?
と思ったけれど、古いのは残して別のものとしてインストールされました。
なので、今後、新しいバージョンを配布するとしたら、その時は古いのをアンインストールしてからインストールしてください。
==== テーマ ====
[[ハイハイスクールアドベンチャー Android版]]のコードを整理したときに、テーマ回りもかなり整理しました。
そういえば、Windowsにもテーマがあるよね?
と、いうことで、せっかくだからテーマ対応もやっちゃおうと思いましたが、結論から言うと WPFアプリのテーマ対応は結構面倒くさい、ということでした。
=== そもそも一撃でキレイにテーマ対応させられない ===
マイクロソフトは「テーマ」というものをどう考えているんでしょうか?
Androidはテーマは、そもそも雑にシステムの用意してるものを継承したとしても、そこそこの見栄えのものになるのに、基本的にWPFであるならば、自前で頑張らなあかん、ということらしいのです。
雑に「テーマ」を適用するだけなら、App.xaml にテーマファイルを用意しておいて、読み込ませるだけです。
基本的に、テーマとしては「ライト」「ダーク」の大枠があります。
今後増えないことを祈りますが。
Windowsのシステムがユーザごとにシステムテーマとして、ダークなのかライトなのかを保持しています。
これはWPFの場合はAPIみたいに気の利いたのじゃなくてレジストリをじかに覗くことで取得できます。
つまり、アプリ動いてる最中にシステムの設定変えられたら、自分でもう一回見に行かない限り変わったことを知るすべはないってことです。
完璧にやりたいならタイマーとかで定期的にチェックしろとAIはいいましたが、めんどくさすぎてそこまでやる気にはなれません。
動作中に切り替えられたら、それはもう放置です。
この辺がめんどくさすぎたからなんでしょうか?
多くのアプリが、テーマをシステム追随するか、あるいはもう、システムは関係なく、ダークまたはライトを強制する設定を持っているのは。
ハイハイスクールアドベンチャーもこの先人たちの知恵に倣って、同じような設定を入れることにしました。
設定に従って、用意しておいた themes/LightTheme.xaml または themes/DarkTheme.xml を切り替えるだけです。
string themePath = mode ? "themes/DarkTheme.xml" : "themes/LightTheme.xml";
var uri = new URI(themePath, UriKind.Relative);
ResourceDictionary themeDict = new ResourceDictionary { Source = uri };
Application.Current.Resources.MergeDictionaries.Clear();
Application.Current.Resources.MergeDictionaries.Add(themeDict);
テーマファイルの中身は、「色」の指定と、それを使って、ウィジェットごとに「色」を割り当てていく作業になります。
Androidだと、ライトテーマだけ全部書いておいて、ダークは色情報だけを上書きする、というような感じで作れますが、Windowsはよくわかりません。
とりあえず、ウィジェットまわりは同じ内容を書いておいて、頭の方に集めた色情報の変更でライトとダークを分ける感じでテーマファイルを作っています。
#FFFFFFFF
#FF000000
#FF1E1E1E
#FFFFFFFF
ダークとライトの違いは最初の色の定義だけなのがわかると思います。((もちろんもっと複雑にライトだけの処理、ダークだけの処理を入れていくこともできますが。))
こんな感じで、メニューがアクティブの時の色とか、必要なだけ色を作って、必要なウィジェットに色を割り当てていきます。
静的なウィジェットはまあ問題ないんですが、メニューのドロップダウンとか、スクロールビューにおけるスクロールバーなど、フローティングなウィジェットはテーマの割り当て方が何通りかあるようで、理解しきれません。
AIの言うとおりにしておきました。
うまくいかないときだけ頑張って調整しました。
Androidはほとんど色をいじればあとは放置しておいてもウィジェットがいい感じに色を拾ってくれたのとは大違いです。
柔軟性が高いといえるのかもしれませんが、Androidだって、別に細かくやろうと思えばできるので、たぶんそういう話でもないです。
要は、あんまりテーマ使ってもらいたくないんじゃないかっていうのが正直な感想です。
そういったことは、この後の「タイトルバー」などの扱いにも表れているように感じます。
とにかく、テーマは WPFに関しては、とてもめんどくさい存在です。
=== タイトルバーは自前でつくれ ===
苦労してアプリの部品をいろいろとテーマ対応させていったのですが、タイトルバーは置き去りです。
AIに聞いたら、「WPFアプリのタイトルバーはテーマ対応しない」ということらしいのです。
じゃあどうするの?
といったら、「自前で作ればいいじゃん」っていわれました。
XAMLでタイトルバーを自前で作る方法があるんですよ。
でも、それは、そこそこに面倒くさい話でした。
== 右端のコントロールも自前でつくれ ==
タイトルバーを自分で作るってことは、タイトルバーのあらゆるものを自分でやるってことです。
何でもできるから、Visual Studioとかはタイトルバーにメニューがのっかってたりするわけです、たぶん。
何を言ってるかっていうと、まあタイトルとかを自前で書かないといけない((TextBlock置くだけですが))とかはわかりますが、右端に出てる"-□×"のボタンも自前でやれってことなんです。
まあ、いいですけれど。
雑に、StackPanelで テキスト + ボタン領域 みたいな配置を作ったんですよ、もちろん、ボタン右寄せで。
ところがテキスト→ボタン領域の順でスタックしたら、ボタン領域右に寄らなくなっちゃいました。
テキスト領域が幅を自動で埋めるので、先に書いちゃうとおかしなことになるようで、先にボタン置いて右側確定してからテキスト置くか、グリッドできちっと先に割り当てを作ってからやれって言われました。
なお、ボタンの動作も自分で全部書かないといけません。
大した処理ではないですが、いちいちです。
private void Minimize_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState.Minimized;
private void Maximize_Click(object sender, RoutedEventArgs e)
=> WindowState = (WindowState == WindowState.Maximized) ? WindowState.Normal : WindowState.Maximized;
private void Close_Click(object sender, RoutedEventArgs e)
=> Close();
// タイトルバーのドラッグ移動用イベントハンドラを追加
private void TitleBar_DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
this.DragMove();
}
}
}
あと、タイトルをドラッグしたときのイベントハンドラもつけとかないとビルドできません。
さらに、ここまでやったのに、ボタンが全然反応しないのです。
あくまでタイトルバーに配置されただけで、タイトルバーの一部と認識されるため、ボタンとして機能させるには、ボタン要素ごとにいちいち WindowChrome.IsHitTestVisibleInChrome="True" をつけてやる必要があります。
やればやっただけ、次に必要なことが襲ってきます。
自前の道は長く果てしないのです。
== アイコンを出せ ==
アイコンが、左端に鎮座していたいさこちゃんの顔がなくなっちゃったのです。
アイコン用の領域作って、アイコンファイル持ってきて、this.Icon にそれ置けばいいじゃないか、っていわれたんですが、え、今更、アイコンファイルをこのためだけにつけるの?
インストーラとかもアイコン結構使ってるけれど、全部、EXEから取り出してるんだけれど?
って聞いたら、「それもできるね」ってAIがさくっとサンプルくれました。
ありがたい。
が、それ使おうとしたら、System.Drawingがないよ。
マイクロソフト謹製のモジュールだし別に nugetで追加するだけなんですが、微妙に釈然としないながらも追加しました。
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using System.Drawing;
using System.IO;
namespace HHSAdvWin
{
public static class IconHelper
{
public static BitmapSource GetAppIcon()
{
// 実行中のアセンブリのパスを取得
string exePath = Assembly.GetEntryAssembly()!.Location;
using (Icon icon = Icon.ExtractAssociatedIcon(exePath)!)
{
return Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
}
}
}
}
public MainWindow()
{
InitializeComponent();
this.Icon= IconHelper.GetAppIcon();
...
}
とかすればいいわけです。
== 角を丸めろ ==
大体いい感じかなあ、と、思ってウィンドウをしげしげと眺めると、どうも角ばってません?
ほかのアプリと比べると圧倒的角ばり感がでていますよね?
タイトルバーが四角いのです。
ウィンドウと描画しているタイトルバーの両方を CornerRadius="12" とかして、角を丸める指示を出せばいいらしいです。
== タイトルバーのふりをしてるが ==
見た目も大体タイトルバーになっているし、タイトルバーだなって思えるようになったころ、オープニングのメッセージを表示させてたら、するするとタイトルバーの上にメッセージがスクロールして乗るじゃないですか?
そう。
こいつはタイトルバーのふりはしていますが、基本的にはユーザが画面に置いたウィジェットの一つにすぎず、そこもアプリの描画領域扱いなのです。
じゃあ、タイトルバー消して、Androidみたいに全領域スクロールすればいいかな、と思ったんですが、タイトルバーにはほんのり色も載せてりゃ、角を丸く削ったりしているので、雑に消すとかえってみっともないことになることが発覚。
Androidはよくできてるなあ。
試行錯誤の結果、オーバレイの描画領域をクリップして、タイトルバーの部分を対象から外すことで逃げました。
=== 標準のダイアログ類は極力使うな ===
特に何かを受け取る必要がないダイアログ、たとえば、持ち物の一覧を取得したときなど、は楽ですから、MessageBox を使っていたんですが、これもテーマ対応を考えると厄介なのです。
結論から言えば、似たようなのを自分で作れよ、でした。
作りましたよ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace HHSAdvWin
{
///
/// ZMessageBox.xaml の相互作用ロジック
///
public partial class ZMessageBox : Window
{
public ZMessageBox()
{
InitializeComponent();
}
private void Minimize_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState.Minimized;
private void Maximize_Click(object sender, RoutedEventArgs e)
=> WindowState = (WindowState == WindowState.Maximized) ? WindowState.Normal : WindowState.Maximized;
private void Close_Click(object sender, RoutedEventArgs e)
=> Close();
// タイトルバーのドラッグ移動用イベントハンドラを追加
private void TitleBar_DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ButtonState == MouseButtonState.Pressed)
{
this.DragMove();
}
}
private void Okay_Clicked(object sender, RoutedEventArgs e)
=> Close();
}
}
ZMessageBox messageBox = new ZMessageBox
{
Owner = this,
};
messageBox.TitleTextBlock.Text = "情報";
messageBox.textBox.Text = "セーブデータが存在していません。";
messageBox.ShowDialog();
//MessageBoxHelper.ShowCentered(this, "セーブデータが存在していません。", "情報", MessageBoxButton.OK, MessageBoxImage.Information);
==== スライドスイッチ風にしたい ====
チェックボックスはいまどきつまらないよね。
見栄えを考えたらスライドスイッチ。
AIに聞いたらスタイルでできるというのでやってみました。
{{::hhsadvwin:prefs.png?400|}}
まず、XAMLファイルで、スタイルを定義します。
そして、レイアウト用のXAMLファイルでこのスタイルを持つ ToggleButtonを作ります。
スタイルだけでできるので簡単といえば簡単ですが、この程度のウィジェットは今どきデフォルトで用意してほしいですよね?
==== リソースファイルのコピー ====
リソースファイルのコピーをするには、ソリューションエクスプローラから、リソースファイルを一個一個選んで、ビルド時にコピーする設定をいれるといいのですが、実はこの方法だと、リソースディレクトリに階層がある場合にはうまくいきません。
勝手に一階層に改変されてしまうのです。
ハイハイスクールアドベンチャーの場合は、data/themes と data の下に themesのフォルダーがある二階層になっているのですが、これを勝手に、data と themesという二つの別々のフォルダーとしてビルドディレクトリにコピーしてしまいます。
これを避けようと思ったら、プロジェクトファイル((.csproj))を直接いじって、下のような設定を入れることでディレクトリ構造丸ごとコピーしてくれるようになります。
PreserveNewest
data\%(RecursiveDir)%(Filename)%(Extension)