EveryDay.DevUp

[Unity] UI 개발 시 GameObject 의 참조 레퍼런스 관리 본문

Unity

[Unity] UI 개발 시 GameObject 의 참조 레퍼런스 관리

EveryDay.DevUp 2020. 6. 26. 23:59

 

게임의 UI를 개발하게되면 Prefab 또는 Scene의 GameObject에서 필요한 UI 요소들을 참조해서 코드를 작성하게 된다.

예를들어 UI프리팹의 TextMeshPro 와 같은 텍스트를 보여주는 컴포넌트가 있다고 할 때, 게임의 언어가 한국어 일 때는 한국이라고 나타내고 게임의 언어가 영어 일때는 USA 라고 나타내는 경우가 있다고 하자. 게임의 언어에 따라 텍스트를 바꿔줘야 하기 때문에 코드에서 언어로 분기 처리를 해야한다. 그리고 분기처리를 하기 위해 코드에서 TextMeshPro 컴포넌트의 정보를 참조해야 한다.

이때 UI 요소를 참조하는 방법은 3가지 정도가 있다.

▶ MonoBehaviour를 상속한 스크립트에서 public으로 선언한 변수에 참조할 ui요소를 드래그 & 드랍으로 가져올 수 있다.

using UnityEngine;
using TMPro;

public class Test : MonoBehaviour
{
	public TMP_Text textMeshPro;

    void Start()
    {
		textMeshPro.text = "Test";
	}
}

GameObject.Find 코드로 직접적으로 경로를 입력해서 찾은 후 GetComppnet로 원하는 ui요소를 가져올 수도 있다.

using UnityEngine;
using TMPro;

public class Test : MonoBehaviour
{
	void Start()
	{
		TMP_Text textMeshPro = GameObject.Find( "2DCamera/Canvas/Text (TMP)" ).GetComponent<TMP_Text>();
		textMeshPro.text = "test";
	}
}

UI 에서 사용할 기능을 하나의 컴포넌트로 만들어서 사용할 UI요소에 추가하여 사용하는 방법도 있다.

using UnityEngine;
using TMPro;

public class Test : MonoBehaviour
{
	void Start()
	{
		TMP_Text textMeshPro = gameObject.GetComponent<TMP_Text>();
		textMeshPro.text = "test";
	}
}


▶ 각 참조 방법의 대한 비교

각각의 방법에는 서로간의 장점이자 단점인 것들이 존재한다. 각 방법의 장점은 살리고 단점을 보완하기 위해 아래의 방법을 생각하게 됬다.

▶ UI 참조 방식의 대한 비교를 통해, UI 요소를 어떤 방식으로 참조할 지에 대한 정리를 다음과 같이 했다.

: 미리 정의된 타입으로 참조할 오브젝트를 검색한 뒤 ScriptableObject로 만들어 데이터화 한다.

: 참조할 오브젝트의 정보를 해당 오브젝트의 이름과 경로를 같이 저장하여 변동 시에 추적이 용이할 수 있도록 한다.

: 참조할 오브젝트의 검색 및 데이터화는 사람이 개별적으로 하는 것이 아닌 자동화 할 수 있도록 한다.

: UI의 기능을 구현할 때 참조할 오브젝트를 직접 접근하지 않고 함수의 파라미터로 간접 접근할 수 있도록 한다.

※ UI 참조 데이터를 관리하는 ScriptableObject

using UnityEngine;
using System;
using TMPro;

namespace EverydayDevup.Framework
{
	/// <summary>
	/// 참조하는 UI를 관리하기 위한 데이터 
	/// </summary>
	[Serializable]
	public class UIReferenceData
	{
		public string name;	// 오브젝트의 이름
		public string path; // 오브젝트의 프리팹에서의 경로
		public GameObject obj; // 오브젝트
	}

	/// <summary>
	/// ScriptableObject에서 Dictionary는 Serialize가 되지 않아 별도의 Serialize가 되는 별도의 Dictionary를 구현
	/// </summary>
	[Serializable]
	public class SerializeDicGameObject : SerializeDictionary<string, UIReferenceData> { }

	/// <summary>
	/// UI의 참조 데이터를 관리하는 SciptableObject
	/// </summary>
	[CreateAssetMenu( fileName = "UIReference", menuName = "Scriptable Object Asset/UIReference" )]
	public class UIReference : ScriptableObject
	{
		/// <summary>
		/// 특정 UI 요소를 참조할 수 있는지 확인 하기 위한 type 정의 값
		/// </summary>
		public Type[] typeArray = { typeof( TMP_Text ), typeof( Canvas ) };

		/// <summary>
		/// 하나의 ScriptableObject 데이터에서 관리하는 UI Prefab 
		/// </summary>
		[HideInInspector]
		public GameObject uiTarget;

		/// <summary>
		/// 관리되는 UI Prefab의 GameObject 정보를 관리하는 Dictionary
		/// </summary>
		[HideInInspector]
		public SerializeDicGameObject dic = new SerializeDicGameObject();

		/// <summary>
		/// key 값을 가지고 GameObject를 찾은 후 가져올 타입 T로 GetComponent를 함
		/// </summary>
		/// <typeparam name="T">GameObject에서 가져올 타입</typeparam>
		/// <param name="key">UI를 가져올 수 있는 키 값 (오브젝트의 이름)</param>
		public T GetData<T>(string key) where T : class
		{
			T obj = null;
			GameObject go = null;
			UIReferenceData data = null;
			dic.TryGetValue( key, out data );
			if( data != null )
			{
				go = data.obj as GameObject;

				if( typeof( T ) != typeof( GameObject ) )
				{
					obj = go.GetComponent<T>();
				}
				else
				{
					obj = go as T;
				}
			}

			if( obj == null )
			{
				Debug.LogError( "해당 타입의 UI가 존재하지 않습니다." + typeof( T ).ToString() );
			}

			return obj;
		}
	}
}

※ UI 참조 데이터를 관리하는 ScriptableObject의 Editor

using UnityEditor;
using UnityEngine;
using System.Text;
using System.Collections.Generic;
using System;

namespace EverydayDevup.Framework
{
	[CustomEditor( typeof( UIReference ) )]
	public class UIReferenceEditor : Editor
	{
		UIReference uiReference;
		StringBuilder stringBuilder = new StringBuilder();

		Dictionary<int, List<UIReferenceData>> dicUIElement = new Dictionary<int, List<UIReferenceData>>();
		List<bool> isFoldList = new List<bool>();
		List<string> errorString = new List<string>();

		private void OnEnable()
		{
			uiReference = (UIReference)target;
			Refresh();
		}

		public override void OnInspectorGUI()
		{
			base.OnInspectorGUI();


			uiReference.uiTarget = EditorGUILayout.ObjectField( "ui target", uiReference.uiTarget, typeof( GameObject ), false ) as GameObject;
			if( GUILayout.Button( "SET NAME" ) )
			{
				if( uiReference.uiTarget != null )
				{
					stringBuilder.Clear();
					stringBuilder.Append( uiReference.uiTarget.name );
					stringBuilder.Append( "_UIReference" );
					string changeName = stringBuilder.ToString();

					string assetPath = AssetDatabase.GetAssetPath( uiReference.GetInstanceID() );
					AssetDatabase.RenameAsset( assetPath, changeName );

					EditorUtility.SetDirty( uiReference );
				}
			}

			if( GUILayout.Button( "REFRESH" ) )
			{
				Refresh();
			}

			DisplaErrorList();
			DisplayUIElementList();
			DisplayGameObjectList();
		}

		void Refresh()
		{
			if( uiReference.uiTarget == null )
			{
				return;
			}

			uiReference.dic.Clear();
			dicUIElement.Clear();
			isFoldList.Clear();
			errorString.Clear();

			GameObject obj;
			UIReferenceData uiReferenceData;
			Transform[] objList = uiReference.uiTarget.GetComponentsInChildren<Transform>();
			for( int i = 0, icount = objList.Length; i < icount; i++ )
			{
				obj = objList[i].gameObject;
				uiReferenceData = AddGameObject( obj );
				AddUIElement( uiReferenceData );
			}

			EditorUtility.SetDirty( uiReference );
		}

		UIReferenceData AddGameObject(GameObject obj)
		{
			UIReferenceData uiReferenceData = new UIReferenceData();
			uiReferenceData.name = obj.name;
			uiReferenceData.obj = obj;
			uiReferenceData.path = GetPath( obj.transform );

			if( uiReference.dic.ContainsKey( uiReferenceData.name ) )
			{
				UIReferenceData containData = uiReference.dic[uiReferenceData.name];
				string errorText = "Exist : " + containData.path + " Add : " + uiReferenceData.path;
				errorString.Add( errorText );
				Debug.LogError( "<color=red>Exist</color> : " + containData.path + " <color=red>Add</color> : " + uiReferenceData.path );
			}
			else
			{
				uiReference.dic.Add( uiReferenceData.name, uiReferenceData );
			}

			return uiReferenceData;
		}

		void AddUIElement(UIReferenceData data)
		{
			for( int i = 0, icount = uiReference.typeArray.Length; i < icount; i++ )
			{
				isFoldList.Add( true );
				if( data.obj.GetComponent( uiReference.typeArray[i] ) != null )
				{
					if( dicUIElement.ContainsKey( i ) == false )
					{
						dicUIElement.Add( i, new List<UIReferenceData>() );
					}
					dicUIElement[i].Add( data );
				}
			}
		}

		void DisplaErrorList()
		{
			if( errorString.Count > 0 )
			{
				EditorGUILayout.Space();
				EditorGUILayout.HelpBox( "Error", MessageType.Error );
				for( int i = 0, icount = errorString.Count; i < icount; ++i )
				{
					EditorGUILayout.BeginVertical();
					EditorGUILayout.BeginHorizontal();
					EditorGUILayout.TextField( errorString[i] );
					EditorGUILayout.EndHorizontal();
					EditorGUILayout.EndVertical();
				}
			}
		}

		void DisplayUIElementList()
		{
			List<UIReferenceData> dataList = null;
			UIReferenceData data;
			Type type;
			int index = 0;
			foreach( KeyValuePair<int, List<UIReferenceData>> pair in dicUIElement )
			{
				index = pair.Key;
				type = uiReference.typeArray[index];
				isFoldList[index] = EditorGUILayout.Foldout( isFoldList[index], type.ToString() );
				if( isFoldList[index] )
				{
					dataList = pair.Value;
					for( int i = 0, icount = dataList.Count; i < icount; ++i )
					{
						data = dataList[i];
						EditorGUILayout.BeginVertical();
						EditorGUILayout.BeginHorizontal();
						EditorGUILayout.TextField( data.name );
						EditorGUILayout.TextField( data.path );
						EditorGUILayout.ObjectField( data.obj, type, false );
						EditorGUILayout.EndHorizontal();
						EditorGUILayout.EndVertical();
					}
				}

				EditorGUILayout.Space();
			}
		}

		void DisplayGameObjectList()
		{
			EditorGUILayout.Space();
			EditorGUILayout.HelpBox( "Managed Data", MessageType.Info );
			foreach( KeyValuePair<string, UIReferenceData> pair in uiReference.dic )
			{
				name = pair.Key;

				EditorGUILayout.BeginVertical();
				EditorGUILayout.BeginHorizontal();
				EditorGUILayout.TextField( pair.Key );
				EditorGUILayout.TextField( pair.Value.path );
				EditorGUILayout.ObjectField( pair.Value.obj, typeof( GameObject ), false );
				EditorGUILayout.EndHorizontal();
				EditorGUILayout.EndVertical();
			}
		}

		string GetPath(Transform trans)
		{
			string path = "/" + trans.name;
			while( trans.parent != null )
			{
				trans = trans.parent;
				path = "/" + trans.name + path;
			}
			return path;
		}
	}
}

◆ UI 참조 관리 사용 가이드

[ UIReference ScriptableObject 를 생성 ]
[ 만들어진 ScriptableObject에서 ui target에 참조 데이터를 만들 UI Prefab을 추가함 ]
[ SET NAME 버튼 클릭 시 자동으로 ui target에 따라 이름을 생성함 ]
[ Refresh 버튼 클릭 시 ui target에 Prefab을 확인 후 참조할 Object 데이터를 만듬 ]
[ 사용할 UI Code에서 만들어진 ScriptableObject 데이터의 참조를 설정 ]

using UnityEngine;
using EverydayDevup.Framework;

public class UILoading : UIBase
{
	public UIReference refData;

	protected override void OnInit()
	{
		base.OnInit();
        /// company_name 키로 TMPro.TMP_Text 를 가져와서 사용
		TMPro.TMP_Text text = refData.GetData<TMPro.TMP_Text>( "company_name" );
		text.SetText( "EveryDay.DevUp" );
	}
}
public class UIReference : ScriptableObject {
	// 이 부분에 type을 추가하면 자동으로 해당 type을 검색함
	public Type[] typeArray = { typeof( TMP_Text ), typeof( Canvas ) };
}

※ Serialize Dictionary에 대한 정보는 하단의 게시물을 참조 

https://everyday-devup.tistory.com/88

 

[Unity] Serialize (직렬화) - Serialize Dictionary

▶ Serialize ( 직렬화 ) : Unity에서 사용하는 데이터나 GameObject의 정보를 저장하고 불러오기 위해 사용하는 포맷으로 자동으로 변환하는 프로세스이다. : Unity Editor에서 인스펙터 창의 정보나 프리��

everyday-devup.tistory.com