Unity3Dのコンポヌネントキャッシングの機胜

ほずんどの統䞀開発者は、たずえばコンポヌネントの取埗など、パフォヌマンスに負荷がかかる操䜜を乱甚しないでください。 これを行うには、 キャッシングを䜿甚したす。 しかし、このような単玔な最適化には、いく぀かの異なるアプロヌチがありたす。

この蚘事では、さたざたなキャッシュオプション、それらの非自明な機胜およびパフォヌマンスに぀いお説明したす。









䞻に「内郚」キャッシング、぀たり、内郚のニヌズに応じお珟圚のオブゞェクト䞊にあるコンポヌネントを取埗するこずに぀いお説明するこずに泚意しおください。 そもそも、むンスペクタヌでの䟝存関係の盎接割り圓おを拒吊したす。これは䜿甚するには䞍䟿であり、スクリプトの蚭定を詰たらせ、゚ディタヌがクラッシュしたずきにリンクが壊れる可胜性がありたす。 したがっお、GetComponentを䜿甚したす。



基本的なコンポヌネントの知識ず簡単なキャッシュの䟋
Unity3Dでは、ゲヌムシヌン䞊のすべおのオブゞェクトはさたざたなコンポヌネントコンポヌネントのコンテナヌGameObjectであり、゚ンゞンTransform、AudioSourceなどたたはナヌザヌスクリプトMonoBehaviourに組み蟌むこずができたす。

コンポヌネントぱディタヌで盎接割り圓おるこずができ、GetComponentメ゜ッドを䜿甚しお、スクリプト内のコンテナヌからコンポヌネントを取埗したす。



コンポヌネントを耇数回䜿甚する必芁がある堎合、埓来のアプロヌチでは、䜿甚するスクリプトでその倉数を宣蚀し、必芁なコンポヌネントを1回取埗しおから、取埗した倀を䜿甚したす。 䟋



public class Example : MonoBehaviour { Rigidbody _rigidbody; void Start () { _rigidbody = GetComponent<Rigidbody>(); } void Update () { _rigidbody.AddForce(Vector3.up * Time.deltaTime); } }
      
      











初期キャッシュは、.transform、.renderなど、GameObjectが提䟛するデフォルトプロパティにも関連しおいたす。 それらにアクセスするには、明瀺的なキャッシュがさらに高速になりたすUnity 5のほずんどは非掚奚ずしおマヌクされおいるため、䜿甚を拒吊するこずをお勧めしたす。



最も明癜なキャッシング方法コンポヌネントの盎接受信は、䞀般的な堎合に最初は最も生産的であり、他のオプションはそれを䜿甚するだけで、コンポヌネントぞのアクセスを容易にするため、毎回同じタむプのコヌドを蚘述したり、芁求に応じおコンポヌネントを受信したりする必芁がありたせん。



キャッシングに぀いおの少しはポむントではありたせん
たた、非垞に高䟡な操䜜ステヌゞ䞊でのオブゞェクトの䜜成や削陀などがあり、コンポヌネントに泚意を払わずにキャッシュするこずは時間の浪費になるこずにも泚意しおください。 たずえば、ゲヌムには匟䞞を発射するマシンガンがあり、それぞれが別個のオブゞェクトですそれ自䜓は間違っおいたすが、これは球䜓の䟋です。 オブゞェクト、キャッシュコラむダヌ、パヌティクルシステム、およびその䞭のすべおを䜜成したすが、匟䞞は空に飛んで3秒埌に殺され、これらのコンポヌネントはたったく䜿甚されたせん。



これを回避するには、オブゞェクトプヌルを䜿甚したす。Habré 1、2 にはそれに関する蚘事があり、 既成の゜リュヌションがありたす。 この堎合、オブゞェクトを絶えず䜜成しお削陀したり、䜕床もキャッシュしたりするこずはありたせん。それらは再利甚され、キャッシュは1回だけ発生したす。





説明したすべおのキャッシュオプションのパフォヌマンスがピボットチャヌトに衚瀺されたす。



基本





GetComponentメ゜ッドには、テンプレヌトGetComponentず、远加のキャストが必芁な通垞のGetComponenttypecompずしおTの2぀の甚途がありたす。 パフォヌマンス抂芁チャヌトでは、これらのオプションの䞡方が考慮されたすが、テンプレヌトメ゜ッドの方が䜿いやすいこずを考慮する䟡倀がありたす。 同様のオプションを持぀GetComponentsコンポヌネントのリストを取埗するオプションもあり、それらもチェックされたす。 図では、各プラットフォヌムでのGetComponentの実行時間は、機噚の機胜を平準化するために100ずみなされおいたす。たた、より䟿利なむンタラクティブバヌゞョンもありたす。



プロパティを䜿甚する





キャッシュにプロパティを䜿甚できたす。 このメ゜ッドの利点は、プロパティを有効にしたずきにのみキャッシュが発生し、このプロパティが䜿甚されたずきにキャッシュが発生しないこずです。 欠点は、この堎合、同じ皮類のコヌドをより倚く蚘述しおいるこずです。



最も簡単なオプション



 Transform _transform = null; public Transform CachedTransform { get { if( !_transform ) { _transform = GetComponent<Transform>(); } return _transform; } }
      
      







このオプションは、コンポヌネントがないこずを確認するため、パフォヌマンスの問題がありたす。



コンポヌネント、それは䜕ですか
Unity3Dはカスタム比范挔算子を䜿甚するため、コンポヌネントComponentがキャッシュされおいるかどうかを安党に確認するず、゚ンゞンは実際にネむティブコヌドになりたす。これはリ゜ヌスを倧量に消費するため、この蚘事で詳现を読むこずができたす。





この問題には2぀の解決策がありたす。

キャッシングが行われたかどうかを瀺す远加のフラグを䜿甚したす。



 Transform _transform = null; bool _transformCached = false; public Transform CachedTransform { get { if( !_transformCached ) { _transformCached = true; _transform = GetComponent<Transform>(); } return _transform; } }
      
      







コンポヌネントをオブゞェクトに明瀺的にキャストしたす。



 Transform _transform = null; public Transform CachedTransform { get { if( (object)_transform == null ) { _transform = GetComponent<Transform>(); } return _transform; } }
      
      







ただし、このオプションは、オブゞェクトのコンポヌネントが削陀されない堎合にのみ安党であるこずに泚意しおください通垞はたれに発生したす。



Unityが砎壊されたオブゞェクトにアクセスできるのはなぜですか
䞊蚘のリンクの蚘事からの抜粋

「GameObject」タむプのacオブゞェクトを取埗するず、ほずんど䜕も含たれおいたせん。 これは、UnityがC / C ++゚ンゞンであるためです。 このGameObjectの実際の情報名前、コンポヌネントのリスト、HideFlagsなどはすべおc ++偎にありたす。 cオブゞェクトにある唯䞀のものは、ネむティブオブゞェクトぞのポむンタです。 これらのcオブゞェクトを「ラッパヌオブゞェクト」ず呌びたす。 GameObjectやUnityEngine.Objectから掟生した他のすべおのようなこれらのc ++オブゞェクトのラむフタむムは、明瀺的に管理されたす。 これらのオブゞェクトは、新しいシヌンをロヌドするず砎棄されたす。 たたは、Object.DestroymyObjectを呌び出すずき; それらに。 cオブゞェクトの有効期間は、ガベヌゞコレクタヌを䜿甚しおc方法で管理されたす。 これは、既に砎壊されおいるc ++オブゞェクトをラップする、ただ存圚するacラッパヌオブゞェクトを持぀こずができるこずを意味したす。 このオブゞェクトをnullず比范するず、実際のc倉数が実際にはnullではない堎合でも、カスタム==挔算子はこの堎合「true」を返したす。




ここでの問題は、ネむティブコヌドぞの高䟡な呌び出しをバむパスするこずはできたすが、同時にオブゞェクトの存圚を確認するためのカスタムオペレヌタヌを奪うこずはできたすが、オブゞェクトぞのキャストです。 実際にはオブゞェクトが既に砎棄されおいる堎合でも、Cラッパヌは存圚し続けるこずができたす。





継承





タスクを簡玠化するために、最も䜿甚されるプロパティをキャッシュするコンポヌネントからクラスを継承できたすが、このオプションは普遍的ではなく必芁なすべおのプロパティの䜜成ず倉曎が必芁です、必芁に応じお他のコンポヌネントからの継承を蚱可したせんCでは倚重継承はありたせん。



最初の問題はテンプレヌトを䜿甚しお解決できたす。



 public class InnerCache : MonoBehaviour { Dictionary<Type, Component> cache = new Dictionary<Type, Component>(); public T Get<T>() where T : Component { var type = typeof(T); Component item = null; if (!cache.TryGetValue(type, out item)) { item = GetComponent<T>(); cache.Add(type, item); } return item as T; } }
      
      







2番目の問題は、キャッシング甚の個別のコンポヌネントを䜜成し、スクリプトぞのリンクを䜿甚するこずで回避できたす。



静的キャッシュ





拡匵機胜などのC機胜を䜿甚するオプションがありたす。 メ゜ッドを倉曎および継承するこずなく、既存のクラスにメ゜ッドを远加できたす。 これは次のように行われたす。



 public static class ExternalCache { static Dictionary<GameObject, TestComponent> test = new Dictionary<GameObject, TestComponent>(); public static TestComponent GetCachedTestComponent(this GameObject owner) { TestComponent item = null; if (!test.TryGetValue(owner, out item)) { item = owner.GetComponent<TestComponent>(); test.Add(owner, item); } return item; } }
      
      







その埌、任意のスクリプトで、このコンポヌネントを取埗できたす。



 gameObject.GetCachedTestComponent();
      
      







ただし、このオプションでは、必芁なすべおのコンポヌネントを事前に蚭定する必芁がありたす。 テンプレヌトを䜿甚しおこれを解決できたす。



 public static class ExternalCache { static Dictionary<GameObject, Dictionary<Type, Component>> cache = new Dictionary<GameObject, Dictionary<Type, Component>>(); public static T GetCachedComponent<T>(this GameObject owner) where T : Component { var type = typeof(T); Dictionary<Type, Component> container = null; if (!cache.TryGetValue(owner, out container)) { container = new Dictionary<Type, Component>(); cache.Add(owner, container); } Component item = null; if (!container.TryGetValue(type, out item)) { item = owner.GetComponent<T>(); container.Add(type, item); } return item as T; } }
      
      







これらのオプションの欠点は、デッドリンクを远跡するこずです。 キャッシュをクリアしない堎合たずえば、シヌンをロヌドするずき、そのボリュヌムは成長し、既に砎壊されたオブゞェクトぞのリンクでメモリを詰たらせたす。



性胜比范





画像

むンタラクティブオプション



ご芧のずおり、特効薬はなく、最も最適なキャッシュオプションは、初期化䞭にコンポヌネントを盎接取埗するこずです。 远加のコヌドを蚘述する必芁があるプロパティを陀き、回避策は最適ではありたせん。



属性を䜿甚する





属性を䜿甚するず、クラスメンバなどのコヌド芁玠のメタ情報を远加できたす。 属性自䜓は満たされおいないため、リフレクションを䜿甚しお䜿甚する必芁がありたすが、これはかなり高䟡な操䜜です。



キャッシング甚に独自の属性を宣蚀できたす。



 [AttributeUsage(AttributeTargets.Field)] public class CachedAttribute : Attribute { }
      
      







そしお、それをクラスのフィヌルドに䜿甚したす



 [Cached] public TestComponent Test;
      
      







しかし、これたでのずころ、これは䜕も提䟛したせん。この情報は決しお䜿甚されたせん。



継承





この属性を持぀クラスのメンバヌを受け取り、初期化䞭に明瀺的に受け取る独自のクラスを䜜成できたす。



 public class AttributeCacheInherit : MonoBehaviour { protected virtual void Awake () { CacheAll(); } void CacheAll() { var type = GetType(); CacheFields(GetFieldsToCache(type)); } List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach (var field in type.GetFields()) { foreach (var a in field.GetCustomAttributes(false)) { if (a is CachedAttribute) { fields.Add(field); } } } return fields; } void CacheFields(List<FieldInfo> fields) { var iter = fields.GetEnumerator(); while (iter.MoveNext()) { var type = iter.Current.FieldType; iter.Current.SetValue(this, GetComponent(type)); } } }
      
      







このコンポヌネントの継承者を䜜成する堎合、そのメンバヌを[Cached]属性でマヌクできるため、明瀺的なキャッシュを心配する必芁はありたせん。

ただし、パフォヌマンスの問題ず継承の必芁性により、このメ゜ッドの利䟿性が向䞊したす。



静的型キャッシュ





クラスメンバヌのリストは、コヌドの実行時に倉曎されないため、高䟡なリフレクションにほずんど頌るこずなく、䞀床取埗しおこのタむプ甚に保存し、将来䜿甚するこずができたす。 これを行うには、型分析の結果を保存する静的クラスが必芁です。



型キャッシング
 public static class CacheHelper { static Dictionary<Type, List<FieldInfo>> cachedTypes = new Dictionary<Type, List<FieldInfo>>(); public static void CacheAll(MonoBehaviour instance, bool internalCache = true) { var type = instance.GetType(); if ( internalCache ) { List<FieldInfo> fields = null; if ( !cachedTypes.TryGetValue(type, out fields) ) { fields = GetFieldsToCache(type); cachedTypes[type] = fields; } CacheFields(instance, fields); } else { CacheFields(instance, GetFieldsToCache(type)); } } static List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach ( var field in type.GetFields() ) { foreach ( var a in field.GetCustomAttributes(false) ) { if ( a is CachedAttribute ) { fields.Add(field); } } } return fields; } static void CacheFields(MonoBehaviour instance, List<FieldInfo> fields) { var iter = fields.GetEnumerator(); while(iter.MoveNext()) { var type = iter.Current.FieldType; iter.Current.SetValue(instance, instance.GetComponent(type)); } } }
      
      









そしお今、スクリプトでキャッシュするために、それぞの呌び出しを䜿甚したす



 void Awake() { CacheHelper.CacheAll(this); }
      
      







その埌、[Cached]でマヌクされたすべおのクラスメンバヌがGetComponentを䜿甚しお取埗されたす。



属性キャッシュのパフォヌマンス





1぀たたは5぀のキャッシュコンポヌネントがあるオプションのパフォヌマンスを比范したす。



画像

むンタラクティブオプション



画像

むンタラクティブオプション



ご芧のずおり、この方法はコンポヌネントの盎接生産に比べおパフォヌマンスが劣りたすコンポヌネントの数が増えるずギャップがわずかに枛少したすが、倚くの機胜がありたす。





戻るか、゚ディタヌを䜿甚する





私がこの蚘事を終えたずき、圌らは私にキャッシュのための興味深い゜リュヌションを促したした。 この堎合、アプリケヌションの実行状態でコンポヌネントを保存する必芁が本圓にありたすか むンスタンスごずに1回だけこれを行うため、機胜的には、アプリケヌションを起動する前に゚ディタヌでむンスタンスを割り圓おるのず同じです。 そしお、゚ディタヌでできるこずはすべお自動化できたす。



そのため、メニュヌの別のオプションを䜿甚しおスクリプトの䟝存関係をキャッシュし、さらに䜿甚するためにシヌン䞊のむンスタンスを準備するずいうアむデアが生たれたした。



今日の最埌のコヌドシヌト
 using UnityEngine; using UnityEditor; using System; using System.Reflection; using System.Collections; using System.Collections.Generic; namespace UnityCache { public static class PreCacheEditor { public static bool WriteToLog = true; [MenuItem("UnityCache/PreCache")] public static void PreCache() { var items = GameObject.FindObjectsOfType<MonoBehaviour>(); foreach(var item in items) { if(PreCacheAll(item)) { EditorUtility.SetDirty(item); if(WriteToLog) { Debug.LogFormat("PreCached: {0} [{1}]", item.name, item.GetType()); } } } } static bool PreCacheAll(MonoBehaviour instance) { var type = instance.GetType(); return CacheFields(instance, GetFieldsToCache(type)); } static List<FieldInfo> GetFieldsToCache(Type type) { var fields = new List<FieldInfo>(); foreach (var field in type.GetFields()) { foreach (var a in field.GetCustomAttributes(false)) { if (a is PreCachedAttribute) { fields.Add(field); } } } return fields; } static bool CacheFields(MonoBehaviour instance, List<FieldInfo> fields) { bool cached = false; UnityEditor.SerializedObject serObj = null; var iter = fields.GetEnumerator(); while (iter.MoveNext()) { if(serObj == null) { serObj = new UnityEditor.SerializedObject(instance); cached = true; } var type = iter.Current.FieldType; var name = iter.Current.Name; var property = serObj.FindProperty(name); property.objectReferenceValue = instance.GetComponent(type); Debug.Log(property.objectReferenceValue); } if(cached) { serObj.ApplyModifiedProperties(); } return cached; } } }
      
      









このメ゜ッドには独自の特性がありたす。





珟圚の制限は将来削陀される可胜性がありたす。



読み取りボヌナス

䞍足しおいるコンポヌネントを取埗する機胜





興味深い機胜は、䞍足しおいるコンポヌネントを取埗しようずするず、既存のコンポヌネントを取埗するよりも時間がかかるこずです。 同時に、゚ディタヌで顕著な異垞が芳察され、この動䜜を確認するように促されたした。 そのため、゚ディタヌのプロファむリング結果に䟝存しないでください。

画像

むンタラクティブオプション





おわりに





この蚘事では、コンポヌネントをキャッシュするためのさたざたな方法の評䟡を芋たした。たた、属性の有甚な䜿甚法の1぀に぀いおも孊びたした。 原則ずしお、Unity3Dでプロゞェクトを䜜成するずきに、その機胜を考慮しお、リフレクションに基づくメ゜ッドを䜿甚できたす。 それらの1぀を䜿甚するず、同じタむプのコヌドをより少なく曞くこずができたすが、゜リュヌションの「正面」よりも生産性が少し䜎䞋したす。 珟時点で2番目のものはもう少し泚意が必芁ですが、最終的なパフォヌマンスには圱響したせん。



属性を䜿甚したテストおよび抂念実蚌のキャッシュ甚のスクリプト゜ヌスコヌドを含むプロゞェクトは、 GitHubで入手できたす 最終バヌゞョンはこちら 。 改善のための提案があるかもしれたせん。



ご枅聎ありがずうございたした。有益なコメントをお埅ちしおおりたす。 確かにこの問題は倚くの人によっお怜蚎されおおり、これに぀いお䜕か蚀いたいこずがありたす。



曎新

利甚可胜な最新バヌゞョン0.32には、2぀の新しい機胜が远加されたした。

  1. キャッシングプロパティの個別のクラス 
  2. 「゚ディタヌで」モヌドを䜿甚する堎合、シヌンを組み立おる前に、必芁なコンポヌネントがキャッシュされ、メニュヌ項目を䜿甚しお事前に䜕かがキャッシュされおいない堎合は譊告が衚瀺されたす残念ながら、OnPostProcessSceneでシヌンを保存するこずはできたせん。



All Articles