유니티 - Editor Folder

Unity 2019. 8. 4. 15:38
728x90

1장 에디터 확장에 사용하는 폴더
1.1 Editor 폴더

Editor폴더는 에디터 API를 사용하기 위한 특별한 폴더.
보통 에디터 API는, 런타임으로 동작하지 않음.

아래 코드를 Assets 폴더 바로 아래에 작성해서 빌드해보면 빌드 실패 뜸.

using UnityEngine;
using UnityEditor;

public class NewBehaviourScript : MonoBehaviour
{
}

개발 중의 유니티 에디터 상에서 생성되는 Assembly-CSharp.dll에서는 UnityEditor.dll에의 참조가 발생하므로 스크립트의 컴파일 에러는 발생하지 않음.
빌드 시에 생성되는 Assembly-CSharp.dll에서는  UnityEditor.dll에의 참조가 발생하지 않기 때문에 빌드 에러가 발생하는것. 중요. 이거 몰라서 빌드 에러 원인 못찾고 그럼.

UnityEditor에서는 Assembly-CSharp-Editor.dll을 생성해서, 에디터 API와 런타임 API를 구분하는 것으로 문제를 해결함. Assembly-CSharp-Editor.dll는 빌드 시에는 포함되지 않으므로 빌드 에러도 발생 안함. 

그리고 Assembly-CSharp-Editor.dll은 Editor 폴더 안의 스크립트 파일이 컴파일 되어 생성되는것.

Editor 폴더의 생성장소는 별 제한 없음. 여러개의 Editor 폴더를 생성할수도 있음.
단, Standard Assets, Pro Standard Assets, Plugins 폴더 안에 생성하면 그 안의 스크립트는
Assembly-CSharp-Editor-firstpass.dll에 컴파일됨.

Assembly-CSharp-Editor.dll에서 firstpass를 참조할수는 있지만
firstpass에서 Assembly-CSharp-Editor.dll를 참조할수는 없음. 주의.

[Editor 폴더에 포함 안시키고 동작시키는 방법]

런타임에서 동작하는 스크립트에 에디터 API 꺼를 사용하는 겨우가 있음. 이 경우에는 아래와같이 처리할것.

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class NewBehaviourScript : MonoBehaviour
{
    void OnEnable ()
    {
        #if UNITY_EDITOR
        EditorWindow.GetWindow<ExampleWindow> ();
        #endif
    }
}

1.2 Editor Default Resources 폴더
Resources 폴더랑 마찬가지로 에디터 확장에서만 사용할 리소스를 넣어둘 수 있는 폴더임.
Editor Default Resources 폴더 안에 있는 에셋은 EditorGUIUtility.Load로 접근 가능.

var tex = EditorGUIUtility.Load ("logo.png") as Texture;

https://blog.naver.com/PostView.nhn?blogId=hammerimpact&logNo=220769879313&parentCategoryNo=&categoryNo=19&viewDate=&isShowPopularPosts=false&from=postView

728x90

'Unity' 카테고리의 다른 글

유니티 - 충돌 (Trigger, Collider)  (0) 2019.08.07
유니티 - 에디터 확장 기능  (0) 2019.08.05
유니티 - PropertyDrawer  (0) 2019.08.03
유니티 - ScriptableObject  (0) 2019.08.02
유니티 에디터 - MenuItem  (0) 2019.08.01
Posted by 정망스
,

유니티 - PropertyDrawer

Unity 2019. 8. 3. 18:52
728x90

10장 PropertyDrawer

예를들어, 캐릭터를 위한 스크립트가 있고, 히트포인트의 hp 변수가 있다고 합시다.

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    int hp;
}

위의 코드는 SerializedField 속성이 붙어 있고, 인스펙터에 표시되게 되어 있습니다.

이 hp 변수에 다음의 제한이 걸려 있는 경우, 어떻게 하는게 좋을까요.
- HP의 상한 하한은 정해져 있다.
- 이 수치는 조정되지 않은 것으로, 수치를 변경하면서 적절한 수치를 찾아야 한다.
이들 제한(모양)은 개발할 때는 고려해야 하는 것으로, 특히 인스펙터에서 수치를 편집할 때는, 이러한 제한을 붙이는것은 표준 기능으로는 어렵습니다.

10.1 PropertyDrawer란
유니티는, Serialize된 데이터를 유니티가 자동판단해서 적절한 GUI를 사용해, 인스펙터에 표시합니다.
PropertyDrawer는 그 유니티에 의한 자동판단 처리를 Hook해서 스스로 GUI를 사용하기 위한 기술입니다. 이것을 통해 특정 GUI만을 커스터마이즈 할 수 있습니다.
인스펙터에 표시되는 컴포넌트의 GUI를 변경하기 위해서는 CustomEditor가 적절합니다. 하지만, 이것은 컴포넌트 전체의 커스터마이즈입니다. 지금은, 컴포넌트의 일부인 hp 변수(프로퍼티)만을 커스터마이즈 하고 싶은것이므로 CustomEditor가 아니라 PropertyDrawer를 사용합니다.


왼쪽은 유니티가 자동으로 IntField라고 판단했을 경우이고,
오른쪽은 PropertyDrawer로 Slider를 지정한 경우입니다.

예를들어 다음과 같이 Serialize 가능한 클래스가 있다고 합시다.

[Serializable]
public class Character
{
        [SerializeField]
        string name;

        [SerializeField]
        int hp;
}

이것을 인스펙터로 표시하려고 하면 컴포넌트에는 있지만 실로 보기 힘들게 표시됩니다.

이것을 PropertyDrawer를 통해 GUI의 표시를 커스터마이즈해서, 1행에 표시시킬 수 있습니다.

이렇게 인스펙터의 조자에서 불편하다고 생각한 부분을 커스터마이즈해나갈수 있습니다.

10.2 PropertyAttribute
PropertyAttribute는 단순히 Attribute를 상속한 클래스입니다. CustomEditor가 컴포넌트의 Editor 오브젝트를 확장하였듯이, PropertyDrawer는, PropertyAttribute를 확장합니다. (정확히 말해 PropertyAttribute가 붙은 Serialize 가능한 필드입니다).

using UnityEngine;

public class ExampleAttribute : PropertyAttribute
{
}

10.3 CustomPropertyDrawer 와 PropertyDrawer
CustomEditor와 마찬가지인 구현방법으로, PropertyDrawer도 확장을 실행합니다.
PropertyDrawer를 상속한 자식 클래스를 생성하고, 확장하고 싶은 클래스를 CustomPropertyDrawer의 인자에 넘깁니다. Serialize 가능한 클래스이면 아래의 클래스를 작성합니다.

[CustomPropertyDrawer (typeof(Character))]
public class CharacterDrawer : PropertyDrawer
{
}

PropertyAttribute의 자식 클래스의 경우도 같습니다.

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof(ExampleAttribute))]
public class ExampleDrawer : PropertyDrawer
{
}

남은것은 작성한 PropertyAttribute를 필드에 추가시키는 것 뿐입니다.

using UnityEngine;

public class Hoge : MonoBehaviour
{
    [SerializeField, Example]
    int hp;
}

10.4 RangeAtribute를 시험사기
이미 표준에서 몇개 정도 PropertyDrawer가 구현되어 있습니다.
그 중 하나인 RangeAttribute를 사용해 봅시다.

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Range (0, 10)]
    int hp;
}

속성으로써 Range(0, 10)을 추가하는것 뿐인데, 0부터 10까지 슬라이드 가능한 Slider를 작성할 수 있습니다.

10.5 RangeAttribute를 자작하기
시험삼아, 표준 구현되어 있는 RangeAttribute와 같은 것을 작성해 봅니다.

[Range2Attribute의 작성]
최저치와 최고치를 유지하도록 하여, Attribute의 사용 제한을 설정합니다. AttributeUsage에 대해서는 이쪽 링크를 참고하여 주십시오.
(역주 : 한글 버젼 링크를 걸었습니다)
https://msdn.microsoft.com/ko-kr/library/system.attributeusageattribute(v=vs.110).aspx

using UnityEngine;

[System.AttributeUsage (System.AttributeTargets.Field,
                               Inherited = true, AllowMultiple = false)]
public class Range2Attribute : PropertyAttribute
{
    public readonly int min;
    public readonly int max;

    public Range2Attribute (int min, int max)
    {
        this.min = min;
        this.max = max;
    }
}

[Range2Drawer의 작성]
속성에 붙은 필드는 SerializedProperty를 경유해 다룹니다. propertyType이 int이면 IntSlider를 사용하고, int 이외라면 표준 GUI를 사용합니다.

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer (typeof(Range2Attribute))]
internal sealed class RangeDrawer : PropertyDrawer
{
    public override void OnGUI (Rect position,
                      SerializedProperty property, GUIContent label)
    {
        Range2Attribute range2 = (Range2Attribute)attribute;

        if (property.propertyType == SerializedPropertyType.Integer)
            EditorGUI.IntSlider (position, property, range2.min, range2.max, label);
        else
            EditorGUI.PropertyField (position, property, label);
    }
}

10.6 Range2Attribute를 사용하기
이상으로 구현이 끝났으므로 Range2Attribute를 사용해보았습니다. int 이외에서는 다룰 수 없는것을 확인하기 위해서 string에 대해서도 속성을 추가해보았습니다.

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Range2 (0, 10)]
    int hp;

    [SerializeField, Range2 (0, 10)]
    string str;
}

10.7 다양한 PropertyDrawer
유니티에 표준 구현되어 있는 것은 2장 [표준에서 사용하는 에디터 확장 기능]에서 소개해드렸습니다. 여기서는 제가 지금까지 작성한 PropertyDrawer를 소개하고자 합니다.
[Angle]
API로서 노브(손잡이)를 표시하는 EditorGUILayout.Knob가 있습니다. 하지만 PropertyDrawer에서는 EditorGUILayout의 사용이 금지되어 있어서 쓸 수 없습니다. 내부적으로는 EditorGUI.Knob이 구현되어 있어, 리플렉션을 사용해서 호출하는 것으로 사용이 가능하게 됩니다.

private readonly MethodInfo knobMethodInfo = typeof(EditorGUI).GetMethod("Knob",
       BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static);

private float Knob(Rect position, Vector2 knobSize,
                      float currentValue, float start,
                      float end, string unit,
                      Color backgroundColor, Color activeColor,
                      bool showValue)
{
    var controlID = GUIUtility.GetControlID("Knob".GetHashCode(),
                                              FocusType.Native, position);

    var invoke = knobMethodInfo.Invoke(null, new object[] {
        position, knobSize, currentValue,
        start, end, unit, backgroundColor,
        activeColor, showValue,
        controlID });
    return (float)(invoke ?? 0);
}

[GetPropertyHeight]
GUI의 기본 높이값(EditorGUIUtility.singleLineHeight)을 변경하고 싶을 때는 GetPropertyHeight를 오버라이드 합니다.

[CustomPropertyDrawer(typeof(AngleAttribute))]
public class AngleDrawer : PropertyDrawer
{
  private AngleAttribute angleAttribute { get { return (AngleAttribute)attribute; } }

  public override void OnGUI (Rect position,
                    SerializedProperty property, GUIContent label)
  {
    //생략
  }

  //반환값으로써 반환된 값이 GUI의 높이로써 사용됩니다
  public override float GetPropertyHeight(SerializedProperty property,
                                                            GUIContent label)
  {
      var height = base.GetPropertyHeight(property, label);

      var floatType = property.propertyType != SerializedPropertyType.Float;

      return floatType ? height : angleAttribute.knobSize + 4;
  }
}

[AnimatorParameter]
Animator 윈도우에 있는 파라메터 이름을 타입 상관없이 필드에 Attach할 수 있습니다. 얻어올 수 있는 파라메터는 같은 게임 오브젝트에 Attach되어 있는 Animator Controller의 파라메터가 됩니다.

using UnityEngine;

[RequireComponent(typeof(Animator))]
public class AnimatorParameterExample : MonoBehaviour
{
    //모든 타입의 파라메터를 얻어오기
    [AnimatorParameter]
    public string param;

    //Float만
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Float)]
    public string floatParam;

    //Int만
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Int)]
    public string intParam;

    //Bool만
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Bool)]
    public string boolParam;

    //Trigger만
    [AnimatorParameter(AnimatorParameterAttribute.ParameterType.Trigger)]
    public string triggerParam;
}

[같은 오브젝트의 컴포넌트를 얻어오기]
이번에는 같은 게임오브젝트에 Attach되어 있는 Animator 컴포넌트로부터 AnimatorController를 얻어옵니다. 컴포넌트는 SerializedProperty->SerializedObject->Component의 순서로 얻어올 수 있습니다.

AnimatorController GetAnimatorController(SerializedProperty property)
{
    var component = property.serializedObject.targetObject as Component;

    if (component == null)
    {
        Debug.LogException(new InvalidCastException("Couldn't cast targetObject"));
    }

    var anim = component.GetComponent<Animator>();

    if (anim == null)
    {
        var exception = new MissingComponentException("Missing Aniamtor Component");
        Debug.LogException(exception);
        return null;
    }

    return anim.runtimeAnimatorController as AnimatorController;
}

[DisableAttribute]
프로퍼티를 인스펙터 상에서 편집 못하게 막습니다. 인스펙터에 표시하고 싶은데, 수치를 변경시키고 싶지 않은 때에 사용합니다.

using UnityEngine;

public class DisableExample : MonoBehaviour
{
    [Disable]
    public string hoge = "hoge";

    [Disable]
    public int fuga = 1;

    [Disable]
    public AudioType audioType = AudioType.ACC;
}

구현 방법은 간단합니다.
BeginDisabledGroup과 EndDisabledGroup, 혹은 DisabledGroupScope를 사용해 PropertyField를 감싸는 걸로 구현할 수 있습니다.
인스펙터에서 편집할 수 없게 되었다 해도, 인스펙터를 Debug 모드로 하면 편집할수 있고, 스크립트에서 수치의 편집이 가능하므로 주의해주세요.

public override void OnGUI(Rect position,
                             SerializedProperty property, GUIContent label)
{
    EditorGUI.BeginDisabledGroup(true);
    EditorGUI.PropertyField(position, property, label);
    EditorGUI.EndDisabledGroup();
}

}

[EnumLabel]
Enum의 표시 이름을 변경합니다.

using UnityEngine;

public class EnumLabelExample : MonoBehaviour
{
    public enum Example
    {
        [EnumLabel("テスト")]
        HIGH,
        [EnumLabel("その2")]
        LOW
    }

    [EnumLabel("例")]
    public Example test = Example.HIGH;
}

GUI를 표시할 때에 EnumLabel에 건네준 문자열을 사용해서 Popup을 표시합니다. 위의 예시같이 test 변수에도 속성을 붙이지 않으면 적용되지 않습니다. 이것은 PropertyAttribute가 필드에 붙어있지 않으면 이벤트가 발생하지 않기 때문입니다.

[Popup]
Attribute에 넘겨준 파라메터를 사용해 Popup을 표시합니다. 수치를 Popup으로 선택할 수 있게 됩니다.

using UnityEngine;
using System.Collections;

public class PopupExample : MonoBehaviour
{
    [Popup("Hoge","Fuga","Foo","Bar")]
    public string popup;

    [Popup(1,2,3,4,5,6)]
    public int popup2;

    [Popup(1.5f,2.3f,3.4f,4.5f,5.6f,6.7f)]
    public float popup3;
}

수치를 가진 쪽을 object로 하고 있으므로 다수의 타입을 지원합니다.

public class PopupAttribute : PropertyAttribute
{
    public object[] list;

    // 인자는 object 배열
    public PopupAttribute (params object[] list)
    {
        this.list = list;
    }
}

하지만 이렇게 하면 Popup에서 선택된 수치를 대입할 때에 조금 고생합니다.
SerializedProperty에 수치를 대입하는 것은 property.stringValue, property.intValue, property.floatValue와 같이 각자의 변수에 대입해야 합니다.

[PreviewTexture]
텍스쳐의 프리뷰를 표시합니다.

using UnityEngine;

public class PreviewTextureAttributeExample : MonoBehaviour
{
    //60초 캐쉬일것
    [PreviewTexture(60)]
    public string textureURL = "https://www.hogehoge.com/image.png";

    [PreviewTexture]
    public Texture hoge;
}

[텍스쳐 표시는 GUIStyle을 쓴다]
텍스쳐의 표시는 EditorGUI.DrawPreviewTextrue를 사용하고 있는데, PropertyDrawer에서는 렌더링 타이밍의 관계에서 텍스쳐가 표시/비표시되는것을 조절해버리는 문제가 발생합니다. 이런 문제가 있어서 대안으로서 GUIStyle을 사용해서 스타일의 배경으로서 텍스쳐를 렌더링합니다.

void DrawTexture(Rect position, Texture2D texture)
{
    float width = Mathf.Clamp(texture.width,
                              position.width * 0.7f,
                              position.width * 0.7f);

    var rect = new Rect(position.width * 0.15f,
                        position.y + 16,
                        width,
                        texture.height * (width / texture.width));

    if (style == null)
    {
        style = new GUIStyle();
        style.imagePosition = ImagePosition.ImageOnly;
    }

    style.normal.background = texture;
    GUI.Label(rect, "", style);
}

SceneName
: 유효한 씬 이름을 Popup으로 선택할 수 있습니다. Popup으로 표시되는 것은 Build Settings 의 Scene In Build에 포함되어 있는 씬 이름입니다.

using UnityEngine;

public class SceneNameExample : MonoBehaviour
{
    [SceneName]
    public string sceneName;

    //무효 상태의 씬을 표시합니다.
    [SceneName(false)]
    public string sceneName2;
}

[EditorBuildSettings.scenes에서 씬의 목록을 얻어오기]
씬은 EditorBuildSettings.scenes 변수에서 관리되고 있습니다. 단, Build Settings의 Scene In Build에 씬 등록되어 있지 않으면 목록에 포함되지 않기 때문에 주의해주세요.

https://blog.naver.com/PostView.nhn?blogId=hammerimpact&logNo=220775187161&targetKeyword=&targetRecommendationCode=1

728x90

'Unity' 카테고리의 다른 글

유니티 - 에디터 확장 기능  (0) 2019.08.05
유니티 - Editor Folder  (0) 2019.08.04
유니티 - ScriptableObject  (0) 2019.08.02
유니티 에디터 - MenuItem  (0) 2019.08.01
유니티 에디터 - EditorWindow  (0) 2019.07.31
Posted by 정망스
,
728x90

4장 ScriptableObject

4.1 ScriptableObject란
ScriptableObject란 독자적인 Asset을 작성하기 위한 구조입니다. 또한, 유니티의 Serialize 구조를 사용하는 형식이라고도 할 수 있음.
유니티에서는 독자적인 Serialize 구조를 갖고 있어, 모든 오브젝트(UnityEngine.Object)는, 그 Serialize 구조를 통해서 데이터의 Serialize / Deserialize를 실행해, 파일과 Unity 에디터간의 조합을 하고 있음. Serialize 구조에 대해서는 5장 SerializedObject에 대해서 를 참조.

유니티 내부의 Asset(Material과 애니메이션 클립 등)은 모두 UnityEngine.Object의 자식 클래스임. 독자적인 Asset을 만들기 위해서, UnityEngine.Object의 자식 클래스를 작성하고 싶은데, 유저 측에서는 UnityEngine.Object의 자식 클래스를 작성하는게 금지되어 있음. 유저가 Unity의 Serialize 구조를 이용한, 독자적인 Asset을 작성하기 위해서는 ScriptableObject를 사용할 필요가 있음.

4.2 ScriptableObject는 유니티 에디터의 요소
ScriptableObject는 유니티 에디터의 다양한 곳에서 사용되고 있음. 씬 뷰나 게임 뷰 등의 에디터 윈도우는, ScriptableObject의 자식 클래스에서 생성되었고, 또한 인스펙터에 GUI를 표시하는 Editor 오브젝트도 ScriptableObject의 자식 클래스에서 생성됨. 유니티 에디터는 ScriptableObject로 작성되어 있다고 해도 과언이 아님.


4.3 ScriptableObject를 작성
ScriptableObject를 작성하기 위해서는 먼저 ScriptableObject 클래스를 상속한 클래스를 작성해야함. 이때, 클래스 이름과 Asset이름은 통일시켜야함. MonoBehaviour와 같은 제한.

using UnityEngine;

public class ExampleAsset : ScriptableObject
{

}


[인스턴스 화]
ScriptableObject는 ScriptableObject.CreateInstance로 생성합니다. new를 사용해서 인스턴스 화해선 안됨. 이유는 MonoBehaviour와 마찬가지로, 유니티의 Serialize 구조를 경유해서 오브젝트를 만들 필요가 있으니까.

using UnityEngine;
using UnityEditor;

public class ExampleAsset : ScriptableObject
{
    [MenuItem ("Example/Create ExampleAsset Instance")]
    static void CreateExampleAssetInstance ()
    {
        var exampleAsset = CreateInstance<ExampleAsset> ();
    }
}


[Asset으로 저장]
다음은 인스턴스화시킨 오브젝트를 Asset으로 저장시킵니다. Asset의 작성은 AssetDatabase.CreateAsset을 사용해서 작성할 수 있음.
Asset의 확장자는, 반드시, .asset이 아니면 안됨. 다른 확장자로 해버리면, 유니티는 ScriptableObejct를 상속한 Asset으로서 인식하지 못함.

[MenuItem ("Example/Create ExampleAsset")]
static void CreateExampleAsset ()
{
    var exampleAsset = CreateInstance<ExampleAsset> ();

    AssetDatabase.CreateAsset (exampleAsset, "Assets/Editor/ExampleAsset.asset");
    AssetDatabase.Refresh ();
}

또한, CreateAssetMenu 속성을 사용하는 것으로 간단하게 Asset을 작성할 수 있습니다.

using UnityEngine;
using UnityEditor;

[CreateAssetMenu(menuName = "Example/Create ExampleAsset Instance")]
public class ExampleAsset : ScriptableObject
{
}

CreateAssetMenu를 사용한 경우에는 [Assets/Create] 아래에 메뉴가 작성됩니다.


[스크립트에서 Asset의 ScriptableObject를 로드]
불러들이는 방법은 간단한데, AssetDatabase.LoadAssetAtPath를 사용해 불러들입니다.

[MenuItem ("Example/Load ExampleAsset")]
static void LoadExampleAsset ()
{
    var exampleAsset =
        AssetDatabase.LoadAssetAtPath<ExampleAsset>
                               ("Assets/Editor/ExampleAsset.asset");
}


[인스펙터에 프로퍼티를 표시]
MonoBehaviour와 마찬가지로, 필드에 SerializeField를 붙이면 표시됨. 또한 PropertyDrawer도 적용시킴.

using UnityEngine;
using UnityEditor;

public class ExampleAsset : ScriptableObject
{
    [SerializeField]
    string str;

    [SerializeField, Range (0, 10)]
    int number;

    [MenuItem ("Example/Create ExampleAsset Instance")]
    static void CreateExampleAssetInstance ()
    {
        var exampleAsset = CreateInstance<ExampleAsset> ();

        AssetDatabase.CreateAsset (exampleAsset, "Assets/Editor/ExampleAsset.asset");
        AssetDatabase.Refresh ();
    }
}


4.4 ScriptableObjecet의 부모 자식 관계
먼저, [부모인 ScriptableObject]와, 그 부모가 변수로서 가지는 [자식인 ScriptableObject]를 상상해주세요. 아래 코드는 그 이미지를 코드로 옮긴겁니다.

<부모 ScriptableObject>

using UnityEngine;

public class ParentScriptableObject : ScriptableObject
{
    [SerializeField]
    ChildScriptableObject child;
}

<자식 ScriptableObject>

using UnityEngine;

public class ChildScriptableObject : ScriptableObject
{
  // 아무것도 없으면 인스펙터가 허전하므로 변수 추가
  [SerializeField]
  string str;

  public ChildScriptableObject ()
  {
    // 초기 Asset 이름을 설정
    name = "New ChildScriptableObject";
  }
}

다음으로, ParentScriptableObject를 Asset으로 해서 저장합니다. child도 인스턴스화시킨 상태로 해봤습니다. 

<자식 ScriptableObject>

using UnityEngine;
using UnityEditor;

public class ParentScriptableObject : ScriptableObject
{
  const string PATH = "Assets/Editor/New ParentScriptableObject.asset";

  [SerializeField]
  ChildScriptableObject child;

  [MenuItem ("Assets/Create ScriptableObject")]
  static void CreateScriptableObject ()
  {
    // 부모를 인스턴스화
    var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();

    // 자식을 인스턴스화
    parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

    // 부모를 Asset으로 저장
    AssetDatabase.CreateAsset (parent, PATH);

    // Import 해서 최신 상태로 함
    AssetDatabase.ImportAsset (PATH);
  }
}

ParentScriptableObject를 Asset으로 저장한 후, 인스펙터를 보면, child 프로퍼티가 Type mismatch로 되어 있음. 

시험삼아, Type mismatch 부분을 더블클릭하면, ChildScriptableObject의 정보가 인스펙터에 표시되어, 문제 없이 올바른 동작을 하는걸 볼 수 있음.

[UnityEngine.Object를 Asset으로써 다루기 위해서는 디스크에 저장해야함]
Type mismatch 상태의 child를 가진 ParentScriptableObject를 만들었으면, 그대로 유니티를 다시 실행해보세요. 또다시 ParentScriptableObject의 인스펙터를 보면 child 부분이 None(null)로 되어 있음.

이유는 ScriptableObject의 루트 클래스인 UnityEngine.Objec를 Serialize 데이터로써 다루기 위해서는, 디스크 상에 저장해야 하기 때문입니다. Type mismatch 상태는, 인스턴스가 존재하지만, 디스크 상에 Asset으로써 존재하지 않는 상태를 가리킵니다. 즉, 그 인스턴스가 어떤 상황(유니티 재시작 등)에서 파괴되어버리면 데이터에 접근할 수 없게 됩니다.


[ScriptableObject는 모두 Asset으로써 저장할것]
Type mismatch 상태를 회피하는것은 간단합니다. ScriptableObject를 모두 Asset으로써 저장해, 그 참조를 Serialize 가능한 필드로 가지면 해결됩니다.

단, 이번과 같은 부모 자식 관계가 있는 상태에서, 각자 독립된 Asset을 작성해버리는것은 관리 면에서 보면 효율적이지 않습니다. 그 수가 늘어나거나, 리스트를 다루는 경우가 되면 그 만큼 Asset을 작성하는 것은 파일 관리가 매우 귀찮아집니다.

그래서 Sub Asset이라는 기능을 사용해 부모 자식 관계인 Asset을 하나로 뭉칠 수 있습니다.

[Sub Asset]
부모인 메인 Asset에 Asset 정보를 추가하는 것으로 UnityEngine.Object가 Sub Asset이 됩니다. 이 Sub Asset의 예시로 가장 알기 쉬운게 Model Asset입니다.

Model Asset의 안에는, 메쉬와 애니메이션 등의 Asset이 포함되어 있습니다. 이들은 보통, 독립된 Asset으로 존재해야 하지만, Sub Asset으로써 다뤄지면, 메쉬와 애니메이션 등의 Asset을 메인 Asset 정보에 포함해 디스크 상에 저장하는 일 없이 사용할 수 있습니다.

ScriptableObject도 Sub Asset의 기능을 사용하는 것으로, 디스크 상에 쓸데없는 Asset을 늘리지 않고 부모 자식 관계의 ScriptableObject를 만들 수 있음.

[AssetDatabase.AddObjectToAsset]
UnityEngine.Object를 Sub Asset으로써 등록하기 위해서는, 메인이 되는 Asset에 오브젝트를 추가합니다.
<자식 ScriptableObject>

using UnityEngine;
using UnityEditor;

public class ParentScriptableObject : ScriptableObject
{
  const string PATH = "Assets/Editor/New ParentScriptableObject.asset";

  [SerializeField]
  ChildScriptableObject child;

  [MenuItem ("Assets/Create ScriptableObject")]
  static void CreateScriptableObject ()
  {
    // 부모를 인스턴스화 
    var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();

    // 자식을 인스턴스화
    parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

    // 부모에 자식 오브젝트를 추가
    AssetDatabase.AddObjectToAsset (parent.child, PATH);

    // 부모를 Asset으로서 저장
    AssetDatabase.CreateAsset (parent, PATH);

    // Import 해서 최신 상태를 유지
    AssetDatabase.ImportAsset (PATH);
  }
}

부모인 ParentScriptableObject가 둘 있는듯 보이는 점, 실질적인 데이터를 가지고 있는 것은 계층적으로는 Sub Asset의 ParentScriptableObject인 점 등 좀 특수한 계층구조로 되어 있습니다. 

이 상태는, 유저가 (Sub Asset을 작성하는 것으로 인해) 특수한 Asset을 만들었다고 유니티가 판단해서, 메인 Asset을 아무것도 맡지 않은 Default Asset으로써 표시한 상태입니다.
메인 Asset으로써 다루고 싶은 Asset을 Sub Asset 쪽에 이동시키는 상황은 매우 안좋습니다. 이런 식으로 유저의 손으로 Sub Asset을 작성하는 것은 할 수 있지만, 이걸 Model같이 최대한 활용하는것은 안됩니다.

[HideFlags.HideInHierarchy로 Sub Asset을 숨기기]
Sub Asset 자체를 숨기는 것으로, 메인 Asset만이 존재하는듯이 외관을 만들 수 있습니다.

[MenuItem ("Assets/Create ScriptableObject")]
static void CreateScriptableObject ()
{
  var parent = ScriptableObject.CreateInstance<ParentScriptableObject> ();
  parent.child = ScriptableObject.CreateInstance<ChildScriptableObject> ();

 // Sub Asset인 child 를 표시하지 않는다
  parent.child.hideFlags = HideFlags.HideInHierarchy;

  AssetDatabase.AddObjectToAsset (parent.child, PATH);

  AssetDatabase.CreateAsset (parent, PATH);
  AssetDatabase.ImportAsset (PATH);
}

이와 같이, 계층 표시는 안되지만 2개의 Asset을 하나로 뭉쳐 관리할 수 있게 되었습니다.

이 Sub Asset을 숨기는 방법은, AnimatorController에서도 이루어지고 있습니다. 확인해봅시다.

[MenuItem ("Assets/Set to HideFlags.None")]
static void SetHideFlags ()
{
  // AnimatorController를 선택한 상태에서 메뉴를 실행
  var path = AssetDatabase.GetAssetPath (Selection.activeObject);

  // Sub Asset 포함해서 모두 얻기
  foreach (var item in AssetDatabase.LoadAllAssetsAtPath(path)) {
    // flag를 모두 None으로 하고 비표시 설정을 해제
    item.hideFlags = HideFlags.None;
  }
  // 다시 Import해서 최신 상태로 유지
  AssetDatabase.ImportAsset (path);
}

위에 적은 코드를 실행하면, HideFlags가 해제되어, Sub Asset이 표시됩니다.


[메인 Asset에서 Sub Asset을 제거하기]
Sub Asset의 제거 방법은 간단합니다. Object.DestroyImmediate를 사용하는 것으로 Sub Asset을 해제할 수 있습니다.

[MenuItem ("Assets/Remove ChildScriptableObject")]
static void Remove ()
{
  var parent = AssetDatabase.LoadAssetAtPath<ParentScriptableObject> (PATH);

  // Asset의 CarentScriptableObject 를 파괴
  Object.DestroyImmediate (parent.child, true);

  // 파괴하면 Missing 상태가 되므로 null 을 대입
  parent.child = null;

  // 다시 Import해서 최신 상태를 갱신
  AssetDatabase.ImportAsset (PATH);
}


4.5 아이콘의 변경
기본 아이콘


딱히 중요한 부분은 아닌데 이 아이콘을 변경하는 방법을 알아봅시다.

[스크립트에 아이콘을 설정]
스크립트 Asset을 선택해서 아이콘 부분을 선택하면, 아이콘 변경 패널이 뜨고, 거기에 Other 버튼을 클릭하면 변경하고싶은 아이콘으로 변경 가능.

[Gismos에 아이콘을 설정]
한가지 아이콘을 변경하는 방법으로써는, Gizmos 폴더에 [클래스 명] Icon이라고 이름지어서 이미지를 넣으면 변경됨. Gizmos 폴더가 Assets 폴더 아래에 있어야 하는 형태로 쓰기 힘들지도 모르겠는데, 같은 아이콘 이미지가 3개 늘어놔져 있어도 괜찮다는 점에서, 이 방법도 기억해두면 편리.

https://blog.naver.com/PostView.nhn?blogId=hammerimpact&logNo=220770261760&targetKeyword=&targetRecommendationCode=1

728x90

'Unity' 카테고리의 다른 글

유니티 - Editor Folder  (0) 2019.08.04
유니티 - PropertyDrawer  (0) 2019.08.03
유니티 에디터 - MenuItem  (0) 2019.08.01
유니티 에디터 - EditorWindow  (0) 2019.07.31
Unity 2018.3 이후 prefab 변경점  (0) 2019.07.29
Posted by 정망스
,
728x90

https://blog.naver.com/PostView.nhn?blogId=hammerimpact&logNo=220774683204&parentCategoryNo=&categoryNo=19&viewDate=&isShowPopularPosts=false&from=postView

8장 MenuItem

모르면 에디터 확장에서는 아무것도 할 수 없을 정도로 중요한 MenuItem에 대해 설명합니다. 이번 장에서는 MenuItem에서 할 수 있는 것을 소개하고 있습니다. [실제로 어떻게 쓰는가]에 대해서는 이 장에서도 간략하게 설명하겠지만 이 장을 넘어가서도 싫증이 날 정도로 다루게 되므로 자연스럽게 이해하시게 되겠지요.

8.1 MenuItem이란
MenuItem이란 [유니티 에디터의 위쪽에 있는 메뉴 바나, 컨텍스트 메뉴에 항목을 추가하기 위한 기능]입니다.


8.2 MenuItem의 사용 예시
메뉴는 어떤 트리거로 되어 있습니다. 유니티 표준에서는
- 게임 오브젝트의 생성(GameObject/Create Empty)
- 씬의 생성(File/New Scene)
- 윈도우의 표시(Window/Inspector)
등이 메뉴로서 이미 등록되어 있습니다.

유저가 MenuItem을 사용해서 독자적인 메뉴를 추가하려고 한다면 임의의 타이밍에 Editor 스크립트를 실행하고 싶을 때입니다. 목적 별로 나열해보면
- 독자적인 윈도우를 표시
- AssetBundle 의 작성
- Asset의 작성
- 선택한 Asset에 대해서 어떤 액션을 실행
등이 있습니다. 이들을 실행하기 위한 입구(트리거)가 되는게 MenuItem입니다.

8.3 MenuItem을 사용해서 메뉴를 표시하기
먼저 간단하게 메뉴를 추가해봅시다. MenuItem은 Attribute로서 제공되어 있고, static 함수에 첨가되어 있는 기능입니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("CustomMenu/Example")]
    static void Example ()
    {
    }
}

물론 기존 메뉴에 자식 메뉴를 추가하는것도 할 수 있습니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("Assets/Example")]
    static void Example ()
    {
    }
}

자식 메뉴의 자식 메뉴를 작성하는것도 가능합니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("CustomMenu/Example/Child1/Grandchild")]
    static void Example1 ()
    {
    }

    [MenuItem("CustomMenu/Example/Child2/Grandchild")]
    static void Example2 ()
    {
    }
}

 

8.4 실행할 수 없는 MenuItem을 작성하기
MenuItem에서 추가한 메뉴가 실행되어 버리면 보기가 안좋은 경우도 있습니다. 그럴때 MenuItem에 추가한 메뉴를 실행할 수 없게 막는 기능이 있습니다.

MenuItme의 두번째 인자에 isValidateFunction이 있는데, 이것은 메뉴를 표시할 때에 실행가능한지 어떤지를 체크하기 위해 있는 것입니다. isValidateFunction에 true를 설정하는 것으로 함수는 Validate 함수가 됩니다. 거기에 더해 Validate 함수는 반환값이 bool 형의 함수가 되어, 반환값으로서 true를 반환하면 실행 가능, false를 반환하면 실행불가가 됩니다.

또한, Validate 함수는 단독으로는 동작할 수 없습니다. isValidateFunction이 false인 함수를 준비할 필요가 있습니다. isValidateFunction의 기본값이 false이므로 인자를 생략해도 문제는 없습니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("CustomMenu/Example/Child1")]
    static void Example1 ()
    {
    }

    //isValidateFunction이 false
    [MenuItem("CustomMenu/Example/Child2")]
    static void Example2 ()
    {
    }

    //isValidateFunction이 true
    [MenuItem("CustomMenu/Example/Child2", true)]
    static bool ValidateExample2 ()
    {
        //이번에는 false 고정으로 해서 실행할 수 없게 합니다
        return false;
    }
}

8.5 MenuItem의 표시 순서를 변경하기
메뉴의 표시 순을 지정할 수 있습니다. MenuItem의 세번째 인자 priority에서 지정합니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("Assets/Example", false, 1)]
    static void Example ()
    {
    }
}

[priority의 형태]
priority의 수치를 작게 설정할수록 위쪽에 표시되게 됩니다.

priority가 왼쪽부터 1, 20, 40, 1000으로 설정될때의 사진입니다.

또한 priority의 수치가 앞쪽과 비교해 11 이상 건너뛴 수치라면 메뉴 항목의 사이에 구분선이 생깁니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("CustomMenu/Example1", false, 1)]
    static void Example1 ()
    {
    }

    [MenuItem("CustomMenu/Example2", false, 2)]
    static void Example2 ()
    {
    }

    [MenuItem("CustomMenu/Example3", false, 13)]
    static void Example3 ()
    {
    }
}

8.6 MenuItem에 체크를 넣기
Menu.GetChecked와 Menu.SetChecked를 사용하여 자식 메뉴에 체크를 넣을 수 있습니다.

using UnityEditor;

public class NewBehaviourScript
{
    [MenuItem("CustomMenu/Example")]
    static void Example ()
    {
        var menuPath = "CustomMenu/Example";
        var @checked = Menu.GetChecked (menuPath);
        Menu.SetChecked (menuPath, !@checked);
    }
}

8.7 Hot키(단축키)를 구현
MenuItem에 추가한 메뉴는 단축키로 실행시킬 수 있습니다. 첫번째 인자인 메뉴 패스의 마지막에 [반각 스페이스 + 수식자 키 + 임의의 문자]로 되어있는 문자열을 붙이면 구현할 수 있습니다.

using UnityEditor;
using UnityEngine;

public class NewBehaviourScript
{
    //command(ctrl) + shift + g 로 실행
    [MenuItem("CustomMenu/Example %#g")]
    static void Example ()
    {
        Debug.Log ("실행되었습니다");
    }
}

표 8.1 수식자 키를 나타내는 특수문자
% : Ctrl(Windows) 혹은 command(MacOSX)
# : Shift
& : Alt(Windows) 혹은 option(Mac OS X)
_ : 수식자 키 없음
F1 ... F12 : Function키
HOME : Home 키
END : End 키
PGUP : PageUp 키
PGDN : PageDown 키
KP0 ... KP9 : 0부터 9까지의 숫자키
KP. : .
KP+ : +
KP- : -
KP* : *
KP/ : /
KP= : =

[Function 키만으로 되어 있는 단축키는 만들 수 없습니다]

8.8 CONTEXT
각 컴포넌트의 톱니바퀴 아이콘을 누르면 표시되는 컨텍스트 메뉴에 메뉴 항목을 추가할 수 있습니다.

표시하기 위한 규칙이 있는데, 메뉴 패스의 앞쪽에 [CONTEXT/]를 추가합니다. 그리고 [CONTEXT/컴포넌트 이름/메뉴 이름]이라고 치면 컨텍스트 메뉴에 표시됩니다.

using UnityEditor;

public class NewBehaviourScript
{
    //Transform 에 메뉴를 추가
    [MenuItem("CONTEXT/Transform/Example1")]
    static void Example1 () { }

    //컴포넌트 (전체)에 메뉴를 추가
    [MenuItem("CONTEXT/Component/Example2")]
    static void Example2 () { }

    //ExampleScript 스크립트에 메뉴를 추가
    [MenuItem("CONTEXT/ExampleScript/Example3")]
    static void Example3 () { }
}

컨텍스트 메뉴에 대해서도 단축키는 적용시킬 수 있습니다. 컨텍스트 메뉴에 표시되지 않으면 실행되지 않습니다.

[MenuCommand]
컨텍스트 메뉴만이 MenuCommand를 인자로 하여 컴포넌트 정보를 받을 수 있습니다.

using UnityEditor;
using UnityEngine;

public class NewBehaviourScript
{
    [MenuItem("CONTEXT/Transform/Example1")]
    static void Example1 (MenuCommand menuCommand)
    {
        // 실행한 Transform의 정보를 얻을 수 있음
        Debug.Log (menuCommand.context);
    }
}

위의 코드를 MainCamera의 Transform에서 실행시킨 모습입니다.

728x90
Posted by 정망스
,
728x90

요즘 에디터 작업을 주로 하고 있는데 정리가 잘된 글을 발견하여 참고하기위해 가져옴

https://blog.naver.com/PostView.nhn?blogId=hammerimpact&logNo=220774325360&parentCategoryNo=&categoryNo=19&viewDate=&isShowPopularPosts=false&from=postView

7장 EditorWindow

에디터 확장을 처음 접하면, 먼저 윈도우를 표시하는 것에서 시작할 수도 있습니다. 이 장에서는 간단하게 EditorWindow를 표시하는 방법에서 목적에 맞는 EditorWindow 선택, 그 특성에 대해서 설명합니다.

7.1 EditorWindow 란
씬 윈도우, 게임 윈도우, 인스펙터 윈도우 등, 이들 모두는 EditorWindow입니다. 유니티 에디터는 다양한 기능을 가진 EditorWindow의 집합으로 되어 있습니다.



7.2 HostView와 SplitView와 DockArea
EditorWindow는 혼자서 표시 될 수 없고, 부모로써 DockArea를 가지고, 이 DockArea에 의해 EditorWindow는 표시됩니다.


DockArea는 [웹 브라우저의 탭]과 같은 기능을 제공합니다.
예를들어, 윈도우는 각각 독립된 3개의 윈도우로도 될 수 있고, 3개의 탭으로써 1개의 윈도우에 합쳐들어가는 것도 할 수 있습니다.


외관도 닮았고, 탭의 기능으로써, 탭을 끌어내 별개의 윈도우로 만들 수 있는것도 가능합니다.


이와 같이, DockArea에는 1개 이상의 EditorWindow를 표시하기 위한 기능이 포함되어 있습니다. 예를들어, 2개 이상의 EditorWindow가 DockArea에 있을 경우, 탭 기능을 사용해 각각의 EditorWindow를 표시하거나, SplitWindow로 DockArea의 영역을 분할하여 표시합니다.


게다가 DockArea는 HostView의 역할도 가지고 있습니다. HostView는, 다양한 오브젝트, 이벤트와의 통신을 실행하기 위한 기능을 가지고 있습니다. 윈도우의 [Update 함수]와 [OnSelectionChange 함수] 등을 실행하기 위한 기능이 포함되어 있습니다.

3개의 윈도우 [HostView] [SplitView] [DockArea]를 소개했습니다. 이들의 영역(클래스)에는 유감스럽게도 접근할 수 없습니다. 하지만, 기억해두면 EditorWindow의 구조를 보다 잘 이해할 수 있을 것입니다.

7.3 EditorWindow의 작성
먼저 일반적인 EditorWindow를 작성해봅시다.

[EditorWindow를 표시하기까지]
EditorWindow를 표시하기까지의 기본적인 흐름은 3개의 스텝으로 구성되어 있습니다. 먼저 이 3개 스텝을 기억합시다. 
1. EditorWindow를 작성하기 위해 EditorWindow를 상속한 클래스를 작성합니다.

using UnityEditor;

public class Example : EditorWindow
{
}

2. 다음엔 EditorWindow를 표시하기 위한 트리거로써의 메뉴를 추가합니다.

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}


3. 마지막으로 EditorWindow를 표시합니다. EditorWindow는 ScriptableObject를 상속하고 있으므로, EditorWindow.CreateInstance로 EditorWindow 오브젝트를 생성합니다. 그리고 Show를 호출하면 EditorWindow가 표시됩니다.

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var exampleWindow = CreateInstance<Example> ();
        exampleWindow.Show ();
    }
}

"Window/Example" 메뉴를 실행해서 표시합니다. 실행하기 위해서는 EditorWindow를 새로 작성합니다.

7.4 EditorWindow.GetWindow
EditorWindow를 작성할 경우, [다수의 존재를 허가하는 EditorWindow]와 [1개만 허가하는 EditorWindow] 2종류를 생각해볼 수 있습니다. 다수의 존재를 허가하는 EditorWindow는 방금전 설명한 EditorWindow.CreateInstance를 사용해 EditorWindow를 작성해 표시합니다.

[1개만 허가하는 EditorWindow]
[이미 EditorWindow가 존재하고 있는 경우에는 생성하지 않는다]라는 확인처리를 구현하지 않으면 안됩니다.

using UnityEditor;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.Show ();
    }
}

이렇게 해도 되지만, [이미 EditorWindow가 존재하면 그 인스턴스를 얻는다. 없으면 생성한다. 마지막으로 Show 함수를 호출한다]라는 다수의 기능을 1개에 집약한 API가 존재합니다. EditorWindow.GetWindow입니다.

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}


GetWindow를 실행하면 내부에서 인스턴스가 캐쉬됩니다.
게다가 GetWindow 함수에는 편리한 기능이 있어서, 특정 EditorWindow에 탭윈도우로써 표시할 수 있습니다. (DockArea에 EditorWindow를 추가)

씬 윈도우에 탭 윈도우로써 추가할 수 있습니다.

using UnityEditor;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> (typeof(SceneView));
    }
}


7.5 어느 Show 함수를 호출하는가로 바뀌는 특수한 EditorWindow
지금까지 이 장에서 다루었던 EditorWindow는 기본적인 탭 기능을 쓸수 있는 윈도우입니다. 그 외에도 EditorWindow는 다양한 종류의 윈도우를 작성할 수 있습니다.

Show 함수는 여러 종류 준비되어 있으며, 어느 Show 함수를 호출할 것인지로 표시되는 EditorWindow가 변화합니다.

[Show]
기본 기능의 탭 윈도우로써 사용됩니다. EditorWindow.GetWindow를 사용하고 있는 경우, 내부에서 Show가 호출됩니다.

[ShowUtility]
탭 윈도우로써 사용되지 않고, 항상 앞에 표시되는 윈도우입니다. 예를들어 다른 윈도우에 Focus를 맞추어도 그 Window자체가 뒤쪽으로 돌아가지 않습니다. 설정 윈도우같이 다른 윈도우를 조작하더라도 제일 앞에 표시하고 싶은 경우에 사용합니다.


GetWindow는 사용할 수 없으므로 그 대신에 CreateInstance를 사용합니다.

using UnityEditor;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowUtility ();
    }
}

[ShowPopup]
윈도우 타이틀과 닫기 버튼이 없는 윈도우입니다. 예를들어 다른 Window에 Focus를 맞춰도 그 Window 자체가 뒤쪽으로 돌아가는 일 없이, 닫음 버튼이 없기 때문에, 윈도우를 닫는 처리는 스스로 구현할 필요가 있습니다.

 

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }
        exampleWindow.ShowPopup ();
    }


    void OnGUI ()
    {
        // Escape를 누른 때에 닫는다
        if (Event.current.keyCode == KeyCode.Escape) {
            exampleWindow.Close();
        }
    }
}

SpriteEditor같은 슬라이스 메뉴 버튼에서 Popup을 표시하곤 합니다.

[PopupWindow]
이전에 소개한 ShowPopup과 거의 비슷하여, Popup을 표시하기 위한 기능입니다. PopupWindow는 Popup을 범용적으로 다루기 위한 것이라고 생각하시면 됩니다.

버튼을 누르면 아래에 Popup이 표시됩니다.

사용법은 매우 간단합니다. 먼저 PopupWindowContent를 상속한 클래스를 작성합니다. 그리고 PopupWindow.Show에서 Popup을 표시합니다.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }

    //인스턴스화
    ExamplePupupContent popupContent = new ExamplePupupContent ();

    void OnGUI ()
    {
        if (GUILayout.Button ("PopupContent",GUILayout.Width(128))) {
            var activatorRect = GUILayoutUtility.GetLastRect ();
            //Popup을 표시한다
            PopupWindow.Show (activatorRect, popupContent);
        }
    }
}

public class ExamplePupupContent : PopupWindowContent
{
    public override void OnGUI (Rect rect)
    {
        EditorGUILayout.LabelField ("Lebel");
    }

    public override void OnOpen ()
    {
        Debug.Log ("표시할 때에 호출됨");
    }

    public override void OnClose ()
    {
        Debug.Log ("닫을때 호출됨");
    }

    public override Vector2 GetWindowSize ()
    {
        //Popup 의 사이즈
        return new Vector2 (300, 100);
    }
}

[ShowAuxWindow]
탭 윈도우로써 다루어지지 않는 윈도우를 작성합니다. 외관은 ShowUtility와 같습니다만 다른 윈도우에 Focus를 두면 윈도우는 소거됩니다. 윈도우를 생성해놓고 지우는걸 깜빡하는 일이 없으므로 수가 늘어나지 않으니, 간단한 설정, 조작에서 사용하는것을 추천합니다.


[ShowAsDropDown]
팝업과 마찬가지로, [윈도우 타이틀] [닫기 버튼]이 없는 윈도우입니다. 단, PC의 화면 사이즈를 고려해서, 윈도우를 표시하는 위치에서 충분한 넓이를 확보할 수 없는 경우, 윈도우의 표시 영역을 화면 안으로 넣기 위한 X/Y축의 위치가 자동적으로 보정됩니다. 바꿔 말해, 화면의 구석에서 윈도우를 연다고 하여도, 반드시 PC의 표시영역에 모두 표시됩니다.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    static Example exampleWindow;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        if (exampleWindow == null) {
            exampleWindow = CreateInstance<Example> ();
        }

        var buttonRect = new Rect (100, 100, 300, 100);
        var windowSize = new Vector2 (300, 100);
        exampleWindow.ShowAsDropDown (buttonRect, windowSize);
    }
}

그외에는 Popup과 같은 기능을 합니다.

[ScriptableWizard]
[GameObject를 만든다] [Prefab을 만든다] [Asset을 만든다] 이와 같이 뭔가를 [만들] 때에 사용하는 윈도우입니다. ScriptableWizard는 지금까지 소개해온 윈도우와는 조금 다릅니다.

[ScriptableWizard의 사용법]
1. ScriptableWizard를 상속하는 클래스를 만듭니다.

using UnityEditor;

public class Example : ScriptableWizard
{
}

2. 다음으로 ScriptableWizard를 표시하기 위한 트리거로써 메뉴를 추가합니다.

using UnityEditor;

public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
    }
}

3. ScriptableWizard를 표시합니다. 표시는 ScriptableWizard.DisplayWizard로 합니다.

using UnityEditor;

public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
}

표준에서 우측 하단에 Create버튼이 표시됩니다.

[ScriptableWizard에는 클래스의 필드가 표시됩니다]
다른 EditorWindow에서는 GUI의 표시에 EditorGUI 클래스를 사용합니다만, ScriptableWizard에서는 사용할 수 없습니다. ScriptableWizard에서는 인스펙터에서 표시되듯이 [public 필드] [Serialize 가능한 필드]가 윈도우에 표시됩니다.

 

using UnityEditor;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }
}

[OnWizardCreate]
ScriptableWizard의 오른쪽 아래에 있는 Create 버튼을 눌렀을때 호출되는 함수입니다.

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }
}

[OnWizardOtherButton]
Create 버튼 외에 또 1개 버튼을 추가할 수 있습니다. 작성 관련해서 2개의 패턴을 만들고 싶은 경우에 사용해주세요. 버튼을 추가하기 위해서는 ScriptableWizard.DisplayWizard의 3번째 인자로 버튼 이름을 지정해야 합니다.

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard", "Create", "Find");
    }

    void OnWizardCreate ()
    {
        new GameObject (gameObjectName);
    }

    void OnWizardOtherButton ()
    {
        var gameObject = GameObject.Find (gameObjectName);

        if (gameObject == null)
        {
            Debug.Log ("게임 오브젝트를 찾을 수 없습니다");
        }
    }
}

}

[OnWizardUpdate]
모든 필드의 수치를 대상으로, 수치의 변경이 있을 경우에 호출되는 함수입니다.

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    void OnWizardUpdate ()
    {
        Debug.Log ("Update");
    }
}

[DrawWizardGUI]
위자드 안에서의 GUI를 표시하기 위한 함수입니다. 이 함수를 오버라이드하는 것을 통해 GUI를 커스터마이즈 할 수 있습니다.
단, 반환값으로 true를 반환하도록 해주세요. true를 반환하지 않으면 OnWizardUpdate가 호출되지 않게 되어 버립니다.

지금까지 표시된 프로퍼티가 없어져버리고, 라벨이 표시됩니다.

using UnityEditor;
using UnityEngine;

public class Example : ScriptableWizard
{
    public string gameObjectName;

    [MenuItem ("Window/Example")]
    static void Open ()
    {
        DisplayWizard<Example> ("Example Wizard");
    }

    protected override bool DrawWizardGUI ()
    {
        EditorGUILayout.LabelField ("Label");
        //false 를 반환하면 OnWizardUpdate 가 호출되지 않게 됩니다
        return true;
    }
}

[OnGUI 함수는 사용하지 말것]
ScriptableWizard 클래스는 EditorWindow를 상속합니다. 그러므로 OnGUI 함수를 사용하면 보통의 EditorWindow로써 표시되어 버리므로, 필드의 수치와 Create버튼이 표시되지 않습니다.

[PreferenceItem]
PreferenceItem은 Unity Preferences에 메뉴를 추가하기 위한 기능입니다. Unity Preferences는 Unity 에디터 전체에 영향을 주는 설정을 하기 위해 있습니다.

추가된 메뉴는 가장 마지막 위치에 추가됩니다.

using UnityEditor;

public class Example
{
    [PreferenceItem("Example")]
    static void OnPreferenceGUI ()
    {

    }
}

7.6 메뉴를 추가하는 IHasCustomMenu
탭에서 우클릭, 혹은  을 클릭하면 표시되는 컨텍스트 메뉴에 메뉴를 추가합니다.

IHasCustomMenu는 인터페이스로서 구현되어 있습니다.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow, IHasCustomMenu
{

    public void AddItemsToMenu (GenericMenu menu)
    {
        menu.AddItem (new GUIContent ("example"), false, () => {

        });

        menu.AddItem (new GUIContent ("example2"), true, () => {

        });
    }

    [MenuItem("Window/Example")]
    static void Open ()
    {
        GetWindow<Example> ();
    }
}


7.7 EditorWindow의 사이즈를 변경할 수 없게 하기

우측 하단에 있는 사이즈조절하는 3각 마크가 사라집니다.
EditorWindow.minSize와 EditorWindow.maxSize에 의해 EditorWindow의 크기를 제한할 수 있습니다. 최소 수치와 최대 수치가 같으면 EditorWindow의 크기를 변경할 필요가 없다고 판단해서 우측 하단의 3각 마크가 표시되지 않습니다.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        var window = GetWindow<Example> ();
        window.maxSize = window.minSize = new Vector2 (300, 300);
    }
}

7.8 윈도우에 아이콘을 추가하기
아이콘을 추가하기 위해서는 EditorWindow.titleContent에 아이콘을 가진 GUIContent를 대입합니다.

아이콘은 매우 작으므로 알아보기 쉬운것으로 하는걸 추천.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem ("Window/Example")]
    static void SaveEditorWindow ()
    {
        var window = GetWindow<Example> ();

        var icon = AssetDatabase.LoadAssetAtPath<Texture> ("Assets/Editor/Example.png");

        window.titleContent = new GUIContent ("Hoge", icon);
    }
}

7.9 GetWindow를 사용하지 않고 이미 있는 EditorWindow를 얻기 위해서는?
싱글톤을 구현하든지, GetWindow를 통해 내부에 캐쉬해두든지 하는 방법으로 EditorWindow에 접근할 수 있습니다. 하지만, 먼저 올렸던 2가지 방법을 쓸 수 없는 상황에서는 Resources 클래스에 있는 Resources.FindObjectsOfTypeAll을 사용합니다.

FindObjectOfAll은 현재 로드되어 있는 모든 오브젝트로부터 특정 오브젝트를 검색해서, 얻습니다. 이것은 런타임에서 사용하는 오브젝트뿐만 아니라 에디터에서 사용하는 오브젝트도 검색 대상입니다.

using UnityEditor;
using UnityEngine;

public class Example : EditorWindow
{
    [MenuItem("Window/Example")]
    static void Open ()
    {
        // 모든 SceneView를 얻습니다
        var sceneViews = Resources.FindObjectsOfTypeAll<SceneView> ();
    }
}

7.10 EditorWindow도 ScriptableObject임을 확인

어셈블리 브라우저에서 보면, ScriptableObject가 상속되어 있는것을 확인가능.

EditorWindow는, ScriptableObject의 동작에 따라, EditorWindow 오브젝트를 Asset으로서 저장할 수 있습니다. 그 때 인스펙터에서는 Serialize된 프로퍼티도 표시합니다.

Example 윈도우를 Asset으로써 저장할 때의 인스펙터

using UnityEditor;
using UnityEngine;
public class Example : EditorWindow
{
    [MenuItem ("Assets/Save EditorWindow")]
    static void SaveEditorWindow ()
    {
        AssetDatabase.CreateAsset (CreateInstance<Example> (), "Assets/Example.asset");
        AssetDatabase.Refresh ();
    }

    [SerializeField]
    string text;

    [SerializeField]
    bool boolean;
}

윈도우 위치와 사이즈 등도 저장됩니다. 그것들의 데이터는 직접 YAML 형식의 파일을 텍스트 에디터로 보면 확인할 수 있습니다.

m_MinSize: {x: 100, y: 100}
m_MaxSize: {x: 4000, y: 4000}
m_TitleContent:
m_Text: Example
m_Image: {fileID: 0}
m_Tooltip:
m_DepthBufferBits: 0
m_AntiAlias: 0
m_Pos:
serializedVersion: 2
x: 0
y: 0
width: 320
height: 240
728x90
Posted by 정망스
,


맨 위로
홈으로 ▲위로 ▼아래로 ♥댓글쓰기 새로고침