tanaka's Programming Memo

プログラミングについてのメモ。

【Unity6.x】WebGLのアプリをスマホでタッチ操作するためのInputSystemの設定方法

この記事は、Unity - Qiita Advent Calendar 2025 - Qiitaのシリーズ2の4日目の記事です。

11月に開催された技術書典19で、パズルゲームのステージの作り方を解説する趣味のゲーム開発:レベルデザイン編 パズルゲームのステージを作ろう!:アミューズワンという同人誌を出版しました。

techbookfest.org

この本の中で作成したステージを、実際に遊べるようにWebGLにビルドして、こちら(https://am1tanaka.github.io/VBirdHiyokoStageBook/digiga2025/index.html)で公開しています。

Unityは、WebGLビルドに力を入れていて、PCは当然のこと、スマホのWebブラウザーでも結構動きます。これで、スマホでも手軽に遊んでもらえると思っていたのですが、公開してから、タッチ操作に対応していないことに気づきました。本記事は、その対応方法をまとめたものです。

目次

動作環境

動作環境は、次のとおりです。

  • Unity 6.0.58f2
  • Input System 1.14.2
    • InputActionにアクションを定義して利用

動かなかった原因

WebGLビルドしたものが、スマホのWebブラウザー上でも動くようにできるだけ頑張ってはいますが、公式にはサポートされていません。ということで、油断してました。動作していない状態のInputActionは、次のとおりでした。

修正前のInputAction

ClickとPointとTapの3種類のアクションを定義しています。ClickとPointは、マウスのクリックとカーソル座標を取得するPC用の設定です。Tapが、Touchscreenを最初にタッチしたPrimary Touchを取得するスマホ用の設定です。

ここで問題となるのが、スマホのWebブラウザーでは、TouchscreenのPositionは取得できるのですが、タッチ操作に対応していないようなのです。あれこれ調べてみると、Pointerで対応できるということでした。ということで、その方向で対応させます。

アクションごとの設定

アクションごとのAction TypeやInitial State Checkの設定は、次のとおりです。

  • Click
    • Action Type: Pass Through
    • Initial State Check: false
  • Point
    • Action Type: Pass Through
    • Initial State Check: true
  • Tap
    • Action Type: Pass Through
    • Initial State Check: false

Pass Throughってなんだっけという場合に備え(そうなった)、こちらの記事にしました。

Clickは、マウスがクリックされるか、WebGLの画面がタップされた時のperformedを使っています。startedやcanceledは使いません。また、マウスと画面タップの違いは気にしないので、最後に操作された入力が得られるPassThroughにしました。開始時に操作されているものは無視したいので、Initial State Checkは無効にしています。

Pointは、マウスの座標の読み取りのためのアクションです。マウスの座標は、デフォルト値が操作に影響せず、startedとcanceledの意味がありません。そのため、処理が軽いPassThroughを選択しました。Initial State Checkを有効にしているのは、開始時のマウス座標を報告してもらうためです。これにより、シーンが切り替わった直後のマウスの座標が取得できるので、最初からマウスカーソルを表示できます。

Tapの設定は、タッチスクリーンのみバインドしています。startedとcanceledは使っていないので、処理が軽いPassThroughにしました。有効になった時点でタッチしていても無効にしたいので、Initial State Checkは無効にしました。

解決策

あれこれ調査した結果、次の2手で解決できました。

  1. InputActionのClickに、PointerのPressを追加する
  2. 座標は、クリックやプレスを検出したときに、PointerのPositionを取得する

修正したInputAction

InputActionに、次のようにPointerのPressを追加しました。

修正後のInputAction

これで、タッチに反応するようにはなります。しかし、タップ座標がズレるという問題が起きます。座標の取得方法の改良が必要です。

タップ座標を取得する

座標の取得は、マウスとタッチで性質が異なります。マウスの場合、クリックをしていなくても、マウスカーソルの位置をPointアクションで受け取れます。その値を記録しておけば、クリックした座標として使えます。

ところが、Pointerの場合は、pressされるまで座標は分かりません。Pointアクションにpositionをバインドしていても、Clickアクションの時点では、その座標はまだ受け取れていません。そのため、マウスと同じ実装だと、前回にタッチした座標をタッチしたことになってしまいます。

これを解決するために、クリック時に、Pointerデバイスの座標を読み取って、現在の座標として使うことにしました。Clickアクションに登録するメソッドは、次のような感じです。

        void OnClickPerformed(InputAction.CallbackContext context)
        {
            if (context.ReadValueAsButton())
            {
                // 座標を更新
                currentPoint = Pointer.current.position.ReadValue();

                // クリック処理のユーザーメソッドの呼び出し
                Action(currentPoint);
            }
        }

まず、context.ReadValueAsButton()で、クリックした時であることを確認しています。ClickアクションはPass Throughなので、離した時にもperformedが呼び出されます。このif文で、離した処理を無視します。

Pointer.current.position.ReadValue()で、現在のPointerの座標を読み取って、マウスの現在の座標を代入しているcurrentPointに代入しています。

あとは、currentPointに対して行動させるためのユーザーメソッドであるAction(currentPoint)を呼び出しています。

InputActionのPointに、Pointer用の座標アクションを追加しなかったのは、このように座標をPress時に直に読み取るので、Pointer用の座標アクションが不要だからです。

以上で、対応が完了できました。

まとめ

UnityのWebGLビルドは優秀なので、スマホのWebブラウザー上でも、かなりしっかりと動作します。ただ、InputSystemの扱いがスマホと異なるため、対応が必要でした。

スマホのWebブラウザーでは、タッチ操作をPointerとして認識するので、Pointerの操作をバインドしました。今回は、タッチした座標が必要なだけなので、Clickアクションに、PointerのPressをバインドしました。座標は、Clickアクション内で、引数から読み取るようにしました。これで、サンプルゲームを、スマホでも遊べるようになりました。

am1tanaka.github.io

この記事は、Unity - Qiita Advent Calendar 2025 - Qiitaのシリーズ2の4日目の記事でした。

【Unity】InputSystemのPass Throughってなんだっけ

Pass Throughとはなんぞや、というのをすぐに忘れるので、ざっくり整理です。情報源は、以下の公式マニュアルです。

docs.unity3d.com

目次

ActionTypeとは

ActionTypeは、主に次の2つの動作を設定するものです。

  1. アクションの値の変更に対するstarted、performed、canceledの呼び出し方
  2. 同じアクションに複数のコントローラーの操作がバインドされていた時の、入力値の選択方法

それぞれ、以下のとおりです。

Pass Through

  • 値が変更されたら、performedで通知
  • Interactionを追加しなければ、値の変更に伴うstartedやcanceledは呼び出さない
  • 同じアクションに、複数のコントローラーが割り当てられていた場合、すべてのコントローラーの変化を報告する。入力値によらず、最後に変化したコントローラーの入力が得られる
  • ボタンタイプの入力を割り当てた場合、値が変化するたびにperformedが呼ばれるので、performed内でReadValueAsButton()を使うと、ボタンを押したか、離したかを判定できる

Value

  • 入力値が、デフォルト値から変化すると、startedが呼び出される
  • startedの呼び出し後、同じフレームですぐにperformedが呼び出される
  • その後、デフォルト値になるまで、値の変化ごとにperformedが呼び出される
  • 値がデフォルト値になると、canceledが呼び出される
  • Initial state checkを有効にすると、操作が有効になった瞬間に、入力がデフォルト値かどうかを判定して、デフォルト値でなければ、startedとperformedを呼び出す
    • シーンの切り替えなどの直後に、前のシーンの入力が残っていた場合に、その入力を有効にしたいならチェックする
  • 1つのアクションに、複数のコントローラーの入力が割り当てられていると、そのうちの入力値が一番大きいもののみが通知される

Button

  • 入力値が、規定の値以上になったら、startedが呼び出されて、同じフレームですぐにperformedも呼び出される
  • 入力値が、規定の値未満になったら、canceledが呼び出される
  • その他の機能は、Valueとほぼ同じ
  • 1つのアクションに、複数のコントローラーの入力が割り当てられていると、そのうちの入力値が一番大きいものを通知する
  • あるアクションにデジタル入力のコントローラーを複数バインドした場合、先に押された操作が離されない限り、他の入力が押されても無視される

まとめ

  • Pass Through
    • 最後に操作されたコントローラーの入力を取得したい場合
    • startedやcanceledが不要な場合
    • アクションゲームの操作など、素早いレスポンスや、最新の変化を反映したい場合に向いている
    • マウスの座標などのデフォルト値に戻らない入力
  • Value
    • デフォルト値があるコントローラーの移動操作など向き
    • started、canceledが使いたい場合
    • デフォルト値からの違いが一番大きいコントローラーの入力のみを取得したい場合
  • Button
    • クリックやタッチ、ボタンなどを取得したい場合
    • 先に押しっぱなしにした操作があったら、他の操作は無効にしたい場合

基本的には、Unityがデフォルトで生成するInputSystem_Actionsを参考にするのがよいです。

Unity6.xでプロジェクトを作成すると、自動的にデフォルトのInputActionが生成される

移動操作はVector。AttackやJumpはButton。UIのマウス座標などは、Pass Throughに設定されています。

移動操作は、接続しっぱなしのゲームコントローラーのアナログ入力が微妙に残る場合があります。そうすると、キー操作をしていて、キーを離しても、ゲームコントローラーの入力の影響で、微妙にキャラが移動することがあります。そのような場合は、Pass Throughの方が便利な場合があります。

参考URL

FireAlpacaでCMYK形式のPSDファイルをエクスポートする

2025年の春に開催された技術書典18では、ありがたいことに紙の本が完売しました。多少の在庫は手元に置こうと思い、はじめて、後から印刷を利用しました。

利用している印刷所は、技術書典のバックアップ印刷所である日光企画さんです。オンデマンド印刷では、表紙をRGB入稿できるので、これまではテンプレートのPSDファイルをRGB化したものを、GIMPなどで編集して、入稿データを作成していました。ところが、後から印刷は、入稿形式がCMYKです。RGBで入稿しても、CMYKに変換してくれるようなのですが、仕上がりが分からないので不安があります。そこで、CMYKのPSDデータの作成方法を調べて、挑戦してみました。その手順です。

目次

FireAlpacaで、CMYKの画像を編集する

フリーのペイントツールのFireAlpacaを使えば、CMYKのPSDファイルが出力できることが分かりました。同人誌印刷所のPICOさんのこちらの記事が分かりやすかったです。流れは、次のとおりです。

  • FireAlpacaをダウンロードして、インストールして、起動する
  • 日光企画さんなどでダウンロードしたテンプレートを、FireAlpacaで開く

この段階では、CMYKが適用されていません。設定をします。

テンプレートを開いた状態

表示メニューから、カラーマネジメント設定を開く

  • RGBプロファイルをsRGB IEC61966-2.1、CMYKプロファイルをJapan Color 2001 Coatedに設定する

CMYKの設定

  • 「有効にする」と、「CMYKソフトグループ」が、チェックできるようになるので、チェックする
  • OKをクリックする

以上で、CMYKの色味が、モニターで確認できるようになります。

CMYK設定適用

この状態で、色を調整します。

CMYK形式のPSDファイルとして出力

表紙ができたら、CMYK形式のPSDファイルで出力します。

  • 入稿に不要なレイヤーがあれば、削除する
  • ファイルメニューから、書き出し(CMYK形式PSD)...を選択する

書き出し

  • 任意の場所に、任意のファイル名で保存する

以上で、無事に印刷できました。

参考・関連URL

macでdocker-composeが使えなくなった時にやったこと

気づいたら、macのターミナルからdocker-composeが呼び出せなくなっていました。使用しているのは、Docker Desktopです。

.zshrcを確認すると、/Users/yutanaka/.docker/completionsにコマンドラインツールが入ってそうなのですが、中身が空でした。

Geminiで調べた次のコマンドで、docker-composeが見つかりました。

find /Applications/Docker.app -name "docker-compose"

.zshrcに、次の行を加えて、解決しました。

export PATH=/Applications/Docker.app/Contents/Resources/cli-plugins:$PATH

VSCodeのRe:VIEWプラグインにpagebreakブロック構文を認識させる

技術書典18が発表されました!

techbookfest.org

次の本を書き始めるタイミングにあわせて、pagebreakコマンドがエラーにならない対策をしました。設定方法と、作業の備忘録です。

目次

まずはじめに

技術書は、kmutoさんのRe:VIEWと、TechBoosterさんのReVIEW-Templateを使って書いています。生成したPDFを日光企画さんにそのまま入稿できるので、書籍作成初心者の自分には、絶大なる助けになりました。

エディターは、VSCodeを使っています。atsushienoさんによるRe:VIEWプラグインと、vscode-language-mugeneプラグインで、プレビューや自動校正を利用しています。このあたりの環境構築については、以下の記事に書いてあります。

仕上げのページ調整をする際に、pagebreakコマンドで改ページをしています。この時に、Re:VIEWプラグインが、pagebreakをエラーにしてしまうのが気になっていました。Re:VIEWのマニュアルに載っていないコマンドなので、正規のものではないようです。

以前から模索していた対策に成功したので、共有します。正規コマンドではなく、webやtextに出力できないものなので、自分用の改良ということで、ここで方法を書いておくにとどめます。

対応手順

対策したファイルを用意しました。ダウンロードして、インストールしたプラグインに上書きコピーすれば、対応できます。VSCodeに、Re:VIEWプラグインをインストールしてあることが前提です。

  • Releases · am1tanaka/review.js-vscode · GitHubを開く
  • Assetsにあるv0.20.0-add-pagebreak.zipをダウンロードする
  • ダウンロードしたzipファイルを展開する
  • ユーザーフォルダーから、.vscode\extensions\atsushieno.language-review-0.7.5\node_modules\review.js-vscodeを開く
  • zipファイルを展開したフォルダー内のdist, lib, testフォルダーを、開いたフォルダーに上書きコピーする

以上で完了です。

//pagebreakがエラーにならず、[改ページ]と表示

注意

//pagebreakは、公式コマンドではありません。rakeでの出力では、pdfやepubには対応していますが、ページがないwebやtextなどではエラーになります。webやtextに出力する際には、reファイルから//pagebreakを削除してください。

やったこと

備忘録として、やったことを残しておきます。

Re:VIEWプラグインが、Re:VIEWコマンドの処理で利用しているyfakariyaさんのGitHub - yfakariya/review.js-vscode: Another implementation of ReVIEWに、手を加えました。ReVIEW.js for VSCodeは、vvakameさんのGitHub - vvakame/review.js: Another implementation of ReVIEWに、不具合対応したものです。さまざまな方々の恩恵に預かって、助けてもらえています。

準備

操作は、npmで実行できます。package.jsonを開くと、scriptsにコマンドが定義されています。npm runに続けて、コマンドを書けば実行できます。

ビルドとテスト

ビルドは、以下で実行できます。

npm run prebuild
npm run build
npm run postbuild

テストは、以下で実行できます。

npm run pretest
npm run test

ちなみに、手元の環境だと、テストは14個ほど失敗しています。失敗が出ても、気にしなくて大丈夫そうです。

テストを加える

テストがあるので、利用すると開発が楽です。test/fixture/validフォルダーに、テスト用のファイルを追加します。

  • test/fixture/validフォルダーに、新しいフォルダーを作成して、block_pagebreakという名前にする

他のコマンドを参考に、テスト用のデータを作ります。今回は、block_blanklineが似ているので、参考にしました。

  • block_pagebreakフォルダー内に、新しいファイルを作成して、content.reという名前にする
  • pagebreakを使ったreファイルの例として、次を入力する
= pagebreak のテスト

aaa
//pagebreak
bbb
//pagebreak
ccc
  • 改行コードを、LFにして保存する

確認用の正解ファイルを作成します。

  • 新しいファイルを作成して、content.htmlという名前にして、以下を入力する
// 1:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xmlns:ops="http://www.idpf.org/2007/ops" xml:lang="ja">
<head>
  <meta charset="UTF-8" />
  <link rel="stylesheet" type="text/css" href="stylesheet.css" />
  <meta name="generator" content="Re:VIEW" />
  <title>pagebreak のテスト</title>
</head>
<body>
<h1><a id="h1"></a><span class="secno">第1章 </span>pagebreak のテスト</h1>
<p>aaa</p>
<p>[改ページ]</p>
<p>bbb</p>
<p>[改ページ]</p>
<p>ccc</p>
</body>
</html>
  • 改行コードを、LFにして保存する

テキストの正解データを作成します。

  • 新しいファイルを作成して、content.txtという名前にして、以下を入力する
■H1■第1章 pagebreak のテスト

aaa

[改ページ]
bbb

[改ページ]
ccc
  • 改行コードを、LFにして保存する

以上で、テストファイルができました。TERMINALで以下を入力して、テストを実行してみます。

npm run pretest
npm run test

あれこれエラーが発生します。pagebreakのものを探すと、11と12番目でエラーが出ています。

pagebreakのエラー

エラー表示を確認すると、Re:VIEWプラグインで表示されるエラーが出てます。

ブロック構文としてサポートされていないエラー

これに対応できれば、いけそうです。

コマンドの追加

コマンドは、libフォルダー内の次の4つのファイルに追加します。

  • builder/htmlBuilder.ts
  • builder/textBuilder.txt
  • i18n/ja.ts
  • parser/analyzer.ts

htmlBuilder.ts

  • lib/builder/htmlBuilder.tsを開く
  • 今回は、blanklineを参考にしたので、検索する
  • blanklineの定義をすぐ下に複製して、pagebreakとして定義する
// 644:
    block_pagebreak(process: BuilderProcess, _node: BlockElementSyntaxTree) {
        process.outRaw("<p>[改ページ]</p>\n");
        return false;
    }

textBuilder.ts

  • lib/builder/textBuilder.tsを開く
  • 同様に、blanklineを検索して、参考にする
  • 以下を追加する
// 476:
    block_pagebreak(process: BuilderProcess, _node: BlockElementSyntaxTree) {
        process.out("\n[改ページ]\n");
        return false;
    }

ja.ts

  • lib/i18n/ja.tsを開く
  • blanklineを検索して、参考にする
  • 以下を追加する
// 26:
        "block_pagebreak": "改ページします。\n//pagebreak\nという形式で書きます。正式コマンドではありません。PDFやepubのみ対応です。",

analyzer.ts

  • lib/parser/analyzer.tsを開く
  • blanklineを検索して、参考にする
  • 以下を追加する
// 579:
    block_pagebreak(builder: AcceptableSyntaxBuilder) {
        this.blockDecorationSyntax(builder, "pagebreak", 0);
    }

ビルド

できたら、TERMINALでビルドします。

npm run prebuild
npm run build
npm run postbuild

以上で、対応完了です。テストを実行すると、pagebreakのテストが成功します。もし失敗したら、改行がずれているか、改行コードLF以外になっている可能性があります。確認してみてください。

あとは、対応手順でやったのと同様に、対策したファイルを、インストールしてあるRe:VIEWプラグインへ上書きコピーします。

まとめ

Re:VIEWプラグインが利用しているreview.js-vscodeに、pagebreakの処理を追加して、pagebreakコマンドを認識するようにしました。ビルドしたファイルを、Re:VIEWプラグインのインストール先のnode_modulesフォルダーのreview.js-vscodeフォルダーの同ファイルに上書きすれば、VSCodeで//pagebreakコマンドがエラーにならなくなります。

対応させたのは、プレビュー機能だけです。rakeでwebやtextを出力しようとすると、pagebreakはエラーになります。使えるのは、pdfやepubのみなので、気を付けてください。そのあたりが微妙なので、フォークしたまま、上書きコピーでの対応にとどめました。

参考・関連URL

Unity6ではじめてのAwaitable

本記事は、Qiita Advent Calendar 2024. Unity Advent Calendar 2024のシリーズ2の12日目の記事です。

です。

最近まで、諸々の事情でUnity2021を使っていて、がっつりタスクを使う場面もなかったので、コルーチンにはIEnumeratorを使い続けていました。ようやくUnity6に更新したので、新たに導入されたAwaitableを使ってみようということで、リアル「はじめてのAwaitable」です。

目次

基本的なこと

これまでずっとIEnumeratorでやってたので、基本的なことを調べるところからはじめました。

そもそも非同期処理とは

流石に非同期処理は知ってましたが、念の為。

非ではない同期処理から。同期処理とは、ある処理が完了するまで、次に処理を進めない処理方法です。たとえば次のようなコードがあったとします。

Debug.Log($"こんにちは!");
Debug.Log($"同期処理で、");
Debug.Log($"表示します。");

Unityでこれを実行すると、コンソールに次のような感じで表示されます。

こんにちは!
同期処理で、
表示します。

当たり前のようですが、このように順序よく表示されるのは、UnityのC#が基本的に同期的に実行されるからです。

非同期処理は、同期しない、つまり、ある処理の完了を待たずに、すぐに次の処理をはじめる処理方法のことです。先のコードを非同期に実行すると、次のように表示されるかも知れません。

こ同表ん期示に処しち理てはでい!
、ま

す。

3つのDebug.Logが次々に開始して、それぞれが並行して処理を進めていきます。実行順は、その時々の状況に応じて変わるため、実行するたびに結果は変わります。

同期処理は、手順通りに処理を進めるプログラムの実行に向いています。しかし、時間がかかる大量のデータを処理するプログラムや、サーバーからの返信を待つような処理では、システムの応答が止まってしまいます。複数の処理をスレッドなどに分割して、非同期に実行することで、システムの停止を回避できます。非同期処理は、複数の処理や重い処理を実行したい場合に活躍します。

Unityでは、IEnumeratorを使ったコルーチンや、AsyncOperationを返すLoadSceneAsyncやUnloadSceneAsyncなどの非同期処理の機能を提供してきました。

Awaitableとは

日本語にすると、「待つことができる」。Unity6(Unity2023.1)で追加されたAwaitableクラスは、Unityのライフサイクルを、awaitで待機できるようにするクラスです。裏スレッドに実行を切り替える機能もあります。

Awaitableは、awaitと一緒に使うことを想定したクラスです。

awaitとは

awaitは、非同期操作を受け取る演算子です。かなりややこしい動きをします。

  • awaitが出てくるまでは、普通のメソッドと同じように、同期して処理を進める
  • awaitが非同期操作を受け取ったら、その場で処理を中断して、呼び出し元に制御を返す
  • 非同期操作が完了したら、awaitで中断したところから処理を再開する

処理を途中で中断して、呼び出し元に制御を戻すことで、システムのフリーズを防ぎます。非同期操作が完了したら、中断した場所から再開されます。これにより、UIや他のゲームオブジェクトの動作を停止させることなく、いつ終わるかわからないような処理の完了を待ってから、続きの処理を実行できます。手順通りに進めたい処理で、途中で時間がかかったり、操作待ちがあるような場合でも、手軽に実装できます。

awaitのうしろに、非同期操作を返すラムダ式や、非同期メソッドを書くことができます。その場合は、awaitで中断するより先に、ラムダ式や非同期メソッドを呼び出して、同期的に処理が進みます。どこかでawaitに非同期操作が渡されて、待機状態になったら、awaitをさかのぼって、呼び出し元まで制御が戻ります。そこからは、通常のawaitと同様の流れで実行されます。戻り値を受け取るで、実際の進行を例示します。

asyncとは

asyncは、awaitを使いたいメソッドに付けて、非同期メソッドとして定義するものです。

awaitを含まないメソッドの宣言に、asyncを付ける意味はありません。awaitを含まないメソッドにasyncをつけると、以下のような警告が出ます。

awaitを持たないasyncメソッドは警告が出る

asyncとawaitは、同一スレッド上の制御が滞らないようにする機能です。これら自体には、別スレッドで処理を実行するような機能はありません。

TaskとAwaitable

TaskとAwaitableは、どちらもawaitに渡せる非同期操作を返す機能を持ちます。

Taskは、別のスレッドで処理を実行する機能を提供します。メインスレッドを止めずに、時間がかかる計算を実行するのに適しています。名前が示す通り、何らかの処理を実行することを目的としています。.NETで提供されるクラスなので、Unityのライフサイクルにあわせて実行することはできません。

Awaitableは、Unityから提供されるクラスです。Awaitableという名前が示す通り、awaitで待機することを主な目的としています。Unityの特定のライフサイクルや、指定の秒数が経過するのをawaitで待機する機能を提供します。これにより、IEnumeratorと同じようなUnityのライフサイクル内で実行できるコルーチンを、async/awaitを使って実装できるようになりました。また、実行スレッドをメインとバックグラウンドで切り替える機能も提供されています。Taskと同様に、重い処理をバックグラウンドスレッドで実行したいときにも使えます。

Awaitableの使い方

基本的なことがわかったので、Awaitableを使ってみます。Awaitableが提供する機能は、次のとおりです。

  • IsCompleted
    • Awaitableが完了していたら、trueを返す
  • Cancel
    • Awaitableが待機中なら、System.OperationCanceledExceptionをスローして、処理をキャンセルする
  • BackgroundThreadAsync
    • awaitで処理を中断して、次のフレームまで待機する。次のフレームになったら、バックグラウンドスレッドに切り替えて、処理を再開する。すでにバックグラウンドスレッドだったら、何もしない
  • EndOfFrameAsync
    • 現在のフレームの更新処理が完了するまで待機してから、処理を再開する
  • FixedUpdateAsync
    • 次の物理更新まで待機してから、処理を再開する
  • FromAsyncOperation
    • LoadSceneAsyncなどで返されるAsyncOperationのインスタンスを、Awaitableに変換する
  • MainThreadAsync
    • awaitで処理を中断して、次のフレームまで待機する。次のフレームになったら、メインスレッドに切り替えて、処理を再開する。すでにメインスレッドだったら、何もしない
  • NextFrameAsync
    • 次のフレームまで待機してから、処理を再開する
  • WaitForSecondsAsync
    • 指定の秒数待機してから、処理を再開する

Awaitable - Unity スクリプトリファレンスより

Unityのライフサイクルを考慮したコルーチン

Awaitableのうち、Unityのライフサイクルや、秒数を待つコルーチンとしての使い方です。

using UnityEngine;

public class CoroutineSample : MonoBehaviour
{
    private void Start()
    {
        DoCoroutine();
        Debug.Log("Start終わり");
   }

    /// <summary>
    /// コルーチン的な使い方
    /// </summary>
    async void DoCoroutine()
    {
        Debug.Log("処理開始");
        await Awaitable.NextFrameAsync();
        Debug.Log("1フレーム経過");

        await Awaitable.EndOfFrameAsync();
        Debug.Log("フレーム処理が完了。カメラの移動など、他のオブジェクトの更新後の処理を書くとよい");

        await Awaitable.FixedUpdateAsync();
        Debug.Log("物理更新のタイミング。物理更新関連の処理を書くとよい");

        await Awaitable.WaitForSecondsAsync(1f);
        Debug.Log("1秒経過");
    }
}

ざっくりと処理の流れです。

  • 5行目:シーンが開始したら、Startが呼ばれる
  • 7行目:非同期メソッドDoCoroutineを呼び出す。従来のコルーチンのようなStartCoroutine()は不要
  • 14-16行目:「処理開始」とログ表示。ここまでは、普通のメソッドと同じ動作
  • 17行目:awaitのオペランドのAwaitable.NextFrameAsync()を呼び出して、次のフレームまで待機する非同期操作を受け取ったら、呼び出し元に制御を戻す
  • 8行目:awaitによって制御が戻るので、「Start終わり」と表示して、Startメソッドを終える
  • 次のフレームになるまで、処理なし
  • 18行目:次のフレームになったら、18行目から処理を再開。「1フレーム経過」と表示
  • 20行目:awaitが出てきたので、メソッドから抜ける。呼び出し元のStartは終わっているので、他のオブジェクトの処理などへ
  • フレームの更新処理が終わるまで、処理なし
  • 21行目:フレームの更新処理が一通り終わったら、21行目から処理を再開。「フレーム処理が完了・・・」と表示
  • 23行目:awaitが出てきたので、メソッドから抜ける。20行目と同様
  • 物理更新がはじまるまで、処理なし
  • 24行目:物理更新がはじまったら、24行目から処理を再開。「物理更新のタイミング・・・」と表示
  • 26行目:20, 23行目と同様
  • 1秒経過するまで、処理なし
  • 27行目:1秒経過したら、27行目から処理を再開。「1秒経過」と表示
  • 28行目:非同期メソッド終わり

コンソールは、以下のように表示されます。

コルーチン的使い方のサンプルの表示例

処理は、メインスレッド上で実行されます。「Start終わり」の表示が出力されるタイミングが腑に落ちれば、async/awaitが大体理解できたものと思います。フレーム更新や物理更新するまで、awaitで待機する、文字通り「Awaitable」な使い方をしています。

スレッドの切り替え

Awaitableには、BackgroundThreadAsyncと、MainThreadAsyncという、スレッドを切り替える機能も提供されています。これらを使うと、Taskと似たような使い方ができます。以下、サンプルコードです。

using UnityEngine;

public class ThreadChangeSample : MonoBehaviour
{
    private void Start()
    {
        BackThread();
    }

    async void BackThread()
    {
        Debug.Log($"処理開始");
        await Awaitable.BackgroundThreadAsync();

        Debug.Log($"バックグラウンド切り替え。重い処理開始");
        HeavyTask();
        Debug.Log($"重い処理完了");

        // transform.position = Vector3.zero; // エラー

        await Awaitable.MainThreadAsync();
        Debug.Log($"メインスレッドに帰還");

        transform.position = Vector3.zero; // 実行可能
    }

    void HeavyTask()
    {
        double x = 1;
        for (int i = 0; i < 100000000; i++)
        {
            x += Mathf.Sqrt(i);
        }
    }
}

HeavyTaskに書いた重い処理を、バックグラウンドスレッドで実行するサンプルです。このスクリプトを、任意のゲームオブジェクトにアタッチして実行すると、コンソールに経過が表示されます。

バックグラウンドスレッドで実行

バックグラウンドスレッドに切り替えてから、HeavyTaskを呼び出しているので、その前に表示しているログが2つ表示されます。そして、HeavyTaskの終了が終わったら、残りの「重い処理完了」と「メインスレッドに帰還」が表示されます。システムが停止していないので、Debug.Logを実行したタイミングで、ログが表示されています。

13行目のawait Awaitable.BackgroundThreadAsync();をコメントアウトして実行すると、処理が終了するまで、ログが表示されなくなります。

デフォルトのまま実行

HeavyTaskが、メインスレッドで実行されるので、すべての処理が終わるまでコンソールへの表示ができないからです。

最後にメインスレッドに戻していますが、メインスレッドに戻してから実行する処理がなければ、これは不要です。バックグラウンドスレッドで実行されるのは、asyncメソッド内のみです。

バックグラウンドスレッドは使えない機能が多い

便利そうなバックグラウンドスレッドなのですが、Unityの多くの機能が使えません。19行目のコメントアウトを外すと、以下のような例外が出力されます。

バックグラウンドスレッドではいろいろとエラーになる

同じAwaitableクラスのAwaitable.NextFrameAsync()やAwaitable.WaitForSecondsAsync()といった、Unityライフサイクルに関する機能も使えません。そのような機能を使いたい場合は、Awaitable.MainThreadAsync()で、メインスレッドに戻してください。

スレッドの切り替えには1フレームかかる

スレッドの切り替えは、一度awaitで呼び出しもとに処理を返してから、次のフレームで処理を再開させるときに実行されます。瞬時に切り替わらないことを前提に、利用してください。

戻り値を受け取る

Awaitableは、Taskと同様に戻り値を受け取ることができます。戻り値は、非同期メソッドの結果として返します。awaitで待機する必要があるので、非同期メソッドで利用することになります。

ユーザーがYかNのどちらかを押すまで待機して、押されたキーをログに表示する例です。

using UnityEngine;

public class ReturnValue : MonoBehaviour
{
    void Start()
    {
        YorNAsync();
        Debug.Log("Start終了");
    }

    async void YorNAsync()
    {
        Debug.Log("YかNを押してください。");
        string res = await InputYorNAsync();
        Debug.Log($"{res}が押されました。");
    }

    async Awaitable<string> InputYorNAsync()
    {
        Debug.Log($"キー入力待機");
        while (true)
        {
            if (Input.GetKeyDown(KeyCode.Y))
            {
                return "Y";
            }
            else if (Input.GetKeyDown(KeyCode.N))
            {
                return "N";
            }

            await Awaitable.NextFrameAsync();
        }
    }
}
  • 5-7行目:開始したら、YorNAsyncメソッドを呼び出す
  • 11-13行目:「YかNを押してください。」と表示する
  • 14行目:awaitのうしろがメソッドなので、InputYorNAsyncメソッドを呼び出す
  • 18-20行目:「キー入力待機」と表示する
  • キー入力がなければ、32行目まで進む
  • 32行目:awaitで、次のフレームまで待機するので、呼び出し元に制御を返す
  • 14行目:awaitに非同期操作が渡されるので、さらに呼び出し元に制御を戻す
  • 8行目:「Start終了」と表示して、Startメソッドを終える
  • 次のフレームになるまで、処理なし
  • 32行目:次のフレームになったら、32行目から処理再開
  • 21-33行目:while文で、YかNが押されるまで、フレーム更新ごとにループ
  • Yが押されたら
  • 23-25行目:Yを戻り値として返す
  • 14行目:非同期操作が終了して、戻り値としてYが返るので、resに代入
  • 15行目:「Yが押されました。」と表示
  • 16行目:YorNAsyncを終了

以上です。Nが押された時は、23-25行目と同様に27-29行目が実行されて、「Nが押されました。」と表示されます。

「キー入力待機」が、「Start終了」よりも先に表示されています。これにより、awaitに非同期操作が渡されるまでは、通常の同期処理と同様に処理が進むことが確認できます。

待機をキャンセルする

先の例では、YかNを押すまで処理を停止できませんでした。InputYorNAsyncに、処理を抜ける機能を追加することもできますが、Awaitableのインスタンスを使えば、外部から中断させることもできます。

using System;
using UnityEngine;

public class ReturnValue : MonoBehaviour
{
    Awaitable<string> inputYorNAsync;

    void Start()
    {
        YorNAsync();
        Debug.Log("Start終了");
    }

    private void OnDestroy()
    {
        CancelInvoke();
    }

    async void YorNAsync()
    {
        Debug.Log("YかNを押してください。");
        
        inputYorNAsync = InputYorNAsync();
        Invoke(nameof(AbortAwait), 3);

        try
        {
            string res = await inputYorNAsync;
            CancelInvoke();
            Debug.Log($"{res}が押されました。");
        }
        catch (OperationCanceledException e)
        {
            Debug.Log(e);
        }
    }

    void AbortAwait()
    {
        inputYorNAsync?.Cancel();
    }

    async Awaitable<string> InputYorNAsync()
    {
        Debug.Log($"キー入力待機");
        while (true)
        {
            if (Input.GetKeyDown(KeyCode.Y))
            {
                return "Y";
            }
            else if (Input.GetKeyDown(KeyCode.N))
            {
                return "N";
            }

            await Awaitable.NextFrameAsync();
        }
    }
}
  • 6行目:Awaitableのインスタンスを保存しておくinputYorNAsyncを定義
  • 23行目:InputYorNAsyncメソッドから返されるAwaitableの非同期操作インスタンスを直にawaitせずに、inputYorNAsyncに代入しておく
  • 43-57行目:非同期メソッドでも、awaitが出てくるまでは通常のメソッドと同様に処理されるので、57行目のawaitまでそのまま処理が進む
  • 57行目:awaitで、次のフレームの待機をはじめたら、呼び出し元に制御を返す
  • 24行目:3秒後に、待機をキャンセルするメソッド呼び出しを設定
  • 28行目:InputYorNAsyncメソッドがreturnするまで、待機を開始して、呼び出しもとのStartメソッドに制御を返す
  • 11行目:「Start終了」と表示して、Startメソッドを終える
  • YかNが押されたら
    • 先の例と同様に、戻り値で文字を返す
    • 29行目:時間切れ処理が呼ばれないように、CancelInvokeメソッドを呼び出して、Invokeをキャンセル
    • 30行目:押された文字を表示する
  • 3秒経過したら
    • 38行目:Invokeで設定した、AbortAwaitメソッドが呼ばれる
    • 40行目:inputYorNAsyncに保存していたAwaitableのインスタンスのCancelメソッドを呼び出して、処理をキャンセルする
    • 32行目:非同期処理をキャンセルすると、OperationCanceledExecptionが発生するので、catchして、エラーになることを防止
    • 34行目:例外の内容を表示。予定の動作なら、表示はしなくてよい

OperationCanceledExeptionをログに表示

Awaitableのインスタンスをキャッシュしておいて、Cancelメソッドを呼べば、待機をキャンセルできます。待機しているawaitでOperationCanceledExceptionが発生するので、try-catchで囲んでおくとよいでしょう。

キャッシュしたAwaitableは再利用できない

Taskのインスタンスは、キャッシュしておいて再利用できます。一方、Awaitableのインスタンスは、パフォーマンス上の理由から、再利用できません。次のようなコードは、エラーになります。

using UnityEngine;

public class UseCache : MonoBehaviour
{
    async void Start()
    {
        Awaitable cache = Awaitable.NextFrameAsync();

        Debug.Log($"実行前");
        await cache;
        Debug.Log($"1フレーム経過");
        await cache;  // ここでエラー
        Debug.Log($"2フレーム経過");
    }
}

2回目の利用時にエラー

シーンの切り替えシーケンス

シーンの読み込みや解放をするSceneManager.LoadSceneAsyncやUnloadSceneAsyncが返すAsyncOperationも、awaitに渡せます。

    async void ChangeScene()
    {
        // シーン読み込み
        await FadeOut();
        await SceneManager.LoadSceneAsync("SceneA", LoadSceneMode.Additive);
        await FadeIn();

        // シーン解放
        await FadeOut();
        await SceneManager.UnloadSceneAsync("SceneA");
        await FadeIn();
    }

AsyncOperationをキャンセル可能にする

AwaitableのFromAsyncOperationに、AsyncOperationのインスタンスとCancellationTokenを渡すと、キャンセル可能なawaitができます。Webアクセスのキャンセルなどに使えそうです。

タスクのキャンセル - .NET | Microsoft Learn

以下、意味はありませんが、シーンの読み込みのキャンセルを試みるサンプルです。

using System;
using System.Threading;
using UnityEngine;
using UnityEngine.SceneManagement;

public class LoadSceneCancel : MonoBehaviour
{
    private void Start()
    {
        var token_source = new CancellationTokenSource();
        CancelableLoadScene(token_source.Token);
        token_source.Cancel();
    }

    async void CancelableLoadScene(CancellationToken token)
    {
        var asyncOperation = SceneManager.LoadSceneAsync("SceneA", LoadSceneMode.Additive);
        var awaitable = Awaitable.FromAsyncOperation(asyncOperation, token);
        try
        {
            await awaitable;
        }
        catch (OperationCanceledException e)
        {
            Debug.LogException(e);
        }
    }
}
  • 10-11行目:CancellationTokenSourceのインスタンスを生成して、Tokenをメソッドに渡して呼び出し
  • 17-18行目:シーンの非同期読み込みを実行して、AsyncOperationと、引数で受け取ったトークンから、awaitableを用意
  • 21行目:シーンの読み込みが完了するのと待機

シーンが読み込まれるより先に、12行目が実行されれば、シーンの読み込み待機がキャンセルされます。処理がキャンセルされたら、OperationCanceledExceptionが発生するので、19-26行目はtry-catchで囲んでいます。

Awaitableのパフォーマンス

Awaitableは、従来のIEnumeratorを使ったコルーチンよりも、パフォーマンスは向上するとのことです。Unity6以降を使うのであれば、IEnumeratorから乗り換えた方がよさそうです。

UpdateやFixedUpdateの代わりにAwaitableのループを使うことは推奨されていません。以下、Unity公式マニュアルから引用します。

Although Unity’s Awaitable class is optimized for performance, you should avoid running hundreds of thousands of concurrent coroutines. Similar to coroutine-based iterators, a behavior with a loop similar to the following example attached to all your game objects is very likely to cause performance problems:

Await support - Unity マニュアルより

Awaitableを使うと、小さいループで処理が書けるため、パフォーマンスがよさそうに見えます。しかし、処理を再開するために状態を保存したり、待機処理から再開できるようにコードを調整したりと、内部的には結構な処理をしています。通常のゲームオブジェクトの更新処理は、UpdateやFixedUpdateに実装しましょう。

Awaitableは、多少のパフォーマンスを犠牲にしても、ややこしい手順をシンプルに書きたい場合や、シナリオの再生処理などの限定された場面で利用します。パフォーマンスを向上させたい場合は、DOTSなどの利用を検討してください。

まとめ

ようやく、asyncとawaitに足を踏み入れました。非同期処理のややこしさが、awaitに集約されているように感じました。疑問に感じたところを自分自身であれこれ試して、ようやく腑に落ちました。

Awaitableクラスは、Unityのライフサイクルにあわせて、async/awaitを利用する機能を提供してくれます。これにより、従来のIEnumeratorを使ったコルーチンは、より効率が良いasync/awaitを使ったものに書き換えられます。

awaitのオペランドとして、処理を再開するための非同期操作を渡すことで、呼び出し元に制御を戻します。これにより、メインスレッドの処理を進められるので、システムが停止しなくなります。awaitした非同期操作が完了したら、awaitの続きの処理を呼び出して、処理を再開します。これにより、メインスレッドだけでも、非同期的な処理が実現できます。

また、スレッドの切り替え機能も持っています。awaitした非同期操作から再開する時に、スレッドを切り替えます。切り替えたスレッドは、非同期メソッドのチェーン内で有効です。バックグラウンドスレッドでは、Unityの主要な機能が使えません。PureなC#や、スレッドセーフな機能だけで処理をします。Unityの機能を使う場合は、メインスレッドに戻します。次のフレームの更新タイミングで、awaitから戻るタイミングでスレッドを切り替えるため、スレッドを切り替えるごとに1フレーム進むことに注意が必要です。

AsyncOperationも、awaitできます。待機をキャンセルするときは、Cancelメソッドを使います。

自分の利用用途としては、IEnumeratorの置き換えがほとんどです。シナリオの制御や、シーンの切り替え、エンドロールやUIのシーケンス処理といったところです。今のところ、特に不足点はなさそうなので、これからビシバシ使っていきたいと考えています。

本記事は、Qiita Advent Calendar 2024. Unity Advent Calendar 2024のシリーズ2の12日目の記事でした。

です。

参考・関連URL

【Godot4.3】Macで2Dゲームの動きがガタついたときにやったこと

本記事は、Qiita Advent Calender 2024のGodot Engine Advent Calender 2024シリーズ1の4日目の記事です。

qiita.com

2Dゲームを、手持ちのMac Book Pro(以降、MBP)で動かしたところ、動きがガタつく症状が発生しました。その原因と、調査のまとめです。

目次

原因は互換性レンダラー

あまりに簡単なオチだったので先に書きます。原因は互換性レンダラーでした。Forward+やモバイルを使っていたら滑らかに動きます。また、互換性レンダラーでも、WindowsのWebブラウザーでは、滑らかに動きました。

動きがガタつくのは、互換性レンダラーで、WindowsやMac上で実行した場合です。普通は、PC向けならForward+を使うので、問題は起きません。うちの古いインテルMBPだと、MoltenVKが不安定で、互換性レンダラーしか動きません。このような特殊な場合に起きる問題でした。

以下、調査内容です。

ことのおこり

Godot Meetup Tokyo Vol.3に、展示枠で応募したのがはじまりでした。ゲームジャムに、未完成で出したものを完成させて、この機会に遊んでいただこうと考えました。せっかく展示するなら、Webブラウザーではなく、ネイティブのフルスクリーンで動くようにしようと、手持ちのMBP上で動くようにビルドしてみました。

滑らかに動くことを期待していたのですが、動きがガタつきます。ブロック崩しとクリッカーをまぜたSTORM OF BALLSで、特にガタつきが目立ちました。

am1tanaka.itch.io

こんなシンプルなゲームが滑らかに動かないのはおかしい、というところから、今後に備えて原因を調査しはじめました。

フレームレートを確認する

画面がガタつく原因として考えられるのは、画面の更新タイミングが安定しないことか、処理が間に合っていないことです。まずは、フレームレートを確認しました。

1秒ごとにログに出力

Godotの標準の機能で、フレームレートを確認する方法があります。

  • プロジェクトメニューから、プロジェクト設定を開く
  • 高度な設定を有効にすると、デバッグ欄が表示されるので、設定からFPSを表示するにチェック

FPSをログに表示

この設定をして実行すると、1秒ごとに、出力にFPSが表示されます。

フレームレートを出力

デバッガーのモニターで確認する

もう少ししっかり確認したい場合は、下部パネルのデバッガーのモニターを利用します。モニターは自動的に記録されるので、実行したあとに確認できます。

  • ゲームを実行する
  • 画面下部のデバッガーをクリックして、デバッガーパネルを表示する

デバッガーのモニターでFPSを確認

フレームレート(FPS)の値の欄で、直近のフレームレートが分かります。チェックを入れれば、推移が確認できます。

MBPで、フルスクリーンで実行した結果が以下です。

MBPでのフレームレートの推移

フレームレートが安定していれば、グラフは一直線になるはずです。ガタガタしているということは、フレームレートが安定していないということです。

スクリプトを書いて自力で表示する

フレームレートが安定しない原因は、Godotエディターかも知れません。以下のような簡単なスクリプトを作成して、ビルドしたアプリ単体でフレームレートを確認しました。

// 1:
class_name FrameRateLabel
extends Label

var _last_time : int
var _counting: int


func _ready() -> void:
    _last_time = Time.get_ticks_msec()


func _process(_delta: float) -> void:
    _counting += 1
    if (Time.get_ticks_msec() - _last_time) < 1000:
        return
    
    _last_time = Time.get_ticks_msec()
    text = "%d fps" % [_counting]
    _counting = 0

このスクリプトをLabelにアタッチすれば、フレームレートが表示されます。これでも、フレームレートは不安定でした。

ラベルに表示

フレームレートが安定しない現象のまとめ

フルスクリーンなら、モニターのリフレッシュレートに同期するので、フレームレートが安定すると考えていました。しかし、Mac Bookは可変リフレッシュレートがデフォルトの動作で、内臓モニターだとリフレッシュレートを指定できないようでした。

MacBook Pro や Apple Pro Display XDR でリフレッシュレートを変更する - Apple サポート (日本)

120fpsで安定して動くときがあって、その時はスムーズに動きます。しかし、再現性がありません。AppleシリコンのMacだと、フルスクリーンにするとゲームモードで動いてくれるらしいのですが、インテルMacは対応していないようです。

Mac でゲームモードを使う - Apple サポート (日本)

これが原因だと思ったのですが、フレームレートが固定のWinでもガタつくことが分かりました。ほかの原因を探します。

スクリプトの実行速度を確認する

タイトル画面が特に不安定で、70fpsになったりします。重い処理がないか、プロファイラーで調べることにしました。プロファイラーは、下部パネルのデバッガーから選べます。

下部パネルからデバッガー>プロファイラーを選択

プロファイラーは、モニターと違って、手動で開始をクリックする必要があります。また、実行するたびに下部パネルが出力に変わるのは面倒です。以下のような設定で動かしました。

  1. プロジェクトメニューから、プロジェクト設定を開く
  2. 表示>ウィンドウ欄のモードを、Windowedに設定
  3. エディターメニューから、エディター設定を開く
  4. 一般タブの、実行>Bottom Panelを選択
  5. Action on Play欄を、Open Debuggerに変更

実行時に、デバッガーパネルを開く設定

  1. デバッガーパネルを選択して、プロファイラーを選択

これで、ウィンドウで起動して、下部パネルが自動的にデバッガーになります。実行して、プロファイラーの開始ボタンを押します。

プロファイラーを開始

ボールを200個以上表示させてみました。

ボール関連の処理にかかっている時間

223個のボールを処理して、3msもかかっていません。Physics 2Dなども、負担はなく、処理時間は余裕がありそうです。

処理ごとの実行時間は、時間の表示設定を、包括から自己に変更すると確認できます。

各処理ごとの処理時間

当たり判定のShapeCastが一番重くて、0.86msかかっています。それでも、1msもかかっていないので、気にしなくてよいでしょう。

設定で試したこと

ここまでの調査で、次のようなことが分かりました。

  • MBPでは、フレームレートが安定しない
  • 処理速度には余裕があるので、スクリプトの高速化などは不要

最大フレームレートを設定する

フレームレートを一定にできれば、滑らかに動かせそうです。MBPが可変リフレッシュレートでも、最大フレームレートをGodotで設定すればいけるのではないかと考えました。設定は、プロジェクト設定の実行欄にあります。

最大FPSを60、デルタ時間のスムージングをオフ

これでも安定しないため、デルタ時間のスムージングをオフにしてみました。この設定で、やや安定したように感じたのですが、気のせいでした。その時は、運よく120fpsで安定動作していたのでしょう。デフォルトに戻しても、120fpsで安定していれば滑らかに動くので、解決策にならないことが分かりました。

処理をprocessに移動

MBPでの解決は難しそうなのでひとまず保留して、固定フレームレートのWindowsで滑らかに動くことを確認しようと考えました。しかし、期待に反して、フレームレートが固定されるWindowsのフルスクリーンでも処理がガタつきました。

こうなると、フレーム更新と物理更新の干渉が疑われます。そこで、物理更新に実装していた移動処理を、_processに移してみました。move_and_collideで実装していた移動処理は、次のようにグローバル座標移動に変更しました。

   #move_and_collide(step * Vector2.DOWN)
    global_position += step * Vector2.DOWN

フレームレートが一定で、移動量が同じなら、滑らかに動くはずです。ところが、これでもガタつきが解消しません。こうなると、Godotの画面更新が不安定だとしか考えられません。

もはや、エンジンのコードに手を入れるしかなさそうです。ここで一度諦めたのですが、こんな簡単な処理ができないはずがありません。ここで、互換性レンダラーが疑わしいことに思い至りました。レンダラーをForward+にしたところ、無事、滑らかに動きました。モバイルも滑らかです。ここまでにやった対策を、すべてデフォルトに戻しても、滑らかなままです。ということで、原因が互換性レンダラーだったという結論になりました。

今回の調査で気になったところ

今回の調査で気になったことを、おまけで書きます。

モニターのインポートプロセス欄はProcessの間違い

デバッガーのモニターに表示されるインポートプロセスで混乱しました。

謎のインポートプロセス

インポートプロセスって、アセットをインポートする作業ですよね?なぜ、実行中に発生するのか。しかも、フレーム更新と同程度の16msもかかっています。ググったりあれこれ調べてみたのですが、よくわからず。ふと、英語表記にしてみたところ、Processとなっていました。それは16msかかりますね。翻訳のミスでした。

すでに修正が出ているかも知れませんが、まだなら報告方法を調べて報告します。とりあえず、Weblateで修正して保存しました(2024/12/4)

プロファイラーで待ち時間が知りたい

Godotのプロファイラーですが、各項目にかかっている時間はわかるのですが、「待ち時間」が分かりません。

待ち時間がない

Godotのプロファイラーでは、個別の項目や、ある処理グループにかかった時間はわかるのですが、それらがフレームの更新時間内にどのような順序で、総合してどれぐらい時間を使っていて、余っている時間がどれぐらいかが分かりません。見落としがありそうな気がしますが、見つけることができませんでした。

UnityのProfilerでは、以下のようにターゲットの時間に対するCPUとGPUの使用率を見ることができます。

UnityのProfiler

また、スレッドごとにかかった時間を、並べて見せてくれます。処理の余力など、把握しやすくなっています。

GODOT DOCSのThe Profiler — Godot Engine (stable) documentation in Englishを見ると、主要なデータとして、Frame time, Physics frame, Idle time, Physics timeという項目が示されています。名前的に、Idle timeがフレーム更新までの待ち時間だろうと思ったのですが、プロファイラーに見当たりません。はて?と思ってマニュアルを読んでみると、Idle timeは、_processを含めた物理更新以外にかかった時間の合計だと書かれていました。どうやら、Process Timeのことを、以前はIdle timeと書いていたようです。本来のIdle timeが欲しい・・・。

まとめ

Webブラウザーだと、互換性レンダラーでもPCよりもガタつきが気になりません。問題になるのは、WinやMac上で、互換性レンダラーを使った場合のようです。PC向けなら、通常はForward+を使うので、想定されていないのかも知れません。

うちの古いMBPのように、Vulkanが安定して動かないビデオカードを持ったPCでは、互換性レンダラーにしないとGodotが落ちて使えません。Godot4.4で、Metalの対応がはじまったようですが、残念ながらインテルのMacは当面は対象外とのことでした。

Dev snapshot: Godot 4.4 dev 1 – Godot Engine

別件ですが、Forward+やモバイルは、互換性に比べて、入力への反応が鈍いような気がしました。フレームバッファの扱いが違っているからかも知れませんし、気のせいかもしれません。

今回の件は、Windowsがなければ解明できませんでした。複数の環境を持っているのは大切ですね。また、Mac向けには、通常のForward+版だけではなく、互換性レンダラー版のビルドを用意しないと、落ちる可能性があるのは気を付けたいところです。

本記事は、Qiita Advent Calender 2024のGodot Engine Advent Calender 2024シリーズ1の4日目の記事でした。

qiita.com

参考URL