====== ハイハイスクールアドベンチャー SDL版 ======
===== 概要 =====
そういえば、Windowsで動くバージョンがないじゃないか。
まあ[[https://wiki.wildtree.jp/doku.php?id=%E3%83%8F%E3%82%A4%E3%83%8F%E3%82%A4%E3%82%B9%E3%82%AF%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%99%E3%83%B3%E3%83%81%E3%83%A3%E3%83%BC_web%E7%89%88|ウェブ版]]は動くわけだし、全くないわけじゃないけれど、Windows版とはいいがたい。
なければ作ればいいじゃないか。
ということで、Windows版を作ろうと思ったわけです。
基本的に、ハイハイスクールアドベンチャーが移植できるかどうかはビットマップに点を打てて、打ってある点の色を読み出せるかどうかが鍵なのですが、バカ正直に読みだそうとすると今どきのグラフィックスシステムは驚くほど遅いので、大抵はバッファを用意しといて、読み出しはそこから読むので、実際には読み出しはできなくてもあまり問題はないのですが。
逆に言うと、ダブルバッファ的な処理ができないと実装困難ということになります。
今更 Windows版を作るなら C++でごりごり Win32 APIをひっぱたくのではなく、C#で作りたい。
ユニバーサルアプリを作るときに、ビットマップラスをバッファ代わりに使うのがどうも簡単ではないようなので、じゃあSDLでも使えばいいんじゃない?
なので、SDL2版を作ることにしました。
===== ビルドと実行 =====
ソース一式は例によって [[https://github.com/wildtree/HHSAdvSDL.git|Github]]に置いてあるので、それを持ってきてください。
Net 8.0をターゲットにしていますので、Net 8.0 SDKがインストール済みであることを前提としています。
Net 8.0なので Windows版といいながら、実は Ubuntuや Debianでも動きます((Windows11, Ubuntu24.04(x64), Ubuntu25.04(x64), Debian 12(ARM64)での動作は確認しました))、Net 8.0が入っていれば。
ビルドは、Visual Studio や Visual Studio Codeを使ってもできますが、コマンドラインからもできます。
C:\Users\foo\source\HHSAdvSDL> dutnet build
SDL2版なので、実行には SDL2のDLL((Linux版なら共有ライブラリ))が必要になります。
もしインストールされていないなら、SDL2, SDL2_image, SDL2_tff, SD2_mixier をインストールしてください。
単純にDLLをHHSAdvSDLと同じフォルダーに放り込んでおくのでも構いません。
Linux版の場合は実はSDL2のC#バインディングである SDL2-CSにやや問題があって、標準的にインストールされている libSDL2.so などを認識しません。HHSAdvSDLのバイナリがあるディレクトリに、SDL2.dll.so, SDL2_image.dll.so, SDL2_ttf.dll.so, SDL2_mixer.dll.so という名前のシンボリックリンクをそれぞれの共有ライブラリに対して作成しておくことでこの問題を回避できます。
Ubuntu 24.04の場合はこんな感じにやればいいです。
lrwxrwxrwx 1 foo bar 36 9月 10 21:34 SDL2.dll.so -> /usr/lib/x86_64-linux-gnu/libSDL2.so
lrwxrwxrwx 1 foo bar 42 9月 10 21:36 SDL2_image.dll.so -> /usr/lib/x86_64-linux-gnu/libSDL2_image.so
lrwxrwxrwx 1 foo bar 42 9月 10 21:36 SDL2_mixer.dll.so -> /usr/lib/x86_64-linux-gnu/libSDL2_mixer.so
lrwxrwxrwx 1 foo bar 40 9月 10 21:35 SDL2_ttf.dll.so -> /usr/lib/x86_64-linux-gnu/libSDL2_ttf.so
実行に際してはデータファイルも必要になります。
AppData というフォルダ名で同梱してありますので、これを配置してください。
Windowsで、ユーザ名が foo だとしたら、
C:\Users\foo\source\HHSAdvSDL> mkdir C:\Users\foo\AppData\Local\HHSAdvSDL
C:\Users\foo\source\HHSAdvSDL> copy AppData\Local\HHSAdvSDL\*.* C:\Users\foo\AppData\Local\HHSAdvSDL
のようにやってください。
Linuxの場合は、.local/share/AppData においてください。
$ cp -ax AppData/Local/HHSAdvSDL ~/.local/share
これで準備完了です。
あとは
C:\Usrs\foo\source\HHSAdvSDL> dotnet run
$ dotnet run
のように dotnetコマンドで実行するか、生成されたバイナリを直接起動してください。
{{ :hhsadvsdl:game.png?400 |SDL2版}}
===== 設定の変更 =====
SDL2は基本的にゲームを実装するための仕掛けなので、メニューやダイアログといったツールキットが基本的に提供しているような気の利いたものがありません。
必要ならSDL2の機能を使って実装することになります。
ゲーム中のダイアログっぽいものもSDL2を使って実装しています。
そんなわけで、設定を変更するためのUIを作る必要があるわけですが、ぶっちゃけめんどくさかったので作っていません。
それでも、いくつかの項目については、ユーザ側でいじれるように設定ファイルを用意してあります。
設定ファイルは、一度起動して、終了させるとデータフォルダ内に HHSAdvSDL.json という名前で、JSON形式のファイルが作成されます。
現状サポートしているのは、ゲーム中で使用するTrueType フォント((Windowsなら C:\Windows\fonts\YuGothR.ttc、Linuxなら /usr/share/fonts/truetype/font-japanese-gothic.ttf))、オープニングのストーリーの表示をするかどうかと、音を鳴らすかどうかの三項目です。
{"FontPath":"C:\\Windows\\Fonts\\YuGothR.ttc","OpeningRoll":true,"PlaySound":true}
それぞれを好みの値に変更して起動すればその設定で動きます。
フォントを変えてみたり、何度もリトライするときにいちいちオープニングは見たくない、音はちょっと困る、など、事情に応じてこのファイルを編集してご利用ください。
===== 実装についてのあれこれ =====
==== きっかけ ====
そもそも、SDL2で行こうと思ったのは、Copilot に、「320x240のビットマップの下に、一行のコマンド入力、その下にスクロール可能なログエリアがあるアプリのサンプルを作って」といったら、ほぼほぼ、完成形に近い画面レイアウトのアプリが出来上がりました。
もちろん、レイアウトだけで、ゲームができたわけではないのですが、上にも書いたように、キモは、バッファに書いたデータが画像として表示できるか否かなので、ここまでできればほとんど勝ったも同然なのです。
あと、めんどくさくなりそうなダイアログも「タブキーを押したら、三つボタンがあるモーダルダイアログを作って。」といってひな型を作ってもらいました。
ここまでできれば、あとは、手を動かすだけ。
AIがあることで、面倒な部分をかなり軽減してくれるのはありがたいです。
==== ビットフィールド型 ====
シナリオデータは4バイト一組のビットフィールドでルールが記述されています。
なので、ビットフィールド処理は必須なのですが、C#ではビットフィールドは実装出来ないので、しょうがないからシフトと論理積を使ってマスキングすることで逃げてます。
まあ、ビットフィールド型にしても、コンパイラが裏で同じようなことをしているだけなので、別にいいですし、クラスで隠蔽してしまえばいい話なので、一度クラスを書いてしまえばどうでもいい話ではあるのですが。
public class ZRuleBlock
{
private byte[] rules;
public ZRuleBlock(byte[] r) { rules = r; }
public byte Action { get { return (byte)((rules[0] >> 7) & 1); } }
public byte Op { get { return (byte)((rules[0] >> 4) & 7); } }
public byte Type { get { return (byte)((rules[1] >> 5) & 7); } }
public byte Id { get { return (byte)(rules[1] & 0x1F); } }
public byte Offset { get { return rules[2]; } }
public byte BodyType { get { return (byte)((rules[2] >> 5) & 7); } }
public byte BodyId { get { return (byte)(rules[2] & 0x1F); } }
public byte Value { get { return rules[3]; } }
...
==== 結局 unsafe ====
==== データ欠損 ====
デバッグプレイ中に、突然例外をはいて落ちた。
3階の西廊下を北に進んだら落ちた。
なんで?
ハイハイスクールアドベンチャーのゲームシステムは、地図情報の中に、その場所で行った動作に対するメッセージが仕込まれている。
まあ、多くは、その場所で何かを見たときとかに対するリアクションだ。
メッセージは、メッセージ本体と、そのメッセージへのリンク情報からなる。
このリンク情報がポイントしているメッセージ本体が存在していないのだ。
このデータは、もともとShift JISで作られていたが、Android版を作るときに、扱いにくいからUTF-8に変換したのだ。
なので、その際に取りこぼしたのかと、オリジナルのデータファイルをがさがさあさったところ、オリジナルでもそもそも欠落していた。
データファイルの作成時の問題か、或いは、そもそもうっかりデータを作り忘れていたのか、その辺はもはや定かではないが、とにかくないものはない。
他にそういうところがないか、不安になって、地図データのダンプをするスクリプトを作って走らせたところ、欠落はここ1か所だけだった。
JavaやC++はこのあたりの境界越えに比較的寛容でいままでたまたま発覚しなかったが、C#は思ったより厳格だったということ。