【Win32】WM_MOUSEWHEELのlParamはクライアント座標じゃないよという話
そういえば、マップエディタを実装していてひとつハマったポイントがあったので備忘録として記しておく。
まあ、内容は記事タイトルがほぼすべて物語っているのだが…
WM_MOUSEWHEEL
画面の縮尺変更を実装するためにマウスホイールを使おうと思って WM_MOUSEWHEEL
イベントを購読してみたのだが、どうも挙動がおかしい。
なんというか、ウインドウを動かすと場所によっては WM_MOUSEWHEEL
が発生しないようで、マウスホイールが効かなくなってしまうのだ。
原因
調べたところ、lParam
の仕様によるものであることが分かった。
なんと、WM_MOUSEWHEEL
の lParam
には クライアント座標ではなくスクリーン座標が格納されている とのこと。
ええ…!? WM_MOUSEMOVE
とか、ほかのマウス系イベントは全部クライアント座標が格納されてたじゃないか。なんだこの罠は…
どうりでウインドウの位置によってマウスホイールが効いたり効かなかったりするわけだ。
解決方法
スクリーン座標をクライアント座標に変換する必要がある。以下はD言語で書いたもの。
auto pt = POINT(cast(short) LOWORD(lParam), cast(short) HIWORD(lParam)); ScreenToClient(hWnd, &pt); // ptがクライアント座標に書き換わる。
MAKEPOINTS
や GET_X_LPARAM
、GET_Y_LPARAM
のようなマクロは dmd v2.097.0 時点では移植されていないようなので、LOWORD
と HIWORD
を駆使して POINT
構造体を生成している。まあ、回りくどくなるだけで難しいことはしていないと思う。
環境によっては座標が負数になるとのことなので、cast(short)
を忘れずに。
参考文献
【D言語】-SUBSYSTEM:WINDOWS と writeln の確執
マップエディタの実装が遅々として進まないな。周辺ツール最大の難所なのでまあ予想はしていたが…
キリのいい進捗がないからブログも放置しがちになってしまっていけない。
というわけで、お茶を濁すわけじゃないけど、技術的なハマりポイントについてちょっと書こうと思う。
D言語で GUI アプリケーションを作る際
D言語で Windows 向けの GUI アプリケーションを作成し、出力された exe を直接実行すると、目的のウインドウとは別にコンソールウインドウが表示されてしまう。
実際の画像はこんな感じ。裏にコンソールウインドウがいるのがおわかりいただけるだろう。
これを解決するには、リンカフラグとして dub.json に以下の設定を追加すればよい。
"lflags": [ "-SUBSYSTEM:WINDOWS", "-ENTRY:mainCRTStartup" ]
これでコンソールウインドウは表示されなくなる。
しかしとんでもない代償が
そう、確かにコンソールウインドウは消えるのだが、なんと代わりに writeln
などの出力が使えなくなる という特大の副作用があるのだ。
上述のリンカフラグを設定した状態で writeln
を呼ぶと、以下のようなエラーが出てアプリケーションが落ちる。
Program exited with code -1073740791
おいおい… これじゃあログ出力できねぇよ…(絶望)
理屈としては単純で、writeln
の出力先であるコンソールウインドウを抹殺してしまったから。当然と言えば当然の結果なのだ。
解決方法
出力先がないから落ちるのなら、出力先を作ってやればよい。
ソースコードは以下のとおり。
// 標準出力を file.log というファイルに差し替えている。 stdout.reopen("file.log", "a");
repoen
でファイル名を指定すれば、出力内容はすべてそのファイルに書き出される。(ファイルは exe と同階層に出力されるはず)
ちなみに、出力内容を捨てて構わないなら、ファイル名の代わりに "NUL"
と指定すればよさそうだ。
うーん、ちょっと微妙
ええ、わかります。
このやり方だと確かにエラーは出ない。しかし、VSCode のターミナルなどにリアルタイム出力するという機能は失われたままなのだ。
これは例えば、デバッグ実行中にログ出力をリアルタイムに確認したいといった場合に大きな障害になる。
解決方法 その2
まず断っておくのは、ターミナルにログを出力しつつ、exe を直接実行した際のコンソールを消す、といった 両立はできない ようだ。(筆者調べ)
ただ、状況に応じて使い分けは可能だ。
dub.json の設定で対応していく。
設定例1
例えば、デバッグモードのみターミナルでログを確認したい、ということであれば "buildTypes"
の設定が使える。
リリースモードにだけ "lflags"
を適用する、という寸法だ。
"configurations": [ { "name": "application", "targetType": "executable", "buildTypes": { "release": { "lflags": [ "-SUBSYSTEM:WINDOWS", "-ENTRY:mainCRTStartup" ] } } } ]
設定例2
デバッグモードでもリリースモードでもターミナルでログを確認したい、でも完成版ではコンソールを消したい、ということであれば、"configurations"
を分離してしまうという手もある。
完成版用の設定として、例えば "application-shipment"
のようなものを新しく追加し、"lflags"
の設定はその中に記載すればよい。
"configurations": [ { "name": "application", "targetType": "executable" }, { "name": "application-shipment", "targetType": "executable", "lflags": [ "-SUBSYSTEM:WINDOWS", "-ENTRY:mainCRTStartup" ], "versions": [ "Shipment" ] } ]
"versions"
に "Shipment"
という完成版専用のフラグを設定している。
D言語のソースコードのほうでフラグを見て分岐してやれば、ログ出力先も設定で差し替えられるためだ。
// 完成版では file.log に出力するよ! version (Shipment) { stdout.reopen("file.log", "a"); }
なお、完成版の設定を明示的に選択してビルドするには -c
コマンドを使用する。
dub build -c=application-shipment
以上
個人的に writeln
が使えなくなったときは滅茶苦茶あせったけど、解決方法が見つかってよかった。
これでログ出力がはかどるわい。(マップエディタの実装もはかどれ)
参考文献
- https://forum.dlang.org/thread/ppkqfcbqskdadixskmsl@forum.dlang.org
- https://qiita.com/mono_shoo/items/04499a22c5d7ec298321
マップチップの配置を実装した
マップを作れるようになりました
前回のブログでちょっと触れたやつ。
かろうじて人様に見せられるレベルのマップが作れたので公開。
マップデータはテキストで管理しているのだが、今のところ手打ちでブロックを配置しないといけないので、これっぽっちのマップを構成するのにも骨が折れた。
あとやっぱり色合いがどぎつい気がする。まあペイントのデフォルトのパレットで絵を描いたから当然か…。
↓こいつ
(すごくどうでもいいが、実は今まで公開してきた画像は全部この16色で構成されているのだ。そろそろ別のパレット用意しないとなあ)
軽い技術的な話
マップチップ1枚のサイズは 8x8px になっている。
ただ、毎フレームこいつらを1枚1枚描画していると時間がかかって仕方ないので、レンダリングターゲットテクスチャにプリレンダリングすることで地形の描画を高速化している。
ブロックの破壊があったら、破壊個所をキューとかに積んでピンポイントに再描画すればいい。
この手のゲームでは常套手段かもしれない。
それから、テキストのマップデータをそのままゲームに埋め込んでしまうと重くてしょうがないので、事前にコンパイル&圧縮し、独自のバイナリ形式で埋め込むようにしている。
exe のサイズはやっぱり小さく収めたい。今のところ x64 リリースビルドで約 670KB となっている。
最終的に 5MB 以内にしたい!とかいう謎のこだわりを持っております。
あとやるべきこと
とにかく次にやらなきゃいけないのは、マップエディタの作成だ。
いよいよ来たな大物。
周辺ツールの中では最大級にキツイが、これがなくちゃ話にならない。
元気出していきましょ。
ユニットテスト強化しなきゃ
ソースコードがだいぶクソになりつつあるなあ。
pure
や const
で修飾できないメソッドが散見されてイライラしてくるぞー
ゲーム本体もさることながら、周辺ツールがとりわけ酷い。ツールだってプロジェクトの一部なのだ。
ユニットテストは書いてはいるんだが、面倒なところやテストしづらいところを尻込みしてるんじゃ意味がない。
リファクタリングをするにも、動いているものを壊しゃしないかどんどん不安になるしいいことがない。手を打つならまだスタートアップしたばかりの今しかないかもしれん。
テスト駆動開発しろ、とまでは言わんが、D言語にはせっかく組み込みの unittest
構文があるんだから、もっと積極的にテスト書こう(自戒)
private 関数でも、精神衛生上プラスになるならテスト書けばいいんじゃない? 変更の起こりやすさという観点で切り分けられていれば、多少の仕様変更のあおりを食らってもテストコードが重荷になることはないと思うし。
しかし、なかなか絵になる進捗ができなくてもどかしいな。プログラミングはこつこつ進めているんだが、成果物がブログに上げるにはどうにも地味で。
今日はマップ読み込み機能のおおまかな動作を確認できたので、体裁を整えて近く公開したいところだが。
【D言語】rdmdって便利
D言語ってコンパイル言語なんでしょ?って思ってたんだが、スクリプト言語的な 使い方もできるらしい。
D言語をインストールすると付いてくる rdmd というものを使う。
rdmd
という流れを踏まないといけないのだが、rdmd ならば(乱暴に言えば)「2.」やら「3.」やらの手順をすっ飛ばすことができる。
ソース書く → 実行する。
まさにスクリプト的。
例としてひとつ
環境を Windows と想定する。
以下のような構成をどこかお好きなフォルダ内に作り…
app.d launch.bat
それぞれのファイルの内容をこうするじゃろ?
// app.d import std.stdio; void main(string[] args) { writeln("Hello ", args[1], " World!"); }
// launch.bat @echo off cd /d %~dp0 rdmd -O -release app.d "D Language" pause
はい、おわり。あとは launch.bat を叩くだけ。
// 出力はこのとおり。 > Hello D Language World!
コンパイルもリンクもまったく意識する必要ありません。
あらすごい
このように、rdmd を使うと、D言語をまるでスクリプト言語のように書いて実行することができる。
まあ、正確には裏でコンパイルやリンクは走っているので、初回の実行はちょっと時間はかかる。
しかし、ソースコードを変更しない限り、生成した exe はキャッシュされているので、2回目以降の実行は速い。
-O
や -release
など、コンパイラオプションは通常の dmd と同じである。
例で示したように、main
を含むソースコードさえ指定してやれば、依存関係は勝手に解決してくれる というのもグッド。複数ファイル import
していても、それらをだらだら指定する必要はない。
アプリケーションへの引数は main
を含むソースコードの後に指定してやればいいらしい。(例では app.d のうしろ)
使いどころとしては、筆者は個人的に「exe としてわざわざコンパイルするほどでもないけど、D言語の機能は欲しいなあ」くらいの複雑さのタスクをこなすツールの実装に使用している。
D言語の標準ライブラリ Phobos の機能を一通り使えるので、バッチファイルだけで頑張るよりはるかに強い。
D言語インストールしてたら一度試してみてはいかが?
余談1
rdmd の生成した exe は以下のフォルダに出力されているっぽい。
どこにキャッシュされているか気になる人は見てみよう。
C:\Users\(ユーザ名)\AppData\Local\Temp\.rdmd\
余談2
shebang
を使えば起動用スクリプトファイルすら不要らしいですわよ…! たまげたなあ。
詳しくは公式サイトをご覧いただきたい。
【D言語】リンカエラー LNK4255
突然よりよい設計が降ってきたので、ディレクトリ構造が変わるレベルのわりと大きなリファクタリングをしていた。
しかし、いざ完了してコンパイルしてみると、何やら妙なリンカエラーが出るのだ。
LNK4255 ?
warning LNK4255: ライブラリは、同じ名前の複数のオブジェクトを含んでいます。デバッグ情報を伴わずにオブジェクトをリンクしています
はて? 何のことだろう?
どうやらデバッグビルドでしか発生しないようだ。
おまけに、コンパイルは問題なく終了し、吐き出されたアプリケーションは見た感じ普通に動作している。(ちなみに dmd は v2.097.0)
別に動くんだからいいじゃないかと考えてもいいが、こんな警告はいままで見たこともないし、やはり warning
なんて気持ちのいいものじゃない。
しかし、なんとかしようと検索してもほとんどそれらしい情報が見つからない(いつもの)
大体、もしも「同じ名前のオブジェクト」なんか含んでたら、そりゃ コンパイルエラー なのでは…?
原因(たぶん)
どうやら、module
の指定が問題だったらしい。
プロジェクト(出力形態は静的ライブラリ)が例えばこのような構成だったとして…
.dub source project aaa my_program.d bbb my_program.d .gitignore dub.json
それぞれのソースコード my_program.d
の module
定義がこうなっていた場合…
// aaa\my_program.d module project.aaa.my_program;
// bbb\my_program.d module project.bbb.my_program;
たとえ途中のパッケージ名が異なっていたとしても my_program
というモジュール名が被っているがゆえに警告になることがあるらしい。
実際、片方の my_program
のモジュール名(およびファイル名)を改名したところ、無事 LNK4255
は出なくなった。
しかし、おかしい… なんでこんなことになるんだろう? パッケージ名が異なっていればそれは別のシンボルでしょ? アクセス修飾子に package(...)
構文を使ったからかな?
正直、詳しい原因はよくわからん。
まあ直ったからええか…
また暇があったら調べておこう(絶対やらないやつ)