Unity3dのPropertyDrawerの最適化

前回の記事では、OneLine-PropertyDrawerについて説明しました。これにより、1行にネストされたオブジェクトを描画できます。







今回は、インスペクタで数百行からなるデータベースを自由に編集できるようにコードを最適化する方法を説明します。













注意してください、カットの下にたくさんのGIFや写真があります!







問題の本質



標準インスペクターでは、複雑な構造を持つすべてのフィールドが折りたたまれて描画されます。これにより、かなりのリソースが消費され、数百のオブジェクトの配列を簡単に描画できます。







デフォルト







プロファイラーを見ると、配列の100要素をレンダリングするために4.3 msが表示されます。







デフォルト







OneLineは、インスペクターでマウスを何度もクリックすることから開発者を救うと考えられており、ネストされたフィールドはすべてすぐに描画されます。 同時に、レンダリング中に、要素の位置のかなり高価な計算が実行されます。







シプリーヴ同志の報告書「パフォーマンス:あなたに私の名前は何ですか?」 コードのパフォーマンスと複雑さのグラフを提供します(AからEへの曲線に沿った時間の移動):













チャートの説明
これはパラメトリックグラフです。時間はポイント「A」からポイント「B」、「C」、「D」、「E」に流れます。 縦座標軸に沿ってパフォーマンスがあり、横座標軸に沿って抽象的なコードの複雑さがあります。



通常、すべては、ゆっくりとしかしゆっくりと動作するプロトタイプをサイクリングする人々から始まります。 私たちは自転車が自重で崩れないように自転車で走るので、それは非常に複雑です。



最適化が開始された後、ゆっくりと、さまざまな部分の書き換えが開始されます。 このグリーンゾーンにいる開発者は、通常、プロファイラーを使用して、明らかにひどく書かれたコードを書き換えます。 これにより、同時にコードの複雑さが軽減され(不良部分を排除するため)、パフォーマンスが向上します。



ポイント「B」で、プロジェクトは「主観的」な「美」のピークに達します。これは、パフォーマンスが良好で、製品のすべてが優れているように見えるときです。



さらに、開発者がより高いパフォーマンスが必要な場合は、より正確なプロファイラーを取得し、適切なワークロードを作成し、ナットを慎重に締めると、イエローゾーンに移動します。 このプロセスで、彼らは生産性のためでなければ彼らがしないであろうことをそこで行います。



さらに必要な場合は、開発者が生産性の最後のパーセントを得るために製品をひねり始めると、プロジェクトはレッドゾーンになります。 このゾーンで何をするかは明確ではありません。 少なくともこの会議には、レシピがあります-JPoint / JokerConf / JBreakに行き、製品開発者を拷問して、下層の湾曲を繰り返すコードの書き方を教えてください。 原則として、赤いゾーンには、下層で発生する問題を繰り返すものがあるからです。

グラフは記事全体と同様に非常に優れているため、読むことを強くお勧めします。







AからBへの移動は、かなり退屈で些細なことです。 私たちの記事では、まずBからCへの動きについて説明し、次にUnityの機能に適した松葉杖/カウンターウェイトを求めてDをさまよいます。







「だれかが幼稚園ですか?そして、Dの周りをさまようことを何と呼びますか? CLRおよびIL2CPP機能はどこで使用されますか?すべてのルールによるベンチマークはどこにありますか?0.05%の速度を得るためのコード再複雑化はどこにありますか?」







おそらく、この記事はこの読者向けではありません。 私が書いていることはかなり単純で、Unityでの開発キャリアを始める若い読者をより対象としています。 したがって、実装を簡素化し、PropertyDrawerを最適化するアプローチを説明する可能性が高くなります。







同僚ゲームオブジェクトのフットプリントでグリッドを表示するPropertyDrawerを書いているのを見たことがあります。 彼はその場で画像を生成し、ピクセルごとに色を変え、さらに多くの恐ろしいものを作りました。 最終的に、そのようなフィールドを1つ表示すると、エディターでFPSが大幅に描画されます。







もちろん、これは「誰かが愚かなことをするなら、それはできる」という意味ではありません。 ナンセンスをやらざるを得ず、その後最適化するだけです。







最適化まではどうでしたか。

純粋な







同じ100個の要素には104ミリ秒、つまり約24倍の時間が必要でした。 この速度は一言で説明できます。







純粋な







できることをキャッシュします



OneLine自体は、フィールドの数、タイプ、およびそれらに掛かっている属性に応じてフィールドの位置を計算します(たとえば[Width]



または[Weight]



)。 そして、OnGUIを呼び出すたびに、これらの計算が再び行われます。







同じ状況が抽象PropertyDrawerでもあります。これは非常に遅く動作するため、プログラマVasyaに休息を与えません。 明らかに、遅い場合、それはおそらく同じ計算をたくさん行うことを意味します。







解決済み:キャッシュします!







続行するには、各タイプの1つのインスペクターウィンドウに対して、このタイプのすべてのフィールドを描画するPropertyDrawerオブジェクトが1つだけ作成され、オブジェクトを変更すると破棄されます。







これは、画面上に同じタイプの2つのフィールドがある場合、同じPropertyDrawerオブジェクトがそれらを描画することを意味します。 これにより、キャッシュが多少複雑になります。







一方、インスペクターごとに1つのPropertyDrowerがある場合、キーがproperty.propertyPath



であるDictionary<string, YourData>



に任意のデータを保存できproperty.propertyPath









最後に、単純なキャッシュを取得します。







 public delegate <T> T CalculateValue (SerializedProperty property); public class PropertyDrawerCache<T> { private Dictionary<string, T> cache; private CalculateValue<T> calculate; public PropertyDrawerCache(CalculateValue<T> calculate){ cache = new Dictionary<string, T>(); this.calculate = calculate; } public T this[SerializedProperty property] { get { T result = null; if (cache.TryGetValue(string, out result)){ result = calculate(property); cache.Add(property.propertyPath, result); } return result; } } }
      
      





キャッシュで得たもの。

キャッシュ







最初の呼び出しは同じくらいゆっくり実行されますが、その後の呼び出しはすべて2倍高速です。 悪くはないが、それでも不快に感じた。







最初の呼び出しで:

キャッシュ-1







後続の呼び出しで:

キャッシュ-2







OneLineの問題:ネストされた配列



キャッシュの主な問題:最新の状態に保つ必要があります。 クラスフィールドの計算された位置をキャッシュし、それらを変更しないと見なします(実行時にクラス構造は変更されません)。 ただし、1つの点を考慮していません。OneLineは、子配列のすべての要素を文字列に合わせます。







これは見た目です







幸いなことに、この問題はキャッシュされたアイテムの位置をフラッシュすることで解決されます。







 public void DrawPlusButton(Rect rect, SerializedProperty array) { if (GUI.Button(rect, "+")) { array.InsertArrayElementAtIndex(array.arraySize); ResetCurrentElementCache(); } }
      
      





OneLineの問題:ルート配列



キャッシュの主な問題:最新の状態に保つ必要があります。

OneLineの使用中に、興味深いキャッシュバグに遭遇しました。







それはどのように見えますか













同じ言葉:









解決策:各配列のサイズを記憶し、次の図面で配列が変更されたかどうかを確認します。 読み取りを簡素化するために、OneLineがScriptableObjectのルートの配列にハングアップする場合にのみソリューションを提供します。







同時に、コード内のすばらしいIsReallyArray



関数に読者の注意を引きます。







 private Dictionary<string, int> arraysSizes = new Dictionary<string, int>(); public bool IsArraySizeChanged(SerializedProperty arrayElement){ var arrayName = arrayElement.propertyPath.Split('.')[0]; var array = arrayElement.serializedObject.FindProperty(arrayName); return IsReallyArray(array) && IsRealArraySizeChanged(arrayName, array.arraySize); } private bool IsReallyArray(this SerializedProperty property){ return property.isArray && property.propertyType != SerializedPropertyType.String; } private bool IsRealArraySizeChanged(string arrayName, int currentArraySize){ if (! arraysSizes.ContainsKey(arrayName)){ arraysSizes[arrayName] = currentArraySize; } else if (arraysSizes[arrayName] != currentArraySize){ arraysSizes[arrayName] = currentArraySize; return true; } return false; }
      
      





できることを描かない



配列には100個の要素があり、画面には20〜25個(gif)しか表示されていないため、別の標準的な最適化が行われます: カリング :画面に収まらないものは描画しません!







これを行うには、現在のウィンドウのサイズとScrollViewの位置を知る必要があります。 ファウル(こんにちは、 Unity-Decompiled )の寸前のソリューションを提供します。







 internal class InspectorUtil { private const string INSPECTOR_WINDOW_ASSEMBLY_QUALIFIED_NAME = "UnityEditor.InspectorWindow, UnityEditor, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"; private const string INITIALIZATION_ERROR_MESSAGE = @"OneLine can not initialize Inspector Window Utility. You may experience some performance issues. Please create an issue on https://github.com/slavniyteo/one-line/ and we will repair it. "; private bool enabled = true; private MethodInfo getWindowPositionInfo; private FieldInfo scrollPositionInfo; private object window; private float lastWindowWidth; public InspectorUtil() { try { Initialize(); enabled = true; } catch (Exception ex){ //      //     Unity  , //   . enabled = false; Debug.LogError(INITIALIZATION_ERROR_MESSAGE + ex.ToString()); } } private void Initialize(){ var inspectorWindowType = Type.GetType(INSPECTOR_WINDOW_ASSEMBLY_QUALIFIED_NAME); window = inspectorWindowType .GetField("s_CurrentInspectorWindow", BindingFlags.Public | BindingFlags.Static) .GetValue(null); scrollPositionInfo = inspectorWindowType .GetField("m_ScrollPosition"); getWindowPositionInfo = inspectorWindowType .GetProperty("position", typeof(Rect)) .GetGetMethod(); } public bool IsOutOfScreen(Rect position){ if (! enabled) { return false; } var scrollPosition = (Vector2) scrollPositionInfo.GetValue(window); var windowPosition = (Rect) getWindowPositionInfo.Invoke(window, null); bool above = (position.y + position.height) < scrollPosition.y; bool below = position.y > (scrollPosition.y + windowPosition.height); return above || below; } }
      
      





次に使用します:







 public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (inspectorUtil.IsOutOfScreen(position)) { return; } <..> }
      
      





このコードは、オブジェクトがインスペクターウィンドウに描画されている場合にのみ機能します。 カスタムウィンドウでOneLine(または別のPropertyDrawer)を使用する場合、この最適化は機能しません。 もちろん、その理由は、画面のスクロールの実装と密接な関係にあります。 この場合、普遍的なツールを作成することは不可能です。 ただし、コードは非常にシンプルで、いつでもニーズに合わせて調整できます。







カリングで何を得たのか。

カリング







それは非常に異なって感じられ、スクロールははるかに滑らかで、FPSの数はウィンドウの高さに大きく依存します。







カリング







カリングの問題



前のgifをよく見ると、わかります。 スクロールすると、要素のフォーカスが上端に「固定」され、画面を超えないようになります。







明らかに、Unityはイベント処理の開始からのシーケンス番号に基づいてアクティブな要素を記憶します。 そして、カリングは目に見えない要素をすべて破棄するため、この順序に違反します。







この問題は非常に簡単に解決できます。ホイールの動きごとに制御からフォーカスをリセットします。







 if (Event.current.type == EventType.ScrollWheel){ EditorGUI.FocusTextInControl(""); }
      
      





しかし、このソリューションが十分であると確信していないため、このソリューションをOneLineに統合していません。 いつかもっと掘り下げて、もう少し良くするかもしれません。







すべてをまとめる



私たちは何を得ました。

一杯







最初の呼び出しで:

フル-1







後続の呼び出しでは、すべてがはるかに速く発生します。

フル-2







比較のために、1つのウィンドウ内のすべてのオプション:

すべて







作業の結果、ライブラリが大幅に高速化されました。 レンダリング要素の時間はウィンドウの高さに大きく依存するため、「10倍の加速」を記述しません(ウィンドウを2倍にすると、時間もほぼ2倍になります)。 しかし、この結果は私に合っています。










私はHabréですでに習慣的な広告でブロックを挿入しません:必要な人、それは見つけます。







すべての人に良い!








All Articles