<UI 자동화>
tool에서 하나하나 객체를 연결해주는 것이 아닌, 코드로 접근
1. 각 객체에 대한 이름을 enum으로 하여 enum 값을 받아서 값이 일치하면 어떤 함수를 불러오는 형태로 : 자동화
-> 어떻게 enum 값을 어떻게 넘겨줄 것인가? : c#의 reflaction 기능 이용
*reflaction: 객체에 대한 모든 정보를 엑셀에 찍어서 추출하는 기능 -> enum도 마찬가지로 해당 enum에 대한 정보를 알 수 있다.
아래의 코드를 보면 Object에 대한 이름 정보를 가지고 있는 enum을 선언하고 함수 Bind에서 Type 인자를 받아 해당 type과 일치하는 Object를 찾아야한다.
Start 함수에서 enum 을 typeof를 통해서 Bind 함수로 넘겨서 불러온다.
이때 Binde 함수를 제너릭으로 하여 각 Button과 Text에 대한 Obeject를 넘길 수 있도록 한다.
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Button : MonoBehaviour
{
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입ㅇ르 넘긴 것
}
// enum에 있는 값과 일치하면 Bind 함수를 불러서 자동화하도록
void Bind<T>(Type type)
{
}
int _score = 0;
public void OnButtonClicked()
{
Debug.Log("Button Click");
_score++;
}
}
2. Bind 함수에서 type과 일치하는 Object를 찾아야한다.
▶ c#의 기능에서 reflaction을 사용하여 Enum.GetNames(type)으로 enum의 정보를 가져온다.
▶ Unity의 모든 Object는 UnityEngine.Object로 받을 수 있기 때문에, 아래와 같이 딕셔너리를 통해 Type이 들어오면 해당 Object에 맞는 Object 리스트를 반환한다.
=> Scene에서의 Object와 연결해서 저장하는 Dictionary*
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Button : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입ㅇ르 넘긴 것
}
// enum에 있는 값과 일치하면 Bind 함수를 불러서 자동화하도록
void Bind<T>(Type type)
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string [] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] =
}
}
int _score = 0;
public void OnButtonClicked()
{
Debug.Log("Button Click");
_score++;
}
}
▶ Bind 함수에서 해당 type의 객체 이름에 대한 string 리스트를 만들고 해당 type의 이름 길이 만큼 UnityEngine.Object를 만들고 기존의 딕셔너리에 저장공간을 만들어준다.
▶ for문으로 실제 Scene에서의 Object와 UnityEngine.Object 리스트 object를 연결해줘야하는데 어떻게??
=> gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다. -> 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
3. Utils 폴더 아래에 Util.cs를 만들어서 기능성 함수들을 넣는다.
최상위 Object의 자식을 찾는 FindChild 함수를 만든다. recursive하게 찾을 것인지 아닌지에 대한 boolean 변수를 두고, name이 null 일시, 모든 component를 반환하도록 한다.
recursive로 할 때에는 바로 component를 가져오고 아닐 때에는 각 child에 대해 for문으로 하여 component를 가져오도록 코드를 짰다.
*Util.cs
using System.Collections;
using System.Collections.Generic;
using System.Resources;
using UnityEngine;
using UnityEngine.Rendering.VirtualTexturing;
public class Util
{
// 최상위 Object의 자식 Object를 반환하는 함수
// 이름이 없으면 해당 type의 모든 Object 반환하도록
// <T>: 찾고 싶은 Component ex) Text, Button
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++)
{
Transform transform = go.transform.GetChild(i); // i번째 직속 자식들만 찾음
if (string.IsNullOrEmpty(name) || transform.name == name)
{
// component를 들고 있는지 확인
T component = transform.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;
}
}
*UI_Button.cs
Util.FindChild 함수를 통해 Object의 자식 객체들을 가져와서 딕셔너리의 UnityEngine.Object 리스트에 mapping
void Bind<T>(Type type) where T: UnityEngine.Object
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string [] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
}
}
- Bind된 Object 가져오기
1. Get 함수를 정의하여 해당 idx 번째 인덱스의 Object를 가져오기
*제너릭에서 TryGetValue를 통해 해당 T type의 UnityEngine.Object 리스트를 objects리스트로 가져온다.
// Bind된 Object를 가져오도록
T GET<T>(int idx) where T: UnityEngine.Object // T type의 idx 인덱스번째의 Object를 가져옴
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false) // type T의 Object 배열을 objects로 가져옴
return null;
return objects[idx] as T; // objects의 type은 UnityEngine.Object 이기 때문에 type을 T로 가져옴
}
2. 완성된 코드는 다음과 같다
Start()에서 ScoreText 객체를 불러와서 텍스트를 다음과 같이 바꾼다.
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Button : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입ㅇ르 넘긴 것
// Test
GET<Text>((int)Texts.ScoreText).text = "Bind Test";
}
// enum에 있는 값과 일치하면 Bind 함수를 불러서 자동화하도록
void Bind<T>(Type type) where T: UnityEngine.Object
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string [] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
}
}
// Bind된 Object를 가져오도록
T GET<T>(int idx) where T: UnityEngine.Object // T type의 idx 인덱스번째의 Object를 가져옴
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false) // type T의 Object 배열을 objects로 가져옴
return null;
return objects[idx] as T; // objects의 type은 UnityEngine.Object 이기 때문에 type을 T로 가져옴
}
int _score = 0;
public void OnButtonClicked()
{
Debug.Log("Button Click");
_score++;
}
}
3. 기존의 Prefabs에서 UI_Button을 삭제한 후 다음 변경된 Prefabs UI_Button을 추가한다. 후에 Scene을 다음과 같이 정리한 후 실행하면, PlayerController에서 UI_Button을 불러오기 때문에 위에서 가져온 ScoreText의 object의 text가 "Bind Test"로 변한 것을 확인할 수 있다.
<UI 자동화 - Object mapping 하기>
위에서 처럼 UI Component를 가져오는 것이 아닌, Scene에서의 Object를 자동적으로 mapping하도록
1. Prefabs의 UI_Button에서 GameObject를 추가한다 (create Empty)
2. 위에서 처럼 enum으로 GameObject를 정의하고 Bind로 해당 GameObject를 mapping하도록 한다.
-> 이때 문제 FindChild는 Component만을 찾는데 현재 GameObject를 찾고자하기 때문에 에러 발생
*FindChild는 최상위 Object의 자식 Component를 찾아서 반환한다.
*UI_Button.cs
// GameObject
enum GameObejects
{
TestObject,
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
=> GameObject를 찾아주는 다른 FindChild 함수를 만들어야한다.
3. Util.cs아래의 GameObject를 찾아주는 FindChild 함수를 만든다.
위에서 제너릭으로 정의한 FindChild<T>를 약간 변형하여 다음과 같이 코드를 만든다. 이때 모든 GameObeject는 Transform Component를 가지고 있기 때문에 이를 이용하여 GameObject를 찾는다.
// GameObject를 반환하는 함수
public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
{
// 모든 GameObject는 Transform Component를 가지고 있기 때문에 이를 이용한다.
Transform transform = FindChild<Transform>(go, name, recursive);
if (transform == null)
return null;
return transform.gameObject;
}
4. 다음과 같이 코드를 한 후, 플레이를 하면 아무런 error없이 동작하는 것을 확인할 수 있다.
*Util.cs
using System.Collections;
using System.Collections.Generic;
using System.Resources;
using UnityEngine;
using UnityEngine.Rendering.VirtualTexturing;
public class Util
{
// GameObject를 반환하는 함수
public static GameObject FindChild(GameObject go, string name = null, bool recursive = false)
{
// 모든 GameObject는 Transform Component를 가지고 있기 때문에 이를 이용한다.
Transform transform = FindChild<Transform>(go, name, recursive);
if (transform == null)
return null;
return transform.gameObject;
}
// 최상위 Object의 자식 Object를 반환하는 함수
// 이름이 없으면 해당 type의 모든 Object 반환하도록
// <T>: 찾고 싶은 Component ex) Text, Button
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++)
{
Transform transform = go.transform.GetChild(i); // i번째 직속 자식들만 찾음
if (string.IsNullOrEmpty(name) || transform.name == name)
{
// component를 들고 있는지 확인
T component = transform.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;
}
}
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Button : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
// GameObject
enum GameObejects
{
TestObject,
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
// Test
GET<Text>((int)Texts.ScoreText).text = "Bind Test";
}
// enum에 있는 값과 일치하면 Bind 함수를 불러서 자동화하도록
void Bind<T>(Type type) where T: UnityEngine.Object
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string [] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// GameObject를 찾는 건인지 확인
if(typeof(T) == typeof(GameObject))
objects[i] = Util.FindChild(gameObject, names[i], true);
else
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
}
// Binding에 실패했을 경우
if (objects[i] == null)
Debug.Log($"Failed to bind!({name[i]})");
}
}
// Bind된 Object를 가져오도록
T GET<T>(int idx) where T: UnityEngine.Object // T type의 idx 인덱스번째의 Object를 가져옴
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false) // type T의 Object 배열을 objects로 가져옴
return null;
return objects[idx] as T; // objects의 type은 UnityEngine.Object 이기 때문에 type을 T로 가져옴
}
int _score = 0;
public void OnButtonClicked()
{
Debug.Log("Button Click");
_score++;
}
}
4. 자주 가져오는 Text, Button, Image에 대해 바로 가져올 수 있도록 다음과 같이 함수를 작성한다.
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Button : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
// GameObject
enum GameObejects
{
TestObject,
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
// Test
GetText((int)Texts.ScoreText).text = "Bind Test";
}
// enum에 있는 값과 일치하면 Bind 함수를 불러서 자동화하도록
void Bind<T>(Type type) where T: UnityEngine.Object
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string [] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// GameObject를 찾는 건인지 확인
if(typeof(T) == typeof(GameObject))
objects[i] = Util.FindChild(gameObject, names[i], true);
else
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
}
// Binding에 실패했을 경우
if (objects[i] == null)
Debug.Log($"Failed to bind!({name[i]})");
}
}
// Bind된 Object를 가져오도록
T GET<T>(int idx) where T: UnityEngine.Object // T type의 idx 인덱스번째의 Object를 가져옴
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false) // type T의 Object 배열을 objects로 가져옴
return null;
return objects[idx] as T; // objects의 type은 UnityEngine.Object 이기 때문에 type을 T로 가져옴
}
// 자주 사용한는 Component를 함수로
Text GetText(int idx){return GET<Text>(idx);}
Button GetButton(int idx){return GET<Button>(idx);}
Image GetImage(int idx){ return GET<Image>(idx); }
int _score = 0;
public void OnButtonClicked()
{
Debug.Log("Button Click");
_score++;
}
}
- 공용적으로 가져올 수 있는 Bind, Get 함수 정의
1. Scripts 폴더 -> UI 폴더 아래에 UI_Base.cs 파일을 만들어서 해당 파일 안에 Bind함수와 Get 함수를 넣고 UI_Base class를 UI_Button 클래스가 상속받도록 한다.
*이때 위의 함수를 그냥 가져와서 상속을 하면 '보호 수준 때문에' error가 발생하기 때문에 UI_Base class 아래의 함수를 protected로 하여 상속받은 클래스에 대해 함수를 접근할 수 있도록 해야함
*UI_Base.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UI_Base : MonoBehaviour
{
// 딕셔너리를 이용하여 Buttons를 받으면 Unity Object의 Button을 반환하고,
// Texts를 받으면 Unity Object의 Text를 반환하도록
Dictionary<Type, UnityEngine.Object[]> _objects = new Dictionary<Type, UnityEngine.Object[]>();
protected void Bind<T>(Type type) where T : UnityEngine.Object
{
// c#의 reflaction 기능을 이용하여 enum의 이름을 뽑아온다.
string[] names = Enum.GetNames(type);
// 딕셔너리에 저장 공간 만들어 줌 -> names.Length만큼 공간이
UnityEngine.Object[] objects = new UnityEngine.Object[names.Length];
_objects.Add(typeof(T), objects);
// 실제 Scene의 Object와 mapping을 해야함 => objects의 리스트와 연결
for (int i = 0; i < names.Length; i++)
{
// GameObject를 찾는 건인지 확인
if (typeof(T) == typeof(GameObject))
objects[i] = Util.FindChild(gameObject, names[i], true);
else
{
// gameObject를 통해 UI_Button component의 최상위 Object는 가져올 수 있다.
// => 자식을 Scan하면서 같은 이름이 있는 Object를 mapping
objects[i] = Util.FindChild<T>(gameObject, names[i], true);
}
// Binding에 실패했을 경우
if (objects[i] == null)
Debug.Log($"Failed to bind!({name[i]})");
}
}
// Bind된 Object를 가져오도록
protected T GET<T>(int idx) where T : UnityEngine.Object // T type의 idx 인덱스번째의 Object를 가져옴
{
UnityEngine.Object[] objects = null;
if (_objects.TryGetValue(typeof(T), out objects) == false) // type T의 Object 배열을 objects로 가져옴
return null;
return objects[idx] as T; // objects의 type은 UnityEngine.Object 이기 때문에 type을 T로 가져옴
}
// 자주 사용한는 Component를 함수로
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 자동화 - mapping 후 Button event로 score 점수 올리기>
1. Event 처리는 Call back 방식으로 처리해야함. * 이때 UI에 대한 Input Event는 다음을 통해 걸러냈다.
// UI인지 확인하는 부분
if (EventSystem.current.IsPointerOverGameObject()) // UI가 Click된 상황인지 알 수 있음.
return;
=> EventSystem을 통해 UI Event를 처리하는 것을 알 수 있다.
=> Unitiy에서 제공하는 다양한 handler를 통해 Event를 처리할 수 있다.
2. Script 폴더 -> UI 폴더 아래에 UI_EventHandler.cs 파일을 만든다. 후에 Scene에 UI_Button Prefabs을 추가하고 Player Controller에서 Prefabs Resource를 가져오는 부분을 주석처리한다.
다음과 같이 UI_Button 아래에 Image Component 부분을 추가한다. (이때 Prefab unpack을 통해 Prefab과 독립적으로 되도록)
-> UI_Button에 UI_EventHandler.cs 파일을 추가한다.
3. Event 시스템에서 던져주는 특정 Event를 받을려면, 특정 interface에 맞는 함수들을 정의해야한다.
-> 이때 IBeginDragHandler, IDragHandler를 상속받아서 그에 맞는 interface를 정의한다. (우선적으로 Debug.Log 출력)
*UI_EventHandler.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class UI_EventHandler : MonoBehaviour, IBeginDragHandler, IDragHandler
{
public void OnBeginDrag(PointerEventData eventData)
{
Debug.Log("OnBeginDrag");
}
public void OnDrag(PointerEventData eventData)
{
Debug.Log("OnDrag");
}
}
4. 이렇게 하고 난 후 Player를 하면, UI에서 Image, Text, Button을 drag하면 다음과 같이 console 창에 뜨는 것을 확인할 수 있다.
5. 이때, 최상위 Object에 EventHandler.cs를 component로 두지않고 Image에 component로 두면, 해당 Image에서만 Event가 발생하여 위와 같이 console 창이 뜨고 나머지 component(Text, Button)에 대해서는 Event가 발생하지않는다.
=> 최상위 Object에서 EventHandler를 처리하면 아래의 모든 자식 Component 들에 대해 Event가 처리된다.
6. Drag event를 처리하기 위해 position을 현재 마우스가 클릭하고 있는 부분으로 해당 Component를 이동하도록 다음과 같이 코드를 작성한다. PointerEventData에 Event에 관한 다양한 Data가 들어있다 -> 이것을 이용
UI_Button Object 아래의 Image Component에 UI_EventHandler.cs 파일을 component로 하여 실행하면 Image가 drag drop으로 옮겨지는 것을 확인할 수 있다.
*UI_EventHandler.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class UI_EventHandler : MonoBehaviour, IBeginDragHandler, IDragHandler
{
public void OnBeginDrag(PointerEventData eventData)
{
Debug.Log("OnBeginDrag");
}
public void OnDrag(PointerEventData eventData)
{
// PointerEventData에 다양한 Event 정보들이 들어있다.
transform.position = eventData.position;
Debug.Log("OnDrag");
}
}
7. InputManager에서 구현한 바와 같이 Action과 Invoke를 통해 UI_EventHandler를 코딩한다.
*UI_EventHandler.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class UI_EventHandler : MonoBehaviour, IBeginDragHandler, IDragHandler
{
Action<PointerEventData> OnBeginDragHandler = null;
Action<PointerEventData> OnDragHandler = null;
public void OnBeginDrag(PointerEventData eventData)
{
// Event가 들어오면 Invoke로 Event를 알린다.
if (OnBeginDragHandler != null)
OnBeginDragHandler.Invoke(eventData);
}
public void OnDrag(PointerEventData eventData)
{
if (OnDragHandler != null)
OnDragHandler.Invoke(eventData);
}
}
8. UI_Button.cs 파일에서 기존의 Image Component를 Binding 한 후 UI_EventHandler를 가져와 해당 event를 변경한다.
Image component를 Itemicon으로 바꾼 후 play 한다.
GetImage 함수를 통해 Itemicon Object를 가져온 후, 해당 Object의 Component인 UI_EventHandler를 가져오고 위에서 작성했던 이동을 위한 코드를 짠다. 이때 UI_Button.cs에서 transform은 UI_Button Object의 위치를 가르키기 때문에 해당 Event의 GameObject의 transform을 이용한다.
=> 이렇게 Play 하면 Itemicon이 dragdrop으로 옮겨진다.
*UI_Button.cs
enum Images
{
Itemicon,
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
Bind<Image>(typeof(Images));
// Test
GetText((int)Texts.ScoreText).text = "Bind Test";
GameObject go = GetImage((int)Images.Itemicon).gameObject; // image Component가 아닌 gameObject로 가져옴
UI_EventHandler evt = go.GetComponent<UI_EventHandler>(); // Itemicon(Image) component의 UI_EventHandler 가져옴
evt.OnDragHandler += ((PointerEventData data) => { evt.gameObject.transform.position = data.position; });
}
** UI component는 transfom이 아니라 Rect transform인데 어떻게 바로 transform으로 접근이 가능한지?
-> Rect transform도 transform을 상속 받기 때문에 가능
<UI 자동화 - Handler 깔끔하게 정리>
위에서 Event를 연동하는 부분을 더욱 깔끔하게 정리하자
- 코드 정리
1. 위의 코드에서 EventHandelr를 자동으로 붙이는 것을 깔끔하게 정리하기 위해 UI_Base.cs 에 UI Event를 더하는 함수를 새로 작성한다.
2. 이때, Object에 EventHandler Component가 없을 수 있기 때문에, 만약 EventHandler Component가 없다면 Component를 추가해주는 것이 필요 -> 이런 것은 많이 사용하기 때문에 Util.cs 파일에 새로운 함수 작성한다.
*Util.cs
public class Util
{
// Component를 추가하거나 반환하는 함수
public static T GetOrAddComponent<T>(GameObject go) where T: UnityEngine.Component
{
T component = go.GetComponent<T>(); // Itemicon(Image) component의 UI_EventHandler 가져옴
// 만약 UI_EventHandler가 없으면 추가를 해주면 됨
if (component == null)
component = go.AddComponent<T>();
return component;
}
3. UI_Base에서 event 연동하는 함수는 다음과 같다.
우선 UI Event에 대해 정의하는 Define 함수에 다음과 같이 추가한다.
*Define.cs
// UI에 필요한 Event
public enum UIEvent
{
Click,
Drag,
}
4. UI_Base에서 UI Event를 추가하는 함수는 다음과 같다. 우선 GameObject를 함수 인자로 받고 추가하고자하는 action 을 받는다. UIEvent는 default로 Click으로 한다.
위에서 정의한 Util의 함수를 이용하여 UI_EventHandler를 추가하거나 Get하는 함수를 참조한다.
type에 따라 다른 event handler를 불러서 action을 추가한다.
*UI_Base.cs의 일부 함수
// callback 으로 받을 함수-> Action
public static void AddUIEvent(GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
{
// Util에서 정의한 함수를 참조하여 EventHandler를 가져온다.
UI_EventHandler evt = Util.GetOrAddComponent<UI_EventHandler>(go);
switch (type)
{
case Define.UIEvent.Click:
evt.OnClickHandler -= action;
evt.OnClickHandler += action;
break;
case Define.UIEvent.Drag:
evt.OnDragHandler -= action;
evt.OnDragHandler += action;
break;
}
evt.OnDragHandler += ((PointerEventData data) => { evt.gameObject.transform.position = data.position; });
}
Click event가 추가 Begin event삭제
*UI_EventHandler.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class UI_EventHandler : MonoBehaviour, IPointerClickHandler, IDragHandler
{
public Action<PointerEventData> OnClickHandler = null;
public Action<PointerEventData> OnDragHandler = null;
public void OnDrag(PointerEventData eventData)
{
if (OnDragHandler != null)
OnDragHandler.Invoke(eventData);
}
public void OnPointerClick(PointerEventData eventData)
{
if (OnClickHandler != null)
OnClickHandler.Invoke(eventData);
}
}
5. UI_Button.cs 파일에서 위에서 정의한 함수를 이용하여 Image drag event를 실행한다. 똑같이 Image component(Itemicon)이 이동하는 것을 확인할 수 있다.
*UI_Button.cs 일부
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
Bind<Image>(typeof(Images));
// Test
GetText((int)Texts.ScoreText).text = "Bind Test";
GameObject go = GetImage((int)Images.Itemicon).gameObject; // image Component가 아닌 gameObject로 가져옴
// UI_Base에서 정의한 event 추가 함수 가져오기
AddUIEvent(go, (PointerEventData data) => { go.transform.position = data.position; }, Define.UIEvent.Drag);
}
- Extension method
기존의 유니티엔진에서 제공하는 GameObject 같은 변수에 내가 지정한 함수를 추가로 정의하여 사용하고 싶은 경우 Extension 을 사용한다.
1. Scripts 폴더 -> Utils 폴더 아래에 Extension.cs 파일을 만들고 다음과 같이 코딩한다.
기존의 UI_Base에서 AddUIEvent 함수의 틀을 가져오고 GameObject go에서 this를 앞에 붙이면 된다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public static class Extension
{
public static void AddUIEvent(this GameObject go, Action<PointerEventData> action, Define.UIEvent type = Define.UIEvent.Click)
{
UI_Base.AddUIEvent(go, action, type);
}
}
2. 후에 다음과 같이 Button event에 대해 자동으로 연동해주는 코드를 짜고 실행을 하면 Button을 누를때마다 점수가 올라가는 것을 확인할 수 있다.
*UI_Button.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class UI_Button : UI_Base
{
enum Buttons
{
PointButton
}
enum Texts
{
PointText,
ScoreText
}
// GameObject
enum GameObejects
{
TestObject,
}
enum Images
{
Itemicon,
}
private void Start()
{
Bind<Button>(typeof(Buttons)); // Buttons enum 타입을 넘긴 것
Bind<Text>(typeof(Texts)); // Texts enum 타입을 넘긴 것
Bind<GameObject>(typeof(GameObejects)); // GameObjects 타입을 넘긴 것
Bind<Image>(typeof(Images));
GameObject go = GetImage((int)Images.Itemicon).gameObject; // image Component가 아닌 gameObject로 가져옴
// UI_Base에서 정의한 event 추가 함수 가져오기
AddUIEvent(go, (PointerEventData data) => { go.transform.position = data.position; }, Define.UIEvent.Drag);
// Extension Test
GetButton((int)Buttons.PointButton).gameObject.AddUIEvent(OnButtonClicked);
}
int _score = 0;
public void OnButtonClicked(PointerEventData data)
{
Debug.Log("Button Click");
_score++;
// Test
GetText((int)Texts.ScoreText).text = $"점수: {_score}";
}
}
'Development > 유니티' 카테고리의 다른 글
[섹션7] 인벤토리 실습 & 코드 정리 (0) | 2021.08.01 |
---|---|
[섹션 7] UI Manager (0) | 2021.07.30 |
[섹션 7] UI(유아이) (0) | 2021.07.23 |
[섹션6] 애니메이션(Animation) - 심화 (0) | 2021.07.22 |
[섹션6] 애니메이션(Animation) (0) | 2021.07.21 |