こんにちは、Unityエンジニアのオオバです。
UniTask のループを destroyCancellationToken で止める書き方、便利ですよね。
僕も開発中の「勇者のどうぐ屋さん」で、あちこちに使っています。
ところが先日、この書き方でもループが止まらないケースがあることに気付きました。
GameObject を Destroy しているのに、歩き回るループだけが生き残る。
しかも、破棄済みの SpriteRenderer を触り続けるという状態です。
本記事では、どんな条件でこの罠を踏むのか、原因と対処法をまとめます。
後半では、同じ作業中にハマった Input System の小ネタも2つ紹介します。
一度もアクティブにならなかった MonoBehaviour は、Destroy しても OnDestroy が呼ばれません
先に結論です。
非アクティブな親の下に Instantiate され、一度もアクティブにならなかった GameObject の MonoBehaviour は、Awake が呼ばれていません。
Awake が呼ばれていないオブジェクトは、Destroy しても OnDestroy が呼ばれず、destroyCancellationToken もキャンセルされない。
一方で UniTask の Yield や Delay は PlayerLoop で回るので、GameObject の active とは無関係に進み続けます。
対処はシンプルで、親を一度アクティブにしてから生成し、その後で非表示に戻すだけです。
- 非アクティブな親の下に生成し一度もアクティブにならなかったMonoBehaviourはAwakeが呼ばれない
- Awakeが呼ばれていないとDestroyしてもOnDestroyが呼ばれない
- destroyCancellationTokenもキャンセルされずUniTaskのループが残る
- 親を一度アクティブにして生成してから非表示に戻せば解決する
起きたこと
勇者のどうぐ屋さんで、フィールドマップ(タイルマップ)を Unity 上で眺めるためのビューアを作りました。
Claude Code と一緒に開発したツールで、カメラを動かしてマップを見て回れます。
マップには、背景の動物(猫・犬・馬・牛)が置かれています。
それぞれに FieldMapViewerPet という MonoBehaviour が付いていて、UniTask のループで歩き回る作り。
ループは destroyCancellationToken で止めるようにしていました。
ビューアには「P で動物を隠す」機能もあります。
隠すときは、動物たちの親(Pets)を SetActive(false) にしているだけです。
問題が出るのは、次の手順を踏んだときでした。
Pで動物を隠す- 隠したままマップを切り替える(新しいマップの動物は、非アクティブな親の下に Instantiate される)
- もう一度マップを切り替えて、動物を Destroy する
3 の時点で、2 で生成した動物の歩行ループが止まりません。
破棄済みの SpriteRenderer を触り続けてしまいます。
実はこれ、動かして気付いたわけではありません。
コードを見直していて 「隠したまま生成した動物って、ちゃんと止まるんだっけ?」 と引っかかったのがきっかけです。
普段の操作ではまず踏まない手順なので、見直していなければ気付かなかったと思います。
原因:Awake が呼ばれていないと OnDestroy も呼ばれない
原因は、MonoBehaviour のライフサイクルにありました。
GameObject が一度もアクティブにならなかった場合、その MonoBehaviour の Awake は呼ばれていません。
そして Unity は、Awake が呼ばれていないオブジェクトを Destroy しても OnDestroy を呼びません。
destroyCancellationToken は、OnDestroy のタイミングでキャンセルされるトークンです。
OnDestroy が呼ばれなければ、当然キャンセルもされません。
もう1つのポイントが、UniTask の待機の仕組みです。
UniTask.Yield や UniTask.Delay は PlayerLoop の上で回っています。
コルーチンと違って、GameObject の active とは関係なく進み続けます。
整理すると、次の3つが重なって罠になっていました。
- 非アクティブな親の下に生成したので、Awake が呼ばれていない
- Destroy しても OnDestroy が呼ばれず、
destroyCancellationTokenがキャンセルされない - UniTask のループは PlayerLoop で回るので、オブジェクトが消えても止まらない
ループの中身を簡略化すると、次のような形です(説明用に省略しています)。
async UniTaskVoid WalkLoop()
{
var token = destroyCancellationToken;
while (!token.IsCancellationRequested)
{
// 行き先を決めて歩く。SpriteRenderer の向きや見た目を更新する
await UniTask.Delay(1000, cancellationToken: token);
}
}
普段はこの書き方で問題ありません。
Destroy すれば OnDestroy が呼ばれて、トークン経由でループが抜けるからです。
「一度もアクティブにならなかった」という条件がそろったときだけ、この前提が崩れます。
解決:親を一度アクティブにしてから生成する
対処として、マップを出すときの順番を変えました。
親を一度アクティブにしてから動物を生成し、その後で表示設定(隠す)を戻します。
// 一度もアクティブにならなかった動物は破棄しても歩行ループが止まらない(OnDestroyが呼ばれない)ので、出してから隠す
_petRoot.gameObject.SetActive(true);
SpawnPets();
SetPetsVisible(_isPetVisible);
ポイントは「一度でもアクティブになったかどうか」です。
一度アクティブになっていれば、そのあと SetActive(false) で隠している間に Destroy しても、OnDestroy は呼ばれます。
destroyCancellationToken もキャンセルされ、ループはきちんと止まる。
この直し方には、うれしい副作用もありました。
隠している間も動物は歩き続けるので、もう一度表示したときに、うろうろしている位置が自然につながります。
補足:同じ作業でハマった Input System の小ネタ
ビューアを作る中で、Input System 1.18 まわりでも2つハマりました。
QueueStateEvent でドラッグを注入しても上書きされる
マウスのドラッグでカメラを動かす処理を検証しようとして、QueueStateEvent でマウスの状態を注入しました。
ところが、実機マウスの状態イベントが次のフレームで上書きしてしまい、ドラッグ移動の検証ができません。
最終的には InputState.Change を使って同じフレームの中で状態を書き換え、入力処理のメソッドを直接呼んで検証しました。
ホイール1目盛りの値が1になる
ホイールでズームする処理では、1目盛りを120として割る前提で書いていました。
ところが Input System 1.18 の InputSettings.scrollDeltaBehavior が UniformAcrossAllPlatforms だと、ホイール1目盛りの値は1になります。
120で割ると、ズームがほぼ効きません。
こちらは、設定値を見て分岐するようにしました。
補足:ビューア全体で決めたこと
ついでに、ビューアの設計で迷ったところも残しておきます。
表示方法は2案を比べました。
1つはトップダウンADVの UI 表示基盤(Canvas にマップを重ねる)を流用する案。
もう1つは、マップをワールド空間に置いて正投影カメラで見る案です。
マップの外まで引いて俯瞰できるのが決め手になり、ワールド空間の案を選びました。
もう1つ気を付けたのが、ゲーム本編との見た目のずれです。
マップはゲームと同じ FieldMapFactory で組み立てています。
動物の行き先の抽選・経路探索・座る寝るといった動きも、ADV のクラスをそのまま共用。
ビューア側で変えたのは描画だけで、SpriteRenderer で表示しています。
ツールのためだけに別実装を作ると、本編とずれた瞬間にツールの意味が無くなってしまうんですよね。
まとめ
destroyCancellationToken で UniTask のループを止める書き方は便利です。
ただし、OnDestroy が呼ばれることが前提になっています。
今回のポイントをまとめます。
- 非アクティブな親の下に生成し、一度もアクティブにならなかった MonoBehaviour は Awake が呼ばれない
- Awake が呼ばれていないと、Destroy しても OnDestroy が呼ばれず、
destroyCancellationTokenもキャンセルされない - UniTask の待機は PlayerLoop で回るので、GameObject が消えてもループは残る
- 親を一度アクティブにしてから生成し、その後で非表示に戻せば防げる
「非表示のまま生成する」という処理は、表示切り替えのある UI やツールでよく出てきます。
UniTask のループを持つコンポーネントを非アクティブな親の下に生成している箇所がないか、一度見直してみてください。
UniTask のキャンセル全般については、こちらの記事でまとめています。

筆者のXをフォローしよう
- Unity 6000.3.9f1 / UniTask / Input System 1.18







