こんにちは、Unityエンジニアのオオバです。
ある日、バトル中にアクションを再生するたび、毎フレーム NullReferenceException が出るようになりました。
Console が赤いログで埋まっていくあの感じ、焦りますよね。
しかも厄介なことに、スタックトレースが指しているのはアニメーションの長さを読んでいる行。
参照を渡したところとは、だいぶ離れた場所です。
「参照が外れたアセットがあるんだろうな」と思って探したものの、未設定の参照は1件も見つかりませんでした。
本記事では、この「見つからない参照切れ」の正体と、全件をまとめて洗い出したやり方を紹介します。
参照切れはfileID 0にならず、消えたGUIDを指したまま残っていました
先に結論です。
アセットの中の AnimationClip の参照には、fileID も guid もちゃんと入っていました。
ところが、その guid を持つファイルがプロジェクトのどこにも無かったんです。
YAML の上では値が入っているので、未設定({fileID: 0})を探す方法では引っかかりません。
しかも AnimationClipPlayable.Create は、クリップが null でも例外を出しません。
そのため、エラーはクリップの長さを読む行まで先送りされ、原因から遠い場所で NRE になっていました。
- 参照切れの参照はfileIDとguidが入ったまま残るのでfileID 0の検索では見つからない
- AnimationClipPlayable.Createはnullを渡しても例外を出さず後の.lengthで落ちる
- .assetの参照guidを.metaのguid集合と突き合わせれば数秒で全件見つかる
- 直すのはデータ側でC#でnullを握りつぶさない
- リネームは.metaごとgit mvする
起きたこと
舞台は、Playable で AnimationClip を再生する自前のアクションシステムです。
アクションの定義は ScriptableObject のアセット(.asset)に持たせていて、その中に再生する AnimationClip の参照が入っています。
症状は次のとおり。
- バトル中、アクションを再生するたびに毎フレーム NullReferenceException が出る
- 例外が出ているのは、再生中のクリップの長さを読むところ
まず疑ったのは、クリップの設定し忘れです。
アクション定義のアセットを開いて、AnimationClip の参照のうち {fileID: 0}(何も入っていない状態)のものを検索しました。
結果は0件。
全部のアクションにクリップが入っている、という結論になってしまいます。
「じゃあ何が null なんだ?」と、ここでしばらく手が止まりました。
原因:参照先のファイルがもう無かった
改めて参照の中身を見ると、fileID も guid も入っています。
ただ、その guid を持つ .meta ファイルが、プロジェクトのどこにもありませんでした。
Unity のアセット参照は、ざっくり言うと「どのファイルか(guid)」と「そのファイルの中のどれか(fileID)」の組です。
住所録に住所は書いてあるのに、その家がもう取り壊されていた、という状態に近いですね。
guid の持ち主がいなくなった経緯は2パターンありました。
- ケース1:モーションの FBX をリネームしたときに .meta も作り直され、GUID が変わっていた
- ケース2:参照先の FBX が「一旦削除」で消されていた
ケース1 のほうが気付きにくいと思います。
ファイル自体は名前を変えて残っているので、プロジェクトを見ても「消えたもの」が無いからです。
原因:NREが原因から遠い場所で起きる理由
もう1つのポイントが、エラーの出る場所です。
参照先が無いクリップは、実行時には null として扱われます。
ところが AnimationClipPlayable.Create(graph, null) は、null を渡しても例外を出しません。
Playable は作られ、そのまま再生の流れに乗っていきます。
落ちるのは、その後でクリップを取り出して長さを読んだときです。
流れを簡略化すると、次のような形になります(説明用に省略しています)。
// クリップの参照が切れていても、ここでは例外が出ない
AnimationClipPlayable playable = AnimationClipPlayable.Create(graph, clip);
// ...再生の処理が続く...
// GetAnimationClip() が null を返し、ここで初めて NullReferenceException
float length = playable.GetAnimationClip().length;
スタックトレースが指すのは .length の行です。
クリップを渡したところは、とっくに通り過ぎています。
つまり、エラーの出た行をいくら眺めても、原因には辿り着けないということです。
見つけ方:.metaのGUID集合と突き合わせる
原因が分かれば、探し方もはっきりします。
「アセットが参照している guid のうち、どの .meta にも無いもの」を列挙すればいいわけです。
僕は Python で次の手順を書きました。
- アクション定義の .asset から、AnimationClip の参照(
{fileID: N, guid: X})を全部抜き出す Assets/とPackages/の全 .meta から guid を集めて集合にする- 1 の guid のうち、2 の集合に無いものを出力する
実行すると、数秒で全件の参照切れが出てきました。
Inspector で1つずつ開いて見ていたら、どれだけ時間がかかったか分かりません。
同じ考え方のスクリプトを、説明用に書き起こしておきます。
参照のフィールド名(例では _animationClip)は、自分のプロジェクトに合わせて変えてください。
import re
import sys
from pathlib import Path
CLIP_REF = re.compile(r"_animationClip: \{fileID: (-?\d+), guid: ([0-9a-f]{32})")
META_GUID = re.compile(r"^guid: ([0-9a-f]{32})", re.MULTILINE)
def collect_guids(roots):
guids = set()
for root in roots:
for meta in Path(root).rglob("*.meta"):
match = META_GUID.search(meta.read_text(encoding="utf-8", errors="ignore"))
if match:
guids.add(match.group(1))
return guids
def main(project):
project = Path(project)
guids = collect_guids([project / "Assets", project / "Packages"])
for asset in (project / "Assets").rglob("*.asset"):
text = asset.read_text(encoding="utf-8", errors="ignore")
for file_id, guid in CLIP_REF.findall(text):
if guid not in guids:
print(f"{asset}: fileID={file_id} guid={guid}")
if __name__ == "__main__":
main(sys.argv[1])
{fileID: 0} の未設定の参照には guid が無いので、正規表現に引っかからず、出力にも出ません。
出てくるのは「値は入っているのに持ち主がいない参照」だけ、という仕組みです。
対策:直すのはデータ側
見つかった参照は、正しいクリップへ再リンクしました。
直したのはデータだけで、C# のコードには手を入れていません。
「C# 側で null チェックを入れておけば落ちないのでは?」と思うかも知れません。
ただ、null を握りつぶすと、エラーは出ないのにモーションだけが出ないという状態になります。
バトル中に1つのアクションだけモーションが出ないバグ、正直かなり気付きにくいですよね。
赤いログが毎フレーム出ていたからこそ、今回はすぐに異常に気付けました。
データの壊れはデータで直す、という判断です。
対策:リネームは.metaごとgit mvする
再発を防ぐために、アセットのリネームのやり方も決めました。
外部ツールでファイル名だけを変え、Unity に .meta を作り直させると、GUID が変わります。
GUID が変われば、そのファイルを指していた参照は全部、持ち主のいない guid を指すことになる。
ケース1 は、まさにこの流れで起きていました。
そこで、Unity の外でリネームするときは、.meta ごと git mv することにしています。
git mv Assets/Motion/old_name.fbx Assets/Motion/new_name.fbx
git mv Assets/Motion/old_name.fbx.meta Assets/Motion/new_name.fbx.meta
.meta が同じ中身のまま付いていけば、GUID は変わりません。
ファイルの引っ越しに、表札も一緒に持っていくイメージですね。
削除のほう(ケース2)は、消す前に「どこから参照されているか」を確かめるしかありません。
先ほどのスクリプトをリネームや削除の後に流しておけば、切れた参照にその場で気付けます。
まとめ
今回の参照切れは、未設定の検索には引っかからず、エラーも原因から離れた場所で出るという、二重に見つけにくいものでした。
ポイントをまとめます。
- 参照先のファイルが消えても、参照は fileID と guid が入ったまま残る
AnimationClipPlayable.Createは null でも例外を出さず、.lengthなどを読んだ先で NRE になる- .asset の参照 guid を、全 .meta の guid 集合と突き合わせれば数秒で全件見つかる
- 直すのはデータ側。C# で null を握りつぶすと気付きにくいバグになる
- リネームは .meta ごと
git mvして GUID を守る
「設定されているはずなのに null」という NRE に出会ったら、参照先の guid がまだ生きているかを疑ってみてください。
同じところでハマっているのは、あなただけではありません。
Playable API の基本は、こちらの記事でまとめています。
👉 そろそろ始めないか?Playable API入門【Unity】

筆者のXをフォローしよう
- Unity 6000.3






