A3サイズの升目帖(旧)

ゲーム制作やプログラミングに関する雑記

【Win32】WM_MOUSEWHEELのlParamはクライアント座標じゃないよという話

Windowsのロゴマーク


そういえば、マップエディタを実装していてひとつハマったポイントがあったので備忘録として記しておく。

まあ、内容は記事タイトルがほぼすべて物語っているのだが…


WM_MOUSEWHEEL

画面の縮尺変更を実装するためにマウスホイールを使おうと思って WM_MOUSEWHEEL イベントを購読してみたのだが、どうも挙動がおかしい。

なんというか、ウインドウを動かすと場所によっては WM_MOUSEWHEEL が発生しないようで、マウスホイールが効かなくなってしまうのだ。


原因

調べたところ、lParam の仕様によるものであることが分かった。

なんと、WM_MOUSEWHEELlParam には クライアント座標ではなくスクリーン座標が格納されている とのこと。

ええ…!? WM_MOUSEMOVE とか、ほかのマウス系イベントは全部クライアント座標が格納されてたじゃないか。なんだこの罠は…

どうりでウインドウの位置によってマウスホイールが効いたり効かなかったりするわけだ。


解決方法

スクリーン座標をクライアント座標に変換する必要がある。以下はD言語で書いたもの。

auto pt = POINT(cast(short) LOWORD(lParam), cast(short) HIWORD(lParam));
ScreenToClient(hWnd, &pt); // ptがクライアント座標に書き換わる。

MAKEPOINTSGET_X_LPARAMGET_Y_LPARAM のようなマクロは dmd v2.097.0 時点では移植されていないようなので、LOWORDHIWORD を駆使して POINT 構造体を生成している。まあ、回りくどくなるだけで難しいことはしていないと思う。

環境によっては座標が負数になるとのことなので、cast(short) を忘れずに。


参考文献



マップエディタ完成した

ようやく完成した

マップエディタ

10日間くらいかかった。大変だったぞ… 座標計算地獄で。


それでもまあ、苦労の甲斐あって、けっこうまともなものが出来上がった気がする。

周辺ツールの出来栄えって、本体のゲームの質やモチベーションに割ともろに影響を及ぼすので馬鹿にならない。

細かい操作性にまだ改善の余地はあるが、合間合間でチューニングしていくとしよう。

あとやはりD言語はいいぞ。


今後の作業

これで周辺ツール最大の難所は超えたわけだ。やったぜ。

次に取り掛かるのはサウンド周りだなあ。サウンドさえ制覇すればすべてのインフラは完成だ。



【D言語】-SUBSYSTEM:WINDOWS と writeln の確執

D言語のロゴマーク

マップエディタの実装が遅々として進まないな。周辺ツール最大の難所なのでまあ予想はしていたが…

キリのいい進捗がないからブログも放置しがちになってしまっていけない。

というわけで、お茶を濁すわけじゃないけど、技術的なハマりポイントについてちょっと書こうと思う。


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 が使えなくなったときは滅茶苦茶あせったけど、解決方法が見つかってよかった。

これでログ出力がはかどるわい。(マップエディタの実装もはかどれ)


参考文献



マップチップの配置を実装した

マップを作れるようになりました

地形が表示されたウインドウ

前回のブログでちょっと触れたやつ。

かろうじて人様に見せられるレベルのマップが作れたので公開。

マップデータはテキストで管理しているのだが、今のところ手打ちでブロックを配置しないといけないので、これっぽっちのマップを構成するのにも骨が折れた。

あとやっぱり色合いがどぎつい気がする。まあペイントのデフォルトのパレットで絵を描いたから当然か…。

↓こいつ

(すごくどうでもいいが、実は今まで公開してきた画像は全部この16色で構成されているのだ。そろそろ別のパレット用意しないとなあ)


軽い技術的な話

マップチップ1枚のサイズは 8x8px になっている。

ただ、毎フレームこいつらを1枚1枚描画していると時間がかかって仕方ないので、レンダリングターゲットテクスチャにプリレンダリングすることで地形の描画を高速化している。

ブロックの破壊があったら、破壊個所をキューとかに積んでピンポイントに再描画すればいい。

この手のゲームでは常套手段かもしれない。


それから、テキストのマップデータをそのままゲームに埋め込んでしまうと重くてしょうがないので、事前にコンパイル&圧縮し、独自のバイナリ形式で埋め込むようにしている。

exe のサイズはやっぱり小さく収めたい。今のところ x64 リリースビルドで約 670KB となっている。

最終的に 5MB 以内にしたい!とかいう謎のこだわりを持っております。


あとやるべきこと

とにかく次にやらなきゃいけないのは、マップエディタの作成だ。

いよいよ来たな大物。

周辺ツールの中では最大級にキツイが、これがなくちゃ話にならない。

元気出していきましょ。



ユニットテスト強化しなきゃ

ソースコードがだいぶクソになりつつあるなあ。

pureconst で修飾できないメソッドが散見されてイライラしてくるぞー

ゲーム本体もさることながら、周辺ツールがとりわけ酷い。ツールだってプロジェクトの一部なのだ。

ユニットテストは書いてはいるんだが、面倒なところやテストしづらいところを尻込みしてるんじゃ意味がない。

リファクタリングをするにも、動いているものを壊しゃしないかどんどん不安になるしいいことがない。手を打つならまだスタートアップしたばかりの今しかないかもしれん。

テスト駆動開発しろ、とまでは言わんが、D言語にはせっかく組み込みの unittest 構文があるんだから、もっと積極的にテスト書こう(自戒)

private 関数でも、精神衛生上プラスになるならテスト書けばいいんじゃない? 変更の起こりやすさという観点で切り分けられていれば、多少の仕様変更のあおりを食らってもテストコードが重荷になることはないと思うし。


しかし、なかなか絵になる進捗ができなくてもどかしいな。プログラミングはこつこつ進めているんだが、成果物がブログに上げるにはどうにも地味で。

今日はマップ読み込み機能のおおまかな動作を確認できたので、体裁を整えて近く公開したいところだが。



【D言語】rdmdって便利

D言語のロゴマーク

D言語ってコンパイル言語なんでしょ?って思ってたんだが、スクリプト言語的な 使い方もできるらしい。

D言語をインストールすると付いてくる rdmd というものを使う。


rdmd

D言語コンパイル言語なので、通常は

  1. ソースコード書く
  2. dmdコンパイルする
  3. リンクする
  4. 実行する

という流れを踏まないといけないのだが、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 を使えば起動用スクリプトファイルすら不要らしいですわよ…! たまげたなあ。

詳しくは公式サイトをご覧いただきたい。

dlang.org



【D言語】リンカエラー LNK4255

D言語のロゴマーク

突然よりよい設計が降ってきたので、ディレクトリ構造が変わるレベルのわりと大きなリファクタリングをしていた。

しかし、いざ完了してコンパイルしてみると、何やら妙なリンカエラーが出るのだ。


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.dmodule 定義がこうなっていた場合…

// aaa\my_program.d
module project.aaa.my_program;
// bbb\my_program.d
module project.bbb.my_program;

たとえ途中のパッケージ名が異なっていたとしても my_program というモジュール名が被っているがゆえに警告になることがあるらしい。

実際、片方の my_program のモジュール名(およびファイル名)を改名したところ、無事 LNK4255 は出なくなった。


しかし、おかしい… なんでこんなことになるんだろう? パッケージ名が異なっていればそれは別のシンボルでしょ? アクセス修飾子に package(...) 構文を使ったからかな?

正直、詳しい原因はよくわからん。

まあ直ったからええか…

また暇があったら調べておこう(絶対やらないやつ)