A3サイズの升目帖

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

座標系をリファクタリングした

ゲーム本体の制作に目立った進捗はないなあ。

というのも、ここ4日ほどかけて座標周りのリファクタリングをしていたからだ。

このリファクタリングだけは今のうちにやっておかないと後々大きな負債になりそうだったので。


事の経緯

Point」型

Point という型があった。

プロジェクトの最初期のコミットで追加された構造体だ。

こいつは、平面上の座標をX軸とY軸で表現するいたってシンプルなものである。

struct Point
{
    float x;
    float y;
}


しかし、これは 大きな間違い だった。

Point は、その ほとんど無情報にも近いシンプルさ汎用的で甘美な響きの命名 ゆえにあちこちで乱用され、 ありとあらゆる概念がごちゃまぜになる というカオスを生じてしまっていた。


ゲーム上の座標も、クライアント上の座標も、テクスチャ上の座標も、十把ひとからげの無階級制。

「座標」という名のもと、みな等しく Point なのである。

しまいには、「表のセルの 番地 も、ある意味座標だよね(暗黒微笑)」とか言って Point 型を持ち出す始末。気でも触れたんか?


ブラックホールさながら座標の概念を遍く飲み込んだ Point は、コードのそこかしこに顔を出し、レイヤーの壁に大穴をあけ、上層から下層まで一気通貫の腐臭漂う依存関係を生み出していたのだ。


コード量が少ないうちはまだ何とかなっていたものの、拡大と膨張をするのがソフトウェアの定め。

プロジェクトが大規模になるにつれて、これらの概念の混乱が喫緊の問題として頭をもたげるようになってきた。


Size」型

悲しいかな、Size という型もあった。

プロジェクトの最初期のコミットで追加された構造体だ。

こいつは、平面上の矩形サイズを幅と高さで表現するいたってシンプルな(以下略


これは…マズい…

このように、本来は無関係の概念を容易に混同させてしまう「無駄に包容力の高い型」が、殊に座標系に多いという事実がプロジェクトを進めるうちに分かってきた。

このままコーディングを断行すると、遠からず自分の足を撃ち抜くことは明白だったので、いったん作業を止めてリファクタリングを行っていたというわけだ。


せっせとリファクタリング

ゴールの設定

PointSize を解体してそれぞれのコンテキストに沿った表現力の高い型に作り替えてやらなきゃいけない。少なくとも異なる概念の値の取り違えは無くすこと。ここがゴール。

ゲーム座標はゲーム座標、テクスチャ座標はテクスチャ座標という具合に、ちゃんと専用の型を作っていく。


不思議なもので、今回の Point とか Size のような極度に汎用的でヤバげなやつらは、大抵 util とか common のような極度に汎用的でヤバげなフォルダに存在するもんだ。

最終的にはこのフォルダごと解体していけるといいが。

一足飛びにやったらダメ

慌てて一気に進めようとすると、そのうちゴールを見失ってトンチンカンなことをやりだすので、小さなステップでこなしていこう(1敗)

コミットできていないファイルがウン十単位でフワフワ漂流していると(個人的に)精神衛生上よろしくないというのもある。

(まあ今回は座標系というインフラレベルの大工事になってしまったので、息継ぎなしの対応になった点はある意味やむなしという感じもするが…)


で、どうにか完了

途中で設計方針に悩んで気が狂いそうになったり、「SOLIDとは…カプセル化とは…」などと考えすぎて気が狂いそうになったり、紆余曲折あって気が狂いそうになったが、どうにかリファクタリングを完了させた。

まあそれなりにましなコードに生まれ変わったかな。

少なくとも、目標としていた「座標系の値の取り違えリスク撲滅」は、このリファクタリングによってほぼ達成できたと自負している。やったぜ。


あと、こんなこともできるようになった。

演算子オーバーロードを用いた直感的な座標計算である。

// 速度 * フレーム数 = 距離
Velocity v;
Frame f;
Distance d = v * f;
// 座標の平行移動。
Position p;
Distance d;
p += d;
// 2点間の距離を得る。
Position p1;
Position p2;
Distance d = p1 - p2;

座標計算をまるで数式を書くかのごとくコーディングできる。至福である。

しかも、X軸とY軸はそれぞれ別の型になっているので、計算や比較処理でアホみたいな取り違えをして時間を溶かすリスクを低減できる。

Position p1;
Position p2;
if (p1.x < p2.y) // X軸とY軸は比較不能。コンパイルエラー!
{
...
}


ひとまず一件落着

懸案事項はこれにて片が付いたので、今度こそゲーム本体の制作に戻ろう。

リファクタリングだけで4日かかったのは正直痛いが、杜撰な設計のツケだと思って甘んじて受け入れるしかないな。

とりとめのない文章になりましたが、現場からは以上です。



セーブデータ管理画面(暫定版)を実装した

セーブデータ管理画面

進捗報告

セーブデータ管理画面のプロトタイプが完成した。

う~ん、見るからに仮置きの「ゲームタイトル」といい、オブジェクトの配置といい、色々と粗削りですね…

まあ、とりあえず必要最低限の動作をするものは作れたから良しとしよう。

細かい調整をここでやったところで、あとで間違いなく手が入るわけだし、今は気にしない。

実装するにあたって、プログラムのモジュールや画像などのリソースを用意しなきゃならなかったわけだが、画面のデザインはそれとは違ったベクトルの難しさがあって大変だった。


次の作業

4連休、終わっちまったなあ。

公開できたスクショは派手とは程遠いものになってしまったが、まあ焦らず進めていこう。

次は引き続きタイトル画面をやるか、それともゲームシーンに着手するか… どうすっかな?

タイトル画面は未確定のことが多すぎるから、ゲームシーンの実装に移るか。いよいよ本丸だ。



【7-Zip】特定の拡張子のファイルやフォルダを除外して圧縮


今やっている個人プロジェクトは外部のリモートリポジトリを使っていないので、週1くらいの間隔でバックアップをとっている。

その際、圧縮には 7-Zip を使っているのだが、プロジェクト内に .lib.exe のようなバイナリファイルが大量にあると、圧縮に時間はかかるしファイルサイズは大きくなるしでいいことがない。

バイナリファイルはソースコードさえあれば何度でも再生成できるので、ソースコードだけバックアップできればそれで十分なわけだ。

調べたところ、特定の拡張子のファイルや特定の名称のフォルダを除外して圧縮する方法があるようなので、備忘録としてまとめておく。

なお、7-Zip のバージョンは 19.00 である。


7-Zip の除外パラメータ -x

前置きが長くなったが、7-Zip ではファイルやフォルダの除外に -x を使用する。

オプションとして、r再帰的処理を、!ワイルドカードを有効にできる。

特定の拡張子のファイルを除外する

例えば、拡張子 .xxx のファイルすべてを除外するなら以下のようなパラメータになる。

-xr!*.xxx

特定の名称のフォルダを除外する

また、folder というフォルダすべてを除外するなら以下のようになる。

-xr!folder

複数パターンまとめて除外するには?

-x はひとつにつき1個のパターンしか指定できないらしく、複数パターンのファイルやフォルダを除外したい場合は愚直に羅列するしかないようだ。(筆者調べ)

-xr!folder1 -xr!folder2 -xr!*.xxx -xr!*.yyy -xr!*.zzz


おまけ

せっかくなので、よくありそうなバックアップ処理を自動化するバッチのサンプルをば。

  1. 同階層の特定のフォルダを
  2. 入力した任意のパスワードで暗号化して
  3. 特定のファイルやフォルダを除外した上で
  4. .7z 形式で圧縮したのち
  5. ファイル名 backup_YYYYMMDD.7z として出力する。
@echo off

cd /d %~dp0

echo パスワードを入力してください。
set /p pwd=

set filename=backup_%date:~0,4%%date:~5,2%%date:~8,2%.7z
"[7-Zipのパス]\7z.exe" a -p%pwd% -xr!folder -xr!*.lib -xr!*.exe %filename% "[対象フォルダ名]"

pause

バックアップ作業が大変楽になるので重宝している。

※もちろん、必要なものまで除外されてしまわないよう、そこだけ注意されたい。


参考文献



エフェクト付きのテキスト表示を実装した

あれ? テキスト表示まだ作ってないやん

タイトル画面で使うセーブデータ管理画面を実装している途中にふと思い出したのだが、そういえばまともなテキストの表示機能をまだ実装していなかった。

テキスト表示といっても、デバッグ用みたいな簡易なやつではなくて、様々なエフェクトをかけることのできる Rich なものだ。

テキスト表示はおそらくセーブデータ管理画面でも使うことになるだろうし、そうでなくても遅かれ早かれメッセージボックス等で使うはずなので、作業を切り替えてそっちの実装を優先していた。


とりあえず出来上がったのがこちら

エフェクト付きテキストを表示するウインドウ

文字送り、色変え、ダイナミックなアニメーションなど、様々な特殊効果をかけることができる。

今は種類が少ないが、今後もっと増やしていく予定。

さて、部品はそろったので、今度こそタイトル画面やっていくぞ。



波形コンバータを実装した

波形画像

インフラ完全攻略

前回の楽譜コンパイラ に続き、楽譜を波形に変換する波形コンバータの実装が完了した。

よっしゃああああああ!! これでインフラの難関は全部突破 したわけだ!

今のところ音源は正弦波だけだが、それでも書いた楽譜のとおりに音楽が再生された時の喜びは ひとしおであった。

成果物が音声なのでブログに貼り付けづらいのが残念だ。

正弦波はピッチベンドをかけるとかなり化けるので面白いぞ。


次どうする?

とりあえず、タイトル画面やセーブデータ選択ウインドウなんかを実装していこうと思う。

これまで殺風景な試験的オブジェクトばかり作ってきたので、ようやくゲームらしいものに取り掛かれそうだ。

気分あがるなあ。次の記事ではいままでよりも見た目が華やかなものをアップできるといいが。



楽譜ファイルのコンパイラを実装した

Piano Music Score Music Sheet Edited 2020


独自のサウンド記述用言語を作成

前回 の XAudio2 の問題はとりあえず置いといて、サウンドの制作に使う独自言語の定義とそのコンパイラをここ2日ほどかけて実装していた。

こんな感じの言語。テキスト形式で記述できる。

; ';'から行末まではコメント

@0 ; 音色は0番
v128 ; 音量

; ドレミファソラシドの音階
cdefgab>c

なんか MML を触っていた経験が色濃く出ていますね。


言語なんて自作しなくても、ほかにやりようあったろ…

うーん… それなあ。

まず、wav の埋め込みを考えたが、どうあがいてもサイズがデカくて断念した。趣味でやる以上、リリースするバイナリは 5MB 以内に収めたいというこだわりがあるんだ。

ogg も候補に挙がったがなんかややこしかったし…。wav ってバイナリ構造がすごく素直で扱いやすいじゃない? それに、結局のところサイズ面では楽譜ファイルには敵わない。

というわけで、

  • wav ファイルの扱いやすさ
  • ファイルの軽さ
  • 音声の再現性の高さ

という点を考慮すれば、「楽譜を表現する独自の言語を定義する」という選択に行き着くのは至極当然のことではなかろうか?(狂気)

これを数時間で可能にしてしまったD言語の書き易さが悪いんや…


あとは、音色を用意し、コンパイルした楽譜バイナリを波形データに変換すれば、どんなサウンドも奏で放題だ。


楽譜コンパイラD言語unittest

話題は変わるけど、コンパイラみたいに入力と出力がはっきりしていて、外部への依存がないものはユニットテストが書きやすくていいな。

今回の楽譜コンパイラなんて、ファイル入出力を除く部分に関しては(筆者のコードにしては珍しく)カバレッジ 100% を記録した。

カバレッジ100%のソースコード

ユニットテストがあると、自信をもってリファクタリングできるので本当にありがたい。

もっとも、カバレッジが必ずしも品質の指標になるわけではないが、コードをいじる際に少なくとも既存の動作を壊していない確信は持てるのでだいぶ気が楽である。あと、数か月後の自分への最高の仕様書になる。


ちなみに、D言語unittestカバレッジを計測するには以下のような dub コマンドを打てばいい。簡単。

dub test -b=unittest-cov

成功すると、カバレッジが記録された .lst という拡張子のファイルがプロジェクトフォルダに出力されるはず。


このように、特別なライブラリ不要で、組み込みのユニットテストが書けるというのも筆者がD言語を気に入っている理由の一つだ。


あとすべきこと

次は波形コンバータを書けばOKだな。

よしよし順調順調。

この調子でこつこつやっていこう。


ライセンス表示



サウンドの再生に成功した…が

D言語でゲームサウンド

とりあえず Windows 上で動作させようということで、サウンドの再生には XAudio2 を採用した。

OS は Windows10 なので、XAudio2.9 がターゲットになる。

もともとは DirectSound を検討していたんだが、あれって知らん間に非推奨になってたんやね。

いつ消えるかもわからん非推奨のもんをごり押しで使い続けるのもアレなので、これを機会に XAudio2 に乗り換えることにした。


D言語向けに色々と移植

というわけで、XAudio2 のマクロましまし黒魔術ヘッダファイルをせっせとD言語に翻訳すること小一時間…

どうにかサウンドを再生できるようになった。ヘッダの移植は DirectX11 で苦労した経験が活きた。

XAudio2 自体のプログラミングだが、DirectSound と比べてめっちゃ楽やん…!(簡単とは言っていない)

パンの設定・取得でちょっとつまずいたくらいで、あとはスイスイ行けた。


「いや~ サウンドも制覇したわけだし、インフラはほぼ完成。これで安泰だな!」

…と思っていたのだが…


なんかFPSがガタ落ちするんですが

XAudio2 を使い始めた途端、FPS がコンスタントに 7~10 近く落ちる現象を確認。

今まで 60FPS で動いてたゲームが、平均 50FPS くらいになってしまうのだ。

そりゃねぇぜ…


発生条件

これが不思議なことに、サウンドを何ら再生していない無音の状態でも発生する。

原因を細かく調べた結果、マスタリングボイスを生成しなければ 60FPS が維持されることがわかった。どうやら、主なトリガーはマスタリングボイスの生成らしい。

また、メッセージループで過負荷を防ぐために入れている Thread.sleep を削除すると、マスタリングボイスが存在しても 60FPS を維持できる。(もちろん CPU 使用率は跳ね上がってしまうが。)


解決方法いまだ不明

どうしたらよいかは現時点ではわかっていない。

困ったもんだな…。FPS が10も落ちるんじゃあ正直言ってお話にならんぞ。

スレッド周りが原因なんだろうか? メッセージループのあるUIスレッドで XAudio2 を起動したのがまずかった?

XAudio2 は、 Microsoft がゲーム開発向けと謳っている技術なので、選定は間違ってないと思うんだがなあ。

使い方がまずいんだろうけど、ゲームで XAudio2 を使用する際の正しいお作法みたいなのがよくわからないので手が打てんなコレ…

XAudio2 で検索かけても、出てくるのはすべてサウンドの再生に終始するサンプルばかりだしなあ。

メッセージループと併用する際の正しいサンプルとかがあれば泣いて喜ぶのだが。

ひょっとしてD言語との相性の問題だったり?

う~ん、なんもわからん。

ぐぬぬ