게임 시작부터 끝까지 사라지지 않는 데이터 관리 시스템이다. 그리고 쉽게 접근이 가능하다

컨트롤 타워

  • 프로그램 내에서 단 하나의 객체만 존재한다. 프로그램 내에서 전역적인 접근이 가능하다. 프로그램의 시작부터 종료 시까지 생존하기 때문에 데이터 관리가 용이하다. 게임 업계에서 가장 많이 쓰이는 디자인 패턴 중 하나. 단, 단점 또한 분명하다

장점

  • 단일의 인스턴스전역적인 접근을 보장하기 때문에 싱글톤을 사용해 오브젝트들이 서로를 참조하고 있는 결합도를 낮출 수 있다
  • 게임 내에서 파괴되지 않기 때문에 보다 편하게 데이터를 보관하기 용이함
  • 하나 뿐인 존재로 주요 클래스 & 관리자 역할에 적합함
  • 전역적 접근으로 참조에 필요한 작업이 없이 빠른 접근이 가능
  • 인스턴스들이 싱글톤을 통하여 데이터를 공유하기 쉬워짐

단점

  • 전역적인 접근이 가능하다는 것은 반대로, 데이터에 대한 보호 수준을 주의하지 않으면, 의도치 않은 데이터 변경이 가능하다
  • 객체들이 싱글톤 객체의 데이터에 의존하게 되는 현상이 발생할 수 있다
  • static을 사용해 정적 메모리에 할당되므로, 싱글톤 객체가 많을수록 가용 메모리가 적어진다
  • 남발하면 스파게티 코드가 된다. 써야 되는 것에만 써야된다
  • 너무 많은 책임을 짊어지는 경우 단일 책임 원칙을 위반
  • 전역 접근으로 오브젝트들 간의 결합도는 낮추지만, 반대로 싱글톤과의 코드 결합도를 높인다. 남발하지 않아야한다
  • 싱글톤은 단위 테스트를 하기 어렵게 함

구성요소

    1. 딱 하나만 인스턴스를 가진다
    1. 해당 인스턴스에 어디서든 접근할 수 있어야 한다

사용처

  • 오직 하나의 책임을 가지는 하나의 객체를 만들어야 하는 경우에 사용한다
  • 게임매니저(게임시작,게임중지,게임종료 등), 씬매니저, 사운드매니저, 리소스매니저, 파일매니저

유니티 예시 1) 씬체인저

  • 싱글톤으로 씬 체인저를 만들어본다

SceneManager

  • 씬 매니저는 씬을 관리하기 위한 기능이다. 유니티가 기본적으로 제공한다. 사용하기 위해서는 네임스페이스를 추가해야한다
using UnityEngine.SceneManagement;
  • 씬매니저는 유니티가 기본적으로 제공하는 static으로 이루어진 함수들을 제공한다. 우리는 이 중 LoadScene() 함수를 사용할 것이다
public class SceneChanger : MonoBehaviour
{
	//LoadScene 함수의 오버로딩 중, int와 string
	public void Load(int sceneNumber)
    {
    	SceneManager.LoadScene(sceneNumber);
    }
    public void Load(string sceneName)
    {
    	SceneManager.LoadScene(sceneName);
    }
}

GameManager

  • 게임에서 점수를 관리하는 기능을 담당할 것이다
public class TestGameManager : MonoBehaviour
{
    // 씬체인저 인스턴스를 가진다
    public TestSceneChanger Scene { get; private set; }
    // 스코어는 전역에서 접근 가능
    [field: SerializeField] public int Score { get; set; }
    // 게임매니저 인스턴스는 static으로 선언
    //다른 객체에서 참조 없이 사용 가능하게 만든다
    public static TestGameManager Instance { get; private set; }

    // 게임매니저 초기화 작업
    private void Awake()
    {
        Init();
    }
    private void Init()
    {
        SetSingleton();
        // gameObject.이 생략되어 있다
        // 게임 오브젝트에는 TestSceneChanger스크립트를 컴포넌트로 추가한다
        Scene = GetComponent<TestSceneChanger>();
    }
    private void SetSingleton()
    {
        // 이미 인스턴스가 있을 경우, 
        // 현재 이 인스턴스 파괴
        if (Instance != null)
        {
            Destroy(gameObject);
        }
        // 없을 경우 생성
        else
        {
            // 현재 이것을 인스턴스로 지정
            Instance = this;
            // 다른 씬으로 전환 되어도 삭제 되지 않는다
            DontDestroyOnLoad(Instance);
        }
    }
}

TestObject

  • A키를 입력 시, 점수 증가
  • 숫자 키 1, 2 로 씬을 전환 가능
public class TestObject : MonoBehaviour
{
	private void Update()
    {
    	if(Input.GetKeyDown(KeyCode.A))
        {
        	// 별도로 GameManager 인스턴스를 참조하는 
            //변수 생성 없이 접근 및 변경 가능
            GameManager.Instance.Score++;
        }
        if(Input.GetKeyDown(KeyCode.Alpha1))
        {
        	GameManager.Instance.Scene.Load(0);
        }
        else if(Input.GetKeyDown(KeyCode.Alpha2))
        {
        	GameManager.Instance.Scene.Load(1);
        }
    }
}

DontDestroyOnLoad

  • 왜 전역에서 하나의 인스턴스만 존재해야 하는가?
  • 게임매니저가 DontDestroyOnLoad()로 삭제가 막히지 않는다면, 다음 씬으로 변경 시 게임매니저가 존재하지 않아 참조 해야 할 인스턴스가 없다

  • 그렇다고 각각의 씬에 게임 매니저를 두자니, 서로 다른 객체이기 때문에 같은 스코어로 관리를 할 수 없다

  • 그렇기에 static으로 전역에서 하나의 인스턴스로 존재하게 한다

실행

  • 먼저, 타이틀 씬테스트 씬을 생성한다. 그리고 빌드 세팅에서 씬들을 추가해준다

  • 테스트 오브젝트에 스크립트 추가
  • 게임 매니저 오브젝트 추가
  • 타이틀 씬에도 동일하게 테스트 오브젝트를 추가하면 된다


  • 게임 매니저는 DontDestroyOnLoad로 달리 분리되는 것을 볼 수 있다
  • 게임매니저를 여러개 만들어도 실행하면 삭제된다
  • 1,2 번 키를 누르면 씬 전환이 이뤄지는 것을 확인할 수 있다

싱글톤 관리 스크립트

  • 게임을 제작하다 보면 싱글톤이 여러 개 만들어질 수 있다
  • 매번 싱글톤을 지정하는 작업을 하기에는 번거로우니 제네릭 클래스를 선언, 코드의 재사용성을 높여보자
  • 권장되는 방법은 아니다!

SingletonBehaviour

  • 제네릭 타입으로 선언, 다양한 타입에 대응하도록 한다
  • MonoBehaviour를 상속받는 것으로 제한하여 잘못된 사용 예방

public class TestSingletonBehaviour<T> :MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            if(instance == null)
            {
                // 프로퍼티를 사용, 인스턴스 호출 시
                // 싱글톤이 초기화가 안되어 있으면
                // 모든 씬에서 해당 타입을 찾아 초기화
                // FindObjectOfType은 유니티 문서 설명에 나와있듯이 느린 함수다
                instance = FindObjectOfType<T>();
                DontDestroyOnLoad(instance.gameObject);
            }
            return instance;
        }
    }

    protected void SetSingleton()
    {
        // 인스턴스가 이미 존재하고, 현재의 인스턴스가 아닐 경우
        if(instance != null && instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            // 없을 경우 해당 컴포넌트를 추가하고 파괴 불가능하게 방지
            instance = GetComponent<T>();
            DontDestroyOnLoad(gameObject);
        }
    }
}
  • 싱글톤 기능에 변동이 있으면, 해당 스크립트를 변경하기만 하면 되므로 유지 보수성이 좋아진다!

    유니티 - FindObjectOfType

GameManager 스크립트 변경

  • 이제 SingletonBehaviour 를 상속받아서 간략화 할 수 있다
public class TestGameManager : TestSingletonBehaviour<TestGameManager>
{
    public TestSceneChanger Scene { get; private set; }
    [field: SerializeField] public int Score { get; set; }

    private void Awake()
    {
        Init();
    }
    private void Init()
    {
        SetSingleton();
        Scene = GetComponent<TestSceneChanger>();
    }
}

유니티 싱글톤 예시2)

  1. 단일 인스턴스를 유지하기 위해, 생성자의 접근권한을 외부에서 생성할 수 없도록 제한. private로 설정
  2. 단일 인스턴스를 보관할 정적 변수를 구성. 인스턴스는 static 선언
  3. 외부에서 접근이 가능한 GetInstance() 함수를 구성. 반환 값싱글톤 인스턴스
  4. GetInstance() 함수에서 단일 인스턴스가 없을 경우, 인스턴스를 생성하여 정적 변수에 참조
  5. GetInstance() 함수에서 단일 인스턴스가 있을 경우, 정적 변수에 참조된 인스턴스를 반환

C# 싱글톤 스크립트 예시

  • MonoBehaviour를 상속받지 않는 싱글톤
public class Singleton
{
    private static Singleton instance;

    public static Singleton GetInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }

        return instance;
    }

    private Singleton() { }
}

유니티 싱글톤 스크립트 예시

  • 유니티의 게임 오브젝트컴포넌트로 추가할 싱글톤을 작성한다
public class Singleton : MonoBehaviour
{
    private static Singleton instance;

    public static Singleton GetInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }
    public int score;
    // 기본 생성자를 private로 놓아야 외부에서 맘대로 만들 수 없다
    private Singleton() { }
}

여러 싱글톤들을 묶기

  • 게임에서 캐릭터의 정보같은 경우도 싱글톤으로 DataManager에 포함할 수 있다. 씬에는 이 정보를 가지고 동작하는 플레이어 인형을 만들게 된다
  • 인벤토리장비 탈착 기능을 따로 만든다면, DataManager에 포함시키는것도 좋다
public class DataManager
{
	// 따로 떨어져있는 싱글톤들의 정보들을 담는 싱글톤으로 DataManager를 쓸 수 있다
	private PlayerStat playerStat;
    public PlayerStat PlayerStat { get{ return playerStat;} }
    
    private Inventory inventory;
    public Inventory Inventory { get{ return inventory;} }
    
    private Equipment equipment;
    public Equipment Equipment { get {return equipment;} }
    
    // DataManager 인스턴스가 생성 될 때, 자동으로 값들을 업데이트 할 수 있게 한다
    private void Awake()
    {
    	stat = DataManager.GetInstance().PlayerStat;
    }
}

스코어 관리

  • 게임에서 스코어를 보관하는 싱글톤을 만들어보자
public class Tester
{
    public void Test()
    {
        // 싱글톤 생성자를 private로 해야 외부에서의 생성을 막는다
        //Singleton num1 = new Singleton();

        // 처음에 쓸 때는 인스턴스가 없으면 인스턴스를 만든다. 있으면 있는 애를 가져온다
        //Singleton.GetInstance() = 반환형이 Singleton 인스턴스다
        // 그래서 .score를 이어 붙일 수 있다
        Singleton.GetInstance().score = 10;
        Singleton.GetInstance().score++;
        Singleton.GetInstance().score = 0;

    }
}
  • 체인매서드를 사용해서 싱글톤 인스턴스 생성과 동시에 스코어를 편집한다

에디터에서 게임 오브젝트로 추가

  • 빈 게임 오브젝트 생성 후, 게임매니저로 이름을 바꾼 다음 게임매니저 스크립트 추가
  • 에디터에서는 드래그&드롭으로 만들 수 있다보니, 하나 이상의 인스턴스를 만드는 것을 막지 못한다

1. 하나의 인스턴스만 존재함

  • 싱글톤 인스턴스를 하나만 존재할 수 있게 코딩한다. 여기서 Destroy를 활용한다
public class GameManager : MonoBehaviour
{
    // 스태틱으로 만들어야 하나만 존재 가능
    private static GameManager instance;
    public int score;

    // 유니티에서 Awake는 생성자와 동일한 기능을 한다
    private void Awake()
    {
    	CreateInstance();
    }
    private void CreateInstance()
    {
    	// 인스턴스가 없으면 새로 만든다
    	if(instance == null)
        {
            instance = this;
        }
        else
        {
            // 이미 인스턴스를 가진 게임 오브젝트가 존재한다는 소리다
            // 현재 이 컴포넌트를 부착한 게임오브젝트를 삭제한다
            Debug.Log("게임 매니저 인스턴스가 이미 존재합니다.");
            Destroy(gameObject);
        }
    }
}

주의

  • MonoBehaviour를 상속받는 컴포넌트는 static으로 쓸 수 없다

  • 하나만 남기고 나머지는 삭제된다

알아두기

  • GameManager(3)만 남은 이유는 유니티에서 Awake()는 랜덤하게 순서가 진행되서 그렇다. 실행 할 때마다 살아남는 오브젝트가 달라진다
  • 해당 설정에서 스크립트 실행 순서를 지정해 줄 수 있으나, 권장하지 않는다

2. 전역적인 접근이 가능하게 함

public static GameManager Instance { get { return instance; } }
  • GameManager 스크립트에서 인스턴스의 프로퍼티를 설정한다
  • 탱크의 공격 이후, 스코어를 올려보자

Tank 스크립트

  • Tank 스크립트에 아래와 같이 추가한다
shooter.Fire();
// 인스턴스 참조 변수 필요없이, 바로 접근 가능
GameManager.Instance.score++;
  • 참조를 위한 인스턴스를 만들 필요 없이, 접근이 바로 가능하다!

Monster 스크립트

  • 몬스터가 죽을 시, 스코어가 올라가게 하려면 아래와 같이 스크립트를 추가 수정 해준다
if(died)
{
	Die();
}
private void Die()
{
	GameManager.Instance.score += 10;
}
  • 싱글톤으로 스코어를 관리하기 편해졌다
  • 게임 전체에서 하나의 스코어만 쓰는 경우 쓸 수 있다

싱글톤 보험

  • 만약, 실수로 씬에서 싱글톤을 객체를 만들지 않았을 경우를 대비한 보험이다. 아래와 같이 GameManager 스크립트에서 Instance의 프로퍼티를 수정한다
private static GameManager instance;
public static GameManager Instance 
{ 
    get 
    { 
        // 만약, 하나도 안만들었을 경우를 위한 보험
        if (instance == null)
        {
            GameObject gameObject = new GameObject("GameManager");
            instance = gameObject.AddComponent<GameManager>();
        }
        return instance; 
    } 
}

TIP

  • 게임 오브젝트를 생성 하는 데 Instantiate로 만들 수도 있지만, new GameObject("이름") 으로 만들 수도 있다

Lazy Initialization (게으른 초기화)

  • 게임에서 처음으로 Instance를 호출 할 때, 인스턴스가 없을 경우 생성하도록 한다.
  • 미리 선언만 해두고 뒤늦게 생성하는 방법이라서 게으른 초기화

씬 매니저

  • 씬 전환을 위한 씬 매니저를 만들어보자
// 씬 메니저 네임스페이스를 추가해야 된다
using UnityEngine.SceneManagement;

public class SceneChanger : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha0))
        {
            // 1. 씬 번호를 적는 법
            SceneManager.LoadScene(0);
            // 2. 씬 이름을 적는 법
            SceneManager.LoadScene("TitleScene");
        }
        else if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            SceneManager.LoadScene("SampleScene");

        }
    }
}

빌드 세팅

  • 게임을 빌드할 때, 원하는 씬만 넣을 수 있다. 하이라키에서 씬을 드래그&드롭 하면 된다

  • 씬 전환을 위해서 필수로 해줘야 하는 작업이다

  • 이러고 나서 예시 1번 때와 같이 각 씬에 씬 매니저 게임 오브젝트를 추가하고, 숫자 키 0, 1을 누르면 각각의 씬으로 전환 된다


싱글톤 VS static

  • static도 전역인데, 싱글톤을 또 만들어서 사용하는 걸까? 싱글톤은 static과 달리 삭제하고 싶을 때 삭제할 수 있기 때문이다

싱글톤 제거하기

  • static과 다르게 싱글톤은 인스턴스를 삭제할 수 있다
public static void ReleaseInstance()
{
	if(instance != null)
    {
    	Destroy(instance.gameObject);
        instance = null;
    }
}

싱글톤 주의점

  • 코드 결합도가 높아져서 생기는 문제들이 있다. 플레이어가 몬스터를 때릴 때, 몬스터로 바로 참조하는게 아니라 싱글톤을 거쳐서 참조하는 경우도 생긴다. 반대도 마찬가지!
  • 객체지향과는 반대되는 개념이라서 안티 패턴(Anti-Pattern)이기도 하다
  • 싱글톤은 많은 수를 쓰지 않기 위해 설계를 잘 해야 한다
  • 플레이어는 싱글톤으로 관리하지 않는다.(추천)
  • 클론, 영화 미키17처럼 매 장면마다 기존의 플레이어는 삭제하고 새로운 씬에서 플레이어 정보를 불러와서 새로 만든다. 싱글톤으로 구현할 경우 Level1에서 Level2로 갔는데, 다시 Level1 에 갈 경우 자신의 복제를 만나게 된다. 타이틀이나 게임오버 스크린에 필요없는 경우도 이유다
  • 플레이어의 정보를 구성하는 싱글톤을 만들고, 그 정보를 기준으로 플레이어를 씬마다 삭제, 생성한다

싱글톤으로 하지말자

  • 게임 매니저플레이어를 가지고 있으면 안된다.
// 게임매니저 싱글톤
public TankPlayer tank;

// 몬스턱가 죽었을 때, 플레이어에게 경험치를 준다 해보자
// 몬스터 컴포넌트
// 죽었을때
GameManager.Instance.tank.exp += 10;
  • 이렇게 해두고 게임 씬에서 탱크를 연결하면 문제가 없는데, 타이틀 씬으로 이동하면 tankMissing이 된다. 그러고 다시 인게임으로 들어가면 tank는 기존의 것이 아니라 다른 tank가 된다. 경험치도 초기화됨. 이는 참조가 풀리기 때문이다. 씬을 나갔다 들어왔다 할때 게임 오브젝트를 전부 삭제하고 다시 만들다 보니 생기는 문제다

  • 지금과 같이 씬을 전환하다 보면 ID가 변경 되는 것을 확인할 수 있다. 서로 다른 객체가 된다
  • 즉, 게임 오브젝트가 싱글톤을 참조하는 것은 되지만, 싱글톤이 게임오브젝트를 참조하면 안된다
  • 싱글톤은 씬에 종속성이 없어야 한다!
  • 플레이어의 데이터를 따로 보관하는 싱글톤을 만들고 정보만 보관한다! (ex. 이름, 레벨, 경험치 등)

Tank 스크립트 수정

  • 게임 매니저에 플레이어 정보를 저장하는 식으로 구현한다
private void Start()
{
	// 씬이 로드됨과 동시에 게임매니저에 해당 정보를 담는다
	GameManager.Instance.tank = this;
}

몬스터가 죽을때

  • 몬스터가 죽을 때 경험치를 준다고 해보자
// 몬스터 컴포넌트
OnDestroy()
{
	GameManager.Instance.tank.exp += 10;
}
  • 이 상황에서 만약 게임을 종료하면, 게임 매니저몬스터보다 먼저 삭제될 수 있어서 조심해야된다!
  • 싱글톤도 결국 게임 오브젝트다. 몬스터가 죽을 때, 게임 오브젝트가 없을 경우 다시 만들어서 불러온다!

강사님 작업방식

  • 회사 실무에서 썼던 방법

가장 먼저 불러오기

  • 유니티 스크립트

  • 해당 어트리뷰트를 추가하고 함수를 작성하면, Awake() 보다 먼저 수행되어 초기화 순서를 정할 수 있다

  • 게임매니저 등의 오브젝트를 Awake() 보다 먼저 생성하게 하고 싶을 경우 쓸 수 있는 방법이다

Manager 스크립트 작성

  • MonoBehaviour를 상속받지 않는다. static으로 선언
public static class Manager
{
	// 이것과 같다 : GameManager Game => { get { return GameManager.GetInstance(); } }
	public static GameManager Game => GameManager.GetInstance();
	
    // Awake() 전에 해당 함수가 수행된다   
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
	private static void Initialize()
	{
		GameManager.CreateInstance();
	}
}
  • GameManager 뿐만 아니라, 여러 싱글톤들 또한 같은 방식으로 추가할 수 있다. 싱글톤들을 관리하는 싱글톤인 셈이다

Resource 폴더

  • 리소스폴더에 있는것은 스크립트 상에서 불러 올 수 있다
  • 게임매니저를 프리팹화 해두고, Resource 폴더를 만들어 안에 넣어둔다
  • 스크립트 상으로 프리팹을 만들고 불러올 수 있다
  • 리소스 폴더Asset 폴더 어디든 존재하면, 인식한다!

GameManager 수정

  • 추가된 기능을 스크립트에 추가한다
public static void CreateInstance()
{
	if(instance == null)
    {
    	GameManager prefab = Resources.Load<GameManager>("GameManager");
        instance = Instance(prefab);
        DontDestroyOnLoad(instance.gameObject);
    }
}

몬스터 죽을 때 스코어 추가

  • Manager로 모든 싱글톤에 접근 가능하게 바꾸었으니, 수정해본다
// 몬스터 컴포넌트
OnDestroy()
{
	// 기존 스크립트
	// GameManager.Instance.tank.exp += 10;
    // 이렇게 쓸 수 있다
    Manager.Game.score += 10;
}
  • Game 뿐만 아니라, Scene,UI,File 등 여러가지로 쉽게 접근 가능하게 바뀌었다

실수 방지 기능 탑재

  • 해당 방식으로 스크립트를 작성하면, 프리팹으로 만들어진 게임 매니저에 씬의 게임 오브젝트를 드래그&드롭으로 참조하는 실수를 방지할 수 있다. 무조건 스크립트로 참조하는 기능을 구현해야 한다

  • 하이라키 창Tank를 프리팹의 Tank 변수에 드래그&드롭할 수 없다

참고

profile
개발 박살내자

0개의 댓글