본문 바로가기
Studies/게임 디자인 패턴

[게임 디자인 패턴] Singleton Pattern

by NCTP 2024. 7. 3.

// 유니티로 배우는 게임 디자인 패턴 제 2판 (데이비드 바론, 구진수) 을 기준으로 작성되었습니다.

 

싱글턴 패턴 Singleton Pattern

유니티 개발자들 사이에서 가장 널리 사용되는 패턴인 싱글턴 패턴.

 

특정 인스턴스의 유일성을 보장한다.

싱글턴 패턴에 의해 초기화된 이후에는, 런타임 동안 단 하나의 인스턴스만 메모리에 존재한다.

 

주로 게임 매니저와 같은 매니저 클래스들이 싱글턴으로 구현된다.

씬 사이를 드나들면서도, 매니저 클래스들은 사라지지 않고 존재하며, 덕분에 다른 씬에서의 정보도 그대로 가져갈 수 있다. 즉, 여러 씬이나 스크립트에서 동일한 데이터를 공유하고 접근하므로 데이터의 일관성이 만들어진다!

예를 들어, 1번 씬에서 플레이어의 골드가 50이 되었다면, 2번 씬으로 이동한 뒤에서 플레이어의 골드가 0부터 시작하는 것이 아닌, 50으로부터 시작하도록 구현할 수 있다. 

(해당 예시는 싱글턴과 유니티의 DontDestroyOnLoad 메서드가 함께할 경우.)

 

 

싱글턴 패턴의 장단점

장점

  • 전역 접근 가능 (Global Access) : 싱글턴 패턴을 사용하여 리소스나 서비스의 전역 접근점을 만들 수 있다.

  싱글턴으로 구현된 클래스는 언제 어디서든 쉽게 접근이 가능하다는 장점이 있다. 이 때문에 게임 곳곳에 관여해 불러올 일이 많은  매니저 클래스에 사용되는 경우가 많다.

  너무 많은 전역 변수의 선언은 시한 폭탄과 같은 것처럼, 너무 많은 클래스를 싱글턴으로 구현해버릴 경우 성능의 저하복잡성 증가, 네임스페이스 오염 (변수명의 중복과 같은 문제) 등의 문제들이 생길 수 있다. 적절히 필요한 곳에서만 사용하는 것이 중요하다.

 

  • 동시성 제어 : 공유 자원에 동시 접근을 제한하고자 사용할 수 있다.

  일단, 싱글턴 패턴 자체는 동시성에 취약하다. 따라서 lock을 걸거나 이른 초기화 등의 방법론으로 보완이 가능하다. 해당 내용은 따로 글을 쓸 정도로 좋은 내용이 될 것 같아서 그 쪽에서 설명한다.

 

  • 메모리 절약 및 효율성 : 단일 인스턴스만 생성되기 때문에 메모리를 절약할 수 있다.

   객체를 반복적으로 생성 및 소멸시키는 오버헤드를 줄일 수 있으므로 메모리 절약 측면에서 장점을 지닌다.

단점

  • 유닛 테스트 : 싱글턴의 과도한 사용은 유닛 단위의 테스트를 어렵게 만든다.
  • 잘못된 습관 : 싱글턴으로 구현된 클래스는 어디서나 쉽게 접근이 가능해진다. 이러한 편의성 덕에 싱글턴 패턴을 남용하는 습관이 생길 수 있다.

 

유니티 게임 매니저 디자인하기

  게임 매니저가 게임 세션을 관리하는 단일 관리자로써 구현을 시작한다. 마치 TRPG의 게임 마스터와 같은 개념으로.

 

  게임 매니저는 싱글턴 클래스로써 구현되며, 게임의 전체 수명동안 하나의 인스턴스로 계속해서 살아있어야 한다.

  

  먼저, Singleton 스크립트를 작성한다.

using UnityEngine;

public class Singleton <T> : MonoBehaviour where T: Component
{
    private static T _instance;

    public static T Instance
    {
        get // get 생성자로 public static 속성 구현 
        {
            if(_instance == null) // 새로운 인스턴스를 초기화하기 전 다른 인스턴스가 있는지 확인
            {
                _instance = FindObjectOfType<T>(); // 지정된 타입의 첫 번째로 로드된 오브젝트 검색
                if(_instance == null) // 만약 없다면, 새로은 게임 오브젝트 생성
                {
                    GameObject obj = new GameObject();
                    obj.name = typeof(T).Name; // 이름을 바꾼 후
                    _instance = obj.AddComponent<T>(); // 지정된 타입의 컴포넌트를 추가
                }
            }
            return _instance;
        }
    }
    public virtual void Awake() // 파생 클래스에서 재정의 가능
    {
        if(_instance == null) // 메모리에 초기화된 자신의 인스턴스가 이미 있는지 확인
        {
            _instance = this as T; // 없다면 자기 자신의 현재 인스턴스가 됨.
            DontDestroyOnLoad(gameObject); // 새로운 씬이 로드될 때 오브젝트 파괴를 막음
        }
        else
        {
            Destroy(gameObject); // 이미 인스턴스가 있다면 복제를 피하기 위해 스스로를 제거
        }
    }
}

  

  해당 스크립트를 상속받으면 상속받은 클래스는 싱글턴 패턴으로 구현이 가능해진다.

  Singleton 클래스를 상속받는 GameManager 클래스 스크립트를 작성하자.

 

using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : Singleton<GameManager>
{
    private DateTime _sessionStartTime;
    private DateTime _sessionEndTime;
    // Start is called before the first frame update
    void Start()
    {
        // TODO:
        // - 플레이어 배치 및 게임 시작
        _sessionStartTime = DateTime.Now;;
        Debug.Log("Game Session start @: " + DateTime.Now);
    }
    void OnApplcationQuit()
    {
        _sessionEndTime = DateTime.Now;
        TimeSpan timeDifference = _sessionEndTime.Subtract(_sessionStartTime);
        Debug.Log("Game session ended @: " + DateTime.Now);
        Debug.Log("Game session lasted @: " + timeDifference);

    }

    void OnGUI()
    {
        if(GUILayout.Button("Next Scene"))
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1); // 다음 인덱스의 씬 로드
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 

  이렇게 GameManager를 싱글턴 패턴으로 구현이 가능하다.

 

  이번에 만들 게임은 2D 사이드뷰 로그라이크 게임을 제작한다. 거기에 사용되는 GameManager를 싱글턴 패턴을 통해 구현했다. 간단한 게임이므로 플레이어의 Save/Load 데이터는 따로 없고, 메인 타이틀부터 게임 매니저가 생성되어 게임이 꺼질 때까지 존재할 것이다.

 

  게임의 전반적인 룰, 씬 이동, 자원관리를 담당할 것이므로, 여러 변수와 메서드들이 추가될 예정이다.

  

  싱글턴 패턴을 통해 GameManager뿐만 아니라 다양한 Manager들을 구현할 수 있다. 게임 전체 스폰을 담당하는 SpawnManager나, 버프 등을 담당하는 BuffManager와 같은 다양한 매니저들을 구현해보거나, 모든 매니저들을 한번에 관리하는 클래스를 싱글턴 패턴으로 만들어 남용을 방지할 수도 있다.

 

  본인은 게임에 적용할 Managers 클래스에 싱글턴 패턴을 적용하고, 해당 클래스를 통해 모든 Manager 들을 관리하는 형식으로 구현하고 있다. 위 게임 매니저의 구현 내용을 그대로 옮긴 것과 같고, 다른 점은 GamaManager를 Managers에서 새로 만들어주고 있는 점이다.

  

  이를 통해서 Mangers클래스를 통해 모든 매니저들에 접근할 수 있게 되었고, 개발의 편의성을 얻을 수 있다.

 

using System;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Managers : Singleton<Managers>
{
    private DateTime _sessionStartTime;
    private DateTime _sessionEndTime;
    public static GameManager _gameManager = new GameManager();
    // Start is called before the first frame update
    void Start()
    {

        _sessionStartTime = DateTime.Now;;
        Debug.Log("Game Session start @: " + DateTime.Now);
    }
    void OnApplcationQuit()
    {
        _sessionEndTime = DateTime.Now;
        TimeSpan timeDifference = _sessionEndTime.Subtract(_sessionStartTime);
        Debug.Log("Game session ended @: " + DateTime.Now);
        Debug.Log("Game session lasted @: " + timeDifference);

    }
    void OnGUI()
    {
        if(GUILayout.Button("Next Scene"))
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1); // 다음 인덱스의 씬 로드
            
        }
    }

}

댓글