Heliodorさんをフォローして、最新情報をチェックしよう!

マイページへ

Ci-enはクリエイターに対して、金銭的な支援を送ることができるサービスです。

投稿記事

プログラムの記事(10)

Windows10でDirectX9

すごく今更なんですが、2016年8月にあったWin10の大型アップデート Anniversary Update によってd3d9.dllに何らかの変更があり、そのせいでDirectX9.0を使っている一部のアプリが劇重になるという事件があったらしいですね。

私はアンテナ感度が大変低いので、恥ずかしながら全く知りませんでした。(Win10使ってるのに……)
が、私と同じように感度の低い方がいるかもしれませんので、あえて記事にします。

幸いヴィータ大脱出などではこの問題は出なかったのですが、これのやっかいなところは、修正しようにもなんせ DirectX9.0 の本家本元が変更されてしまったので、それを利用している側ではどうにもならないことです。
調べてみたところ、商業ゲームメーカーは古いバージョンのd3d9.dllを配布するという手段に出たみたいです。


オーバーフロー
Windows 10 Anniversary Update (Ver.1607)に伴う、一部ソフトの動作不具合に関して
https://www.0verflow.com/days_dx9.html

イリュージョン
Windows 10 Anniversary Update後の動作について
http://www.illusion.jp/support/windows10.html

ただ、怪しい海外サイトなどからDLLをダウンロードするときは十分気を付けてください。
DLL自体は誰でも簡単に作ることができるので、偽物のDLLをつかまされたら目も当てられません。
本物のd3d9.dllだと思ってゲーム側がDirectX9の初期化関数を呼び出した瞬間、すべてのファイルが削除されるなんてこともありうるわけで。


本家の方は具体的にどんな変更があったのかはわかりませんが、下手したらd3d9.dllは単なるエミュレータになっていて、単にDirectX12を呼び出しているだけになった可能性もあるわけです。
……と思って手元のd3d9.dllを Dependency Walker で調べてみたんですが、d3d12.dll をさらに呼び出している、という部分はなさそうでした。
さすがにそんな面倒なことはしないか……しかしなんでそんなに重くなったんでしょうね?


WindowsXPではDirectX9までしか動かない。
(そしてWindowsXPはサポート終了)
Windows7以降ならDirectX11が標準でインストールされている。
Windows10ではDirectX9が激重になる可能性がある

つまり、この事実から導き出される回答は……。
(ル○ーシュっぽいポーズを取りながら)

\いいねで応援!/

意味不明なバグ

疲れてるときにプログラムするのはやめましょう


すっごいハマったバグがありました。

とあるエフェクトの描画結果が、想定したものと違うのです。
そこで、デバッガで内部計算の過程を追ってみました。
その時の計算の条件では、なんやかんやで最終的には「25 % 8 (25÷8の余り)」の値が出てきて、その値を元にしてエフェクトが描画されるはずでした。

ところがですね、その値が「1」になってたんです。
あれ?って思いました。この計算って最後には 25 % 8 になるはずだよな??
なんでそれが1なんだよと。

1時間ぐらいかけて、エフェクト式の計算過程を全部チェックしていって調べてみたんですが、やっぱり最終的に 25 % 8 = 1 になっているんです。

おかしいじゃあありませんか。

どうして 25 を 8 で割った余りが 1 になるのよ???

すっごい悩んだんですが計算がおかしくなっている原因がわからず、どうにもならなくなり、とりあえずコーヒーでも飲もうと立ちあがった瞬間に気づいたんです。




25÷8の余りって、1で合ってるじゃん……。




これだけで数時間つぶしました。そしてバグの本当の原因は全然関係ないところでした。

そういえば液タブ不調直りました。直ったというかACアダプターが外れかかってるだけでした。

なんか色々アレですね……。

\いいねで応援!/

ジョイスティック

プログラム話です。


突然ですが、私がゲームを作り始めた頃は右も左も分からなかったので、DirectX の入門書に書いてあった事に忠実に従って入力処理には Direct Input を使っていました。

ところが、とあるゲームを作った時に格闘ゲーム用の本格的なコントローラが認識されないという報告がありました。詳細は忘れてしまったのですが、調べてみたところ Direct Input では使えないゲームコントローラーが一部存在するということでしたので、「Direct Input ではなく Win32 API を使う」というマニアックな設定項目をオプション画面に追加したことがありました。

わざわざ Direct Input と Win32 API の両方で実装したわけですが、プログラム的には Win32 API のジョイスティック関数だけを使うのがシンプルで簡単なんですよね。なぜ Direct Input を使い続けたのかと考えてみたところ、「フォースフィードバックできるから」という、結局全く使う事の無かった機能のために選択した結果だったことを思い出し、次回作からは迷わず Win32 API だけを使うように変更しました。

余談ですが、プログラム的には普通のゲームパッド(ファミコンみたいな単純なものから、アナログスティックつきの豪華なものまで)はすべてジョイスティックコントローラーとして扱います。ファミコンのコントローラーのような明らかにアナログ入力できないものでも、倒したか倒していないかの2通りしかないアナログスティックとして扱います。数あるゲームライブラリでもコントローラからの入力を扱う部分には Joystick と名前がついている場合がほとんどですので、もしプログラマーが「ジョイスティック」とか言っていたらそれは普通にゲームパッドの事だと思ってください。

そんなわけで、プログラム的にはあくまでもガチのジョイスティックを基本にしているので、ゲームコントローラの処理関数とかには必ず「ハットスイッチの向き」を調べるものがあります。しかしハットスイッチなんてフライトシミュレーターで使うような本格的なジョイスティックにしか付いていないマニアックなボタン、ぐらいにしか思っていなかったので、使うことはないだろうと完全に無視してました。少なくとも自分が持っていた安物のゲームコントローラやテストプレイをお願いした人の環境ではそれで問題なかったのです。

で、ヴィータ大脱出をリリースしてからしばらくたった時、プレイステーションのコントローラが使えないという苦情が着ました。よく聞いてみると、PSコントローラーのアナログスティックでは操作できるが十字キーで操作できない、という事でした。その時になって初めて、アナログスティックと十字キーが両方ある場合は十字キーがハットスイッチとして働くという事を知りました。

試しにアナログスティックが付いている割と豪華なゲームパッドを買ってみて初めて気が付いたのですが、十字キーとアナログスティックのどちらががハットスイッチでどちらがアナログ方向キーになるのか、というのはゲームパッド側の設定で変えられるんですね。それがPSのパッドだとハットスイッチ固定になっていたという話で。

なので、ハットスイッチとアナログ軸の入力を両方調べて、絶対値の大きなほうを採用するようにし、一件落着しました。
(入力軸ごとに絶対値の大きなほうを見ているだけなので、例えば十字キーで上を入力し、アナログスティックで右を入力した場合は右上の入力として扱います。キーボードとゲームパッドで同時に操作した場合も同様です)

\いいねで応援!/

謎のエラー

プログラム話です。

今回もすんごいへんてこなエラーに悩まされた話です。
結果から言うと、1日1回クリーンビルドしようね、ってことです。


以前お話しした通り、現在製作中のアクションゲームの開発ツールには Visual Studio を使っています。いつものように、ビルド(Ctrl+Shift+B)して実行、実行結果を確認、プログラムをいじって再ビルド、という風に作業していたところ、突然、見慣れないエラーが出てビルドできなくなりました。

しかも、全く触っていないはずのファイルでエラーが出ています。ためしに、今変更したファイルだけをピンポイントでコンパイル(Ctrl+F7)すると、そのファイルに関してのコンパイルは成功します。

たった今変更したファイルではコンパイルが通るのに、全く無関係な別のソースファイルでいきなりエラーが起きるようになったのです。

おかしい。さっきまで普通にビルドできてたのに。そもそも、そのファイルは1バイトたりとも変更してないぞと。
具体的には、名前空間 std が定義されていませんとか、cでは許されないキーワード使ってるとか、その他諸々のエラーが1000件ぐらい出てきます。見てみると、標準のc++ライブラリのヘッダでエラーが起きてるんですね。

まず疑ったのは、何かのミスで標準ライブラリのヘッダファイルに文字を書き込んでしまったのでは?という事です。試しに適当なc++ライブラリのヘッダにわざと無意味なコードを書き込んで保存しようとしましたが、当然ながら書き込みロックがかかっていて保存できません。これなら知らないうちに間違って書き込んで、さらに無意識にCtrl+Sして上書き保存してしまったなんて事は無さそうです。

次に疑ったのは、c++ではなくcでコンパイルするように設定が変わってしまったのでは?という事です。c++のソースをcコンパイラでコンパイルすれば、上記のようなエラーが山ほど出るのもうなずけます。

が、確認してみたところ普通の設定になってました。拡張子が.cppならc++コンパイラを、.cならcコンパイラを使うって設定ですね。まあ、普段いじるような設定ではないので、デフォルトのままになっています。当たり前です。

もしかしたら何かの具合で、それ以外のどこか細かい設定がおかしくなったのかもしれません。プロジェクトファイルなど、ソリューションファイル .slnを始め、キャッシュファイルなどを全て削除して、プロジェクトをクリーンな状態に戻してみます。

こういう時にcmakeの出番です。プロジェクトファイル一式を全て削除し、cmakeの設定ファイルであるCMakeLists.txtだけが残った状態にして、改めてcmakeでプロジェクトを再生成してみます。

CMakeLists.txtの内容が1バイトも変更されていないというのはTortoiseHgによる編集有無の検出機能で確認済みですから、最初にビルドができていた時と比べて、プロジェクト全体を通して1バイトも差分がないはずです。が、ビルド結果は変わらず、大量のエラーが出ます。

謎なのは、TortoiseHgで数日前の状態にソースを戻してビルドしても、同じエラーが起きるという事です。いやいや、昨日とかも普通にビルドして実行していたぞと。

ところが、さらにもっと前の状態に戻すとビルドが成功するんですね。そして、一度でもビルドが成功した後にソースを現在のものに戻すと、普通にビルドが通るんです。で、その状態でまたプログラムを再開すると、何度かビルドに成功したあと、あるとき突然エラーが出てビルドが失敗します。

そこまできてようやく気づいたのですが、普通にビルドが通っていたはずの昨日や数日前のソースが、クリーンビルドすると通らないんですよ。

つまり昔のソースでビルドした後、あるファイルに変更が加わって本来ならばコンパイルが通らない状態になっていたが、差分ビルド(変更があったファイルだけコンパイルし、それ以外は以前のコンパイル結果を使う)のためにそのファイルについては再ビルドが行われず、正しくコンパイルできた時の結果が使われていた、と言うことになります。

ただ、ファイルに変更があったのに差分ビルドの対象にならないって事があるのでしょうか?ファイル自体は変わってなくても、そのファイルがincludeしているヘッダに変更があれば連鎖的に再ビルドされるはずですが...。

そこで、ソースを昔に戻しながら、どの時点でクリーンビルドが通らなくなったのか調べました。

TortoiseHgでソースを戻し、cmakeのキャッシュを消してクリーンなプロジェクトファイルを作り、フルビルドする。これを何回も繰り返して調べた結果、1週間ほど前のソースを境にエラーが出るようになっていたことが分かりました。その時行った変更は、ソースファイルの分割です。

とあるファイルが巨大になりすぎて扱いにくくなったので、その中からゲームキャラクターやシステム間の通信(様々なコマンドを送ったり、特定のイベントが発生したときに通知したりする)に関する部分だけを抜き出し、signal.h と signal.cpp というファイルを追加して、そこに移動しました。

この変更のうち、どの部分がまずかったのかをさらに調べます。まず、新規ファイルの追加だけをしてみます。すると、signal.h と signal.cpp をプロジェクトに追加した時点でビルドが通らなくなりました。最初は???だったのですが、少ししてピーンと来ました。

なんか signal.h ってすごく標準ライブラリにありそうな名前だぞ、と。

もしかして名前がかぶっているのでは?と。

見てみると、やはり singal.h というのは標準ライブラリに存在するヘッダファイルでした。普段使わないので全く気にしていませんでしたが…。

そこで signal.h ではなく event_signal.h という名前に変えてみたところ、あっさりとビルドできるようになったのです。ここまでくると原因追究は簡単でした。

このゲームのプログラムではスクリプトシステムとして lua を使っているのですが、lua.lib などのコンパイル済みファイルを使わずに lua をソースごとプロジェクトに取り込んでありました。

その中に、#include <signal.h> という、標準ライブラリを include している部分があったのですが、lua のソースは c で書かれているため、lua のファイルをコンパイルする時は自動的に c コンパイラが選択されます。#include <signal.h> の部分で、普通なら c コンパイラは標準ライブラリの signal.h を参照するはずですが、プロジェクト内に同名のファイルが見つかったためにそちらを優先し、 c コンパイラが c++ のヘッダファイルを処理しようとして大量のエラーが発生した、という事でした。

普通ならインクルード先のヘッダファイルが変わっていたら連鎖的に再コンパイルされるはずですが、ファイル名が同じで参照先が変わるだけ、というのは盲点だったのかもしれません。とにかく、再コンパイルが行われずに中途半端に過去の結果が使われてしたために変な挙動になっていました。

そういえば別々の cpp ファイルに同名のローカルクラスを作って挙動がおかしくなった、という事故が以前ありました。

例えば a.cpp 内で MyClass というクラスを作り、b.cpp 内でも MyClass というクラスを作ります。MyClass はヘッダには出さないので、a.cpp の MyClass は a.cpp 内からしか見えないし、b.cpp 内の MyClass は b.cpp 内からしか見えないはずです。

ところが MyClass のメソッドを呼ぼうとしたとき、a.cpp の MyClass のメソッドを呼んだつもりが、b.cpp の MyClass のメソッドが呼ばれて、挙動がおかしくなったという事がありました。メンバ変数は a.cpp で定義した通りなのに、呼ばれたメソッドは b.cpp のものだった、というものです。

それ以来、たとえスコープ的に独立したものであっても同名のクラス名は付けないように気を付けていたのですが…。変な挙動をしたときは、同名のファイルやクラス、メソッドがないかどうか確認した方がよさそうです。

ところで、原因はともかく、今回の事故はこまめにクリーンビルドしていれば気づけたものでした。そうすれば、少なくとも何日も経過してからビルド出来ないことに気づくという事はなかったわけです。


結論:
「1日1回クリーンビルドしよう」

(あと、バージョン管理ツールは、こういう時にすごく役立つ)

以上です。

\いいねで応援!/

有料支援者特典のエロドットビュワー裏話

プログラム話です。

先日公開した支援者様向けのヴィータ大脱出エロドットビュワーですが、ただ敵をON/OFFするスイッチを付けただけのものなので、そんなに手間はかからないだろうと思ってました。
……しかしそんなことはありませんでした(泣)。

まず、素直に敵だけ非表示にできるように作ったのですが、そうすると白濁液とかヨダレとか白い息のエフェクトが何もない空間から出てくるんですよ。


文面では伝わりにくいかもしれませんが、実際に画面で見たときは衝撃的でした……。


ヴィータ大脱出ではキャラクターの表示・非表示を切り替えるといった動作が皆無だったので、エフェクトを生成するときに、生成元となるキャラクターが表示状態なのか、非表示状態なのかをチェックする機構というのはついていませんでした。

そんなわけで、「エフェクトを生成するときにその生成元のキャラクターの状態をチェックし、非表示状態であればエフェクトを生成しない」というコードを追加しました。

ただ、それを一律に適用してしまうと、パーティクルをまき散らすための隠しキャラクター(ジェネレーターとかエミッターとか呼ばれる)がパーティクルを一切生成しなくなってしまいますので、アルバムモードかつCi-en限定版でのみ、このような条件でエフェクト生成するようにしたわけです。

本当ならば敵をOFFにしたときに、敵から生成されたヨダレ、地面にヨダレが落ちた跡、跳ね返りの液体もすべて一括でOFFにできればスマートでよかったのですが、意外と大変なのでボツにしました。

それをやるためには、何が何を生成したのかという関係を追跡して管理する仕組みが必要ですが、そういった仕組みはヴィータ大脱出では不要だったので作っていなかったのです。そもそも、そういう仕組みを作ること自体が意外と大変です。

例えば、敵がヨダレをたらし、そのヨダレが地面に衝突して涎の跡(以下水たまり)を生成すると同時に、付近に飛び散る液体(以下水しぶき)を生成したとします。生成関係の追跡を素直に実装すると、各オブジェクトがそれぞれ自分が誰から生まれたのかという情報を持つようにして、ヨダレは敵から、水たまりはヨダレから、水しぶきは水たまりから、ということが後から調べられるように作ると思います。

これを逆に辿っていくと、水しぶきは元々敵から由来したものだから、敵の表示をOFFにしたら水しぶきも消えるべきだとわかるわけです。

これで問題なさそうですが、実際にはそうはいきません。水しぶきが上がったときにはヨダレはすでに消えているため、水しぶき→水たまり→ヨダレまでは辿ることができますが、この時点でヨダレはすでに存在しないため、ヨダレがどこから来たのかという情報が消えているからです。

となれば、こういった情報は各オブジェクトに持たせるのではなく、外部の管理者に任せるようにすればよいという事になります。しかし、それはこれまでに作成したオブジェクトの履歴をずっと持っているという意味になるので、実行時間が長くなるほど管理するべき履歴の量が増え、負担が大きくなってしまいます。

だったらどこか適当なところで古い履歴を削除して……などとやっていると、こういった仕組みをまじめに作るのは結構大変だという事に気づきます。そんな面倒なことしなくても、空中からヨダレが出ないようにするだけで十分ですしね。

それよりも大変だったのは、アルバムモードでの非同期ロードが原因と思われるエラーです。

今回のアルバムモードでは起動時にすべてのアルバムが強制的に解放されるので、アルバムのロードが40個ぐらい同時進行で進んでいます。これは初めから意図していた動作ですので、当然スレッドセーフになるように作っていたのですが、どうやら穴があったようで、起動直後にアルバムを高速スクロールしていると落ちる時があるんです。

さらにデバッグモードでは落ちずにリリースモードの場合だけ落ちるといういやらしさです。しかも検証用のコードを入れたとたんに落ちなくなるという……。プログラムあるある的な話ですね。

直接の不正落ちの原因は、配列の範囲外のインデックスを参照してNULLが返った事によるNULLポインタ参照エラー(ぬるぽ、懐かしいですね)なのですが、そこに配置してあるインデックス確認用の assert には一度もひっかったことがないんですよね。デバッグモードで assert が有効な時は不正なインデックスが入って来ず、リリースモードで assert が消えた瞬間に不正なインデックスが入ってくるという……。

メモリ関係のエラーはちょっとコードを書き換えただけで挙動が変化してしまうので、本当にやりにくいです。結局今回は NULL チェックを追加して参照を回避しただけで、そもそもなぜ範囲外のインデックスが来たのかという根本的解決にはならず、でした。

https://twitter.com/helio_dor/status/1064442433004482560
https://twitter.com/helio_dor/status/1064442686541721600

この辺のツイートは大体そんな感じです。ヤケクソ感漂いますね。

\いいねで応援!/

記事を検索