반응형

출처: https://coderzero.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-C-%EA%B0%95%EC%A2%8C-15-%EC%A0%9C%EB%84%A4%EB%A6%AD-Generics

1. 제네릭 타입(Generic Type)

1.1 제네릭이란?

 

보통 클래스나 인터페이스, 메서드를 사용할 때, 동일한 기능을 수행하지만, 입력하는 데이터 형식만 틀린 경우가 있습니다.

 

그림. 기능이 동일한 클래스들

 

 

이때 매개변수를 일일히 넣어서 클래스나 인터페이스, 메서드를 만들지 않고, 제네릭 타입(Generic Type)을 사용하여 만들 수 있습니다.

 

동일한 기능을 하는 메서드 제네릭 메서드
void Swap(ref int x, ref int y)
{
    var temp = y;
    y = x;
    x = temp;
}

void Swap(ref float x, ref float y)
{
    var temp = y;
    y = x;
    x = temp;
}
void Swap<T>(ref T x, ref T y)
{
    var temp = y;
    y = x;
    x = temp;
}

 

제네릭이란 형식 매개변수(Type parameter)입니다.

형식 매개변수를 사용해서 클래스나 메서드를 만들게 되면, 그 클래스나 메서드를 호출하기 전까지 데이터 형식 지정을 연기 할 수 있습니다.

 

 

1.2 제네릭 특징

 

 ● 코드 재사용성이 높습니다.

 ● 성능이 좋습니다. (런타임 때 데이터 형식이 결정 되는 것이 아니라, 코드에서 호출 할때 데이터 형식이 결정되기 때문에 성능 저하가 없습니다.)

 

 

2. 제네릭 클래스(Generic Class)

2.1 사용 방법

 

멤버변수의 타입을 미리 결정하지 않고, 사용할 때 제넥릭 클래스를 사용합니다.

 

사용방법 : 제네릭 클래스
public class 클래스-이름<T> 
{ public T ... }

 

using UnityEngine; 

public class GenericsClassExample : MonoBehaviour 
{ 
    public class GenericsClass<T>
    { 
        private T m_Value; 

        public GenericsClass(T value) 
        { 
            m_Value = value; 
        } 

        public void Method1() 
        { 
            Debug.Log(m_Value); 
        } 
    } 

    void Start() 
    { 
        GenericsClass<int> m_GenericsClass1 = new GenericsClass<int>(5); 
        m_GenericsClass1.Method1(); // 출력 : 5 
        GenericsClass<float> m_GenericsClass2 = new GenericsClass<float>(5.1f); 
        m_GenericsClass2.Method1(); // 출력 : 5.1 
    } 
}

 

 

2.2 C# 제네릭 클래스

 

C#는 기본적으로 제공하는 제네릭 클래스가 많습니다.

대표적인 것이 List, Dictionary, LinkedList 등이 있습니다.

 

 

2.3 제네릭 타입 제약 (Type Constraint)

 

제네릭 타입을 선언할 때 제약 조건을 걸 수 있습니다.

 

사용방법은 제네릭 선언 후 where T : 제약조건 과 같은 식으로 where 뒤에 제약 조건을 붙이면 됩니다.

 

2.3.1 new 제약 조건

 

new 제약 조건은 제네릭 클래스 선언의 형식 인수에 공용 매개변수가 없는 생성자가 있어야 함을 지정합니다. 

 

사용 방법 : 제네릭 new 타입 제약1
class Class1<T> where T : new() // T는 디폴트 생성자를 가져야 함

 

다른 제약 조건과 함께 new() 제약 조건을 사용하는 경우 마지막에 지정해야 합니다.

 

사용 방법 : 제네릭 new 타입 제약2
class Class1<T> where T : IComparable, new() // T는 디폴트 생성자를 가져야 함

 

 

2.3.2 형식에 대한 조건

 

사용 방법 : 제네릭 형식에 대한 조건
class Class1<T> where T : struct // T는 Value 타입임
class Class2<T> where T : class // T는 Reference 타입임
class Class4<T> where T : Class3 // T는 Class3의 파생클래스어야 함
class Class4<T> where T : IComparable // T는 IComparable 인터페이스를 가져야 함.
class Class5<T, U>  // 복수 타입의 매개변수 제약
    where T : class
    where U : struct
{ }

 

 

3. 제네릭 메서드

 

매개변수의 타입을 미리 결정하지 않고, 사용시 결정하는 것입니다.

 

사용방법 : 제네릭 메서드
T 메소드-이름<T>(T arg)
{
    T temp = arg;
    //...
    return temp;
}

 

using UnityEngine; 

public class GenericExample : MonoBehaviour 
{ 
    void Start() 
    { 
        int iX = 1, iY = 2; 
        Swap(ref iX, ref iY); 
        Debug.LogFormat($"x = {iX} y = {iY}"); // 출력 : x = 2 y = 1  

        string sX = "ab", sY = "cd"; 
        Swap(ref sX, ref sY); 
        Debug.LogFormat($"x = {sX} y = {sY}"); // 출력 : x = cd y = ab  
    } 

    void Swap<T>(ref T x, ref T y) 
    { 
        var temp = y; 
        y = x; 
        x = temp; 
    } 
}

 

반응형
반응형

출처: https://benxen.tistory.com/49

간단하게 Class와 Enum 두 가지에 대해서 기술하겠다.

 

<첫 번째, Class >

Serialize

위 그림과 같이 인스펙터 창 내 컴포넌트를 표시할 수 있다.

[Serializable]를 클래스 앞에 적시하고, [SerializeField]를 클래스 생성(선언) 앞에 쓴다.

결과는 위과 같다.

 

<두 번째, Enum>

Enum은 [Serializble]이 없어도 된다.

드롭다운으로 생성되면서 정의했던 열거형 문자들이 나오게 된다.

 

=========================================

간단하게 직접 사용해 보고 싶어서 알아본 것인데, 은근 유용해서 공유해 본다.

 

반응형
반응형

출처: https://velog.io/@the_paper__/Unity%EC%97%90%EC%84%9C%EC%9D%98-Singleton-2%ED%8E%B8-MonoBehaviour-Singleton%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90

머리말

이번에는 MonoBehaviour Singleton을 사용하는 이유 및 문제점을 파악해서 해결 방법을 고민해 볼 겁니다.

사실 Singleton 관련 블로그 글을 써보려고 마음먹은 이유도 이러한 문제점들이 있는데 MonoBehaviour Singleton을 남발하는 경우가 많아서 쓰게 되었는데, 한 명이라도 이 글을 읽고 고민이라도 조금 했으면 해서 쓰게 되었습니다.


1. 사용하는 이유

설명

이전 글에서도 설명드렸듯이 몇 가지 꼽아보자면 이 정도가 될 것 같습니다.

  • MonoBehaviour의 기능을 이용하기 위함. (Update, LateUpdate.. 등등 함수들 및 Coroutine기능)
  • Native Plugin이용 시, SendMessage함수로 전달받아야 하는 경우.
  • 에디터에서 쉽게 확인 가능 (Hierarchy View, Inspector View)

이 부분들은 직접 사용해보시면 체감하시게 될 것이라 생각됩니다.

주로 MonoBehaviour의 기능을 사용하기 위함이 가장 큰데, 하나하나 다 설명하자면 너무 길어지고 글 주제인 "문제점"과도 맞지 않아 이 글을 읽는 여러분들이 체감하시길 바랍니다.

하지만, 정말 유용하고 많이 쓰이고 필수로 써야 할 곳들이 존재한 다는 것은 꼭 알아두시길 바랍니다.


2. 문제점

2.1 플레이 중이 아닐 때 호출 시 문제

설명

Unity로 개발하다 보면 너무나 당연하게 플레이 중일 때를 가정하고 코딩할 때가 많은데, 협업을 하다 보면 플레이 중이 아닐 경우에 해당 Singleton을 접근해야 하는 경우가 생깁니다. 보통은 저의 경우에는 기획파트 혹은 아트 파트 요청으로 개발 툴을 개발하면서 발생하게 됩니다.

예로 들자면 Map에 몬스터를 배치하는 Map Tool이라고 있다고 하면, 이 툴에서는 Map ID만으로 Map을 불러와야 하는데 그러기 위해서는 Map의 정보가 저장된 데이터를 로드해야 하는 상황이고, 해당 데이터를 로드하는 코드는 MonoBehaviour Singleton으로 짜여 있는 상황입니다.

만약에 플레이 중이 아닐 때 MonoBehaviour Singleton의 Instance로 접근 시에 우리가 짜두었던 코드대로 동작한다면 GameObject가 생성되고 해당 오브젝트에 Component가 붙게 됩니다.

테스트 코드

[UnityEditor.MenuItem("TestMenu/Singleton2/GetMapDat_10205")]
public static void GetMapData_10205()
{
    TableDataManager.Instance.GetMapData(10205);
}

간단하게 테스트 코드를 만들어보았습니다.

에디터에서 그대로 실행 시에 GameObject가 생성되었습니다.

당장에는 문제가 되지 않을 수 있습니다만, 이대로 불러들인 Scene을 저장하게 될 경우 다음에 소개해드릴 "중복으로 생성되었을 때의 문제"가 발생하게 됩니다.

2.2 중복으로 생성되었을 때의 문제

설명

게임을 플레이 중인데 똑같은 MonoBehaviour Singleton이 있다면 어떤 것을 참조할까 고민해보면, 이전 글에서 만들어둔 코드를 기준으로 한다면 기존에 만들어진 오브젝트가 있더라도 새롭게 만들어서 Instance에 등록해줄 것입니다.

여기서 문제가 되는 부분은 Update, LateUpdate와 같이 MonoBehaviour 함수로 Instance와 동일한 오브젝트가 아니더라도 동작하는 경우입니다.

테스트 코드

public class CameraManager : MonoBehaviourSingletonTemplate<CameraManager>
{
    private void Start()
    {
        Invoke("OnOneSec", 1f);
    }

    private void Update()
    {
        Camera.main.transform.Translate(Vector3.forward * Time.deltaTime);
    }

    private void OnOneSec()
    {
        Debug.Log(Camera.main.transform.position);
    }

    public Camera GetMainCamera() => Camera.main;
}

만약에 위 코드처럼 Update함수에서 Camera의 Transform을 Translate함수로 이동시킨다고 가정했을 때, 중복으로 생성되어있는 상태라 Translate함수 호출이 프레임당 두번씩 호출되게 됩니다.

임시수정

protected void Awake()
{
    if (m_Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    DontDestroyOnLoad(gameObject);
}

임시로 Awake함수를 수정했습니다. Instance와 비교하여 Instance와 다르면 제거하도록 작업해두었습니다.

물론, 제가 생각하기에 이 상황이 제대로 된 상황은 아니며 임시방편으로 사용하는 코드입니다.

하지만 이 코드 역시 다른 문제를 만들게 되는데, 아래 코드로 설명드리겠습니다.

private void OnDestroy()
{
    Destroy(Camera.main.gameObject);
}

Component가 파괴될 때 OnDestroy함수가 호출되는데, 파괴되면서 실행하는 코드들이 또 문제를 일으키게 됩니다.

private void OnDestroy()
{
    if (Instance != this) return;
    Destroy(Camera.main.gameObject);
}

결국 이번에도 Instance와 비교하여 실행할지 말지를 결정해주어야 합니다.
중복 오브젝트 덕분에 덕지덕지 덮어씌워 주는 느낌이라 별로 좋지 않습니다.

2.3 OnDestroy때 호출 시 문제

설명

Unity에서 게임 종료 시에 모든 오브젝트들을 제거하고 OnDestroy함수들이 호출되게 되는데, 여기서 MonoBehaviour Singleton오브젝트가 먼저 파괴되고 다른 오브젝트 OnDestroy함수에서 이미 파괴된 MonoBehaviour Singleton의 Instacne를 참조하는 경우 우리가 짜두었던 코드대로라면 새롭게 GameObject를 생성하게 됩니다.

Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)

위 상황일 때 위와 같은 에러가 나오게 되고, GameObject가 생성되지 못했기 때문에 이후에 Instance의 멤버 접근하는 코드들은 Null Reference Exception을 뱉어내게 됩니다.

어차피 게임이 종료되는 상황이라 이 에러가 나와도 문제가 없다고 생각하실 수 있지만, 실제로 빌드하여 확인해보면 게임 종료 시에 "비정상 종료"도 나오게 되어 저장되어야 하는 것이 저장되지 못하고 사용자에게 에러 창을 보여주는 것으로 부정적인 이미지를 줄 수 있습니다.

이 부분은 OnDestroy함수들 마다 Instance를 참조하는 부분을 다 제거해주거나 이미 파괴되었는지 체크해주는 코드를 넣어야 합니다.

이 또한 매우 번거롭고 임시방편으로 보입니다.

3. 해결 방법

설명

제가 생각하는 가장 쉬운 방법 위 문제점들을 인지하고 MonoBehaviour Singleton을 "꼭 필요할 때만 쓰는 것"입니다.

이 글 가장 처음에 어느 상황에서 사용하면 좋을지 모두 설명드렸습니다. 해당 상황에 잘 맞는 경우 사용하고, 그 외의 상황에서는 최대한 지양하는 것입니다.

하지만, 이 또한 예외적인 상황이 너무 많아 MonoBehaviour Singleton을 만들지 않고 Singleton과 MonoBehavior의 기능을 이용하는 스크립트를 추가하여 해결해 보는 것이 최선이지 않나 싶습니다.

Event Listener 구현

public class MonoBehaviourEventListener : MonoBehaviour
{
    static MonoBehaviourEventListener m_Instance = null;

    public static MonoBehaviourEventListener Instance
    {
        get
        {
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode == false)
                return null;
#endif

            if (m_Instance == null)
            {
                var obj = new GameObject("MonoBehaviourEventListener");
                m_Instance = obj.AddComponent<MonoBehaviourEventListener>();
            }
            return m_Instance;
        }
    }

    public System.Action OnUpdateEvent;
    public System.Action OnLateUpdateEvent;


    private void Update()
    {
        OnUpdateEvent?.Invoke();
    }

    private void LateUpdate()
    {
        OnLateUpdateEvent?.Invoke();
    }
}

우선은 간단하게MonoBehaviourEventListener를 만들어보았습니다.

간단하게 Singleton으로 만들어서 Update, LateUpdate를 콜백으로 호출하도록 짜두었으니, 이제 Singleton에서 해당 콜백 등록해주면 됩니다.

Application.isPlaying가 아니라 UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode로 체크했던 이유는, 플레이 시작할 때 첫 Awake함수를 호출 시 Application.isPlaying이 false로 반환하는 경우가 있어서입니다.

Singleton class 수정

public class CameraManager : SingletonTemplate<CameraManager>
{
    public CameraManager()
    {
        if (MonoBehaviourEventListener.Instance != null)
        {
            MonoBehaviourEventListener.Instance.OnUpdateEvent += OnMonoBehaviourUpdate;
        }
    }

    private void OnMonoBehaviourUpdate()
    {
        Camera.main.transform.Translate(Vector3.forward * Time.deltaTime);
        Debug.Log(Camera.main.transform.position);
    }

    public Camera GetMainCamera() => Camera.main;
}

이전에 만들었던 CameraManager를 MonoBehavior Singleton이 아니라 Singleton으로 만들고, MonoBehaviourEventLister를 참조하여 이벤트 등록하도록 만들어봤고, 로그로 정상작동도 확인했습니다.

앞으로도 MonoBehaviour의 기능들은 MonoBehaviourEventLister를 통해서 작업하면 됩니다.

Update, LateUpdate, FixedUpdate함수의 이벤트로 받거나 Coroutine 역시 MonoBehaviourEventLister.Instance.StartCoroutine 같은 식으로 접근해서 사용하면 됩니다.

대신 MonoBehaviour기능을 사용하지 않고 사용하던 로직을 동작시키려면 다른 방법으로 동작하도록 코드 수정을 해야 해서 이 부분만 추가로 구현을 해주면 됩니다.

하지만, 이렇게 해도 Native Plugin에서 SendMessage함수로 전달받아야 하는 경우, GameObejct가 없기 때문에 이런 식으로 해결하지는 못하고 어쩔 수 없이 GameObject를 써야 합니다.

public class MonoBehaviourSingletonTemplate<T> : MonoBehaviour where T : MonoBehaviour
{
    static T m_Instance = null;

    public static T Instance
    {
        get
        {
#if UNITY_EDITOR
            if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode == false)
                return null;
#endif
            if (m_Instance == null)
            {
                var obj = new GameObject(typeof(T).Name);
                m_Instance = obj.AddComponent<T>();
            }
            return m_Instance;
        }
    }

    protected void Awake()
    {
        if (m_Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        DontDestroyOnLoad(gameObject);
    }
}

어쩔 수 없이 써야 하는 부분은 플레이 중인 것을 체크하여 생성해주도록 구현해주시면 됩니다.
위 코드대로라면 플레이 중이 아닌 경우에는 Null로 리턴될 것이니, 이 부분은 꼭 예외처리가 필요하니 꼭 주의 바랍니다.


마무리

MonoBehavior Singleton의 문제를 체크해보고 저만의 해결 방법을 고민해보았습니다.
이 글을 읽는 여러분들에게 혹시라도 도움이 되셨다면 좋을 것 같네요.

제가 해결해보고자 한 방법이 최선이 아니라고 생각하고 프로젝트마다 해결 방법도 다르다고 생각합니다.

혹시라도 여러분들이 생각하는 방법이 있으시면 덧글로 남겨주시면 감사하겠습니다.

긴 글 읽어주셔서 감사합니다.

 

반응형
반응형

출처:https://velog.io/@the_paper__/Unity%EC%97%90%EC%84%9C%EC%9D%98-Singleton-1%ED%8E%B8-%EC%8B%B1%EA%B8%80%ED%84%B4-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

머리말

이 글과 이후 글에서는 싱글턴에 대한 설명보다는 Unity에서 구현해보는데 집중해보고, 문제점과 해결방법을 이야기해볼 예정입니다.
싱글턴에 대한 좀 더 상세하고 원론적인 내용이 필요하신 분은 다른 글을 보시면 될 것 같습니다.


1. Singleton

설명

싱글턴(Singleton)이라고 하면 디자인 패턴(Design Pattern) 중, 정말 많이 쓰이는 패턴 중 하나입니다.

싱글턴 패턴은 클래스를 글로벌로 접근 가능하도록하는 방법 중 하나인데, 만들기도 간단하고 만들어두면 정말 편하게 사용 가능해서 여기저기 많이 쓰이는 패턴입니다.

구현

public class ItemInventory
{
    static ItemInventory m_Instance = null;
    public static ItemInventory Instance
    {
        get
        {
            if (m_Instance == null) m_Instance = new ItemInventory();
            return m_Instance;
        }
    }

    public Item GetItem(int itemID) { /* ... */ }
    public bool AddItem(Item item) { /* ... */ }
}

간단한 예제로 ItemInventory라는 싱글턴 클래스를 만들어보았습니다.
static 변수로 클래스를 Instance로 접근하도록 구현하면 됩니다.

//프로퍼티로 구현
static ItemInventory m_Instance = null;
public static ItemInventory Instance
{
    get
    {
        if (m_Instance == null) m_Instance = new ItemInventory();
        return m_Instance;
    }
}

//readonly변수로 구현
public readonly static ItemInventory Instance = new ItemInventory();

//함수로 구현
static ItemInventory m_Instance = null;
public static ItemInventory GetInstance()
{
    if (m_Instance == null) m_Instance = new ItemInventory();
        return m_Instance;
}

저의 경우에는 프로퍼티를 좋아해서 프로퍼티로 구현했는데, 위 코드처럼 readonly변수로 Instance를 선언하거나 프로퍼티 대신 함수로 구현해도 무방합니다.

사용

var item = ItemInventory.Instance.GetItem(1000);

실제 호출되는 부분은 이렇게 Instance로 접근해서 싱글턴 클래스의 맴버를 호출하면 됩니다.

구현 (Template)

public class SingletonTemplate<T> where T : class, new()
{
    static T m_Instance = null;
    public static T Instance
    {
        get
        {
            if (m_Instance == null) m_Instance = new T();
            return m_Instance;
        }
    }
}

public class ItemInventory : SingletonTemplate<ItemInventory>
{
    public Item GetItem(int itemID) { /* ... */ }
    public bool AddItem(Item item) { /* ... */ }
}

추후에 재활용성을 고려하여 Template으로 구현한 예제입니다.
그렇게 긴 코드가 아니긴 하지만, 추후에 싱글턴에 기능이 더 추가되거나 싱글턴 클래스들의 관리가 필요할 경우 유용하게 쓰입니다.


2. MonoBehaviour Singleton

설명

Unity에서는 싱글턴 클래스가 MonoBehaviour의 기능을 이용해야할 경우들이 존재하는데, 이러한 문제를 해결하기 위해 Unity에서 사용하는 싱글턴입니다.

구현

public class MonoBehaviourSingletonTemplate<T> : MonoBehaviour where T : MonoBehaviour
{
    static T m_Instance = null;
    public static T Instance
    {
        get
        {
            if (m_Instance == null)
            {
                var obj = new GameObject(typeof(T).Name);
                m_Instance = obj.AddComponent<T>();
            }
            return m_Instance;
        }
    }
    
    protected void Awake()
    {
    	DontDestroyOnLoad(gameObject);
    }
}

MonoBehaviour는 Unity에서 사용하는 Component클래스이기 때문에, GameObject에 AddComponent함수로 추가하여 사용해야 합니다.

Instance 프로퍼티 내부 코드를 보시면 바로 아실 거라 생각됩니다.

사용 이유

아마도 다음 편에서 MonoBehaviour 싱글턴에 대해 좀 더 상세히 장단점을 다뤄볼 예정이긴 한데, 우선 이러한 싱글턴을 Unity에서 주로 사용되는 경우는 아래처럼 몇가지 상황을 위해 사용됩니다.

  1. Update, LateUpdate, FixedUpdate 등등 MonoBehaviour 함수 사용해야 할 경우
    1.1. MonoBehaviour: https://docs.unity3d.com/ScriptReference/MonoBehaviour.html
    1.2. 함수 호출 순서: https://docs.unity3d.com/Manual/ExecutionOrder.html
  2. Coroutine 사용해야 할 경우
  3. SendMessage로 전달받아야 할 경우

마무리

우선 이번 편에서 간단하게 싱글턴 클래스들을 만들어봤습니다.

사실 첫 글이라 어떻게 혹은 어떤 구성으로 쓰는 게 좋을지 고민하고 조사해보느라 시간이 너무 많이 소모되었던 것 같네요.

다음 편부터는 할 말이 정말 많아서 첫 편은 짧고 쉬운 소재로 골랐는데 글이 잘 쓰였을지 잘 모르겠네요.

길지 않은 글이였지만 봐주셔서 감사합니다.

반응형

+ Recent posts