강의, 책/[Unity] C#과 유니티로 만드는 MMORPG 게임 개발 시리즈

Section 7. UI - UI 자동화(바인딩)

hye3193 2024. 1. 26. 00:38
public class UI_Button : MonoBehaviour
{
    Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();

    enum Buttons
    {
        PointButton
    }
    
    enum Texts
    {
        PointText,
        ScoreText
    }
    
    private void Start()
    {
        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));
    }
    
    void Bind<T>(Type type) where T : UnityEngine.Object
    {
        string[] names = Enum.GetNames(type);
        
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects);
        
        for (int i = 0; i < names.Length; i++)
        {
            objects[i] = Util.FindChild<T>(gameObject, names[i], true);
            
            if (objects[i] == null)
                Debug.Log($"Failed to bind({names[i]})");
        }
    }
        
}

UI Canvas에 부착한 스크립트에서 위와 같이 오브젝트를 찾기 위한 코드를 작성한다

해당 오브젝트의 자식 요소에서 특정한 이름을 가진 오브젝트들을 검색할 건데, 이를 Assets > Scripts > Utils 폴더 내에 Util이라는 스크립트를 만들어 기능성 함수들을 관리하는 클래스를 생성한다

 

public class Util
{
    public static T FindChild<T>(GameObject go, string name = null, bool recursive = false) where T : UnityEngine.Object
    {
        if (go == null)
            return null;
        
        if (recursive == false)
        {
            for (int i = 0; i < go.transform.childCount; i++)
            {
                Transfrom transform = go.transform.GetChild(i);
                if (string.IsNullOrEmpty(name) || transform.name == name)
                {
                    T component = transfrom.GetComponent<T>();
                    if (component != null)
                        return component;
                }
            }
        }
        else
        {
            foreach (T component in go.GetComponentsInChildren<T>())
            {
                if (string.IsNullOrEmpty(name) || component.name == name)
                    return component;
            }
        }
        
        return null;
    }
}

위와 같이 FindChild 함수를 만들어준다

 

이어서 찾은 오브젝트들을 받아와서 사용할 수 있게 Get 함수를 만들어준다(UI_Button.cs)

T Get<T>(int idx) where T : UnityEngine.Object
{
    UnityEngine.Object[] objects = null;
    if (_objects.TryGetValue(typeof(T), out objects) == false)
        return null;
    
    return objects[idx] as T;
}
Get<Text>((int)Texts.ScoreText).text = "변경할 텍스트"

사용할 때에는 위와 같이 T 타입 명시, enum을 사용한 텍스트명을 넘겨주면 된다

 

위에서는 component를 기준으로 찾았기 때문에, 그냥 gameobject 자체를 찾고 싶은 경우 에러가 발생하게 된다

public class UI_Button : MonoBehaviour
{
    Dictionary<Type, UnityEngine.Object[] _objects = new Dictionary<Type, UnityEngine.Object[]();

    enum Buttons
    {
        PointButton
    }
    
    enum Texts
    {
        PointText,
        ScoreText
    }
    
    enum GameObjects
    {
        TestObject
    }
    
    private void Start()
    {
        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));
        Bind<GameObjects>(typeof(GameObjects));
    }
    
    void Bind<T>(Type type) where T : UnityEngine.Object
    {
        string[] names = Enum.GetNames(type);
        
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects);
        
        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true);
            
            if (objects[i] == null)
                Debug.Log($"Failed to bind({names[i]})");
        }
    }
    
    T Get<T>(int idx) where T : UnityEngine.Object
    {
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false)
            return null;

        return objects[idx] as T;
    }
}

따라서 위와 같이 gameobject를 받을 수 있도록 버전을 따로 만들어주고, Util 스크립트 내 함수에서도 아래와 같이 다른 버전의 함수를 추가해준다

 

public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
{
    Transform transform = FindChild<Transform>(go, name, recursive);
    if (transform == null)
        return null;
        
    return transform.gameObject;
}

모든 게임 오브젝트가 가지고 있는 컴포넌트인 transform을 가지고 findchild 함수를 호출시켜서 return 시키면 된다

 

마지막으로 text, button, image 같은 경우 좀 더 편리하게 꺼내 쓸 수 있게 몇 가지 함수를 추가시켜준다(UI_Button.cs)

Text GetText(int idx) { return Get<Text>(idx); }
Button GetButton(int idx) { return Get<Button>(idx); }
Image GetImage(int idx) { return Get<Image>(idx); }

 

따라서 Get 함수 대신 GetText 등의 함수를 사용해서 바인딩 된 것들을 뽑아올 수 있다

Get<Text>((int)Texts.ScoreText).text = "Bind Test";
GetText((int)Texts.ScoreText).text = "Bind Test";

 

위와 같이 작성한 코드들을 모든 UI에 복붙시켜서 사용하는 대신 Assets > Scripts > UI > UI_Base.cs 스크립트를 하나 만들어 아래와 같이 작성하여 사용한다

public class UI_Base : MonoBehaviour
{
    protected Dictionary<Type, UnityEngine.Object[] _objects = new Dictionary<Type, UnityEngine.Object[]();

    protected void Bind<T>(Type type) where T : UnityEngine.Object
    {
        string[] names = Enum.GetNames(type);
        
        UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
        _objects.Add(typeof(T), objects);
        
        for (int i = 0; i < names.Length; i++)
        {
            if (typeof(T) == typeof(GameObject))
                objects[i] = Util.FindChild(gameObject, names[i], true);
            else
                objects[i] = Util.FindChild<T>(gameObject, names[i], true);
            
            if (objects[i] == null)
                Debug.Log($"Failed to bind({names[i]})");
        }
    }
    
    protected T Get<T>(int idx) where T : UnityEngine.Object
    {
        UnityEngine.Object[] objects = null;
        if (_objects.TryGetValue(typeof(T), out objects) == false)
            return null;

        return objects[idx] as T;
    }
    
    protected Text GetText(int idx) { return Get<Text>(idx); }
    protected Button GetButton(int idx) { return Get<Button>(idx); }
    protected Image GetImage(int idx) { return Get<Image>(idx); }
}

이렇게 작성한 뒤에, UI_Button.cs 파일에서는 MonoBehaviour 클래스 대신 UI_Base를 상속받아서 사용하면 된다

* UI_Base가 MonoBehaviour을 상속받기 때문에 UI_Button에도 MonoBehaviour이 들어간다

* UI_Base의 함수와 변수는 보호 수준을 protected로 설정하여 자식 클래스가 사용할 수 있게 해주어야 한다

public class UI_Button : UI_Base
{
    enum Buttons
    {
        PointButton
    }
    
    enum Texts
    {
        PointText,
        ScoreText
    }
    
    enum GameObjects
    {
        TestObject
    }
    
    private void Start()
    {
        Bind<Button>(typeof(Buttons));
        Bind<Text>(typeof(Texts));
        Bind<GameObjects>(typeof(GameObjects));
    }
}