Chapter 2. 싱글톤 패턴(Sigleton Pattern)

Date:     Updated:

카테고리:

태그:

인프런에 있는 이재환님의 강의 게임 디자인 패턴 with Unity 를 듣고 정리한 필기입니다. 😀

Chapter 1. Sigleton Pattern

🔔 싱글톤 패턴이란?

오직 한개의 클래스 인스턴스(static)만 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다.

  • 일종의 전역 변수. 모든 곳에서 접근하여 공유할 수 있는 단 하나의 인스턴스.


싱글톤 단점

전역 변수이므로 전역변수가 가지는 모든 장단점을 다 가지고 있다. 쓰지말잔 얘기가 아니라 이런 단점도 있음을 주의하고 사용하잔 의미.

  • 모든 곳에서 접근이 가능하므로 싱글톤 객체의 변경 시점, 변경 주체, 호출 시점을 모두 알기가 어렵다.
  • 여러 클래스와 커플링이 된다.
    • 하나의 코드를 수정했을때, 싱글톤과 연결된 다양한 곳들에서 문제 발생
  • 멀티 쓰레드 환경에서 문제 발생
    • 모든 곳에서 접근이 가능하므로 race condition 발생.
      • 이를 막기 위해 싱글톤은 mutex lock, unlock을 반복적으로 걸기 때문에 코드의 성능은 떨어지게 된다.


유니티 안에서의 싱글턴 패턴 객체

여러 오브젝트들이 데이터를 사용하는 오브젝트를 단 한개를 만들어 두고 이를 공유한다.

  • 같은 Scene 안에서의 데이터 공유시에 사용됨
  • 서로 다른 scene들간의 데이터 공유시에 사용됨
    • DontDestroyOnLoad 메소드를 호출하여 Scene 변경시에도 싱글톤 객체의 Destroy를 막아주는 형태로 구현한다.


🔔 예제 1 : 한 Scene 내에서 사용

공 오브젝트가 중력에 의해 떨어져 바닥에 부딪치면 소리가 나게 구현해보자.

  • 공 오브젝트에 Rigidbody, Collider 컴포넌트가 붙어있는 상태.
    • 바닥에 부딪치면 OnCollisionEnter 이벤트 발생
  • Audio Source컴포넌트를 통해 소리 파일을 실행시켜야 한다.


싱글톤 사용하기 전

📜 WhenDestroyPlay.cs

  • 공 오브젝트에 붙여준다.
  • 자기 자신(공 오브젝트)이 바닥에 부딪쳐 OnCollisionEnter 이벤트 발생시키면
    • 오디오를 플레이 한 후
    • 자기 자신은 파괴하도록.

이 코드의 문제점 👉 오디오가 재생되기도 전에 자기 자신(공 오브젝트)이 파괴되어버려 오디오를 재생할 수 없게 된다.

  • 따라서 오디오를 플레이한 후 소리 재생이 끝날때 까지 대기 시간을 가진 후 파괴해야 한다.
using UnityEngine;
using System.Collections;

public class WhenDestroyPlay : MonoBehaviour {

	void OnCollisionEnter(Collision collision)
	{
		GetComponent<AudioSource>().Play();  // 오디오를 플레이한 후

		Destroy(this.gameObject);  // 자기 자신 파괴
	}
}

📜 DestroyDelayed.cs

  • 공 오브젝트에 붙여준다.
  • Destroy(this.gameObject , myAudio.clip.length );
    • 재생이 끝난 후에 파괴하도록 문제를 해결했다.

이 코드의 문제점 👉 소리가 재생되는 동안엔 오브젝트가 파괴가 되지 않고 기다리므로 어색하다. 바닥에 부딪치긴 했는데 소리 날 동안은 오브젝트가 살아 있으니까.

  • 싱글톤 패턴을 도입하여 이 문제를 해결해보자.
    • ⭐이 전체 시스템을 관할할 게임 매니저가 필요하다.
    • 이 게임매니저 오브젝트의 오디오 매니저 스크립트를 싱글톤으로 지정한 후 공 오브젝트와 이를 공유 할것.
using UnityEngine;
using System.Collections;

public class DestroyDelayed : MonoBehaviour {

	private AudioSource myAudio;

	void Start() {
		myAudio = GetComponent<AudioSource>();
	}

	void OnCollisionEnter(Collision collision)
	{
		myAudio.Play();

        Destroy(this.gameObject , myAudio.clip.length );
	}
}


싱글톤 사용

  • 컴포넌트 패턴도 함께 사용하여 기능별로 클래스(스크립트)를 나누었다.

📜 MyAudioPlay.cs

  • 공 오브젝트를 프리팹으로 만들고 이 프리팹에 붙여준다.

AudioManager.Instance().Play(clip);

  • 싱글톤인 AudioManager의 static 함수 Instance()를 호출하여 싱글톤 Audio Manager를 리턴 받는다.
    • static 함수이기 때문에 클래스 이름인 AudioManager로 호출 가능.
  • 공이 파괴되기 전, 공에 붙어 있는 Audio Source 컴포넌트의 clip 을 싱글톤인 AudioSource 가 재생시켜주도록 자기 자신의 clip을 넘겨준다.
    • clip에 오디오 클립 드래그 앤 드롭
using UnityEngine;
using System.Collections;

public class MyAudioPlay : MonoBehaviour {

	public AudioClip clip;

	void OnCollisionEnter(Collision collision)
	{
		AudioManager.Instance().Play(clip);

		Destroy(this.gameObject);
	}
}

GameManager 오브젝트 만들기

  • 빈 오브젝트를 만들어 이름을 GameManager라고 하였다.
  • Audio Source 컴포넌트를 붙여준다.
    • clip은 아직 없는 상태.
    • 공을 먼저 파괴 시킨후 공의 clip을 가져와 공의 음악 clip을 게임 매니저가 재생시킬 것이다.
  • 📜CreateBall.cs 붙이기
  • 📜AudioManager.cs 붙이기

📜 CreateBall.cs

  • GameManager 오브젝트에 붙여준다.
  • 프리팹 을 오브젝트화 시켜 생성한다.
    • ball에 프리팹 드래그 앤 드롭
    • Instantiate(ball, pos, Quaternion.identity);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CreateBall : MonoBehaviour {

    public GameObject ball;

    void Update()
    {
        if (Input.GetButtonDown("Fire1"))   // 좌클릭 하면, 발사 하면
        {
            Vector3 pos = new Vector3(0.0f, 4.0f, 0.0f);
            Instantiate(ball, pos, Quaternion.identity);
        }
    }

}

📜 AudioManager.cs

  • GameManager 오브젝트에 붙여준다.
  • ⭐⭐얘가 싱글톤 객체가 된다.
    • 오브젝트와 공유하게 될 것.
    • static AudioManager _instance = null;
      • 게임이 시작되면 자기 자신(GameManager)를 static 변수 _instance 에 넣는다.
using UnityEngine;
using System.Collections;

public class AudioManager : MonoBehaviour 
{
	static AudioManager _instance = null;  // 싱글톤 객체
	public static AudioManager Instance()  // static 함수. 공유하고자 하는 외부에서 사용할 것.
	{
		return _instance;  // 자기 자신 리턴
	}
	
	void Start () 
	{
		if (_instance == null)  // 게임 시작되면 자기 자신을 넣음
			_instance = this;
	}
	
	public void Play(AudioClip clip)
	{
		GetComponent<AudioSource>().PlayOneShot(clip);  // 재생
    }
}


정리

image


🔔 예제 2-1 다른 Scene들간의 싱글톤 사용

다른 새로운 Scene으로 전환되면 기존 Scene의 오브젝트들이 Destroy 된다.

따라서 Scene끼리의 전환이 활발한 게임에서도 싱글톤 객체가 유지되려면 DontDestroyOnLoad 함수를 사용해 씬이 전환되더라도 싱글톤 객체가 없어지지 않고 유지될 수 있게 해주어야 한다.

DontDestroyOnLoad(GameObject)

  • 씬이 변경되더라도 파괴되지 않고 유지할 오브젝트를 지정하는 UnityEngine 함수다.

“Scene 1-1” 구조

  • GameManager 오브젝트
    • 📜 PointManager.cs
      • ⭐싱글톤 객체가 될 것.
        • 자기 자신을 담을 static 변수 _instance
        • 그 static 변수를 리턴할 static 함수 Instance()
        • 게임 시작시(Awake) _instance에 자기 자신을 담음
        • PointManager 은 싱글톤 객체가 되기 때문에 여러 스크립트에서 공유된다. 따라서 myPoint 멤버 변수는 누적된다.
      • DontDestroyOnLoad(this.gameObject);
        • 씬이 변경되더라도 자기 자신(싱글톤)을 파괴하지 않고 유지하도록 설정.
        • else
          • 이미 유지되고 있는 싱글톤이 있다면
            • 즉 실행했었던 과거 Scene으로 다시 돌아왔다면
          • if (this != _instance)
            • this : Scene이 변경되면서 방금 새로 생성된 싱글톤
            • _instance : Scene이 변경되기 전에 저장해놨던 싱글톤
            • 둘이 다르다면 새로 생성된 싱글톤은 파괴해주기. (이전에 유지해놨던 싱글톤을 쓰기 위하여)
              • Destroy(this.gameObject)
      • AddPoint(int num) : 인수를 myPoint 멤버 변수에 더해준다.
      • GetPoint() : myPoint 멤버 변수를 리턴한다.
        using UnityEngine;
        using System.Collections;
              
        public class PointManager : MonoBehaviour 
        {
            int myPoint = 0;
                
            static PointManager _instance = null;
              public static PointManager Instance()
              {
                  return _instance;
              }
        	  
              void Awake () 
              {
                if (_instance == null)
                {
                    _instance = this;
                    DontDestroyOnLoad(this.gameObject);
                }
                else 
                {
                    if (this != _instance)
                    {
                        Destroy(this.gameObject)
                    }
                }
            }
                
            public void AddPoint(int num)
              {
                myPoint = myPoint + num;
              }
              
            public int GetPoint()
            {
                return myPoint;
            }
        }
        
  • Cube 1 오브젝트
    • 📜MyAddPoint.cs
    • PointManager.Instance() + AddPoint or GetPoint 함수로 📜PointManager의 멤버 변수 myPoint에 접근.
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
          
      public class MyAddPoint : MonoBehaviour {
      
          public int myNum = 0;
      
          void Start () {
              MyFunc();
          }
      
          void MyFunc()
          {
              PointManager.Instance().AddPoint(myNum);
              int myResult = PointManager.Instance().GetPoint();
              Debug.Log("Point : " + myResult);
          }
      }
      
  • Cube 2 오브젝트
    • 📜MyAddPoint.cs
  • Main Camera 오브젝트
    • 📜 SceneTrans1.cs
      • 각각 다른 씬으로 전환해 주는 두 함수가 구현되어 있다.
        using UnityEngine;
        using System.Collections;
        using UnityEngine.SceneManagement;
              
        public class SceneTrans1 : MonoBehaviour {
              
            public void SceneTrans1_1 () {
                SceneManager.LoadScene("01-Scene1-1");
            }
        	  
            public void SceneTrans1_2 () {
                SceneManager.LoadScene("02-Scene1-2");
            }
        }
        
      • 📜 SceneTrans1.cs을 Gameobject가 아닌 쌩뚱맞게 Main Camera에 붙이는 이유
        • 📜 SceneTrans1.cs을 Gameobject에 붙이면 일어나는 일
          • “Scene 1-2”씬의 Gameobject는 씬이 전환되어도 DontDestroyOnLoad 함수에 의해 보존이 되는 오브젝트이기 때문에 "Scene 1-2" 씬의 `Gameobject`는 Destroy 되기 때문에 버튼 이벤트에 아무것도 들어오는게 없어 "Scene 1-2" 로 전환 버튼을 눌러도 아무런 일이 일어나지 않는다.
            • 📜PointManager.cs에서 기존에 보존된 싱글톤이 있으면 새 싱글톤 오브젝트는 Destroy 하도록 구현해 놨기 때문에.
          • 따라서 Destroy되는 일이 없는 Main Camera에 부착


“Scene 1-2” 구조

  • GameManager 오브젝트
    • 📜 PointManager.cs
  • Cube 1 오브젝트
    • 📜MyAddPoint.cs
  • Main Camera 오브젝트
    • 📜 SceneTrans1.cs


실행

image

  • “Scene 1-1” 에서
    • Cube 1myNum을 1 로 입력.
    • Cube 2myNum을 2 로 입력.
      • Cube 1, Cube 2는 📜 PointManager 싱글톤 객체를 공유함.
        • 즉, myPoint 값 공유.
    • 버튼 이벤트에 📜 SceneTrans1.cs의 SceneTrans1_1 함수 등록
  • “Scene 1-2” 에서
    • Cube 3myNum을 3 로 입력.
      • Cube 1, Cube 2, Cube 3까지 📜 PointManager 싱글톤 객체를 공유함.⭐ myPoint 값 공유.

        • 1-2 씬으로 전환되어 `Cube3`이 생성되어도 else에 걸려 새롭게 생성된 📜 PointManager객체를 파괴하고 DontDestroyOnLoad 함수에 의해 _instance에 값이 유지됐던 기존 📜 PointManager 싱글톤 객체를 사용하기 때문이다.
        • 이런 과정이 없었다면 동일한 씬 내에 있는 Cube 1, Cube 2끼리만 📜 PointManager 싱글톤 객체를 공유했을 것이다.
    • 버튼 이벤트에 📜 SceneTrans1.cs의 SceneTrans1_2 함수 등록

씬 1-1 👉 씬 1-2 👉 씬 1-1 이렇게 Scene이 변경됐다가 다시 씬 1-1로 돌아와도 myPoint 값이 누적되는 것을 알 수 있다. 싱글톤 객체인 PointManager가 유지되었기 때문에

  • 씬 1-1 👉 씬 1-2 👉 씬 1-1 실행한 후
    • 1 3 6 7 9 출력
      • 씬 1-1 : 1 3
        • 0 + 1 = 1
        • 1 + 2 = 3
      • 씬 2-1 : 6
        • 3 + 3 = 6
      • 씬 1-1 : 7 9
        • 6 + 1 = 7
        • 7 + 2 = 9


🔔 예제 2-2 현재 씬에 다른 씬을 덧대어 추가했을 때

  • “Scene 2-1” 위에 “Scene 2-2” 씬을 덧대어 추가했을 때도 두 씬이 싱글톤 객체를 잘 공유하도록 구현 함.
    • Scene 추가
      • 기본적으로는 화면에는 한개의 Scene이 로드되고, 다른 Scene을 다시 로드하는 방식으로 Scene을 교체한다.
      • 기존 Scene에 Scene을 추가할 수도 있는데, 이것을 Additive Loading이라고 한다.
        • Additive Scene은 예를 들면 말풍선 모양으로 상상하는 씬(?) 같은 느낌이다.

“Scene 2-1” 구조

  • GameManager 오브젝트
    • 📜 PointManager.cs
      • ⭐싱글톤 객체가 될 것.
        using UnityEngine;
        using System.Collections;
              
        public class PointManager : MonoBehaviour 
        {
            int myPoint = 0;
                
            static PointManager _instance = null;
              public static PointManager Instance()
              {
                  return _instance;
              }
        	  
              void Awake () 
              {
                if (_instance == null)
                {
                    _instance = this;
                    DontDestroyOnLoad(this.gameObject);
                }
                else 
                {
                    if (this != _instance)
                    {
                        Destroy(this.gameObject)
                    }
                }
            }
                
            public void AddPoint(int num)
              {
                myPoint = myPoint + num;
              }
              
            public int GetPoint()
            {
                return myPoint;
            }
        }
        
  • Cube 1 오브젝트
    • 📜MyAddPoint.cs
  • Cube 2 오브젝트
    • 📜MyAddPoint.cs
  • Main Camera 오브젝트
    • 📜 SceneTrans2.cs
      • 각각 다른 씬으로 전환해 주는 두 함수가 구현되어 있다.
        • 현재 씬에 새로운 씬을 덧대어 추가해주는 함수
          • SceneManager.LoadScene(“04-Scene2-2”, LoadSceneMode.Additive);
            • “04-Scene2-2”씬을 LoadSceneMode.Additive 모드로 한 씬 내에서 추가적으로, 보조적으로 씬을 위에 로드한다.
        • 어떤 씬을 unload 하고 싶을 때
          • SceneManager.UnloadSceneAsync(“04-Scene2-2”);
            • “04-Scene2-2”씬을 언로드한다.
              • “04-Scene2-2”씬 상의 오브젝트들을 파괴한다. ```c# using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement;

        public class SceneTrans2 : MonoBehaviour {

        public void AddScene () {
            SceneManager.LoadScene("04-Scene2-2", LoadSceneMode.Additive);
        }
                
        public void RemoveScene () {
            SceneManager.UnloadSceneAsync("04-Scene2-2");
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        } } ```
        


“Scene 2-2” 구조

  • “Scene 2-1” 위에 덧대어 추가 될 씬이다.
  • Cube 2 오브젝트
    • 📜MyAddPoint.cs
  • GameManager 오브젝트는 없다.
    • "Scene 2-1"에 이미 있으므로 없어도 된다. GameManager한 씬에선 전체 하나만 있으면 됨.("Scene 2-2"는 덧대어지는 씬일 뿐 새롭게 로드되는건 아님)
  • Main Camera 오브젝트는 꺼져있다.
    • "Scene 2-1"의 Main Camera를 공유하여 사용할거라서.


실행

예제 2-1 와 실행 원리 같다.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Design Pattern 카테고리 내 다른 글 보러가기

댓글 남기기