게임을 만들다보면 많이 쓰는 스크립트는
Manager로 만들어두고 싱글톤을 사용하는 경우가 많다
여러 스크립트에서 싱글톤을 만들기보단, 차라리 하나의 싱글톤을 만들고 상속해서 사용하는 것이 더 좋을 수 있다 -> 이것이 '제네릭 싱글톤'
Generic Singleton)여러개의 싱글톤 클래스를 쉽게 생성하기 위해 사용되는 제네릭 싱글톤 패턴
싱글톤 클래스 만들기 (부모)
싱글톤을 사용하고 싶은 클래스들이 상속받아서 사용한다 (자식)
생명 주기를 보면, 부모와 자식의 실행순서는 부모의 Awake()부터 실행된다
-> 부모의 Awake()에서 정의하고 자식에는 정의하지 않았더라도, 부모에 정의된 내용이 자식에게도 정의된다 (상속)
where T : MonoBehaviour
T 타입이 MonoBehaviour을 상속받아야만 한다는 제약조건(where)

private로 T 타입 인스턴스를 생성 -> 다른곳에서 해당 인스턴스를 접근할때는 Instance로 접근 (읽기전용)
만약 instance == null 이라면 아직 인스턴스가 아무곳도 가리키지 않는다는 의미
-> 게임 오브젝트 타입을 찾아서 instance로 할당
-> 하이라키창에서 T타입을 검색해서 찾는다 (FindAnyObjectByType(typeof(T));
-> 더 빠른 성능을 가짐

instance != null이라면 이미 인스턴스가 가리키는 객체가 있다는 의미
-> 중복으로 인스턴스가 생성되면 안되기 때문에 이미 있다면 현재 게임 오브젝트 파괴 (싱글톤은 단 하나의 인스턴스만을 가짐), 그리고 다시 할당 (파괴 후 return되기 때문)
-> 그게 아니라면 인스턴스 할당 후, 파괴되지 않게 해두기 (DontDestroyOnLoad());
Lazy Singleton)객체가 처음 필요할 때까지 생성하지 않는 방식
-> 처음 Instance를 호출할 때까지 객체를 생성하지 않는다
클래스는 있고, 씬에 해당 클래스가 부착된 게임 오브젝트가 없을때의 예외처리를 사용한다
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance; // ❗ 싱글톤 인스턴스를 저장하는 변수
public static T Instance
{
get
{
if (instance == null) // 1️⃣ instance가 비어 있다면
{
instance = FindObjectOfType<T>(); // 2️⃣ 현재 씬에서 T 타입 오브젝트를 찾음
if (instance == null) // 3️⃣ 씬에 없으면 새로 생성
{
GameObject singletonObj = new GameObject(typeof(T).Name);
instance = singletonObj.AddComponent<T>();
DontDestroyOnLoad(singletonObj);
}
}
return instance; // 4️⃣ instance 반환
}
}
protected virtual void Awake()
{
if (instance == null)
{
instance = this as T; // 5️⃣ instance가 없으면 현재 오브젝트를 싱글톤으로 설정
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject); // 6️⃣ 이미 존재하면 중복 방지를 위해 삭제
}
}
}
instance가 null인지 확인하기
-> null 이라면 아직 싱글톤 인스턴스가 생성되지 않았다는 것.
-> 최초로 생성하자
하이라키창에서 T타입의 게임 오브젝트를 찾기 (T타입 즉, 스크립트가 부착된 게임 오브젝트 찾기)
-> FindAnyObjectOfType 사용
-> 만약 찾으면 그 게임 오브젝트로 인스턴스 생성 (return instance)
만약 하이라키창에 T타입 게임 오브젝트가 없다면, 새로운 게임 오브젝트 생성 후 T타입 컴포넌트 추가
-> new로 새 게임 오브젝트 동적할당 후 스크립트를 추가한다



SceneryManager 스크립트에서 페이드 인 & 페이드 아웃을 구현하였다Singleton<T타입 자기자신>Fade - In)코루틴을 사용하여 프레임마다 이미지의 a값을 줄여서 페이드 인 효과 (점점 화면이 밝아짐)
-> yield return null; (프레임 단위로 호출)
-> a값은 Time.deltaTime을 사용하여 시간단위로 서서히 감소
화면이 다 밝아지고 이미지가 가리면 다른 UI들과 상호작용할 수 없기 때문에 비활성화

Fade - Out)비동기적 씬 로드를 사용한 방법의 코드이다
새로운 씬을 로드하는 동안 화면을 점점 어둡게 만들고, 다 로드되면 씬을 전환한다
페이크 로딩 기법도 사용하여 progress 수치가 0.9보다 같거나 커졌다면 천천히 수치가 증가되기 함

해당 씬이 로드될때마다 처리해주고 싶은 기능이 있다면?
UnityEngine.SceneManagement 네임 스페이스안의 SceneManager.sceneLoaded 이벤트에 자신의 이벤트를 추가하면 된다


OnEnable에서, 이벤트 해제는 OnDisable에 정의하기SceneManager.sceneLoaded가 이벤트이고, OnSceneLoaded가 이벤트 핸들러(이벤트를 처리하는 함수)
+= -= 연산자는 이벤트에 핸들러 함수를 등록하는 구문
SceneManager.sceneLoaded이벤트는 씬이 로드된 후 발생하는 이벤트이다
-> 씬이 로드될때마다 자동으로 호출
-> 즉, 씬이 로드될때마다 SceneLoaded 함수에 호출한 페이드인 함수가 호출되는 것



SceneryManager 코드using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SceneryManager : Singleton<SceneryManager>
{
// T타입으로 클래스 타입이 들어간것임
[SerializeField] Image screenImage;
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
public IEnumerator FadeIn()
{
Color color = screenImage.color;
color.a = 1f;
screenImage.gameObject.SetActive(true);
while(color.a >= 0.0f)
{
color.a -= Time.deltaTime;
screenImage.color = color;
yield return null;
}
screenImage.gameObject.SetActive(false);
}
public IEnumerator AsyncLoad(int index)
{
screenImage.gameObject.SetActive(true);
// <asyncOperation.allowSceneActivation>
// 장면이 준비된 즉시 장면이 활성화되는 것을 허용하는 변수입니다.
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(index);
asyncOperation.allowSceneActivation = false;
Color color = screenImage.color;
color.a = 0f;
// <asyncOperation.isDone>
// 해당 동작이 완료되었는 지 나타내는 변수입니다.
// 씬 불러오는 작업이 아직 다 되지 않았다면
while(asyncOperation.isDone == false)
{
color.a += Time.deltaTime;
screenImage.color = color;
// <asyncOperation.progress>
// 작업의 진행 상태를 나타내는 변수입니다.
if(asyncOperation.progress >= 0.9f)
{
color.a = Mathf.Lerp(color.a, 1.0f, Time.deltaTime);
screenImage.color = color;
if(color.a >= 1.0f)
{
asyncOperation.allowSceneActivation = true;
yield break; // 강제로 빠져나오기
}
}
yield return null;
}
}
// 씬이 바뀔때마다 페이드 인 호출
void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
StartCoroutine(FadeIn());
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
}
MenuManager 스크립트using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MenuManager : MonoBehaviour
{
// 동기 방식
public void Excute()
{
StartCoroutine(SceneryManager.Instance.AsyncLoad(1));
}
public void Exit()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#endif
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
Singleton 스크립트using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
// 다른곳에서 접근하려면 public
public static T Instance
{
get { return instance; }
}
protected virtual void Awake()
{
if(instance == null)
{
instance = (T)FindAnyObjectByType(typeof(T)); // 게임 오브젝트 타입 T
}
else
{
// 이미 있다면 자신의 게임 오브젝트 파괴
Destroy(gameObject);
return;
}
DontDestroyOnLoad(instance.gameObject);
}
}