ゲーム開発の備忘録

趣味のゲーム開発でのノウハウや、技術的に嵌ったポイントを忘れないように書き記しておくブログです。

MobiVMでIPAビルド時にException reference used other than as the first statement of an exception handlerが発生した際の対処法

はじめに

今回も短い記事ですが、エラー内容から解決方法に行きつくことが難しい事象だったため、備忘のために残しておくことにします。

XcodeをiOS26に対応したバージョンに更新後、Android Studio PandaでMobiVMを利用したプロジェクトのIPA生成を試したところ、Exception reference used other than as the first statement of an exception handlerというエラーが発生し、ビルドが通らなくなってしまいました。

対処法

プロジェクト内でHttpClient 5.xを利用していたのですが、HttpClient 4.5.xにバージョンを落としたところ、ビルドが通るようになりました。

どうやらHttpClient 5.xはJava 11を前提としているのですが、MobiVMはJava 8を前提としているため、そのバージョン差が悪さをしているようでした。今まで通っていたのが逆に奇跡的だったのかもしれません。

Kotlinコードを含むAndroidアプリ起動時にDidn't find classが発生した時の対処法

はじめに

今回も短い記事ですが、AIとやり取りしながらでも原因究明にそれなりに時間がかかったので備忘として残しておきます。

突如として、Kotlinコードを含むAndroidアプリ起動時にjava.lang.ClassNotFoundException: Didn't find class~が発生するようになってしまいました。

対処法

build.gradleでsourceSetsを指定している場合、kotlin.srcDirsの記述を追加しましょう。

以前は、java.srcDirsの指定のみしておけばKotlinコードにもパスが通っていたのですが、Android Studioのバージョンアップを重ねる中で、Android Studio付属のkotlinプラグインが廃止され、標準のkotlinプラグインに切り替えられたタイミングからパスが通らなくなっていたようです。

設定例を以下に示します。

sourceSets {
 "main" {
  manifest.srcFile 'AndroidManifest.xml'
  java.srcDirs = ['src']
  kotlin.srcDirs = ['src']
  aidl.srcDirs = ['src']
  renderscript.srcDirs = ['src']
  res.srcDirs = ['res']
  assets.srcDirs = ['../assets']
  jniLibs.srcDirs = ['libs']
 }
}

MobiVMでIPAファイル生成時にresource fork finder information or similar detritus not allowedが発生する場合の対処法

はじめに

今回はかなり短い記事ですが、対処法を見つけるまでに結構時間がかかったので、備忘として残しておきます。

MobiVMでIPAファイルを生成する際に、resource fork finder information or similar detritus not allowedというエラーが発生して失敗する場合があります。このエラーメッセージで調べると、xattr -cで、プロジェクトで使用するすべてのファイルの拡張ファイル属性をクリアするように促す記事が多くヒットしますが、私の場合はそれでは直りませんでした。

対処法

IPAファイルの出力先をデスクトップではなく、ユーザディレクトリ直下などに変更しましょう。

理由は不明ですが、デスクトップにIPAファイルを出力しようとすると前述のエラーが発生します。以前はデスクトップでも問題なかったのですが、OSの更新上限に達したMacを買い替えて新しいMacで試したところ、このエラーに直面しました。どこかのOSバージョンから影響を受けるようになったと思われます。もしかしたら、デスクトップにはiCloudの同期が働いているので、それが悪さをしているのかもしれません。

対処法はシンプルですが、エラーメッセージからの対処法の推測が一切できないのでなかなか厄介なエラーでした。

GeometryRealizationを用いてDirectWriteでの縁付き文字列の描画を高速化する

はじめに

以前、以下の記事で、DirectWriteを用いて縁付き文字列を描画する方法について紹介しました。
deep-verdure.hatenablog.com

一度に画面に表示する文字の量が少なければ上記の方法で問題ないのですが、文字の量が多くなるとかなり処理が重くなります。そこで、今回は縁付き文字列の描画を高速化する方法を見つけましたので、その方法を紹介したいと思います。

GeometryRealizationとは

ID2D1DeviceContext#DrawGeometry()や、ID2D1DeviceContext#FillGeometry()を呼ぶ度にエイリアス化/アンチエイリアス化されたプリミティブ情報がテセレーションにより作成され、画面に描画されます。この描画は軽くはなく、文字列の量が多くなると性能劣化に繋がります。この問題を解決するために用意されたDirect2D の機能がGeometryRealizationです。

GeometryRealizationは、テセレーションにより作成されたエイリアス化/アンチエイリアス化されたプリミティブ情報をキャッシュする仕組みで、一度GeometryRealizationを作成しておけば、以降は作成したGeometryRealizationを用いて描画することでプリミティブ情報の作成をスキップでき、大幅に処理が軽くなります。なお、GeometryRealizationは通常のビットマップキャッシュと異なり、ジオメトリを特徴付ける点集合としてキャッシュされるため、ビットマップキャッシュよりもメモリ使用率が低く抑えられています。※ペイントソフトに対するドローソフトと同様の考え方です。

GeometryRealizationは描画を高速化しますが、その代わりにキャッシュの作成に時間がかかります。そのため、毎フレームごとに描画内容が変更され、キャッシュを作成する必要がある場合は、高速化の恩恵を受けられないどころか逆に低速になります。このような場合はID2D1DeviceContext#DrawGeometry()や、ID2D1DeviceContext#FillGeometry()で描画しましょう。

GeometryRealizationを用いた描画の流れは以下のようになります。

1. ID2D1PathGeometryインスタンス、ID2D1GeometrySinkインスタンスを作成する。
2. ID2D1GeometryRealizationインスタンスを作成する。
3. GeometryRealizationを用いて描画する。

手順1については、GeometryRealizationを使用しない場合と手順は同じです。本記事冒頭に掲載した、以前の記事を参考にしてください。手順2以降について、以下で解説していきます。

ID2D1GeometryRealizationインスタンスの作成

ID2D1DeviceContext1インスタンスのメソッドで、GeometryRealizationのキャッシュを示すID2D1GeometryRealizationインスタンスを作成します。ID2D1DeviceContext1インスタンスID2D1DeviceContext#As()メソッドで変換して準備しましょう。

ID2D1DeviceContext1#CreateFilledGeometryRealization()メソッドで、塗り潰しのためのID2D1GeometryRealizationインスタンスを作成できます。第1引数にはGeometryRealizationの元となるID2D1PathGeometryインスタンスを渡し、第2引数には平坦化許容値を、第3引数にはID2D1GeometryRealizationインスタンスの格納先となるポインタのポインタを指定します。

ここで、平坦化許容値とは、ジオメトリの真の曲線と、GeometryRealization生成の過程でDirect2Dが実行する多角形近似の結果との間で最大でどの程度のずれまでを許容するかを示す値であり、DIP値で指定します。平坦化許容値が低いほど、GeometryRealizationは元のジオメトリに近くなりますが、トレードオフとして描画コストが高くなります。平坦化許容値はD2D1::ComputeFlatteningTolerance()関数で計算できます。第1引数には追加のスケーリング変換を行う行列を渡します。通常は、D2D1::Matrix3x2F::Identity()を渡しておけば問題ありません。第2,3引数には画面解像度を渡します。第4引数にはmaxZoomFactorを指定します。これはGeometryRealizationを拡大描画する場合に指定します。どの程度まで拡大する可能性があるかを指定してあげることで、それに合わせた最適な平坦化許容値を算出してくれます。拡大をしない場合、デフォルト値の1.0fで問題ありません。

以下に、ID2D1GeometryRealizationインスタンスの作成コードの一部を掲載します。

const auto flatteningTolerance = D2D1::ComputeFlatteningTolerance(D2D1::Matrix3x2F::Identity(), static_cast<FLOAT>(screenSize.first), static_cast<FLOAT>(screenSize.second), maxZoomFactor);

ComPtr<ID2D1GeometryRealization> filledGeometryRealization = nullptr;
if (FAILED(d2dDeviceContext->CreateFilledGeometryRealization(pathGeometry.Get(), flatteningTolerance, &filledGeometryRealization))) [[unlikely]] {
 return;
}

なお、ID2D1GeometryRealizationインスタンスを作成できるメソッドはもう一つあります。それがID2D1DeviceContext1#CreateStrokedGeometryRealization()メソッドであり、その名の通りストローク部分のGeometryRealizationを作成できます。しかし、このメソッドは非常に重いため、使用できるケースは限られるでしょう。文字列の場合、数文字分作成するだけで数秒~数十秒ほどフリーズする代物であり、マルチスレッドで裏で頑張って作成しておいてもらうなどの工夫が必要です。私は文字列のストロークだけはID2D1DeviceContext#DrawGeometry()で普通に描画することを選びました。文字列の内部をGeometryRealizationで埋めるだけでも高速化の恩恵を十分受けることができますし、数百文字分のストロークのGeometryRealizationを作成すると5分程度かかるため、マルチスレッドを使ったとしても実用が厳しかったためです。

GeometryRealizationを用いた描画

ID2D1GeometryRealizationインスタンスを作成したら、ID2D1DeviceContext1#DrawGeometryRealization()メソッドで、GeometryRealizationを用いた描画を行えます。第1引数には描画元となるID2D1GeometryRealizationインスタンスを渡します。第2引数には色を塗るためのID2D1Brushインスタンスを渡します。この通り、描画は非常に簡単に行えます。

d2dDeviceContext->DrawGeometryRealization(textPathGeometryRealization.Get(), solidColorBrush.Get());

おわりに

以上でGeometryRealizationの解説を終わります。そこまで複雑な手順を踏まなくてもジオメトリ描画を高速化できるので、ジオメトリ描画が遅くて悩んでいる方は是非試してみて下さい!

Google Play Games Servicesサインインの実装 (2025/04/15時点)

はじめに

2025/4/15現在、play-services-games(以下、v1と表記)はdeprecatedになっており、代わりに新しいバージョンであるplay-services-games-v2(以下、v2と表記)の使用が推奨されています。
※2025/2~3頃からdeprecatedになったと思います。

そこで、本記事ではv2を用いてGoogle Play Games Servicesサインインを実装する方法を紹介します。
なお、環境構築部分は以前と変わらないため、以下の記事をもとに事前に実施してください。
※build.gradleの内容は本記事で書き換えます。
deep-verdure.hatenablog.com

build.gradleへの記述

build.gradleに以下を追記します。

dependencies {
 implementation 'com.google.android.gms:play-services-games-v2:20.1.2'
}

以下が残っている場合、不要なため削除します。

dependencies {
 implementation 'com.google.android.gms:play-services-auth:21.3.0'
 implementation 'com.google.android.gms:play-services-games:23.2.0'
 implementation 'com.google.apis:google-api-services-drive:v3-rev110-1.23.0'
}

サインイン処理の流れ

過去にサインインしたことがあるアカウント情報を用いて自動的にサインインを実行するサイレントサインインGoogleが作成した専用のダイアログを用いて、ユーザが手動でアカウントを選択し、サインインを実施するインタラクティブサインインの順でサインインが試みられるのは以前と同じです。インタラクティブサインインに失敗した場合、サインイン処理全体として失敗したことになります。

ただし、この流れはv2では直感的に、簡潔に書けるようになっています。サインインを表す最も外側の窓口メソッドは以下の記述で問題ないでしょう。

fun signIn() {
 if (!isGoogleApiAvailable()) {
  return
 }
 silentSignIn()
}

サインインクライアントの生成

v2では、GamesSignInClientクラスがサインインクライアントとなります。以前はサインインオプションを構築する必要がありましたが、v2ではPlayGames.getGamesSignInClient()でいきなりサインインクライアントを取得できます。一行で書けるのでイニシャライザに書く必要はなく、クラスのフィールドで初期化してしまって問題ありません。

private val signInClient = PlayGames.getGamesSignInClient(activity)

サイレントサインインの実装

サイレントサインインの実装メソッドを以下に示します。

private fun silentSignIn() {
 signInClient.isAuthenticated().addOnCompleteListener(activity) { task ->
  val isAuthenticated = task.isSuccessful && task.result.isAuthenticated
  if (isAuthenticated) {
   //サイレントサインインに成功
   retrievePlayerInformation()
  } else {
   //インタラクティブサインインが必要
   interactiveSignIn()
  }
 }
}

GamesSignInClient#isAuthenticated()でサイレントサインインを実行できます。Tasks APIが利用されているところはv1と同様です。なお、v2ではサインインアカウントの概念が存在せず、v1の時に必要だったサインインアカウントのnullチェックが不要になっています。その分少しだけ記述が簡易になっています。

Tasks APIについては以下を参照ください。
developers.google.com

インタラクティブサインインの実装

インタラクティブサインインの実装メソッドを以下に示します。

private fun interactiveSignIn() {
 signInClient.signIn().addOnCompleteListener { task ->
  val isAuthenticated = task.isSuccessful && task.result.isAuthenticated
  if (isAuthenticated) {
   //インタラクティブサインインに成功
   retrievePlayerInformation()
  } else {
   //インタラクティブサインインに失敗
   //サインイン全体として失敗時のエラー処理を記述
  }
 }
}

GamesSignInClient#signIn()インタラクティブサインインを実行できます。v1ではインテントを利用していたので、インテントの処理のためのコードも記述していましたが、v2では上記のみでインタラクティブサインインを実行できます。REQUEST_CODE_SIGN_INのキャッチは不要なので、v1の時のコードが残っている方は削除してしまいましょう。

サインイン処理成功後の処理

サインイン処理成功後のプレイヤーの取得や、スナップショットの取得といった処理は、PlayGames.getPlayersClient()等の、PlayGames.getXXXClient()という形式のメソッドを利用して行えます。v1利用時のコードが残っている場合、前述のメソッドに差し替えるだけで他のコードはそのまま流用できます。なお、メソッド呼び出し時にサインインアカウントの受渡しは不要で、コンテキストを渡すだけで問題ありません。

例えば、プレイヤーの取得は以下のコードを流用できますので参考にしてください。
https://deep-verdure.hatenablog.com/entry/2018/08/30/012328?_gl=1*1r1xaxs*_gcl_au*MTE2Mzc4OTkwOS4xNzQyMjIwNDcy#%E3%82%B5%E3%82%A4%E3%83%B3%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86%E6%88%90%E5%8A%9F%E5%BE%8C%E3%81%AE%E5%87%A6%E7%90%86

サインアウト処理

v2では明示的なサインアウト処理の記述は不要になりました。裏でよしなに処理しておいてくれます。

終わりに

お疲れ様でした!v1の頃と比較して直感的に、楽に記述できることがお分かりになったかと思います。実装のハードルは確実に下がっていますので、まだGoogle Play Games Services連携を利用していない方もこれを機に是非挑戦してみましょう!

ボーダーレスウィンドウ表示形式を実装する

はじめに

 今回は、ボーダーレスウィンドウ表示形式を実装する方法を紹介します。目的はとてもシンプルですが、意外と解説記事が無く試行錯誤での実装となりました。

 ボーダーレスウィンドウ表示形式がどのようなものかの説明は割愛しますが、最近のPCゲームではほぼほぼ搭載されているメジャーな画面表示形式かと思います。

実装方針

 今回はWindows APIのみで実装しています。最初はDirectX12(DXGI)のスワップチェーンやビューポートの設定を色々弄って実装することを考えていましたが、試した結果不可能なことが分かりました。なお、Windows APIそのものの知識はお持ちであることを前提にして話を進めますので、あまりよく分からないという読者の方は先に本やWebサイトで確認してみて下さい。

 Windowsゲームでは1つのゲームにつき1つのウィンドウという固定観念を持ちがちですが、1つのアプリケーションで複数ウィンドウを表示させることが可能です。そこで、ゲーム画面を表示するウィンドウの裏側に画面全体を覆う単色のウィンドウをもう1つ用意することで、ボーダーレスウィンドウ表示形式の余白部分を表現することにしました。基本はこれだけですが、色々と厄介な部分があります……
 それについてはおいおい触れていきます。

 では、早速2つのウィンドウの設定内容について見ていきましょう。

メインウィンドウ

 ゲーム画面を表示するウィンドウです。本記事ではメインウィンドウと表記します。こちらは従来用意していたウィンドウの設定をほぼそのまま使います。ボーダーレスウィンドウ表示形式では枠とタイトルバーを消すことになりますので、ウィンドウスタイルにはWS_POPUPのみを指定します。

 なお、ボーダーレスウィンドウ表示形式では、ウィンドウの幅や高さのどちらかはディスプレイの幅または高さに合わせることが多いでしょう。そのような場合はGetSystemMetrics(SM_CXSCREEN)、GetSystemMetrics(SM_CYSCREEN)でディスプレイの幅や高さを取得して、そこから計算すれば良いです。

背景ウィンドウ

 黒一色などの単色で塗りつぶし、ディスプレイいっぱいに描画することでボーダーレスウィンドウ表示形式の余白部分を表現します。本記事では背景ウィンドウと表記します。背景ウィンドウのウィンドウスタイルもWS_POPUPのみの指定で良く、ウィンドウの幅と高さはGetSystemMetrics(SM_CXSCREEN)、GetSystemMetrics(SM_CYSCREEN)をそれぞれ指定すれば問題ありません。しかし、ここからが厄介なポイントです。

 背景の塗りつぶしはウィンドウクラス登録時に以下のような記述で簡単に実現できます。

window.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));

 しかし、背景色の設定が上手く反映されない場合があります。そのような場合は、ウィンドウプロシージャでWM_ERASEBKGNDメッセージを揉み消していないかどうかを確認してください。メインウィンドウではWM_ERASEBKGNDメッセージを揉み消すことで背景のちらつきを抑えるというテクニックが使えるのですが、Windows10以降でブラシで背景色設定時にWM_ERASEBKGNDメッセージを揉み消すと背景設定が適用されないという謎の不具合があります。そのため、メインウィンドウのウィンドウプロシージャを流用せずに、WM_ERASEBKGNDメッセージをDefWindowProc()に渡すウィンドウプロシージャを用意して対処しましょう。これはWindows APIを内部で利用しているフレームワークゲームエンジン等でもGitHubのissueで報告されている不具合です……

 また、背景ウィンドウはメインウィンドウの親・子ウィンドウである必要はなく、単にもう一つウィンドウを用意すれば問題ありません。ただし、背景ウィンドウに操作のフォーカスを奪われるのは問題ですので、毎フレームSetForegroundWindow()関数を呼び出して、メインウィンドウをフォアグラウンドにするようにしましょう。また、常に背景ウィンドウがメインウィンドウの裏側に表示されるようにしたいので、SetWindowPos()関数の第2引数にHWND_BOTTOMを指定して実行しておきましょう。

タスクバーを隠す

 ボーダーレスウィンドウ表示ではタスクバーを隠さないと、タスクバーがウィンドウの上に表示されて不自然な見た目になってしまいます。タスクバーは内部的にはウィンドウとして扱われていますので、FindWindow()関数でウィンドウハンドルを取得し、ShowWindow()関数でSW_HIDEを指定すれば良いです。タスクバーのウィンドウ名はShell_TrayWndで固定です。以下にサンプルコードを示します。

HWND taskBarhWnd = FindWindow(L"Shell_TrayWnd", nullptr);
ShowWindow(taskBarhWnd, SW_HIDE);

 ゲーム終了時にタスクバーを再表示させないと、ゲームが終わってもタスクバーが表示されないままになってしまいますので注意しましょう。再表示の際はShowWindow()関数でSW_RESTOREを指定します。

ゲーム中でウィンドウ表示形式を変更する

 ゲーム中でボーダーレスウィンドウ表示形式と他の表示形式を切り替える場合は、背景ウィンドウの表示・非表示、タスクバーの表示・非表示を切り替えることで実現します。

おわりに

 お疲れ様でした。正直、やり方さえわかれば、ボーダーレスウィンドウ表示形式そのものの実装よりも、ウィンドウサイズや表示形式の切替に関する部分の設計・実装のほうが大変かとは思いますが、そちらは純粋な設計力さえあれば対応できると思います。※特にDirectX11on12でDirectWriteを利用している環境だと大変難しい……
 とはいえ、Windows APIの知識だけでモダンに見えるボーダーレスウィンドウ表示形式を実現できるのは大変魅力的かと思いますので、是非トライしてみて下さい!

D3D11On12利用環境でフルスクリーン/ウィンドウ切替を実行できるようにする

はじめに

 DirectX12でフルスクリーンとウィンドウを切り替える方法については、書籍やWeb上でも情報があります。基本的にはDirectX11の時にも使用していたDXGI側の関数を呼んで切り替えればよいのですが、DirectX12の場合、解像度に依存するリソースの事前解放および再生成が必要になります。具体的には、バックバッファと深度ステンシルバッファの再生成が必要になります。これを実施しないと、IDXGISwapChain4#ResizeBuffers()関数実行時にエラーになります。

 しかし、D3D11On12を利用している場合、上記の対処ではエラーを解消できません。以下のstackoverflowでも議論されていますが、最終的にMicrosoftに不具合報告をするという結論で終わってしまっています。
stackoverflow.com

 本記事では、D3D11On12利用時のフルスクリーン/ウィンドウ切替について、最終的に私がどう対処したかをまとめておきます。

バイスロスト対応

 結論から言うと、D3D11On12を利用している場合は、IDXGISwapChain4#ResizeBuffers()関数実行前にデバイスロスト発生時と同じ対処を行わなければなりません。即ち、ID3D12Deviceまで削除し、また再生成する必要があります。事前生成したデバイスやリソースをどこまで削除したらエラーが発生しなくなるのかステップバイステップで検証しましたが、一度全てをまっさらにしないとエラーが発生してしまうという結果になりました。

 余談ですが、デバイスロストはDirectX9時代では対処必須なほどよく発生していましたが、DirectX11、DirectX12では発生率が低くなっています。しかし、それでも発生する可能性はゼロではないため、フルスクリーン/ウィンドウ切替への対応と同時に対処してしまいましょう。
learn.microsoft.com

リソース再生成の手段について

 デバイスロスト対応を行うにあたり、問題になるのがグラフィックス関連リソースをどのように再生成するかです。ゲーム起動直後に一括して生成しているリソースは対処が簡単で、同様に一括生成すればよいだけです。PCゲームはそこまでメモリに厳しくないこともあり、ほとんどのリソースを一括生成しているのではないでしょうか。私の場合、以下のリソースは一括生成しています。

  • シェーダ
  • パイプラインステート
  • テクスチャ
  • フォントデータ
  • ブラシ
  • テキストフォーマット

上記に該当するリソースは、デバイスロストからの復帰時に生成用の関数を呼ぶだけで済ませています。

 さて、問題はゲーム起動時に生成するわけにはいかないリソースです。具体的には以下が挙げられます。

  • コンスタントバッファ
  • テキストレイアウト
  • テキストパスジオメトリ

後者2つは場合によっては一括生成できるかもしれませんが、ゲーム内で文字列を動的に生成する場合は事前に生成することはできませんよね。これら3つはオンデマンドで生成できるような仕組みを作っておく必要があります。私の場合は全てmapに格納していますので、描画命令実行時にmapから要素が無くなっていることを検知したら、各エフェクト・描画物を表すクラスのメンバ関数として用意していた生成用の関数を、すぐに呼び出すようにしました。このため、各エフェクト・描画物のインスタンス生成時に確保し、以降は値を更新しないようなコンスタントバッファ(例えば、値が固定された色情報など)についても、コンスタントバッファが消失したことをチェックできるようにするために、毎描画時に初期値で値の更新をかけるようにしています。