본문 바로가기
유니티/커스텀 에디터

유니티 하이어라키에 활성화/비활성화 버튼 넣기

by 칼루트 2021. 1. 31.

유니티 하이어라키에 활성화/비활성화 버튼 넣기

 

유니티를 이용하다보면 게임오브젝트나 컴포넌트를 활성화/비활성화 할때 약간의 불편함을 느낄데가 있더라구요(나만 그런가?)

 

유니티는 하이어라키에서 활성화/비활성화 할 대상을 누르고 인스펙터에서 설정을 바꾸는 방식으로 되어 있는데

 

유니티 게임 오브젝트 활성화/비활성화 장면

 

컴포넌트는 몰라도 게임오브젝트에 대한 활성화/비활성화는 하이어라키에서 설정 가능해도 되지 않나?라는 생각을 정말 많이 했습니다.

 

실제로 그러한 기능을 제공하는 에셋들도 많고 해서 저 기능들을 추가하는 방법을 조사하고 직접 구현했던 방법들을 소개하겠습니다.

 

최종 결과

 

전체 코드

using UnityEditor;
using UnityEngine;

public static class CustomHierarchy
{

    static GUIStyle activeStyle = new GUIStyle();

    [InitializeOnLoadMethod]
    static void Setting()
    {
        SetActiveStyle();

        EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyItemGUI;
    }

    static void SetActiveStyle()
    {
        var on = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/show.png");
        var off = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/hide.png");

        activeStyle.onActive.background = on;
        activeStyle.onFocused.background = on;
        activeStyle.onHover.background = on;
        activeStyle.onNormal.background = on;
        activeStyle.active.background = off;
        activeStyle.focused.background = off;
        activeStyle.hover.background = off;
        activeStyle.normal.background = off;
        activeStyle.imagePosition = ImagePosition.ImageOnly;
    }

    static void OnHierarchyItemGUI(int id, Rect selectionRect)
    {
        var go = (GameObject)EditorUtility.InstanceIDToObject(id);

        if (go == null)
            return;

        DrawActive(go, selectionRect);
        DrawComponents(go, selectionRect);
    }

    static void DrawActive(GameObject go, Rect selectionRect)
    {
        var toogleRect = new Rect(selectionRect) { x = 32, width = 16 };
        bool active = go.activeSelf;

        EditorGUI.BeginChangeCheck();

        active = GUI.Toggle(toogleRect, active, GUIContent.none, activeStyle);

        bool check = EditorGUI.EndChangeCheck();
        
        if(check)
        {
            go.SetActive(active);
        }
    }

    private static void DrawComponents(GameObject go, Rect selectionRect)
    {
        var position = new Rect(selectionRect)
        {
            width = 18,
            height = 18,
            x = Screen.width - 23,
            y = selectionRect.y - 2.5f
        };

        foreach (var com in go.GetComponents<Component>())
        {
            if (com is Transform)
            {
                continue;
            }

            var icon = AssetPreview.GetMiniThumbnail(com);
            var originColor = GUI.color;

            var behaviour = com as Behaviour;

            if(behaviour != null && !behaviour.enabled)
            {
                GUI.color = new Color(.5f, .5f, .5f);
            }
            
            GUI.DrawTexture(position, icon);
            GUI.color = originColor;

            if (position.Contains(Event.current.mousePosition))
            {

                if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
                {
                    var menu = new GenericMenu();
                    bool enable = behaviour.enabled;

                    menu.AddItem(new GUIContent("Enable"), enable,
                    ()=>
                    {
                        behaviour.enabled = !enable;
                    });
                    
                    menu.ShowAsContext();

                    Event.current.Use();
                }
            }


            position.x -= 18;
        }
    }
}

 

설명

[InitializeOnLoadMethod]
static void Setting()
{
    SetActiveStyle();

    EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyItemGUI;
}

여기서 볼건 두가지가 있는데요

InitializeOnLoadMethodEditorApplication.hierarchyWindowItemOnGUI 이겁니다.

 

InitializeOnLoadMethod는 에디터가 켜졌을때, 스크립트 컴파일 직후에 호출되게 하는 애트리뷰트입니다.

 

EditorApplication.hierarchyWindowItemOnGUI는 OnGUI 이벤트마다 HierarchyWindow에 표시되는 목록 항목마다 호출되는 콜백으로 id와 항목의 rect 값을 인자로 받을 수 있어요

 

그래서 InitializeOnLoadMethod로 하이어라키의 항목을 받을 수 있는 콜백에 저희가 그리려고 하는 함수를 추가해줍니다.

static void SetActiveStyle()
{
    var on = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/show.png");
    var off = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/hide.png");

    activeStyle.onActive.background = on;
    activeStyle.onFocused.background = on;
    activeStyle.onHover.background = on;
    activeStyle.onNormal.background = on;
    activeStyle.active.background = off;
    activeStyle.focused.background = off;
    activeStyle.hover.background = off;
    activeStyle.normal.background = off;
    activeStyle.imagePosition = ImagePosition.ImageOnly;
}

이 함수는 게임오브젝트를 껐다 켰다하는 토글버튼에 사용할 이미지를 셋팅하는 함수인데요

AssetDatabase.LoadAssetAtPath 함수의 <>에 가져오고자 하는 리소스 타입을 넣고 인자로 리소스의 위치와 확장자를 넣어주면 리소스가 로드 됩니다.

 

static void OnHierarchyItemGUI(int id, Rect selectionRect)
{
    var go = (GameObject)EditorUtility.InstanceIDToObject(id);

    if (go == null)
    return;

    DrawActive(go, selectionRect);
    DrawComponents(go, selectionRect);
 }

EditorUtility.InstanceIDToObject

 

인스턴스 아이디를 오브젝트 타입으로 반환해주고 로드되어 있지 않을 경우에는 로드를 해서 반환을 한다고 설명이 되어 있는데

 

정확히는 모르지만 여기서 사용되는 아이디는 하이어라키의 항목에 대한 아이디이기에 게임 오브젝트 타입으로 형변환이 가능한거 같습니다.

 

static void DrawActive(GameObject go, Rect selectionRect)
{
    var toogleRect = new Rect(selectionRect) { x = 32, width = 16 };
    bool active = go.activeSelf;

    EditorGUI.BeginChangeCheck();

    active = GUI.Toggle(toogleRect, active, GUIContent.none, activeStyle);

    bool check = EditorGUI.EndChangeCheck();

    if(check)
    {
    	go.SetActive(active);
    }
}

여기도 크게 어려운건 없습니다.

selectionRect 같은 경우는 아래 사진에서 보이는 범위를 말합니다.

 

설명 예시

토글 위치의 x 시작 위치를 32부터 해놨는데 어느 버전부터인지는 모르겠지만

저 앞쪽에 눈아이콘 ( 저건 그냥 씬뷰에서 보이고 안보이고에 대한 설정하는 버튼입니다. ) 과 손가락 아이콘이 위치한 범위가 32까지라 시작위치를 32부터 시작하는겁니다.

 

EditorGUI.BeginChangeCheck();

 

문서에는 코드블록 내에서 컨트롤이 변경되는지 확인할때 쓰인다고 하는데

코드 블록이 begin과 end 함수 사이를 말하는거 같네요

 

여기서 말하는 컨트롤은 gui로 만들어진 버튼이나 토글 버튼 등을 말하는거 같습니다.

 

private static void DrawComponents(GameObject go, Rect selectionRect)
    {
        var position = new Rect(selectionRect)
        {
            width = 18,
            height = 18,
            x = Screen.width - 23,
            y = selectionRect.y - 2.5f
        };

        foreach (var com in go.GetComponents<Component>())
        {
            if (com is Transform)
            {
                continue;
            }

            var icon = AssetPreview.GetMiniThumbnail(com);
            var originColor = GUI.color;

            var behaviour = com as Behaviour;

            if(behaviour != null && !behaviour.enabled)
            {
                GUI.color = new Color(.5f, .5f, .5f);
            }
            
            GUI.DrawTexture(position, icon);
            GUI.color = originColor;

            if (position.Contains(Event.current.mousePosition))
            {

                if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
                {
                    var menu = new GenericMenu();
                    bool enable = behaviour.enabled;

                    menu.AddItem(new GUIContent("Enable"), enable,
                    ()=>
                    {
                        behaviour.enabled = !enable;
                    });
                    
                    menu.ShowAsContext();

                    Event.current.Use();
                }
            }


            position.x -= 18;
        }

마지막에서 볼건 AssetPreview.GetMiniThumbnailGenericMenu정도 인거 같네요

 

AssetPreview.GetMiniThumbnail는 제가 못 찾는건지 크게 자료가 없더군요

문서에서는 객체의 축소판 즉 썸네일을 반환한다로 간단히 설명해놨네요

 

GenericMenu는 아래 이미지 처럼 저런 메뉴를 구성하고 추가하는데 쓰이는 클래스입니다.

 

설명 예시

 

AddItem라는 함수로 메뉴를 추가하고 ShowAsContext라는 함수를 호출하면 마우스 위치에 저러한 메뉴를 띄어줍니다.

 

저는 그냥 간단히 컴포넌트를 끄고 켜는 기능만 메뉴에 추가했는데 컴포넌트를 제거한다거나 컴포넌트간의 위치를 바꿔주는 기능을 추가해도 되겠네요

 

아니면 저런 메뉴가 아니라 인스펙터의 컴포넌트 부분만 새창으로 띄어서 설정을 변경하는 방식으로 구성해도 괜찮을 듯 합니다.

 

 

댓글