こんにちは、Unityエンジニアのオオバです。

お悩みさん
お悩みさん
  • プレイを止めて再生し直すとエディタ拡張が動かない
  • エラーは出ているけど関係なさそう
  • コンパイルし直すと直るのはなぜ?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    Enter Play Mode OptionsでDomain Reloadを切ると、プレイ開始が一気に速くなりますよね。

    僕も開発中の「勇者のどうぐ屋さん」で、Domain Reloadを無効にしています。

    その代わりに、ときどき妙な不具合に出会います。

    今回は、自作のエディタ拡張が「たまに再生できない」状態になりました。

    テストシーンには入る。

    それでも何も始まらない。

    Consoleにはエラーが1件出ているものの、追ってみると別件でした。

    本記事では、プレイを止めた後に await が永遠に終わらなくなった原因と、僕がやった解決方法をまとめます。

    プレイ停止で完了しなかった「完了待ち」がstaticに残っていました。プレイ終了時にリセットして解決

    先に結論です。

    エディタ拡張の中で、前の再生が片付いたかを UniTaskCompletionSource で待っていました。

    再生中にプレイを止めると、この完了待ちが完了しないまま残ります。

    Domain Reloadが無効なので、static変数は次のプレイにそのまま持ち越されます。

    次の再生は、二度と完了しない完了待ちを await し続けることになりました。

    プレイ終了時(ExitingPlayMode)にstaticの完了待ちを null に戻して解決しています。

    この記事のポイント
    • プレイを止めるとawaitの続きとfinallyが走らないことがある
    • Domain Reload無効だとstaticの完了待ちが次のプレイに残る
    • エディタ拡張のstaticな完了待ちはExitingPlayModeでリセットする
    • 出るはずのログが出ていない位置が止まっている場所

    起きたこと

    環境は次のとおりです。

    「勇者のどうぐ屋さん」には、「よく使うツール」というエディタ拡張を用意しています。

    その中に、ADV(会話シーン)をテストシーンで再生する機能があります。

    ADVには「next:」で次のADVを指定して、数珠つなぎにする仕組みがあります。

    「数珠つなぎ(next:)も続けて再生」をオンにしてトップダウンADVを再生したところ、テストシーンには入るのに何も始まらなくなりました。

    Consoleに出ていたエラーは別件だった

    Consoleに出ていたのは、FurnitureDefinitionRepository.GetAll の NullReferenceException だけです。

    最初は「これが原因か」と思いました。

    ところがスタックトレースの入口を見ると、OnFocusChanged から呼ばれています。

    フォーカスを外したときの通知予約が、まだ読み込まれていないマスターを読みに行っていました。

    ADVの再生とは関係のない、別の不具合です。

    Consoleに出ているエラーが、目の前の症状の原因とは限りません。

    スタックトレースの入口で、どの経路から呼ばれたかを先に確かめると切り分けが早くなります。

    止まっている場所を突き止める

    エラーが手がかりにならないので、実行中の状態を直接覗くことにしました。

    uLoopの execute-dynamic-code で、ADVランナーのprivateフィールドを確認します。

    ランナーの自動再生はオフ

    ランナーの _autoPlay は False でした。

    数珠つなぎの経路では、ランナーの自動再生を切ってエディタ拡張側から再生を指示する作りです。

    ここまでは想定どおりでした。

    再生中なのに、出るはずのログが出ていない

    次に、数珠つなぎ再生の状態を見ます。

    AdvChainPlayback.IsPlaying は True でした。

    再生中の扱いになっているなら、再生開始の直後に [FavoriteTool] 数珠つなぎを再生 のログが出るはずです。

    このログが一度も出ていませんでした。

    コードを読むと、そのログより前にある処理は await previous; だけ。

    前の再生の片付きを待つ1行です。

    止まっているのはここだと分かりました。

    「出るはずのログが出ていない」という事実は、止まっている位置を教えてくれます。

    エラーが出ない不具合では、かなり頼りになる手がかりです。

    プロジェクトの設定

    最後に、EditorSettingsを確認しました。

    Domain Reloadが無効になっています。

    ここで原因の見当がつきました。

    原因:完了しない完了待ちがstaticに残っていた

    問題のコードはこちらです。

    private static UniTaskCompletionSource _finished; // 前の再生が片付いた合図  
    
    public static void Play(AdvChainTarget start)  
    {
        UniTask previous = _finished?.Task ?? UniTask.CompletedTask;  
        ...  
        PlayAfterAsync(previous, start, cts, finished).Forget(); // 中で await previous → finally で finished.TrySetResult()  
    }
    

    Play を呼ぶと、前の再生の完了待ち(_finished)を受け取ります。

    PlayAfterAsync の中で await previous して、前の再生が片付いてから次を始める流れです。

    自分の再生が終わったら、finally で finished.TrySetResult() を呼んで次の再生に合図を送ります。

    画面が2つ重なるのを防ぐための仕組みでした。

    プレイを止めるとfinallyが走らない

    数珠つなぎ再生の途中でプレイを止めると、UniTaskのPlayerLoopが止まります。

    再生中の await は再開されません。

    await の続きが走らないので、finally も走りません。

    TrySetResult() が呼ばれず、_finished はPending(未完了)のまま残ります。

    Domain Reload無効でstaticが持ち越される

    通常のUnityは、プレイ開始のたびにドメインを再読み込みします。

    static変数も初期化されるので、未完了の _finished が残っても次のプレイには影響しません。

    Domain Reloadを無効にすると、static変数は前のプレイの値のまま持ち越されます。

    次のプレイで Play を呼ぶと、未完了の _finished を previous として受け取ってしまいます。

    そして await previous で、永遠に待ち続けるわけです。

    「たまに再生できない」に見えた理由

    スクリプトをコンパイルすると、ドメインが再読み込みされます。

    static変数も初期化されるので、症状は消えます。

    再生中にプレイを止めたときだけ壊れて、コンパイルすると直る。

    この組み合わせのせいで、「たまに再生できない」不具合に見えていました。

    解決:プレイ終了時に完了待ちをリセットする

    プレイモードの切り替えを受け取る処理に、リセットを足しました。

    if (state == PlayModeStateChange.ExitingPlayMode)  
    {
        Stop();  
        _finished = null; // プレイをまたいで待つ理由はない  
    }
    

    _finished が null なら、次の Play では UniTask.CompletedTask を待つことになります。

    前のプレイの完了待ちを引きずらずに、すぐ再生が始まります。

    同じプレイ内の待ち合わせは残した

    「前の再生のADVが片付くまで待つ」仕組み自体は残しています。

    同じプレイの中で続けて再生したときに、画面が2つ重なるのを防ぐためです。

    消したのは、プレイをまたいだ待ち合わせだけ。

    プレイが終わった時点で、前のプレイの再生を待つ理由はもうありません。

    検証

    ツールのボタンと同じ経路で、次の手順を試しました。

    1. 再生する
    2. 途中でプレイを停止する
    3. もう一度再生する

    2回目の再生でも [AdvChain] 再生: topdown/main/5 のログが出て、ADVが進みました。

    エラーは0件です。

    修正後、停止→再プレイでもトップダウンADVが再生された

    修正後、停止→再プレイでもトップダウンADVが再生された

    Domain Reloadを無効にするときの注意点

    Domain Reloadの無効化は、プレイ開始を速くしてくれる便利な設定です。

    その代わり、static変数の後始末は自分で面倒を見る必要があります。

    特に気を付けたいのが、エディタ拡張のstaticな「完了待ち」です。

    この2つがそろっていると、プレイを止めたタイミングで今回と同じ状態になりえます。

    ExitingPlayMode で必ずリセットしておくと安心です。

    Enter Play Mode Optionsの設定方法は、こちらの記事で紹介しています。

    👉 【Unity】EnterPlayModeでUnityエディタを爆速化しよう

    まとめ

    Domain Reload無効のUnityでエディタ拡張の再生が止まった原因は、プレイ停止で完了しなかった完了待ちがstaticに残っていたことでした。

    今回いちばん効いたのは、出るはずのログが出ていないことに気付けた点でした。

    エラーが出ない不具合ほど、ログの「抜け」が止まっている場所を教えてくれます。

    Domain Reloadを切っていて「コンパイルし直すと直る」不具合に当たったら、staticに残っている値を疑ってみてください。

    オススメ記事
    検証環境