부트캠프 분류/부트캠프 TIL

2024 11 27 TIL

noc777 2024. 11. 27. 23:14

최종 프로젝트 3일차

 

최종프로젝트 진행관련 접은 글

더보기

에셋들의 호환성 여부를 확인해보고 프로젝트를 개설하기 시작하였다. 

 

미리 봐둔 에셋의 경우 퀄리티가 높은 쪽은 URP와 HDRP의 경우가 많았고 

URP 와 HDRP만 지원하는 경우 호환성을 장담할 수가 없어 어느 한쪽으로 타협을 보아야 했다.

 

맵 요소로 봐둔 에셋의 경우 Built In 으로 되어 있으나

특정한 기능쪽 에셋은 아니라서 URP 컨버터를 이용해 형식을 변환해보기로 하였다.

 

집의 인터넷이 좋지 않아 맵관련 요소들을 집어넣는 과정에서 커밋에 실패하는 상황이 발생하였는데 

팀원분께서 대신 올려주신다고 하셔서 다행인 것 같다. 

 

본격적인 프로젝트 시작 일시는 내일 오전 혹은 오후 초로 예상되며 시작전에 이번 프레임워크 세션에서 들려주신 내용을 정독하고 해석하여 매니저를 만드는 데 이용해보고 싶다. 

 

프레임워크 세션 관련 

 

1. 매니저 생성

 

매니저의 제일 첫 번째 요소 싱글톤 

싱글톤을 사용하는 이유는 게임에서 유일한 객체를 생성하여 편리하게 접근하고 제어할 수 있게 하기 위함입니다.

 

기본적인 싱글톤 사용법

public static GameManager instance; //GameManager를 전역 변수로 하나 선언

private void Awake()
{
	if(instance != null) //만약 인스턴스가 이미 있다면
    {
    	Destroy(gameobject); //현재 게임 오브젝트를 제거한다.
    }
    else
    {
    	instance = this; //인스턴스가 비어있다면 해당 오브젝트를 넣어준다.
    }
}

 

하지만 위의 싱글톤 사용시 유의점이 있습니다.

1. instance가 여러개인 경우를 생각하지 않았다. 

- 많이 생겨나는 경우가 아니지만 만약 패키지로 멀티스레드 기능을 사용하게 되었을 때 등의 경우에서 예외처리가 안되었다.

2. 실행순서의 문제

- 다른 오브젝트와 상호작용해야할 때 둘다 Awake안에서 서로를 찾는다면 한쪽은 거의 확실하게 null 이 발생하는 게 확실하다. 

 

그래서 이런 문제점들을 보완하기 위해서 프로퍼티를 사용하여 유니티의 실행주기 시작전에 미리 객체를 생성하는 방향으로 취약점을 보완하였었다. 

프로퍼티는 실행주기들이 실행되기 전에 먼저 수행되므로 실행순서가 꼬이는 경우를 어느정도 방지가 가능하였고

또한 Awake문에서 객체가 새로 생성되었을 경우를 확인하기 전 객체가 여러개 생성되는 경우도 방지가 가능하였다.

private static GameManager _instance = null; //처음 시작할 땐 null
public static GameManager Instance
{
	get
    {
       if (_instance == null) // 시작할 때 객체가 비어있으므로
       {
         GameObject obj = new GameObject(nameof(GameManager)); //새로 게임매니저라는 이름을 가진 GameObject객체를 생성한다.
         _instance = obj.AddComponent<GameManager>(); //이 객체에 GameManager 컴포넌트를 추가한다.
       }
      return _instance;
    }
}

private void Awake()
{
    if (_instance != this) //만일 새로운 객체를 생성하였을 때 이미 인스턴스가 존재한다면
    {
        Destroy(gameObject); //현재 생성된 자신을 파괴
    }
    else
    {
        _instance = this; //비어있다면 객체로 자신을 추가
    }
}

 

물론 게임을 만들다보면 다양한 매니저들을 만들게 된다. 그런데 위의 작업을 똑같이 계속 반복하는 것이 상당히 불편하기에 고안된 것이 제네릭 싱글톤이다.

 

제네릭 싱글톤으로 들어가기전 제네릭에 대해서 다시 한번 보면

<T>를 매개로 받아오며 사용하는 곳에서 이 T의 타입을 정한다고 보면된다.  

 

우리가 사용할 매니저들은 GameManager,SoundManager등등 다양한 이름의 타입들이 존재할 것이기에 제네릭을 사용하여 어떤 타입이든 들어가도록 조치를 해놓았다고 보면될 것이다.

 

 

제네릭 싱글톤을 선언하기 위해 먼저 singleton이라 이름을 정해주고 <T>를 붙여주었다.

public class Singleton<T> : MonoBehaviour  //상속받는 곳에서 타입을 보내줄 것이기에 클래스쪽에서<T>를 선언해준다.

 

 

이후 상속을 통해 받아오는 타입을 싱글톤으로 만들어주기 위해 프로퍼티에서 객체가 있는지 체크를 시도한다.

 private static T _instance; // T는 상속받으면서 <T>에 들어갈 타입을 말한다.
 public static T Instance
 {
     get
     {
         if (_instance == null)
         {
             T[] go = FindObjectsOfType<T>(); //여러개의 T타입이 있을 경우를 대비하여 T배열을 만들고 해당 타입의 오브젝트들을 찾아 넣는 것을 시도해준다.
         }
     }
 }

이렇게만 하였을 경우

 

심각도 코드 설명 프로젝트 파일 줄 비표시 오류(Suppression) 상태
오류(활성) CS0314 'T' 형식은 제네릭 형식 또는 'Object.FindObjectsOfType<T>()' 메서드에서 'T' 형식 매개 변수로 사용할 수 없습니다. 

라는 오류가 생길 것이다. 

 

왜 그렇냐면 T[] 배열에 넣어줄 것인데 상속받는 클래스가 T가 아닌 경우의 예외처리가 없다.

따라서 해당 클래스에 제한자를 걸어준다.  

 

public class Singleton<T> : MonoBehaviour where T : Singleton<T> //이 클래스를 상속받기 위해선 Singleton<T>를 상속받아야한다

해당 제한자의 의미는 상속받는 클래스들은 반드시 이 Singleton<T>를 상속받아야한다는 소리다.

이전에 where T : MonoBehaviour의 경우 MonoBehaviour의 기능을 꼭 상속받아야한다는 내용이었던 것이 기억난다.

 

상속받는 클래스는 부모클래스의 특성을 이어받기에 이게 대체 왜 필요하지라고 생각이 들긴하지만 

이런 문제들의 공통적인 특징은 만약에! 라는 경우들을 배제하기 위해서라고 생각하면 될것같다.

 

public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
//프레임워크 세션에선 실질적인 사용처가 아닌 클래스이기에 abstract를 넣어주었다.

프레임워크 세션 때는 abstract를 넣어주어

이 클래스가 사용되는 클래스가 아닌 기능을 상속시키는 목적이라는 것을 확실하게 명시해주었다.

 

public static T Instance
{
    get
    {
        if (_instance == null) 
        {
            T[] objs = FindObjectsOfType<T>(); //T타입을 담을 수 있는 배열을 만들어 T타입을 찾으면 넣어지도록 한다.

            if (objs.Length > 0) //배열안에 객체가 존재할 경우엔
            {
                _instance = objs[0]; //가장 첫번쨰 주소를 참조시킨다.
                
                for (int i = 1; i < objs.Length; i++) //0은 _instance의 참조 주소가 되었으므로 제외
                {
                    DestroyImmediate(objs[i]); //DestroyImmediate는 즉시 오브젝트를 파괴시킨다.
                }
            }
            else
            {
                GameObject obj = new GameObject(string.Format("{0}",typeof(T).Name));//새로 게임오브젝트를 생성하며 이름을 해당 T타입으로 해준다.
                _instance = obj.AddComponent<T>();//오브젝트에 T컴포넌트를 붙여주고 _instance에 주소를 참조시킨다.
            }
        }
        return _instance;
    }
}

이어서 봤을 때

 

만약 배열안에 객체가 있다면 (조건)

 

해당 배열의 첫번째를 _instance의 참조주소로 설정하고

다른 객체들을 파괴시킨다. 

 

DestroyImmediate란

Destroy와 기능은 비슷하지만

Destroy가 바로 삭제되는 것이 아닌 Update루프가 끝나기 전까지 지연되고 렌더링 전에 파괴된다면

DestroyImmediate 는 사용즉시 파괴된다는 차이점이 있다.

다만 DestroyImmediate는 오브젝트를 영구적으로 파괴할 수 있으니

게임코드가 아닌 편집기 코드를 작성할 때 사용을 하라고 권고한다.

 

만일 배열안에 객체가 없다면 (조건)

 

새로 T 타입 이름과 기능을 가진 오브젝트를 생성하여 참조시킨다. 

 

string.Format은  괄호안의 내용을 string 형식으로 바꿔준다.

여기서 사용한 것은 "{0}" 처럼 보간과  typeof(T).name 처럼 해당 타입의 이름으로 까지 변환해준거로 봐선 아주 확실하게 오류가 날 여지도 안남겨놓겠다는 것 같다.

 

그렇게 프로퍼티에서 로직은 종료가 된다.

 

다만 프로퍼티에 기능을 나열하여서 보기 복잡해보이기에 프레임워크 세션에선 이것을 또 따로 빼둔다.

public static T Instance
{
    get
    {
        if (_instance == null)
            Create(); //프로퍼티가 복잡해지기에 메서드로 따로 뻬둠
        return _instance;
    }
}

protected static void Create() //상속된 클래스가 사용하는 기능이기에 protected로 명시
{
    T[] objs = FindObjectsOfType<T>(); //T타입을 담을 수 있는 배열을 만들어 T타입을 찾으면 넣어지도록 한다.

    if (objs.Length > 0) //배열안에 객체가 존재할 경우엔
    {
        _instance = objs[0]; //가장 첫번쨰 주소를 참조시킨다.

        for (int i = 1; i < objs.Length; i++) //0은 _instance의 참조 주소가 되었으므로 제외
        {
            DestroyImmediate(objs[i]); //DestroyImmediate는 즉시 오브젝트를 파괴시킨다.
        }
    }
    else
    {
        GameObject obj = new GameObject(string.Format("{0}", typeof(T).Name));//새로 게임오브젝트를 생성하며 이름을 해당 T타입으로 해준다.
        _instance = obj.AddComponent<T>();//오브젝트에 T컴포넌트를 붙여주고 _instance에 주소를 참조시킨다.
    }
}

따로 빼둔 메서드는 상속받은 클래스에서 사용할 기능임을 명시하기위해 protected 키워드를 붙여주고 

static은 자동으로 붙는데 이유는 static 프로퍼티에 쓰이는 메서드이기 때문이다. (난 아직도 이 키워드가 헷갈린다..)

 

이렇게 빼두는 의도는 언제나 확장성과 유지보수성을 신경쓰는 의도인 것으로 보인다. 

 

 

마지막으로 Awake 이다.

 protected virtual void Awake() //virtual 키워드를 선택한 이유는 파괴될것인지 선택하라는 뜻
 {
     Create(); //한번 더 null 체크를 함

     if (_instance != this) //지금 들어가있는 인스턴스가 현재 오브젝트가 아니라면
     {
         Destroy(gameObject); //중복 생성된 지금 객체를 파괴
     }
     else
     {
         DontDestroyOnLoad(this); //지금 객체가 들어가있던 객체면 씬이동시 파괴되지 않도록 조치
     }
 }

 

Awake 문에 protected virual 키워드가 쓰인 이유는 상속받는 쪽에서 파괴되지 않을 것인지 결정할 수 있도록 한 것이다.

 

Create()는 진짜 진짜 확실하게 null체크를 또 한번하려고 한것이며 프로퍼티에서 기능을 따로 뺀것은 이 의도도 포함된 것 같다.

 

조건문에서 지금 _instance의 참조객체가 현재 객체인지를 판별하여 

현재 객체일땐 파과되지 않도록 하는 로직이다.

 

 

정독하면서 해석하느라 느리지만 최대한 매니저 생성전까지 습득해보고 적용시켜보고 싶다. 아침에 일어날 시 계속해서 시청예정

'부트캠프 분류 > 부트캠프 TIL' 카테고리의 다른 글

2024 11 29 TIL  (0) 2024.11.29
2024 11 28 TIL  (0) 2024.11.28
2024 11 26 TIL  (0) 2024.11.26
2024 11 25 TIL  (1) 2024.11.25
2024 11 24 TIL  (0) 2024.11.24