TA/Unity2013. 9. 11. 11:31

출처 : http://www.unitystudy.net/bbs/board.php?bo_table=writings&wr_id=146&sca=&sfl=wr_subject&stx=%EC%97%90%EB%94%94%ED%84%B0+%ED%99%95%EC%9E%A5&sop=and

unitystudy.net

 

 

 

 

 

 

 

 

1. 에디터 확장 01 - 커스텀 인스펙터

안녕하세요, 우영씨입니다. 이번에 에디터 확장과 관련된 글 연재를 맡게 되었습니다. 많은 경험이 있는 것은 아니지만 최대한 제가 알고 있는 선에서 지식과 경험을 공유해보고자 합니다. 이 연재가 여러분의 개발을 즐겁게 해줄 수 있기를 기대해봅니다.
문의 사항이 있으신 분은 아래 덧글로 남겨주시거나 whoo24@gmail.com 또는 @whoo24 으로 문의주십시오. 편의상 존칭은 생략하였습니다.

에디터

에셋스토에서 구입한 툴이나 플러그 인들은 대체로 자신들만의 에디터를 가지는 경우가 많다. 툴이 필요한 이유는 실수를 줄여주고 컨텐츠의 빠른 추가가 가능하기 때문이다. 특히 프로그래머가 아닌 기획자나 아트 쪽 작업자가 건드리기에도 무척 유용하다.
유니티의 경우 엔진 소스에 접근하지 않고 유니티 툴을 확장 시켜서 개발 효율을 향상시키는 방법을 제공하고 있다. 아래의 이미지는 2D Toolkit의 스프라이트 콜랙션에 관련된 에디터이다. 

TK2D 에디터 이미지


인스펙터

인스펙터는 게임 오브젝트에 대한 속성값을 보여주고 설정을 변경할 수 있도록 도와주는 창이다. 기본적으로 MonoBehaviour의 public 으로 설정한 멤버 변수들이 인스펙터에 읽기/쓰기가 가능한 상태로 노출된다. 대부분의 경우는 맴버 변수의 타입에 따라서 인스펙터가 자동으로 필드를 생성시켜준다.

카메라의 경우 Clear Flags 같이 콤보 필드와 Background 같은 컬러 필드, 그리고 Culling Mask처럼 여러 값이 선택 가능한 마스크 필드를 비롯한 여러가지 필드들을 볼 수 있다.

Main Camera의 인스펙터



인스펙터를 다시 한번 보면 게임 오브젝트에 추가된 컴포넌트 별로 속성이 정리되어 있다. 우리는 이 인스펙터가 보여주는 뷰 자체를 뜯어고칠 수는 없다. 다만 하나의 컴포넌트에 대해 개별적으로 수정할 수 있을 뿐이다.

컴포넌트 인스펙터 확장

유니티 프로젝트를 하나 만들면 두 가지 프로젝트가 생성된다. Assembly-CSharp 프로젝트와 Assembly-CSharp-Editor 프로젝트가 자동으로 만들어진다.

에디터에 관계된 코드들은 게임 상에서는 실행될 수 없다. 빌드조차 되지 않는다. 그 이유는 대부분의 기능이 UnityEditor 라는 네임스페이스 내에 구현되어 있고 이 UnityEditor 참조는 게임 프로젝트에 포함되지 않는다. 

반드시 루트의 Editor 폴더에 스크립트가 있어야 하는 것은 아니다. 어느 서브 폴더든지 Editor 폴더안에만 있다면 OK.

image2013-2-3 23-18-9.png



씬에 큐브를 하나 만들고 이 큐브를 회전시키는 스크립트를 하나 만들어 보자. 두 스크립트 파일을 만든다.

그리고 이 스크립트를 조절할 수 있도록 인스펙터를 확장해보자. 우선 CubeRotator.cs 파일을 생성한다.

using UnityEngine;
using System.Collections;
public class CubeRotator : MonoBehaviour {
    public float rotationSpeed = 180.0f;
     
    void Update () {
        gameObject.transform.Rotate(Vector3.up, Time.smoothDeltaTime * rotationSpeed);
    }
}

image2013-2-3 21-1-10.png



큐브에 이 컴포넌트를 추가하고 플레이 버튼을 누르면 이 큐브는 그 자리에서 뱅글뱅글 돌 것이다.

컴포넌트를 확장해서 해당 큐브의 회전 방향을 바꿔주는 버튼을 하나 만들어보자. 우선은 인스펙터를 확장하기 위한 뼈대를 만들자. Editor/CubeRotatorInspector.cs 파일을 만든다.

using UnityEngine;
using UnityEditor;
 
[CustomEditor(typeof(CubeRotator))]
public class CubeRotatorInspector : Editor {
    public override void OnInspectorGUI()
    {
        base.DrawDefaultInspector();
    }
}
 
image2013-2-3 21-8-16.png



CubeRorator에 대한 인스펙터가 미묘하게 달라진 것을 볼 수 있다. 코드를 보면 우선 네임스페이스에 UnityEditor를 추가했다. 그리고 CubeRotatorInspector 클래스가 UnityEditor.Editor 를 상속 받았다. 

상속 받은 CubeRotatorInspector CustomEditor클래스 어트리뷰트를 가지고 있다. 이 CustomEditor는 인자값으로 받은 타입의 인스펙터에 대한 처리를 담당하게 된다. 따라서 여기서는 CubeRotator 컴포넌트에 대한 인스펙터를 처리하게 되는 것이다.

OnInspectorGUI() 메쏘드는 UnityEditor.Editor 클래스에서 파생된 메쏘드이다. 이 메쏘드를 오버라이드 해서 우리가 원하는 모습을 만들 수 있다.
UnityEditor.Editor 클래스에 대한 더 자세한 사항은 다음의 자료를 찾아보도록 하자.
 
 계속해서 인스펙터를 바꿔보자.

using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(CubeRotator))]
public class CubeRotatorInspector : Editor
{
    public override void OnInspectorGUI()
    {
        EditorGUILayout.LabelField("Custom Inspector for", "CubeRotator"); 
         
        base.OnInspectorGUI();
         
        if(GUILayout.Button("Change Direction"))
        {
            CubeRotator cube = target as CubeRotator;
            if (cube)
            {
                cube.rotationSpeed = -cube.rotationSpeed;
            }
        }
    }
}

OnInspectorGUI() 메쏘드에서 LabelField를 추가했다. Custom Inspector for 라는 라벨은 왼쪽에, CubeRotator는 오른쪽에 위치하게 된다. 

그리고 기본 인스펙터 데이터를 그리고(스크립트와 Public 변수들이 포함된다) Change Direction 이라는 버튼을 생성한다. 이 버튼을 누르면 Editor.target 변수를 이용해 컴포넌트를 가지고 온다. Editor.target 변수는 UnityEngine.Object 형이므로 적절한 형으로 캐스팅해줘야 한다. target의 rotationSpeed를 반대 방향으로 바꿔준다. 이제 이 버튼을 누르는 순간 큐브는 반대로 회전하게 될 것이다.

위에서 보는 것처럼 EditorGUILayout  GUILayout 모두 사용 가능하다. EditorGUILayout은 Field와 관련있는 메쏘드들이 대부분이다. GUILayout은 기본 필드, 스크롤바, 토글 등이 있다.
자세한 사항은 다음 링크에서 알아보자.

image2013-2-3 22-12-13.png



플레이 버튼을 누르고 플레이 중에 Change Direction을 눌러보자. Rotation Speed 값이 -180 으로 변경될 것이다. 커스텀 인스펙터도 기본 인스펙터와 마찬가지로 플레이 중이 아니더라도 작동한다. 에디트 모드에서 버튼을 눌러보면 마찬가지로 동작한다.

여러 객체 편집하기

우리가 만든 Cube 객체를 복사(Duplicate)해서 하나 더 만들어보자.

image2013-2-3 22-18-56.png



그리고 둘 다 선택하면 인스펙터에 우리가 제작한 CubeRotator 컴포넌트에 해당하는 부분에 Multi-object editing not supported. 라는 메시지가 뜬다.

image2013-2-3 22-20-21.png



이 경우에는 CanEditMultipleObjects 어트리뷰트를 추가해주면 된다. 단 이 경우 Editor.target은 Hierarchy에서 가장 상위에 위치한 오브젝트를 지칭하게 되니 Editor.target 대신 Editor.targets를 써주면 된다.

using UnityEngine;
using UnityEditor;
using System.Linq;
using System;
 
[CanEditMultipleObjects]
[CustomEditor(typeof(CubeRotator))]
public class CubeRotatorInspector : Editor
{
    public override void OnInspectorGUI()
    {
        EditorGUILayout.LabelField("Custom Inspector for", "CubeRotator");
        base.OnInspectorGUI();
        if (GUILayout.Button("Change Direction"))
        {
            CubeRotator[] cubes = Array.ConvertAll(targets, _t => _t as CubeRotator);
            foreach (CubeRotator cube in cubes)
            {
                if (cube)
                {
                    cube.rotationSpeed = -cube.rotationSpeed;
                }
            }
        }
    }
}

보면 알겠지만 Array를 캐스팅할 때 Linq를 사용했다. Linq 구문 대신 foreach 문으로 대신해도 된다. 저 Linq 구문은 Object[]인 targets에서 _t 이름으로 하나씩 꺼낸 후 CubeRotator 로 컨버팅 한 배열을 돌려주는 것이다. 만약 Linq에 대해서 모른다면 최소한 기본 문법 만큼은 배워두도록 하자.
 
image2013-2-3 22-38-56.png



이제 Cube를 두 개 선택해도 인스펙터가 정상적으로 작동한다.

정리

첫 강은 컴포넌트 타입에 대한 인스펙터 확장법에 대해 알아보았다. 인스펙터와 관련된 코드들이 어디에 있어야 하는지에 대해서 이야기 했고, 인스펙터에 GUI 요소를 추가하는 방법도 알아보았다. 하나의 오브젝트 뿐만아니라 여러 오브젝트들도 동시에 편집하는 방법을 알아보았다.
다음번 강좌에서는 더 많은 GUI 요소들과 메뉴 확장에 대해서 알아보도록 하겠다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2. 에디터 확장 02 - 메뉴 확장(1)

메뉴 확장

이전 강에서는 인스펙터를 확장해 보았다. 
유니티를 확장할 수 있는 요소로는 인스펙터 뿐만 아니라 메뉴를 확장할 수도 있다. 
메뉴에는 메뉴바에 추가되는- 일반 메뉴와 인스펙터 옆에서 작동하는 컨택스트 메뉴가 있다. 
이번 연재에는 메뉴바에 새로운 메뉴를 넣어보고 메뉴를 작동하는 방법에 대해서 알아보도록 한다.

주메뉴 추가

메뉴를 추가하는 방법은 매우 간단하다. 
MenuItemAttribute를 이용하면 된다. 
주의할 점은 static 함수에만 가능하다는 점이다. 
대신에 액세스 한정자는 무엇이 되든 상관이 없다. public 이나 protected나 심지어 private 여도 상관없이 지정할 수 있다.
 또 어떤 클래스에 있던지 상관이 없다. 일반 클래스여도 상관이 없고, MonoBehavior를 상속받은 컴포넌트내에 있어도 상관이 없다.
 다만 MenuItemAttribute는 UnityEditor 네임 스페이스 안에 있으므로 Editor 폴더 밖이라면 적당한 전처리기(#if UNITY_EDITOR ... #endif) 로 묶어줘야 한다.

02-1.png

using UnityEngine;
using UnityEditor;
public class CustomMenu
{
    [MenuItem("Custom Menu/public custom Menu")]
    static public void CustomMenuItem()
    {
        Debug.Log("public customMenuItem");
    }
    [MenuItem("Custom Menu/protected custom Menu")]
    static protected void CustomMenuItemProtected()
    {
        Debug.Log("protected customMenuItem");
    }
    [MenuItem("Custom Menu/private custom Menu")]
    static private void customMenuItemPrivate()
    {
        Debug.Log("private customMenuItem");
    }
}

MenuItemAttribute는 다음과 같은 생성자를 가진다.
public MenuItem(string itemName);
public MenuItem(string itemName, bool isValidateFunction);
public MenuItem(string itemName, bool isValidateFunction, int priority);

itemName은 메뉴의 경로를 나타낸다. 위에서 사용한 것 처럼 '/' 를 구분자로 이용해 서브 메뉴를 자동으로 생성한다. 
유니티에서 MenuItem의 메뉴 경로를 바꾸면 자동으로 새로운 경로로 메뉴를 생성하여 준다.
 다만 루트 경로를 바꾼 경우에는 에디터를 껐다가 켜야 한다.

itemName 인자를 이용해 단축키를 지정할 수도 있다.
02-2.png


using UnityEngine;
using UnityEditor;
public class CustomMenu
{
    [MenuItem("Custom Menu/public custom Menu %1")]
    static public void CustomMenuItem()
    {
        Debug.Log("public customMenuItem");
    }
    [MenuItem("Custom Menu/protected custom Menu #1")]
    static protected void CustomMenuItemProtected()
    {
        Debug.Log("protected customMenuItem");
    }
    [MenuItem("Custom Menu/private custom Menu %&1")]
    static private void customMenuItemPrivate()
    {
        Debug.Log("private customMenuItem");
    }
}

%는 Ctrl키(맥은 cmd키)를 나타낸다. #는 Shift 키를 나타내고 _는 옵션키를 아무것도 지정하지 않은 키를 나타낸다. &는 alt 키를 나타낸다.
%& 처럼 두 가지 이상의 키를 조합할 수도 있다.

isValidateFunction 인자는 메뉴의 유효성 검사를 할 수 있는 옵션이다. 
유효성 검사를 통해 해당 메뉴를 켜거나 끄거나 할 수 있다.

02-3.png


using UnityEngine;
using UnityEditor;
public class ValidateMenu
{
    [MenuItem("Custom Menu/Validate/Get GameObject Id", validate = true)]
    static public bool GetGameObjectIdValidator()
    {
        bool selected = (Selection.activeGameObject != null);
        Debug.Log(string.Format("Validate({0})", selected));
        if (selected)
            return true;
        return false;
    }
    [MenuItem("Custom Menu/Validate/Get GameObject Id", validate = false)]
    static public void GetGameObjectId()
    {
        Debug.Log(string.Format("GetGameObjectId({0})", Selection.activeInstanceID));
    }
}

유효성 평가 함수가 되려면 bool 형을 리턴하는 함수여야 한다. 
만약 void 형이나 다른 형이 들어온다면 false로 평가된다.

현재 아무 오브젝트도 선택하지 않았기 때문에 유효성 평가 함수인 GetGameObjectIdValidator() 는 false를 리턴한다.
로그를 보면 Validate(false) 를 볼 수 있는데, 매번마다 메뉴를 보여질 때 마다 평가하게 된다.

02-4.png



이제 씬의 아무 오브젝트나 선택하고 메뉴를 열면 Get GameObject Id 메뉴가 활성화 된 것을 볼 수 있다.
로그를 보면 Validate가 두 번 불려진 것을 확인할 수 있는데, 메뉴가 보여질 때 한 번, 메뉴가 눌려저서 실행될 때 한 번 불려지는 것을 알 수 있다.

priority 인자는 메뉴의 배치 순서를 나타낸다.
Custom Menu를 보면 public, protected, private 사이에 Validate 메뉴가 있는 것을 볼 수 있다. 
이 메뉴를 정렬하는 법을 알아보자.

priority 인자를 설정하지 않으면 priority 인자를 설정한 메뉴들과 구분선이 생기게 된다.
priority를 양수로 설정하면 메뉴 하단에 위치하게 되고, 음수로 설정하면 메뉴 상단에 위치하게 된다. 
validate가 true 인 함수는 표시 되지 않으므로 의미가 없고 validate가 false로 설정된 함수에 priority를 설정해두면 우선순위 설정이 정상적으로 된다.

02-5.png


using UnityEngine;
using UnityEditor;
public class CustomMenu
{
    [MenuItem("Custom Menu/public custom Menu", priority = 0)]
    static public void CustomMenuItem()
    {
        ...
    }
    [MenuItem("Custom Menu/protected custom Menu", priority = -1)]
    static protected void CustomMenuItemProtected()
    {
        ...
    }
    [MenuItem("Custom Menu/private custom Menu", priority = 1)]
    static private void customMenuItemPrivate()
    {
        ...
    }
}

정리

이번 강의는 조금 짧게 진행이 되었다. 
이번에는 메뉴바에 MenuItemAttribute를 이용해 새로운 메뉴를 추가하는 방법을 알아보았고 MenuItemAttribute의 다양한 속성에 대해서 알아보았다.
다음 번에는 컨택스트 메뉴에 대해서 자세히 알아보는 시간을 가져보도록 하겠다.

 

 

 

 

 

 

 

 

 

 

에디터 확장 03 - 메뉴 확장(2) Context Menu와 Generic Menu

컨텍스트 메뉴

컨텍스트 메뉴는 주로 마우스 오른쪽 버튼을 눌렀을 때 동작하는 메뉴를 뜻하는데, 유니티에서는 타겟이 존재하는 컴포넌트에 행하는 메뉴라고 보는 편이 더 적합하다고 본다.

컨텍스트 메뉴를 만드는 방법은 두 가지가 있다.

먼저 첫 번째 방법은 주 메뉴를 만드는 것 처럼 MenuItemAttribute를 사용하는 방법이 있다.

그리고 두 번째 방법은 ContextMenuAttribte를 사용하는 것이다.

image2013-3-6 9-50-17.png



우선 예제 씬을 구성한다.

녹색 큐브를 놓고 그 아래에 터레인을 깔았다.

Hierarchy는 다음과 같다.

image2013-3-6 9-52-19.png



NewContext.cs 파일과 Editor/ContextEditor.cs 파일을 만들었고, 지형 데이터와 씬, 머테리얼 애셋을 미리 만들어서 설정해두었다.

 

유니티에서 컨택스트 메뉴는 주 메뉴를 만들었을 때 처럼 특정한 attribute 를 이용해서 설정하면 된다.

주 메뉴를 만들듯이 MenuItemAttribute를 이용해서 컨택스트 메뉴를 만들어 보자.

NewContext.cs
using UnityEngine;
using System.Collections;
 
public class NewContext : MonoBehaviour {
#if UNITY_EDITOR
    [UnityEditor.MenuItem("CONTEXT/Rigidbody/Push")]
    static void Push(UnityEditor.MenuCommand command)
    {
        Rigidbody body = (Rigidbody)command.context;
        body.AddForce(Vector3.up * 500f);
    }
#endif
}


이렇게 하면 RigidBody 컴포넌트에 Push 라는 컨택스트 메뉴를 만들어준다. MenuItem은 UnityEditor 네임스페이스 내에 있으므로 전처리기를 이용해 묶어주었다.

플레이를 누르고 RigidBody 컴포넌트의 Push 메뉴를 누르면 큐브가 위로 올라갔다가 내려올 것이다.

image2013-3-10 21-6-16.png



MenuItemAttribute를 사용하는 이 방법은 이미 만들어진 컴포넌트에 메뉴를 붙일 수 있는 유일한 방법이기도 하다.

주 메뉴를 만드는 것과 비슷하게 메뉴의 경로에 CONTEXT/[컴포넌트 이름]/[메뉴명] 순으로 주었다는 것을 눈여겨 보자.

주 메뉴와 다른 점은 MenuCommand 를 인자로 받는다는 점이다.

static 함수는 인스턴스가 없이 호출되므로 어느 컴포넌트에서 불렸는지 알 수가 없다.

커스텀 인스펙터처럼 Editor 를 상속 받은 경우라면 target 정보를 이용해서 처리할 수 있지만, 지금처럼 MonoBehavior를 상속받은 경우에는 static 함수로는 현재의 컨택스트를 가져올 수가 없다.

이를 위해 MenuCommand 인자를 이용해서 처리할 수 있게 배려해놓았다.

MenuCommand.context 멤버는 해당 컴포넌트(선택된 컴포넌트)를 알고 있으므로 이를 이용해 처리할 수 있다.

 

컨텍스트 메뉴라면 좀 더 편하게

위에서 MenuItemAttribute를 이용해 컨택스트 메뉴를 만드는 방법을 알아보았다.

컨텍스트 메뉴를 만들때는 조금 더 편하게 만들 수 있도록 ContextMenuAttribute를 준비해 놓았다.

using UnityEngine;
using System.Collections;
 
public class NewContext : MonoBehaviour {
#if UNITY_EDITOR
    [UnityEditor.MenuItem("CONTEXT/Rigidbody/Push")]
    static void Push(UnityEditor.MenuCommand command)
    {
        Rigidbody body = (Rigidbody)command.context;
        body.AddForce(Vector3.up * 500f);
    }
#endif
    [ContextMenu("Turn")]
    void Turn()
    {
        gameObject.transform.Rotate(Vector3.up, 30);
    }
}

ContextMenuAttribute는 MenuItemAttribute와 다르게 UnityEngine 네임 스페이스 안에 있다.

그리고 static 함수가 아닌 일반 메서드에 attribute를 설정할 수 있다.
MenuCommand 인자로 받지 않지만 일반 클래스 메소드이기 때문에 this를 컨택스트 정보로 사용하면 된다.

MenuItemAttribute처럼 어느 컴포넌트에 위치할 지 결정하지 못한다.

image2013-3-10 22-7-2.png



따라서 ContextMenuAttribute 가 달린 컴포넌트는 해당 컴포넌트에 컨택스트 메뉴를 만든다.

해당 컨텍스트 메뉴를 선택하면 게임 오브젝트를 30도 회전한다.

 

Generic Menu


이제 메뉴의 마지막으로 제너릭 메뉴에 대해서 알아보도록 하자.

제너릭 메뉴는 커스텀 메뉴를 만들 수 있도록 도움을 준다.

일반적인 드롭다운 방식의 메뉴 뿐만 아니라 컨텍스트 메뉴까지 만들 수 있다.

Generic Menu를 설명하기 위해서는 EditorWindow 클래스를 이용하는 편이 설명하기 쉬우므로 다음 강에 배우게 될 EditorWindow 클래스를 미리 이용해보도록 하겠다.

Editor/ContextEditor.cs
using UnityEngine;
using System.Collections;
using UnityEditor;
public class ContextEditor : EditorWindow {
    [MenuItem("Window/Show Context Editor Window")]
    static void Init()
    {
        EditorWindow.GetWindow<ContextEditor>();
    }
 
    void OnGUI() {
        Event evt = Event.current;
        Rect contextRect  = new Rect (10, 10, 100, 100);
         
        if (evt.type == EventType.ContextClick)
        {
            Vector2 mousePos  = evt.mousePosition;
            GenericMenu menu = new GenericMenu();
            menu.AddItem(new GUIContent("MenuItem1"), false, new GenericMenu.MenuFunction2(MenuArg1), "Menu Item 1");
            menu.AddItem(new GUIContent("MenuItem2"), false, new GenericMenu.MenuFunction2(MenuArg1), "Menu Item 2");
            menu.AddSeparator("");
            menu.AddItem(new GUIContent("SubMenu/MenuItem3"), false, new GenericMenu.MenuFunction(MenuNoArg));
            menu.ShowAsContext();
            evt.Use();
        }
    }
 
    void MenuNoArg()
    {
        Debug.Log("Selected menu");
    }
 
    void MenuArg1(object obj)
    {
        Debug.Log ("Selected: " + obj);
    }
}


Init() 함수를 이용해 커스텀 윈도우를 생성한다.

Window/Show Context Editor Window 메뉴를 만들었고, 이 메뉴를 선택하면 새로운 윈도우가 생성된다.

image2013-3-10 22-25-48.png

image2013-3-10 22-26-54.png



아무것도 없는 윈도우지만 마우스 오른쪽 버튼을 누르면 메뉴가 생성된다.

image2013-3-10 22-29-48.png





이제 코드를 살펴볼 시간이다.

OnGUI() 에서 제너릭 메뉴를 생성하는데 마우스 오른쪽 버튼을 누르면 ContextClick 이라는 이벤트가 발생한다.

GenericeMenu를 만들 때 AddItem 으로 메뉴 항목을 추가하게 된다.

AddItem은 다음과 같은 방식으로 선언되어 있다.

function AddItem (content : GUIContent, on : boolean, func : MenuFunction) : void

function AddItem (content : GUIContent, on : boolean, func : MenuFunction2, userData : object) : void



3번째 인자로 MenuFunction에 대한 델리게이트를 전달해줘야 하는데 2가지 방법이 있다.

MenuFuction은 아무런 인자가 없는 델리게이트이고 MenuFunction2는 1개의 인자를 가지는 델리게이트이다.

image2013-3-10 22-42-33.png



위 그림은 메뉴를 하나씩 순서대로 클릭한 결과이다.

MenuFunction2 의 인자는 object 타입이므로 아무거나 넘길 수 있다. 이 경우 userData로 문자열 "Menu Item 1"과 문자열 "Menu Item 2"를 넘겨주었고 해당 델리게이트에서 인자로 userData를 받아서 처리한 것이다.

그리고 메뉴 추가 아랫줄에 menu.ShowAsContext() 함수가 있다.

메뉴를 추가한 후에는 메뉴를 표시해주어야 하는데 ShowAsContext() 메서드 또는 DropDown() 메서드로 메뉴를 표시할 수 있다.

ShowAsContext()는 마우스 클릭한 위치에 메뉴를 표시해준다.

반면 DropDown()은 Rect 인자를 하나 받는다.

Rect.left와 Rect.top에 메뉴를 펼쳐보이는데, 3.5.6f 버전에서는 y 위치에 Rect.height 를 더해서 표시해준다. x 위치는 Rect.width에 영향을 받지 않는 것으로 보인다.

결론

컨택스트 메뉴를 만드는 방법 3가지를 알아 보았다.

MenuItemAttribute를 이용해서 컨택스트 메뉴를 만들어 보았고, MonoBehaviour에 ContextMenuAttribute를 추가해서 간단하게 만들어보기도 했다.

그리고 GenericMenu를 이용해서 커스텀 메뉴를 만들어보기도 했다.

이 예제에서는 GenericMenu를 컨택스트 클릭 이벤트를 받아서 컨택스트 메뉴처럼 사용했지만, 처리 방식에 따라서 어떠한 메뉴라도 만들 수 있다.

다음 강에서는 에디터 확장의 꽃 EditorWindow 클래스를 이용하는 방법에 대해서 알아보도록 하겠다.

 

 

 

 

 

 

 

 

 

 

 

4. 에디터 확장 04 - EditorWindow

 

 

EditorWindow

이번에는 EditorWindow에 대해 알아보겠다.

EditorWindow는 유니티를 툴로 만들기 위해서 사용해야하는 클래스이다.

인스펙터와 달리 EditorWindow는 프로그래머가 원하는 데로 자유롭게 꾸밀 수 있게 해준다.

기본적인 사용법은 인스펙터와 거의 비슷하지만 훨씬 자유도가 있고 다양한 응용이 가능하다.

인스펙터는 컴포넌트가 반드시 게임오브젝트에 포함되어 있어야 하고 컴포넌트를 가진 게임 오브젝트를 선택해야만 보여진다.

그런 반면 EditorWindow는 언제든 창을 띄워둘 수 있어서 그야말로 툴에 적합한 클래스라고 할 수 있다.

이번에는 CanvasWindow 라는 EditorWindow를 확장시킨 클래스를 만들어보도록 한다.

캔버스를 만들자


image2013-4-11 20-58-14.png



 

위 Window는 우리가 이번 시간에 만들 EditorWindow의 확장 클래스인 CanvasWindow의 샘플이다.

펜의 색상과 크기를 고를 수 있고 팔레트에서 다양한 펜을 고를 수 있다(하지만 펜의 적용을 구현하지는 않을 것이다. 내용이 너무 길어지기 때문이다).

단순하게 메뉴를 생성하고 캔버스를 지우고 출력하는 간단한 구현만 해보도록 하겠다.

우선은 Editor\CanvasWindow.cs 파일을 생성한다.

그리고 윈도우를 띄우기 위해서 메뉴를 생성한다.

using UnityEngine;
using UnityEditor;
using System;
 
public class CanvasWindow : EditorWindow {
 
    [MenuItem("Canvas/Show")]
    public static void ShowWindow()
    {
        CanvasWindow window = (CanvasWindow)EditorWindow.GetWindow(typeof(CanvasWindow));
    }
}

이 코드는 Canvas/Show 메뉴를 만든다.

EditorWindow.GetWindow() 를 이용해서 CanvasWindow의 인스턴스를 만든다. 만약에 창이 띄워져 있는 경우라면 해당 창을 가져오고, 생성한 창이 없다면 새로 창을 생성한다.


image2013-4-11 21-4-51.png



 


위처럼 Canvas 메뉴가 생겼고 Show를 누르면 아무것도 없는 윈도우가 생성된다.

이제 이 윈도우에 컨트롤을 배치한다.

여기에 배치할 컨트롤은 펜의 크기, 펜의 색상, 그리고 가운데 표시할 텍스쳐이다.

...
	Texture2D mytexture;
    Color penColor;
    int penSize;
    readonly Rect texSize = new Rect(0, 0, 300, 400);
    Rect windowRect = new Rect(3, 3, 100, 400);

    void OnGUI()
    {
	}
}

가운데 그려지는 캔버스는 Texture가 되고 나머지는 int형 펜 크기, Color형 펜 색상이다.

나머지 rect 들은 각각 텍스처 크기와 팔렛트박스의 크기이다.

...
	void OnGUI()
	{
		if (mytexture == null)
            mytexture = new Texture2D((int)texSize.width, (int)texSize.height, TextureFormat.RGBA32, false);
        Rect canvasPos = new Rect((position.width - texSize.width) / 2, (position.height - texSize.height) / 2, texSize.width, texSize.height);
        
        EditorGUI.DrawPreviewTexture(canvasPos, mytexture);
        penColor = EditorGUILayout.ColorField("Pen Color", penColor);
        penSize = EditorGUILayout.IntSlider("Pen Size", penSize, 1, 10);
	}
}

우선 GUI 로직에 들어와서 texture가 null 이라면 texture를 생성한다.

캔버스의 위치는 화면의 가운데이고, 텍스처를 먼저 그리고 나머지 컨트롤을 그린다.


image2013-4-11 21-23-18.png



 


만약 텍스처를 나중에 그리면 윈도우를 줄였을 때 컨트롤이 텍스처 아래로 들어가서 보이지 않는다.

텍스처를 먼저 그리면 컨트롤이 위에 그려져서 화면이 작을 때에도 불편함 없이 사용할 수 있다.

image2013-4-11 21-24-40.png



 

이제 팔레트 윈도우를 그려보자.

...
	void OnGUI()
    {
        ...
        
        BeginWindows();
        windowRect = GUILayout.Window(1, windowRect, DoWindow, "Palette");
        EndWindows();
	}
 
	enum DrawMode
    {
        point,
        line,
        rectangle,
        ellipse,
    }
    DrawMode drawmode;
    void DoWindow(int id)
    {
        EditorGUILayout.BeginVertical();
        drawmode = (DrawMode)GUILayout.SelectionGrid((int)drawmode, Enum.GetNames(typeof(DrawMode)), 1);
        if (GUILayout.Button("clear"))
        {
            mytexture = new Texture2D((int)texSize.width, (int)texSize.height, TextureFormat.RGBA32, false);
        }
        EditorGUILayout.EndVertical();
    }
}

이제 그럴싸하게 기본 뷰는 다 나왔다.

팔레트 윈도우를 만드는 방법은 EditorWindow.BeginWindow() 와 EditorWindow.EndWindows() 사이에 GUILayout.Window() 함수를 이용해서 새로운 윈도우를 생성하였다.

GUILayout.Window() 에서는 윈도우 GUI에 대한 콜백을 받는다. 여기서는 DoWindow(window_id) 함수를 콜백으로 지정했고 이 안에서 GUILayout.SelectionGrid를 이용해서 여러 메뉴가 선택가능하게 만들었다. 다만 각 메뉴의 처리는 우리의 주제와 맞지 않기 때문에 point만 처리하도록 했다.

image2013-4-11 21-28-26.png



 

그런데 문제가 발생했다. 팔렛트 윈도우가 마우스 드래그로 창의 위치를 바뀌지가 않는다.

윈도우의 메시지 콜백에 GUI.DragWindow() 를 불러주면 이 창은 움직일 수 있게 된다.

...
	void DoWindow(int id)
	{
		...
		GUI.DragWindow();
	}
}

이제 텍스처에 그리기를 구현할 차례다.

다시 OnGUI() 함수의 가장 끝으로 돌아가서 마우스 이벤트에 대한 처리를 추가한다.

...
    void OnGUI()
    {
		...
        
        if (Event.current.type == EventType.MouseDrag)
        {
            int bx = (int)(Event.current.mousePosition.x - canvasPos.x);
            int by = (int)(texSize.height - (Event.current.mousePosition.y - canvasPos.y));
                
            for (int x = 0; x < penSize; ++x)
            {
                for (int y = 0; y < penSize; ++y )
                    mytexture.SetPixel(bx - penSize / 2 + x, by - penSize / 2 + y, penColor);
            }
            mytexture.Apply(false);
            Repaint();
        }
    }
...

Event.current 객체에 마우스의 이벤트가 넘어온다.

MouseDrag 뿐만 아니라 다운이나 업에 관련된 이벤트도 있다.

MouseDrag 이벤트가 오면 캔버스에 그림을 그린다.

캔버스로 사용하고 있는 mytexture에  Texture2D.SetPixel() 함수를 이용해 텍스처를 편집하고 마지막으로 Texture2D.Apply() 를 이용해 텍스처에 적용해준다. 이때 인자로 들어가는 Boolean 값은 밉맵을 갱신 시킬지에 관련된 함수다.

텍스처에 대한 접근이 끝난 후에는 EditorWindow.Repaint() 함수를 불러서 윈도우를 갱신시켜야 한다.

이제 모든 작업이 완료되었다.

정리

이번에 살펴본 EditorWindow는 우리가 지금까지 배워온 것들을 응용해보는 과정에 불과했다.

그래서 하나의 예제를 구현하는 방식으로 진행하였고, 자잘한 처리들은 패스했다.

우리는 EditorwWindow를 상속받은 새로운 윈도우를 생성하는 법과 텍스처를 직접 편집하고 그려보는 일도 해보았다.

이것을 조금 활용하면 다양한 툴을 만들 수 있을 것이다.

이 강좌의 전체 코드는 여기에 있다: https://gist.github.com/whoo24/5334402

다음 강에서는 Handles와 Gizmos에 대해서 배울 것이다.

 

 

 

 

 

Posted by 프리랜서 디자이너