TL;DR1
VRM
をランタイムでロードするのに必要なVRMImporterContext
はIDisposable
を実装しており、Dispose
しないとメモリリークするVRMImporterContext
は生成済みのアバターGameObject
についているComponent
からは取得できなさそうVRM
のインスタンスを管理するときはGameObject
ではなくVRMImporterContext
で管理する- もしくは拡張メソッドで自作クラスをAddして
OnDestroy
のときにDispose
する
環境
説明と検証
人間牧場
わたしは人間牧場を経営したいのですが、残念ながら資金も権力もありません。
なのでかわいいアバターをいっぱい飼育することで心の慰めにしたいと思います。
というわけで、以下のようなスクリプトを書きました。StreamingAssets/vrm/
から.vrm
を読み込んでぴょんぴょんさせます。これで好きなアバターを牧場に入れられます!
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingCysharp.Threading.Tasks;usingUnityEngine;usingVRM;publicclassHumanRanch:MonoBehaviour{privatevoidStart(){InitializeCage().ContinueWith(Rearing);}#regionRanch[SerializeField]privateTransform[]_cage;[SerializeField]privateRuntimeAnimatorController_animator;privateGameObject[]_liveStocks;privateasyncUniTaskInitializeCage(){// wait for first updateawaitUniTask.Yield(PlayerLoopTiming.Update);_liveStocks=newGameObject[_cage.Length];}privateconstfloatProductionCycle=3f;privatereadonlyVector3CageGap=newVector3(0,-1,0);privateasyncUniTaskRearing(){vartoken=this.GetCancellationTokenOnDestroy();while(!token.IsCancellationRequested){varchangeIndex=UnityEngine.Random.Range(0,_liveStocks.Length);if(_liveStocks[changeIndex]==null){varcontext=awaitLoadVRM();varavatar=context.Root;avatar.GetComponent<Animator>().runtimeAnimatorController=_animator;avatar.transform.SetPositionAndRotation(_cage[changeIndex].position+CageGap,_cage[changeIndex].rotation);_liveStocks[changeIndex]=avatar;context.ShowMeshes();}else{Destroy(_liveStocks[changeIndex]);_liveStocks[changeIndex]=null;}awaitUniTask.Delay(TimeSpan.FromSeconds(ProductionCycle),cancellationToken:token);}}#endregion#regionLoadVRMprivatereadonlyDictionary<string,byte[]>_VRMBufferCache=newDictionary<string,byte[]>();privatestring[]_VRMFullPaths=null;privatestaticstring[]SearchStreamingAssetsVRM(){vardirPath=Path.Combine(Application.streamingAssetsPath,"vrm");if(!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}returnDirectory.GetFiles(dirPath,"*.vrm");}privateasyncUniTask<VRMImporterContext>LoadVRM(){if(_VRMFullPaths==null){_VRMFullPaths=SearchStreamingAssetsVRM();}varpath=_VRMFullPaths[UnityEngine.Random.Range(0,_VRMFullPaths.Length)];if(!_VRMBufferCache.ContainsKey(path)){awaitUniTask.SwitchToThreadPool();varbuffer=File.ReadAllBytes(path);awaitUniTask.SwitchToMainThread();_VRMBufferCache[path]=buffer;}varcontext=awaitLoadVRMFromBuffer(_VRMBufferCache[path]);returncontext;}privatestaticasyncUniTask<VRMImporterContext>LoadVRMFromBuffer(byte[]buffer){varcontext=newVRMImporterContext();context.ParseGlb(buffer);awaitcontext.LoadAsyncTask();returncontext;}#endregion}
人間牧場そのいち pic.twitter.com/r6An34C7wd
— ねこみみだいまおう (@CatEarEvilKing) July 18, 2020
経営の行き詰まり
適度に増えて適度に減って、永遠に眺めていられますね。
ところがだんだん動きがカクついてきて、遂にはエディタがクラッシュしてしまいます。誰の仕業でしょうか。Profiler
で調べたところ、どうやらメモリリークしているようです。心当たりがあるとすれば破棄処理とファイル読み込みの部分ですが、特におかしいところはありません。
Destroy(_liveStocks[changeIndex]);
varbuffer=File.ReadAllBytes(path);
となると残っているのはロードするときに使っているVRMImporterContext
です。IDisposable
を実装している。実にあやしい。というわけで使い終わったらDispose
します。
context.ShowMeshes();// 即座にDisposecontext.Dispose();
そして後には誰もいない牧場が残りました。
経営再開
当然ですが、context
をDispose
したタイミングで生成したアバターも破棄されてしまうみたいです。
そして困ったことに、生成済みのアバターはVRMImporterContext
を取得できるクラスがくっついていません。2
なのでアバターをGameObject
ではなくVRMImporterContext
単位で管理することにします。
// アバターを管理する配列// beforeprivateGameObject[]_liveStocks;// afterprivateVRMImporterContext[]_liveStocks;// アバターの破棄処理// beforeDestroy(_liveStocks[changeIndex]);// after_liveStocks[changeIndex].Dispose();
人間牧場そのに pic.twitter.com/Y5IYO52zwC
— ねこみみだいまおう (@CatEarEvilKing) July 18, 2020
できました!
人間牧場の復活です!
いろんなアバターが増えたり減ったりしながら永遠にぴょんぴょんします!
更にもう一歩
とはいえリソースの開放忘れはよくあるバグの原因の一つです。
なので管理して自分でDispose
するのではなく、
拡張メソッドでアバターがOnDestroy
されたとき、自動的にDispose
されるようにします。
usingUnityEngine;namespaceVRM.Extension{publicclassVRMAutoDisposer:MonoBehaviour{publicVRMImporterContextContext;privatevoidOnDestroy(){Context?.Dispose();}}publicstaticclassVRMAutoDisposerExtension{publicstaticvoidAutoDispose(thisVRMImporterContextcontext){vardisposer=context.Root.AddComponent<VRMAutoDisposer>();disposer.Context=context;}}}
// 拡張メソッドなので、こんな感じで読んでおけば自動でDisposeしてくれるcontext.AutoDispose();context.ShowMeshes();
まとめ
自分がざっと見た限りではそれらしい情報が見当たらなかったのでこの記事を書きました。
本来ならば公式にissueでも立てて要望を出すかなにかしようかとも思ったんですが、もうすぐ1.0.0版とかが出るみたいだし、いいかなって。
個人的には拡張メソッドの方式が好きです。AssetBundle
でもそうですが、リソース管理とかめんどくさすぎて自分でやりたくないんですよね。
拡張Disposeと人間牧場のコードはgist
にあげておいたので好きに使ってください。
あと、gist
に置いてあるコードのライセンスをつけておきました3。CC0ですが、作者を表記してくれたらうれしいです。
おしまい。
参考記事など
記事
使用アセット
ニコニ立体ちゃん (VRM)
ヴィータ
Bird Cage created by Poly by Google
Basic Motions FREE Pack