IMGUIおよびUnity Editorのカスタマイズ。 パート2

新しいUnity UIシステムのリリースから1年以上が経過したため、Richard Fineはその前身であるIMGUIについて書くことにしました。 マテリアルの最後の部分では、MyCustomSliderの作成方法を調べました。 カスタムエディター、PropertyDrawers、EditorWindowsなどで使用できるシンプルなIMGUI機能要素がありましたが、それだけではありません。 記事の第2部では、その機能を拡張する方法、たとえば、マルチ編集機能を追加する方法について説明します。









制御機能



もう1つの重要な点は、IMGUIとシーンビューコンポーネントの関係です。 オブジェクトを移動、回転、拡大縮小できる直交矢印、リング、線などの補助UI要素に精通している必要があります。 これらの要素は制御機能と呼ばれます。 興味深いことに、これらはIMGUIでもサポートされています。



Unity Editor / EditorWindowsで使用されるGUIおよびEditorGUIクラスの標準要素は2次元ですが、コントロール識別子やイベントタイプなどのIMGUIの基本概念はUnityエディターまたは2Dにバインドされていません。 3次元のシーンビュー要素の制御機能は、GUIとEditorGUIを置き換えるHandlesクラスによって表されます。 たとえば、1つの整数を編集するための要素を作成するEditorGUI.IntField関数の代わりに、シーンビューのインタラクティブな矢印を使用してVector3値を編集できる関数を使用できます。



Vector3 PositionHandle(Vector3 position, Quaternion rotation);
      
      







制御機能を使用して、カスタムインターフェイス要素を作成することもできます。 この場合、マウスとの相互作用はやや複雑ですが、基本的な概念はエディター要素を作成する場合と同じままです。3次元環境では、長方形でカーソルの座標をチェックするだけでは不十分です。 ここでは、 HandleUtilityクラスが便利です。



OnSceneGUI関数をエディターのユーザークラスに登録すると、エディターでコントロール関数を使用し、シーンビューでGUIから関数を使用できます。 これを行うには、追加の作業を行う必要があります。GLマトリックスをインストールするか、 Handles.BeginGUI()およびHandles.EndGUI()を使用してコンテキストを設定します。



状態オブジェクト



MyCustomSliderの場合、2つのことを追跡する必要がありました:スライダーの浮動値(ユーザーによって送信されて返された)と特定の時点でのスライダーの変更(このためにhotControl要素を使用しました)。 しかし、要素にさらに多くの情報が含まれている場合はどうでしょうか?



IMGUIは、インターフェイス要素に関連付けられたいわゆる状態オブジェクト用のシンプルなストレージシステムを提供します。 これを行うには、データの保存に使用される新しいクラスを定義し、新しいオブジェクトをコントロールの識別子に関連付ける必要があります。 各オブジェクトには1つしか識別子を割り当てることができず、IMGUIはそれを自分で行います-組み込みコンストラクタを使用します。 エディターコードを読み込むとき、そのようなオブジェクトは([Serializable]ラベルが設定されていても)シリアル化されないため、長期のデータストレージには使用できません。



押すたびにTRUEを返すボタンが必要ですが、2秒以上押し続けると赤く点灯するとします。 ボタンがクリックされた時間を追跡するために、状態オブジェクトを使用します。 クラスを宣言します:



 public class FlashingButtonInfo { private double mouseDownAt; public void MouseDownNow() { mouseDownAt = EditorApplication.timeSinceStartup; } public bool IsFlashing(int controlID) { if (GUIUtility.hotControl != controlID) return false; double elapsedTime = EditorApplication.timeSinceStartup - mouseDownAt; if (elapsedTime < 2f) return false; return (int)((elapsedTime - 2f) / 0.1f) % 2 == 0; } }
      
      







ボタンが押された時間は、MouseDownNow()が呼び出されたときにmouseDownAtプロパティに保存され、IsFlashing関数はその時点でボタンを赤く点灯するかどうかを決定します。 当然、hotControlが関与していない場合、またはボタンが押されてから2秒未満が経過した場合、ボタンは点灯しません。 しかし、そうでなければ、その色は0.1秒ごとに変化します。



次に、ボタン自体のコードを記述します。



 public static bool FlashingButton(Rect rc, GUIContent content, GUIStyle style) { int controlID = GUIUtility.GetControlID (FocusType.Native); // Get (or create) the state object var state = (FlashingButtonInfo)GUIUtility.GetStateObject( typeof(FlashingButtonInfo), controlID); switch (Event.current.GetTypeForControl(controlID)) { case EventType.Repaint: { GUI.color = state.IsFlashing (controlID) ? Color.red : Color.white; style.Draw (rc, content, controlID); break; } case EventType.MouseDown: { if (rc.Contains (Event.current.mousePosition) && Event.current.button == 0 && GUIUtility.hotControl == 0) { GUIUtility.hotControl = controlID; state.MouseDownNow(); } break; } case EventType.MouseUp: { if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0; break; } } return GUIUtility.hotControl == controlID; }
      
      







すべてが非常に簡単です。 mouseDownおよびmouseUpに応答するためのコードスニペットは、以前にスクロールバーのスライダーのキャプチャを処理するために使用したものと非常に似ていることに注意してください。 唯一の違いは、マウスボタンがクリックされたときのstate.MouseDownNow()の呼び出しと、ボタンが再描画されたときのGUI.colorの値の変更です。



redrawイベントに関連する別の違い、つまりstyle.Draw()の呼び出しに気付いたかもしれません。 これについて詳しく説明する価値があります。



GUIスタイル



最初の要素を作成するときに、 GUI.DrawTextureを使用してスライダー自体を描画しました。 しかし、FlashingButton要素では、すべてがそれほど単純ではありません。ボタンには、丸い長方形の形の画像だけでなく、碑文も含める必要があります。 GUI.DrawTextureを使用してボタンを描画し、その上にGUI.Labelを配置することもできますが、より良い方法があります。 GUI.Label自体を使用せずに、 GUI.Labelイメージレンダリングテクニックを使用してみましょう。



GUIStyleクラスには、フォントやテキストの色から要素間の間隔まで、インターフェイス要素の視覚プロパティに関する情報が含まれています。 さらに、GUIStyleは、スタイルを使用してオブジェクトの幅と高さを決定したり、画面に要素を直接描画したりするために使用される関数を格納します。



GUIStyleには、要素を描画するさまざまなスタイルを含めることができます。カーソルがその上にあるとき、キーボード入力フォーカスを受け取ったとき、無効になっているとき、またはアクティブになっているとき(マウスボタンを押したまま)。 任意の状態について、色と背景画像を決定できます。GUIStyleは、コントロールIDに基づいて要素をレンダリングするときにそれらを置き換えます。



GUIStylesを使用してインターフェイス要素を描画するには、4つの方法があります。



•必要な値を指定して、新しいスタイル(新しいGUIStyle())を作成します。

EditorStylesクラスの組み込みスタイルの1つを使用します(カスタム要素を標準の要素のように見せたい場合)。

•たとえば、既存のスタイルをわずかに変更する必要がある場合は、ボタンのテキストを右に揃えます。 EditorStylesクラスの目的のスタイルをコピーし、目的のプロパティを手動で変更できます。

GUISkinからスタイルを抽出します



GUISkinは、プロジェクト自体の個別のリソースとして作成し、Unity Inspectorを使用して編集できるGUIStyleオブジェクトの大きなコレクションです。 新しいGUISkinを作成して開くと、すべての標準インターフェイス要素(ボタン、テキストウィンドウ、スイッチなど)のスロットが表示されます。しかし、ユーザースタイルのセクションは特に重要です。 ここに、 GUISkin.GetStyle( "style_name")メソッドを使用して取得できる一意の名前を持つGUIStyleオブジェクトをいくつでも配置できます。 コードからGUISkinオブジェクトをロードする方法を理解することは残っています。 これを行うにはいくつかの方法があります。 オブジェクトがEditor Default Resourcesフォルダーにある場合は、 EditorGUIUtility.LoadRequired()関数を使用します。 別のディレクトリからロードするには、 AssetDatabase.LoadAssetAtPath()を使用します。 主なことは、リソースパッケージまたはResourcesフォルダーにエディター専用のリソースを配置することではありません。



GUIStyleができたので、 GUIStyle.Draw()を使用して、目的のテキスト、画像、およびツールチップを含むGUIContentを描画できます。 引数として、描画が実行される長方形の座標、GUIContent自体、およびコントロール要素の識別子が使用されます。



IMGUIマークアップ



お気付きかもしれませんが、調査した各インターフェイス要素には、画面上の位置を決定するRectパラメーターがありました。 ただし、GUIStyleにマークアッププロパティがどのように含まれるかについて説明しました。 問題は、マークアップの特性を考慮して、Rectのすべての値を手動で計算することが本当に必要なのでしょうか? 原理的には可能です。 しかし、IMGUIはより簡単なソリューション、つまりこれを自動的に行うマークアップメカニズムを提供します。



これには、EventType.Layoutという特別なタイプのイベントがあります。 IMGUIがそのようなイベントをインターフェイスに送信した後、その要素はマークアップ関数を呼び出します: GUILayoutUtility.GetRect()GUILayout.BeginHorizo​​ntal / Vertical 、およびGUILayout.EndHorizo​​ntal / Verticalなど。 IMGUIは、これらの呼び出しの結果を、すべてのインターフェース要素とそれらに必要なスペースを含むツリーの形で記憶します。 ツリーを構築した後、その再帰トラバーサルが実行され、その間に要素のサイズと互いに対する相対的な位置が計算されます。



EventType.Repaintなどの他のイベントがトリガーされると、要素はマークアップ関数を再度呼び出します。 ただし、今回はIMGUIが「記録された」呼び出しを繰り返し、完成した長方形を返します。 言い換えると、レイアウトイベント中に、GUILayoutUtility.GetRect()関数を使用して長方形のパラメーターが既に計算されている場合、別のイベントがトリガーされると、以前に保存された結果が単純に置き換えられます。



コントロール要素の識別子と同様に、レイアウトイベントやその他のイベントを実行する場合、要素が他の長方形のデータを受け取らないように、レイアウト関数呼び出しの順序を観察することが重要です。 IMGUIはイベントが終了してツリーが処理されるまで各長方形がどの要素に対応するかを知らないため、レイアウトイベント中にGUILayoutUtility.GetRect()を呼び出して返される値は役に立たないことも考慮する価値があります。



それで、スライダーでストリップにマークアップを追加しましょう。 これは難しくありません:IMGUIから正方形を受け取った後、既製のコードを呼び出すことができます:



 public static float MyCustomSlider(float value, GUIStyle style) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style); return MyCustomSlider(position, value, style); }
      
      







Layoutイベント中にGUILayoutUtility.GetRectを呼び出すと、IMGUIは空のコンテンツに特定のスタイルが必要であることを記憶します(画像またはテキストが指定されていないため空です)。 他のイベント中、GetRectは既存の四角形を返します。 レイアウトイベント中に、MyCustomSlider要素が間違った四角形で呼び出されますが、これがないとGetControlID()を呼び出せないため、これは重要ではありません。



IMGUIが長方形のサイズを決定する基準となるすべてのデータは、スタイルに含まれています。 しかし、ユーザーが1つ以上のパラメーターを手動で設定したい場合はどうでしょうか?



これにはGUILayoutOptionクラスが使用されます。 このクラスのオブジェクトは、マークアップシステムの一種の指示であり、長方形の計算方法を示します(たとえば、特定の高さ/幅の値を使用するか、使用可能なスペースを垂直/水平に埋めます)。 このようなオブジェクトを作成するには、 GUILayout.ExpandWidth()GUILayout.MinHeight()などのGUILayoutクラスのファクトリー関数を呼び出し、それらを配列としてGUILayoutUtility.GetRect()に渡す必要があります。 次に、それらはマークアップツリーに保存され、それを処理するときに考慮されます。



GUILayoutOptionオブジェクトから独自の配列を作成する代わりに、C#paramsキーワードを使用します。これにより、配列が自動的に構成される任意の数のパラメーターを使用してメソッドを呼び出すことができます。 これが、ストリップの新しい機能の外観です。



 public static float MyCustomSlider(float value, GUIStyle style, params GUILayoutOption[] opts) { Rect position = GUILayoutUtility.GetRect(GUIContent.none, style, opts); return MyCustomSlider(position, value, style); }
      
      







ご覧のとおり、ユーザーが入力したすべてのデータはGetRectに直接送信されます。



IMGUI要素の機能を、マークアップによる自動レイアウトを使用する同じ機能のバージョンと組み合わせる同様の方法は、GUIクラスに組み込まれているものを含め、すべてのIMGUI要素に適用できます。 GUILayoutクラスは、GUIクラスの要素のホストされたバージョンを提供することがわかります(そして、 EditorGUIに対応するEditorGUILayoutクラスを使用します)。



さらに、自動および手動で配置された要素を組み合わせることができます。 スペースはGetRectを使用して予約され、その後、さまざまな要素の個別のセクションに分割できます。 マークアップシステムはコントロール要素の識別子を使用しないため、1つの長方形に複数の要素を配置できます(またはその逆)。 このアプローチは、完全自動配置よりもはるかに高速に動作する場合があります。



PropertyDrawersを記述するときは、マークアップはお勧めできませんが、PropertyDrawer.OnGUI()オーバーロードに渡された長方形を使用することをお勧めします。 実際には、Editorクラス自体はマークアップを使用せず、次の各プロパティに対して下にシフトする単純な長方形を計算します。 したがって、PropertyDrawerにマークアップが使用されている場合、エディターは以前のプロパティを認識しないため、長方形を正しく配置しません。



シリアル化されたプロパティを使用する



そのため、独自のIMGUI要素をすでに作成できます。 Unityの品質標準に到達するのに役立ついくつかのポイントを議論するために残っています。



1つはSerializedPropertyの使用です。 シリアル化システムについては、次の記事で詳しく説明しますが、ここでは一般化します。SerializedPropertyインターフェイスを使用すると、Unityシリアル化(ロードおよび保存)システムが接続されているプロパティにアクセスできます。 したがって、Unity Inspectorに表示されるスクリプトまたはオブジェクトの任意の変数を使用できます。

SerializedPropertyは、変数の値だけでなく、さまざまな種類の情報へのアクセスを提供します。たとえば、変数の現在値と初期値、または変数の状態をインスペクターウィンドウの子フィールドと比較します(折りたたみ/展開)。 さらに、このインターフェイスは、元に戻すシステムとシーンダーティシステムの変数の値に対するユーザー定義の変更を統合します。 オブジェクトの管理バージョンを使用しないため、パフォーマンスにプラスの影響があります。 したがって、複雑なインターフェイス要素を完全に機能させるには、SerializedPropertyを使用する必要があります。



SerializedPropertyオブジェクトを引数として受け取るEditorGUIクラスメソッドのシグネチャは、通常とは若干異なります。 SerializedPropertyに直接変更が加えられるため、このようなメソッドは何も返しません。 ストリップの改良版は次のようになります。



 public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style)
      
      







現在、valueパラメーターはありませんが、代わりにpropパラメーターがSerializedPropertyに渡されます。 prop.floatValueを使用すると、ストリップをレンダリングするときに浮動小数点数の値を取得し、スライダーをドラッグするときにそれを変更できます。



IMGUIコードでSerializedPropertyを使用することには、他にも利点があります。 prefabOverrideの値が、テンプレートオブジェクトのプロパティ値への変更を示しているとします。 デフォルトでは、変更されたプロパティは太字で表示されますが、GUIStyleを使用して別の表示スタイルを設定できます。



もう1つの重要な機会は、複数のオブジェクトを編集することです。つまり、1つの要素を使用して複数の値を一度に表示します。 EditorGUI.showMixedValueが TRUEに設定されている場合、アイテムは複数の値を表示するために使用されます。

prefabOverrideおよびshowMixedValueメカニズムを使用するには、EditorGUI.BeginProperty ()およびEditorGUI.EndProperty()を使用してプロパティのコンテキストを設定する必要があります。 通常、要素メソッドがSerializedPropertyクラスの引数を取る場合、それ自体がBeginPropertyとEndPropertyを呼び出す必要があります。 「純粋な」値を受け入れる場合(たとえば、intを受け入れ、プロパティを処理しないEditorGUI.IntFieldメソッド)、BeginPropertyとEndPropertyの呼び出しは、このメソッドを呼び出すコードに含まれている必要があります。



 public class MySliderDrawer : PropertyDrawer { public override float GetPropertyHeight (SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight; } private GUISkin _sliderSkin; public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { if (_sliderSkin == null) _sliderSkin = (GUISkin)EditorGUIUtility.LoadRequired ("MyCustomSlider Skin"); MyCustomSlider (position, property, _sliderSkin.GetStyle ("MyCustomSlider"), label); } } // Then, the updated definition of MyCustomSlider: public static void MyCustomSlider(Rect controlRect, SerializedProperty prop, GUIStyle style, GUIContent label) { label = EditorGUI.BeginProperty (controlRect, label, prop); controlRect = EditorGUI.PrefixLabel (controlRect, label); // Use our previous definition of MyCustomSlider, which we've updated to do something // sensible if EditorGUI.showMixedValue is true EditorGUI.BeginChangeCheck(); float newValue = MyCustomSlider(controlRect, prop.floatValue, style); if(EditorGUI.EndChangeCheck()) prop.floatValue = newValue; EditorGUI.EndProperty (); }
      
      







おわりに



この記事がIMGUIの基本を理解するのに役立つことを願っています。 真のプロフェッショナルになるには、SerializedObject / SerializedPropertyシステム、CustomEditor / EditorWindow / PropertyDrawerを使用する機能、Undoクラスの使用など、他の多くの側面を習得する必要があります。 Asset Storeまたは個人使用。



All Articles