내일배움캠프/TIL

1차 디자인패턴 특강 정리

서보훈 2024. 10. 28. 20:36

디자인패턴

  • 복잡하고 난해한 문제를 패턴화해서 풀기위해 사용
  • 다른 개발자와 소통할때, 디자인패턴을 통해 간단하게 소통할 수 있음

주의사항

  1. 무작정 사용하는것은 지양해야함. 적절한 위치에 적절하게 사용
  2. 문제를 풀기위해 사용해야함. 디자인패턴을 쓰기위해 문제를 늘리면 안됨

※ 왜 디자인패턴을 사용했는지 설명할 수 있도록 사용해야함


싱글톤 패턴

  • 중요하고, 유일하게 존재하는 대상에게 쉽게 접근하기위해 사용함
  • 접근이 잦은 핵심기능에 대한 전역접 접근을 허용하기위해 사용함
  • ex) Manager 계열 클래스들 - 하나만 존재해야하며, 다양한 위치에서 자주 접근함

구현

  • 유일해야한다 : 싱글톤 패턴이 사용된 클래스가 추가로 생성되는것을 막을 방법이 필요함
  • 전역적으로 접근 가능해야한다 : public static 을 사용하여 전역접근 구현
public class CharacterManager : MonoBehaviour
{
    private static CharacterManager _instance;
    //전역접근지점 제공
    public static CharacterManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = new GameObject("CharacerManager").AddComponent<CharacterManager>();
            }
            return _instance;
        }
    }
    
    //하나만 존재해야한다 -> instnace가 존재할경우, 새로 생성된 오브젝트 파괴
    private void Awake()
    {
        if(_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            if(_instance != this)
            {
                Destroy(gameObject);
            }
        }
    }
}

제네릭 싱글톤

  • 싱글톤 객체간 유사성이 높음
  • 단순한 상속으로 상위클래스로 접근할경우, 중복되는 코드가 많음
  • 제네릭으로 싱글톤을 상속하여 하나의 클래스로 다수의 싱글톤 클래스를 만들 수 있음
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static T instance;
    public static T Instance { get { return instance; } }
    public static bool InstanceExist { get { return instance != null; } }

    protected virtual void Awake()
    {
        if(InstanceExist)
        {
            Destroy(gameObject);
            return;
        }
        instance = (T)this;
    }

    protected virtual void OnDestroy()
    {
        if (instance == this)
        {
            instance = null;
        }
    }
}

※제네릭

  • 일반화 프로그래밍
  • 클래스를 가변형으로 선언하여 사용
  • List<>, GetComponent<> 등, <> 사이에 들어가는것이 제네릭으로 선언된 클래스 공간
  • ※개인적인 생각 : 클래스를 매개변수처럼 사용할 수 있음

주의사항

  • 라이프 사이클에 주의해야함 
    • Awake에서 인스턴스를 선언하는것, 인스턴스가 선언되지 않은 상태에서 호출시 오류가 발생
    • Awake에서 자신과 관련된 내용을 초기화하고, Start에서 외부 참조를 하는방법으로 나누어주어 해결 할 수 있음
  • 모든 싱글톤이 OnDestroyOnLoad 할 필요는 없음
    • 씬이 넘어갈때 public 을 통해 외부에서 가져온 객체를 잃게됨(missing)
    • 이 삭제된 참조를 되돌리는작업이, 씬을 넘겨줘서 얻는 이득보다 귀찮을 가능성이 높음

오브젝트풀 패턴

  • 할당, 해제에 걸리는 성능낭비, 메모리낭비를 줄이기 위해 사용
  • 생성, 파괴가 반복되는 오브젝트를 재사용하여 생성,파괴를 줄임
  • pool 은 오브젝트를 묶은 단위
  • ※ 게임중 로딩하면서 렉이 생기는것보다, 로딩중에 필요한것을 미리 로딩하여 렉의 가능성을 줄이는것

구현

  • 필요한만큼 생성한뒤, 비활성화
  • 오브젝트가 필요할때, 비활성화된 오브젝트를 활성화하여 사용
  • 미리 생성된 양 이상 요구시 추가 생성
  • 파괴대신 오브젝트 비활성화 (추가 생성된것은 경우에따라 비활성화 or 파괴)
public class ObjectPool : MonoBehaviour
{
    // 오브젝트 풀 데이터를 정의할 데이터 모음 정의
    [System.Serializable]
    public class Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }

    public List<Pool> Pools;
    public Dictionary<string, Queue<GameObject>> PoolDictionary;

    private void Awake()
    {
        // 인스펙터창의 Pools를 바탕으로 오브젝트풀을 만들 것. 
        // 오브젝트풀은 오브젝트마다 따로이며, pool개수를 넘어가면 강제로 끄고 새로운 오브젝트에게 할당.
        PoolDictionary = new Dictionary<string, Queue<GameObject>>();
        foreach (var pool in Pools)
        {
            // 큐는 FIFO(First-in First-out) 구조로서, 줄을 서는 것처럼 가장 오래 줄 선(enqueue) 객체가 가장 먼저 빠져 나올(dequeue) 수 있는 구조
            Queue<GameObject> objectPool = new Queue<GameObject>();
            for (int i = 0; i < pool.size; i++)
            {
                // Awake하는 순간 오브젝트풀에 들어갈 Instantitate 일어나기 때문에 터무니없는 사이즈 조심
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);
                // 줄의 가장 마지막에 세움.
                objectPool.Enqueue(obj);
            }
            // 접근이 편한 Dictionary에 등록
            PoolDictionary.Add(pool.tag, objectPool);
        }
    }

    public GameObject SpawnFromPool(string tag)
    {
        // 애초에 Pool이 존재하지 않는 경우
        if (!PoolDictionary.ContainsKey(tag))
            return null;

        // 제일 오래된 객체를 재활용
        GameObject obj = PoolDictionary[tag].Dequeue();
        PoolDictionary[tag].Enqueue(obj);
				obj.SetActive(true);
        return obj;
    }
}

※ UnityEngine.Pool

  • 유니티에서 지원하는 오브젝트풀 구조
  • new ObjectPool<플될 클래스>(생성로직, 꺼낼때 로직, 반환로직,최대갯수 초과 로직, 자동검사, 초기크기, 최대크기)
public class PoolTest : MonoBehaviour
{
    public GameObject prefab;
    private ObjectPool<GameObject> pool;

    private void Start()
    {
        pool = new ObjectPool<GameObject>(OnCreate, ActivePoolObject, DisablePoolObject, DestroyPoolObject, false, 10, 100);
    }

    //생성시 로직
    private GameObject OnCreate()
    {
        return Instantiate(prefab);
    }

    //활성화시 로직
    private void ActivePoolObject(GameObject poolObject)
    {
        poolObject.SetActive(true);
    }

    //비활성화시 로직
    private void DisablePoolObject(GameObject poolObject)
    {
        poolObject.SetActive(false);
    }

    //최대갯수 초과시 로직
    private void DestroyPoolObject(GameObject poolObject)
    {
        Destroy(poolObject);
    }
}
  • 이벤트로 이루어져있음
  • 생성은 Func 으로, 활성화, 비활성화, 파괴는 Action 으로 이루어져있음
    • 생성시 return 으로 생성할 클래스를 반환해야함
    • 활성화, 비활성화, 파괴는 매개변수로 클래스를 받아서 활용하게됨
  • 시작시 초기 크기만큼 오브젝트를 생성하며, 초기크기를 넘을경우 최대크기까지 오브젝트를 파괴하지 않고 비활성화함
  • 최대크기를 넘을경우 추가로 추가로 오브젝트를 생성하며, 추가 생성된 오브젝트는 반환시 파괴된다.

주의사항

  • 씬 로드시 오브젝트풀의 최소 생성갯수만큼 오브젝트를 생성해야함
    • 로딩 시간이 길어질 수 있음
    • 필요한만큼만 생성하는것이 좋음

전략패턴

  • 원본클래스를 건드리지 않고, 다양하게 추가되는 방식에 대응하기 위해 사용
  • 상황에 맞게 로직을 바꿔주는 방식
    • 전략 : 로직들의 군을 정의하고, 필요에따라 로직군을 교체 가능하게함(코드들을 묶어서 사용)

 

구현

  • 세부내용이 구현된 전략 인터페이스 정의
  • 여러 방식을 전략인터페이스에서 사용 가능하도록 구현

 


상태패턴

  • 여러 상태들의 실행되는 로직이 달라지는것을 체계적으로 관리하기 위해 사용
    • 대표적인 상태패턴 : 애니메이터

유한상태기계 FSM

  • 여러개의 상태들을 상태 기계로 묶어서 관리
  • 하나의 현재 상태와, 현재상태에서 갈 수 있는 다른 상태을 연결
  • 상태는 추상클래스를 상속받아서 구현
/// <summary>
/// Enemy 상태 부모 추상 클래스
/// </summary>
public abstract class State
{
    protected StateMachine stateMachine;    //현재 상태를 관리하는 상태머신
    protected Enemy enemy;                  //상태머신을 가지고있는 Enemy

    //생성자
    public State() { }
    
    //State를 세팅 : stateMachine, enemy
    public void SetState(StateMachine _stateMachine, Enemy _enemy)
    {
        this.stateMachine = _stateMachine;
        this.enemy = _enemy;

        //상태 초기화
        OnInitialize();
    }

    //상태 초기화 함수
    public virtual void OnInitialize()
    {
        
    }

    //상태 들어가기 (1회 호출)
    public virtual void OnEnter()
    {

    }

    public abstract void OnUpdate(float deltaTime); //추상메서드
    
    //상태 나오기(1회 호출)
    public virtual void OnExit()
    {

    }
}

/// <summary>
/// Enemy Ai 상태머신
/// </summary>
public sealed class StateMachine
{
    private Enemy enemy;    //상태머신을 가진 Enemy(부모 클래스)

    private State m_CurrentState;   //현재 상태
    public State CurrentState => m_CurrentState;

    private State m_PreviousState;  //이전 상태
    public State PreviousState => m_PreviousState;

    private float m_ElapseTime = 0;   //현재상태에 진행된 누적 시간 카운팅
    public float ElapseTime => m_ElapseTime;
    
    //상태를 저장하는 변수
    private Dictionary<System.Type, State> states = new Dictionary<System.Type, State> ();

    //생성자
    public StateMachine(Enemy _enemy, State initialState)
    {
        enemy = _enemy; //상태머신을 가진 Enemy
        
        //초기상태 등록, 초기화
        RegisterState(initialState);

        //현재 상태 처리
        m_CurrentState = initialState;
        m_CurrentState.OnEnter();   //상태 들어가기
        m_ElapseTime = 0f;          //누적시간 초기화
    }
        
    //상태를 등록하는 함수
    public void RegisterState(State state)
    {
        //상태를 세팅
        state.SetState(this, enemy);    //상태를 등록하면서, 머신과 머신을 가진 Enemy 세팅

        //상태를 등록
        states[state.GetType()] = state;
    }

    //현재 상태를 업데이트
    public void Update(float deltaTime)
    {
        m_ElapseTime += deltaTime;  //현재 진행중인 상태의 누적 진행 시간
        m_CurrentState.OnUpdate(deltaTime); //
    }

    //상태 변경
    public State ChangeState(State newState)
    {
        //현재상태 체크
        var newType = newState.GetType();
        if(newType == m_CurrentState?.GetType())    //? => m_CurrnetState가 Null 일 경우 호출하지 않음
        {
            return m_CurrentState;
        }

        //현재상태에서 빠져나오기
        if(m_CurrentState != null)
        {
            m_CurrentState.OnExit();
        }

        //현재상태를 새로운 상태로 세팅
        m_PreviousState = m_CurrentState;
        m_CurrentState = states[newType];

        //상태 들어가기
        m_CurrentState.OnEnter();
        m_ElapseTime = 0f;  //누적 시간 초기화

        return m_CurrentState;  //상태 변경
    }
}

 

계층형 유한상태기계 HFSM

  • 큰 개념의 상태와, 실제행동을 결정하는 세부상태로 구분하여 유한상태기계 구현
  • 큰 개념은 애니메이터의 Sub - State Machine, 세부상태는 일반 State