Chapter 2. 싱글톤 패턴(Sigleton Pattern)
카테고리: Design Pattern
인프런에 있는 이재환님의 강의 게임 디자인 패턴 with Unity 를 듣고 정리한 필기입니다. 😀
Chapter 1. Sigleton Pattern
🔔 싱글톤 패턴이란?
오직 한개의 클래스 인스턴스(static)만 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다.
- 일종의
전역 변수
. 모든 곳에서 접근하여 공유할 수 있는 단 하나의 인스턴스.
싱글톤 단점
전역 변수이므로 전역변수가 가지는 모든 장단점을 다 가지고 있다. 쓰지말잔 얘기가 아니라 이런 단점도 있음을 주의하고 사용하잔 의미.
- 모든 곳에서 접근이 가능하므로 싱글톤 객체의 변경 시점, 변경 주체, 호출 시점을 모두 알기가 어렵다.
- 여러 클래스와 커플링이 된다.
- 하나의 코드를 수정했을때, 싱글톤과 연결된 다양한 곳들에서 문제 발생
- 멀티 쓰레드 환경에서 문제 발생
- 모든 곳에서 접근이 가능하므로
race condition
발생.- 이를 막기 위해 싱글톤은 mutex lock, unlock을 반복적으로 걸기 때문에 코드의 성능은 떨어지게 된다.
- 모든 곳에서 접근이 가능하므로
유니티 안에서의 싱글턴 패턴 객체
여러 오브젝트들이 데이터를 사용하는 오브젝트를 단 한개를 만들어 두고 이를 공유한다.
- 같은 Scene 안에서의 데이터 공유시에 사용됨
- 서로 다른 scene들간의 데이터 공유시에 사용됨
🔔 예제 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);
- ball에 프리팹
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); // 재생
}
}
정리
🔔 예제 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; } }
- ⭐싱글톤 객체가 될 것.
- 📜 PointManager.cs
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”씬의
- 📜 SceneTrans1.cs을
- 각각 다른 씬으로 전환해 주는 두 함수가 구현되어 있다.
- 📜 SceneTrans1.cs
“Scene 1-2” 구조
GameManager
오브젝트- 📜 PointManager.cs
Cube 1
오브젝트- 📜MyAddPoint.cs
Main Camera
오브젝트- 📜 SceneTrans1.cs
실행
- “Scene 1-1” 에서
Cube 1
의 myNum을 1 로 입력.Cube 2
의 myNum을 2 로 입력.Cube 1
,Cube 2
는 📜 PointManager 싱글톤 객체를 공유함.- 즉, myPoint 값 공유.
- 버튼 이벤트에 📜 SceneTrans1.cs의 SceneTrans1_1 함수 등록
- “Scene 1-2” 에서
Cube 3
의 myNum을 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
- 씬 1-1 : 1 3
- 1 3 6 7 9 출력
🔔 예제 2-2 현재 씬에 다른 씬을 덧대어 추가했을 때
- “Scene 2-1” 위에 “Scene 2-2” 씬을 덧대어 추가했을 때도 두 씬이 싱글톤 객체를 잘 공유하도록 구현 함.
- Scene 추가
- 기본적으로는 화면에는 한개의 Scene이 로드되고, 다른 Scene을 다시 로드하는 방식으로 Scene을 교체한다.
- 기존 Scene에 Scene을 추가할 수도 있는데, 이것을
Additive Loading
이라고 한다.- Additive Scene은 예를 들면 말풍선 모양으로 상상하는 씬(?) 같은 느낌이다.
- 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; } }
- ⭐싱글톤 객체가 될 것.
- 📜 PointManager.cs
Cube 1
오브젝트- 📜MyAddPoint.cs
Cube 2
오브젝트- 📜MyAddPoint.cs
Main Camera
오브젝트- 📜 SceneTrans2.cs
- 각각 다른 씬으로 전환해 주는 두 함수가 구현되어 있다.
- 현재 씬에 새로운 씬을 덧대어 추가해주는 함수
- SceneManager.LoadScene(“04-Scene2-2”, LoadSceneMode.Additive);
- “04-Scene2-2”씬을
LoadSceneMode.Additive
모드로 한 씬 내에서 추가적으로, 보조적으로 씬을 위에 로드한다.
- “04-Scene2-2”씬을
- SceneManager.LoadScene(“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;
- “04-Scene2-2”씬을 언로드한다.
- SceneManager.UnloadSceneAsync(“04-Scene2-2”);
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); } } ```
- 현재 씬에 새로운 씬을 덧대어 추가해주는 함수
- 각각 다른 씬으로 전환해 주는 두 함수가 구현되어 있다.
- 📜 SceneTrans2.cs
“Scene 2-2” 구조
- “Scene 2-1” 위에 덧대어 추가 될 씬이다.
Cube 2
오브젝트- 📜MyAddPoint.cs
GameManager
오브젝트는 없다.- "Scene 2-1"에 이미 있으므로 없어도 된다.
GameManager
는 한 씬에선 전체 하나만 있으면 됨.("Scene 2-2"는 덧대어지는 씬일 뿐 새롭게 로드되는건 아님)
- "Scene 2-1"에 이미 있으므로 없어도 된다.
- ✨
Main Camera
오브젝트는 꺼져있다.- "Scene 2-1"의 Main Camera를 공유하여 사용할거라서.
실행
예제 2-1 와 실행 원리 같다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기