ユーザ用ツール

文書の過去の版を表示しています。


ハイハイスクールアドベンチャー WIndows版

あらすじ

2019年神奈山県立ハイ高等学校は 地盤が弱く校舎の老朽化も進んだため、 とうとう廃校にする以外方法がなく なってしまった。

ところで大変な情報を手に入れた。 それは、

「ハイ高校にATOMIC BOMBが仕掛けられている。」

と、いうものだ。 どうやらハイ高が廃校になった時、 気が狂った理科の先生がATOMIC BOMBを 学校のどこかに仕掛けてしまったらしい。

お願いだ。我が母校のコナゴナになった 姿を見たくはない。 早くATOMIC BOMBを取り除いてくれ……!!

行動は英語で、“<動詞>” 或いは、“<動詞>”+“<目的語>“のように入れていただきたい。 例えば、”look room”と入れれば部屋の様子を見ることが出来るという訳だ。

それでは Good Luck!!!…………

概要

様々なプラットフォームに移植し続けているハイハイスクールアドベンチャーですが、待望の(?)Windows版の登場です。 ハイハイスクールアドベンチャー SDL版を作った勢いをかって、Windowsネイティブ版を一気に作り上げました。

開発は Visual Studio 2022 Community Editionを使い WPF版として実装してあります。

遊び方

<del>インストーラを作ってないので、GitHubから一式を取り出したら、VIsual Studioでビルドし、AppData\Local\HHSAdvWin を $env:LOCALAPPDATA\HHSAdvWin にまるっとコピーしてから実行してください。

そのうち気が向いたらインストーラを作るかもしれません。</del>

インストーラをダウンロードして展開したら、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に尋ねたら、できるよ、というので、実験コードを作って、思ったように動くことを確認し、一気に移植しました。

そもそも、SDL1番も C#で開発しており、SDL2にべったり依存している部分以外はそのまま流用可能なので、移植にかかった時間は非常に短かったといえます。

SDL2に依存していた描画、サウンド機能、そしてイベントループはすべてWPFのそれで置き換わっています。

さらに、SDL2版では実装がめんどくさすぎてさぼっていた、設定画面を含む、ダイアログなどの様々なウィジェットを利用可能。 実装難易度はSDL2比でずっと低かったです。 最初からこっち作っとけよって感じでした。

DPI問題

オープニングとエンディングでスクロール画面でメッセージを流すのですが、思いもよらぬ落とし穴がありました。 そもそも、この部分に関してはあれこれと問題があったのですが、ようやく想定通りの動作になったと思って喜んでいたところ、まだ罠があったのです。

開発は、自作のデスクトップPC上で行っています。 理由は、Visual Studio入れてるとばかばかWindows SDK関連がインストールされて、あっという間にCドライブを食いつぶすからです。 自作デスクトップにはぜいたくに2TBのSSDをCドライブに割り当ててあります。

まあ、それはいいのです。 このマシンは1920×1080の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アプリのテーマ対応は結構面倒くさい、ということでした。

そもそも一撃でキレイにテーマ対応させられない

タイトルバーは自前でつくれ

苦労してアプリの部品をいろいろとテーマ対応させていったのですが、タイトルバーは置き去りです。 AIに聞いたら、「WPFアプリのタイトルバーはテーマ対応しない」ということらしいのです。

じゃあどうするの? といったら、「自前で作ればいいじゃん」っていわれました。

XAMLでタイトルバーを自前で作る方法があるんですよ。 でも、それは、そこそこに面倒くさい話でした。

右端のコントロールも自前でつくれ

タイトルバーを自分で作るってことは、タイトルバーのあらゆるものを自分でやるってことです。 何でもできるから、Visual Studioとかはタイトルバーにメニューがのっかってたりするわけです、たぶん。

何を言ってるかっていうと、まあタイトルとかを自前で書かないといけない1)とかはわかりますが、右端に出てる“-□×”のボタンも自前でやれってことなんです。

まあ、いいですけれど。

雑に、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();
            }
        }
    }

あと、タイトルをドラッグしたときのイベントハンドラもつけとかないとビルドできません。 自前の道は長く果てしないのです。

アイコンを出せ

アイコンが、左端に鎮座していたいさこちゃんの顔がなくなっちゃったのです。 アイコン用の領域作って、アイコンファイル持ってきて、this.Icon にそれ置けばいいじゃないか、っていわれたんですが、え、今更、アイコンファイルをこのためだけにつけるの?

インストーラとかもアイコン結構使ってるけれど、全部、EXEから取り出してるんだけれど?

って聞いたら、「それもできるね」ってAIがさくっとサンプルくれました。 ありがたい。 が、それ使おうとしたら、System.Drawingがないよ。

マイクロソフト謹製のモジュールだし別に nugetで追加するだけなんですが、微妙に釈然としないながらも追加しました。

iconHelper.cs
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はよくできてるなあ。

試行錯誤の結果、オーバレイの描画領域をクリップして、タイトルバーの部分を対象から外すことで逃げました。

<Window ...
        Foreground="{DynamicResource ForegroundBrush}" Background="{DynamicResource BackgroundBrush}"
        WindowStyle="None"
        AllowsTransparency="True">
    <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="32"
                      CornerRadius="12"
                      GlassFrameThickness="0"
                      UseAeroCaptionButtons="False"/>
    </WindowChrome.WindowChrome>
    <!-- 行分割で厳密に上32px=タイトルバー、下=コンテンツ -->
       <Border CornerRadius="12" Background="{DynamicResource TitleBarBackgroundBrush}"
              Height="32"
              VerticalAlignment="Top">
           <Grid Height="32" Background="{DynamicResource TitleBarBackgroundBrush}" x:Name="TitleBarGrid">
               <Grid.ColumnDefinitions>
                   <ColumnDefinition Width="Auto"/>
                   <ColumnDefinition Width="*"/>
                   <ColumnDefinition Width="Auto"/>
               </Grid.ColumnDefinitions>
 
               <!-- アプリケーションアイコン -->
               <Image Width="16" Height="16" Margin="8,0"
                      Source="{Binding Icon, RelativeSource={RelativeSource AncestorType=Window}}"
                      VerticalAlignment="Center"/>
 
               <!-- タイトル -->
               <TextBlock Grid.Column="1"
                  Text="{Binding Title, RelativeSource={RelativeSource AncestorType=Window}}"
                  VerticalAlignment="Center"
                  Foreground="{DynamicResource TitleBarForegroundBrush}"
                  Margin="8,0"/>
 
               <!-- ボタン群 -->
               <StackPanel Grid.Column="2" Orientation="Horizontal">
                   <Button Content="—" Width="40" Click="Minimize_Click"
               WindowChrome.IsHitTestVisibleInChrome="True"/>
                   <Button Content="□" Width="40" Click="Maximize_Click"
               WindowChrome.IsHitTestVisibleInChrome="True"/>
                   <Button Content="×" Width="40" Click="Close_Click"
               WindowChrome.IsHitTestVisibleInChrome="True"/>
               </StackPanel>
           </Grid>
       </Border>
 
       <!-- コンテンツ -->
       <Grid Margin="0,32,0,0" Background="{DynamicResource BackgroundBrush}">
       <!-- ここからウィンドウの本体 -->

標準のダイアログ類は極力使うな

特に何かを受け取る必要がないダイアログ、たとえば、持ち物の一覧を取得したときなど、は楽ですから、MessageBox を使っていたんですが、これもテーマ対応を考えると厄介なのです。

結論から言えば、似たようなのを自分で作れよ、でした。

作りましたよ。

ZMessageBox.xaml
<Window x:Class="HHSAdvWin.ZMessageBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HHSAdvWin"
        mc:Ignorable="d"
        Title="ZMessageBox"
        WindowStyle="None"
        SizeToContent="WidthAndHeight" ResizeMode="NoResize" WindowStartupLocation="CenterOwner"
        AllowsTransparency="True" Background="{DynamicResource BackgroundBrush}" Foreground="{DynamicResource ForegroundBrush}">
    <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="32"
                      CornerRadius="12"
                      GlassFrameThickness="0"
                      UseAeroCaptionButtons="False"/>
    </WindowChrome.WindowChrome>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="32"/>
            <!-- タイトルバー行 -->
            <RowDefinition Height="*"/>
            <!-- コンテンツ行 -->
        </Grid.RowDefinitions>
 
        <!-- タイトルバー -->
        <Border Grid.Row="0"
                CornerRadius="12"
                Background="{DynamicResource TitleBarBackgroundBrush}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
 
                <!-- ドラッグ領域(テキスト側) -->
                <TextBlock Text="ZMessageBox"
                           x:Name="TitleTextBlock"
                           VerticalAlignment="Center"
                           Margin="8,0"
                           Foreground="{DynamicResource TitleBarForegroundBrush}"
                           MouseLeftButtonDown="TitleBar_DragArea_MouseLeftButtonDown"/>
 
                <!-- 右側ボタン群 -->
                <StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
                    <Button Content="—" Width="46" Click="Minimize_Click"
                            WindowChrome.IsHitTestVisibleInChrome="True"
                            Background="Transparent" BorderThickness="0"/>
                    <Button Content="□" Width="46" Click="Maximize_Click"
                            WindowChrome.IsHitTestVisibleInChrome="True"
                            Background="Transparent" BorderThickness="0"/>
                    <Button Content="×" Width="46" Click="Close_Click"
                            WindowChrome.IsHitTestVisibleInChrome="True"
                            Background="Transparent" BorderThickness="0"/>
                </StackPanel>
            </Grid>
        </Border>
 
        <!-- コンテンツ(Marginで32を空けず、行で分ける) -->
        <Grid Grid.Row="1" Background="{DynamicResource BackgroundBrush}">
            <!-- コンテンツ -->
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBox x:Name="textBox" TextWrapping="Wrap" Text="TextBox" Grid.Row="0" Margin="8,4,8,4" BorderThickness="0" IsEnabled="False"/>
                <StackPanel Grid.Row="1">
                    <Button x:Name="btnOkay" Content="了解" HorizontalAlignment="Right" Margin="8,4,8,4" Click="Okay_Clicked"/>
                </StackPanel>
            </Grid>
        </Grid>
 
    </Grid>
</Window>
ZMessageBox.xaml.cs
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
{
    /// <summary>
    /// ZMessageBox.xaml の相互作用ロジック
    /// </summary>
    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();
    }
}
1)
TextBlock置くだけですが

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information