[C++] 2.6 싱글톤 패턴 Singleton Pattern
카테고리: C++ games
인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀 
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기 
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!
Chapter 2. 객체 지향으로 가는 길 : 싱글톤 패턴
🔔 싱글톤 패턴이란?
- 어떤 클래스의 객체가 프로그램 전체에서 단 하나만 만들어지도록 하는 것.
    - 오직 한개의 인스턴스만을 갖도록 보장.
 
- 그래픽스 엔진이나 사운드 엔진에 종종 사용 된다.
📜SoundEngine.h 설명
#pragma once
#include "fmod.hpp"
#include <iostream>
#include <map>
#include <string>
namespace jm
{
	class SoundEngine
	{
	private:
		FMOD::System  *system = nullptr;
		//FMOD::Channel *channel = nullptr;
		std::map<std::string, FMOD::Sound*> sound_map;  
		std::map<FMOD::Sound*, FMOD::Channel*> channel_map;
																								
		FMOD_RESULT   result;
		unsigned int  version;
		void          *extradriverdata = nullptr;
	public:
		SoundEngine()     
		{
			using namespace std;
			result = FMOD::System_Create(&system);   // 시스템 생성
			if (result != FMOD_OK) {
				cout << "FMOD::System_Create() fail" << endl;
				exit(-1);
			}
			result = system->getVersion(&version);  // 시스템 버전 확인
			if (result != FMOD_OK) {
				cout << "getVersion() fail" << endl;
				exit(-1);
			}
			else printf("FMOD version %08x\n", version); 
			result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);  // 시스템 초기화
			if (result != FMOD_OK) {
				cout << "system->init() fail" << endl;
				exit(-1);
			}
		}
	public:
		~SoundEngine()  //  소멸자
		{
			system->release();  // 시스템 릴리즈 
		}
		void createSound(const std::string & filename, const std::string & sound_name, const bool & use_loop)
		{
			sound_map[sound_name] = nullptr; 
			auto & sound_ptr = sound_map[sound_name];  
			const int loop_flag = use_loop ? FMOD_LOOP_NORMAL : FMOD_LOOP_OFF;
			result = system->createSound(filename.c_str(), loop_flag, 0, &sound_ptr); 
			
			if (result != FMOD_OK) {
				std::cout << "system->createSound() fail" << std::endl;
				exit(-1);
			}
		}
	  
		void playSound(const std::string & sound_name)
		{
			if (sound_map.count(sound_name) <= 0) {
				std::cout << sound_name << " isn't initialized." << std::endl;
				exit(-1);
			}
			const auto & sound_ptr = sound_map[sound_name];
			auto & channel_ptr = channel_map[sound_ptr];  
 
			bool is_playing = false;  
			result = channel_ptr->isPlaying(&is_playing);
			if (is_playing) return; 
			result = system->playSound(sound_ptr, 0, false, &channel_ptr);  
			if (result != FMOD_OK) {
				std::cout << "system->playSound() fail" << std::endl;
				exit(-1);
			}
		}
	
		void stopSound(const std::string & sound_name)
		{
			if (sound_map.count(sound_name) <= 0) {
				std::cout << sound_name << " isn't initialized." << std::endl;
				exit(-1);
			}
			const auto & sound_ptr = sound_map[sound_name]; 
			auto & channel_ptr = channel_map[sound_ptr]; 
			bool is_playing = false; 
			result = channel_ptr->isPlaying(&is_playing); 
			if (is_playing == false) return; 
			result = channel_ptr->stop(); 
			if (result != FMOD_OK) {
				std::cout << "system->playSound() fail" << std::endl;
				exit(-1);
			}
		}
	};
}
교수님이 사운드 엔진에 대한 이해를 돕기 위해 만드신 것이기 때문에 간단함 게임 제작엔 괜찮지만 실무엔 비효율적인 코드임
- 멤버
    - FMOD::System  *system
        - 시스템
 
- map<std::string, FMOD::Sound> sound_map*
        - 사운드 이름과 사운드 포인터를 맵핑하여 담고 있는 변수
            - Key 👉 사운드 이름 (string)
- Value 👉 사운드 포인터 (FMOD::Sound *)
 
 
- 사운드 이름과 사운드 포인터를 맵핑하여 담고 있는 변수
            
- map<FMOD::Sound, FMOD::Channel> channel_map;
        - 사운드 포인터와 채널 포인터를 맵핑하여 담고 있는 변수
            - Key 👉 사운드 이름 (FMOD::Sound *)
- Value 👉 사운드 포인터 (FMOD::Channel *)
 
- 원래는 한 채널에 여러 사운드가 들어갈 수 있는데 1개의 사운드 당 1개의 채널에 맵핑되어 있는 이 부분 때문에 비효율적인 코드라고 말씀하신 것.
 
- 사운드 포인터와 채널 포인터를 맵핑하여 담고 있는 변수
            
 
- FMOD::System  *system
        
- 생성자
    - 시스템 생성
- 시스템 버전 확인
- 시스템 초기화
 
- 함수
    - createSound(filename, sound_name, use_loop)
        - 사운드를 생성한다.
- 매개변수
            - filename
                - string으로 실제 파일 이름 받기 ex) bomb.wav
 
- sound_name
                - 사운드 이름
- string으로 그 파일을 담고 있는 사운드가 어떤 이름인지 받기 (파일 이름 말고)
- 얘랑 사운드 포인터를 맵핑 시킬 것. map<std::string, FMOD::Sound> sound_map*
- ex) createSound(“drumloop.wav”, “background_music”, true); 라면 “drumloop.wav” 파일을 이제부터 “background_music”라는 사운드 이름으로 부르겠단 의미.
 
- use_loop
                - 이 사운드가 무한반복 될 것인가를 설정하는 플래그
- FMOD_LOOP_NORMAL, FMOD_LOOP_OFF 둘 중 하나다.
 
 
- filename
                
- 설명
            - sound_map[sound_name] = nullptr;
                - 사운드 이름을 Key값으로 넣으면 나오는 사운드 포인터 Value 값을 nullptr로 초기화
 
- auto & sound_ptr = sound_map[sound_name];
                - sound_ptr은 사운드 이름을 Key값으로 넣으면 나오는 사운드 포인터 Value 값을 참조한다.
- sound_ptr은 곧 해당 사운드 이름의 사운드 포인터가 된다.
 
- result = system->createSound(filename.c_str(), loop_flag, 0, &sound_ptr);
                - 시스템이 매개변수로 받은 사운드 파일로 사운드를 만들고 그 주소를 sound_ptr에 담는다.
- 이제 sound_ptr로 작업 가능
 
- 시스템이 매개변수로 받은 사운드 파일로 사운드를 만들고 그 주소를 
 
- sound_map[sound_name] = nullptr;
                
 
- playSound(sound_name)
        - 사운드를 재생한다.
            - const auto & sound_ptr = sound_map[sound_name];
                - sound_name을 sound_ptr 과 매핑
- map<std::string, FMOD::Sound> sound_map*
 
- auto & channel_ptr = channel_map[sound_ptr];
                - sound_ptr과 channel_pttr을 매핑
- map<FMOD::Sound, FMOD::Channel> channel_map;
 
- bool is_playing = false;
                - 이미 재생중이라면 추가적으로 재생할 필요가 없음
 
- result = channel_ptr->isPlaying(&is_playing);
                - 이미 재생 중이라면 is_playing 값을 true로 바꿔주는 함수.
 
- if (is_playing) return;
                - 이미 재생 중이라면 사운드를 재생하지 않고 종료
 
- result = system->playSound(sound_ptr, 0, false, &channel_ptr);
                - 이미 재생중이 아니라면 system의 playSound 통해 정상 재생 !
 
 
- const auto & sound_ptr = sound_map[sound_name];
                
 
- 사운드를 재생한다.
            
- stopSound(sound_name)
        - 사운드를 정지한다.
- 과정은 playSound(sound_name)와 비슷
            - 재생 중이 아니라면 정지할 필요가 없음
- 재생 중일 때만 정지
 
 
 
- createSound(filename, sound_name, use_loop)
        
🔔 싱글톤 유무 비교하기
사운드 엔진을 객체로 생성하여 사용한 경우 (싱글톤X)
SoundEngine sound_engine;
사운드 엔진을 사용하려고 할 때 SoundEngine을 객체 로 찍어내 사용한 경우.
- #include “SoundEngine.h”
- SoundEngine sound_engine;
    - ✨✨ 사운드 엔진을 객체를 생성함 ✨✨`
        - 사운드 엔진의 함수들을 사용하기 위해서! (사운드 재생 및 정지)
- 이 부분이 싱글톤과의 차이점이다.
 
 
- ✨✨ 사운드 엔진을 객체를 생성함 ✨✨`
        
- sound_engine.playSound(“background_music”);
    - “drumloop.wav”이 무한루프로 재생될 것.
 
사운드 엔진을 싱글톤으로 사용한 경우
싱글톤을 사용하는 이유
- 사운드 엔진은 여러가지 많은 클래스, 객체에서 많이 사용된다.
    - 사운드 기능을 쓰는 객체마다 계속 사운드 엔진 객체를 생성해내고 매개변수로 넘기고 하는 것 너무 낭비.
 
- 사운드 엔진은 객체가 여러개일 필요가 없다. 단 하나면 된다.
    - 사운드 엔진의 기능들만 가져다 쓰면 된다.
- 사운드 엔진의 map에 저장되어있는 사운드들 굳이 다 객체로 찍어낼 필요 없이 기존에 만들어진 사운드 엔진 객체를 공유하는게 더 효율적이다.
 
따라서 사운드 엔진이 1️⃣객체 생성 없이 사용될 수 있게끔 한다. 2️⃣시운드 엔진 객체 딱 하나를 여러군데서 공유할 수 있게 한다. 이와 같은 디자인 패턴을
싱글톤이라고 한다.
- ✨사운드 엔진 클래스에서 자기 자신(사운드 엔진 객체)을 static변수에 넣어두면 된다.
- ✨그리고 사운드 엔진 객체는 딱 1개만 생성될 수 있게 여러 장치를 통하여 제어한다.
    - 생성자를 private으로 하여 객체로 찍어내는 것을 막는다.
- static변수에 사운드 엔진 객체를 넣어줄 때 변수 값이 null일때만 넣는다.
 
static 멤버 변수 특징
- static 멤버 변수의 값을 모든 인스턴스들이 공유할 수 있다.
- 딱 한번만 생성된다.
- 인스턴스 생성 없이도 클래스 이름으로 접근하여 사용할 수 있다.
- 초기화는 반드시 클래스 밖에서 해준다.
    - 공유되는 변수이기 때문에 객체 생성시마다 변수가 중복 선언 되는것을 방지 하기 위해
- 초기화는 보통 cpp 파일에서 따로 초기화 1회 해주는 함수를 만들어서 해준다.
 
static 멤버 함수 특징
- 인스턴스 생성 없이도 클래스 이름으로 접근하여 사용 가능
- static 변수를 리턴할 수 있다.
📜SoundEngine_Singleton.h
- static SoundEngine_Singleton * instance
    - 이 instance변수에 사운드엔진 객체를 넣어둘 것.- 넣어둔 사운드엔진 객체는 공유된다.
 
- static이므로 외부에서 “SoundEngine_Singleton.instance” 이렇게 클래스 이름으로 접근이 가능하다.
- private이기 때문에 Getter함수로만 전달받을 수 있다.
 
- 이 
- static SoundEngine_Singleton * getInstance();
    - instance변수에 사운드엔진 객체를 넣어준다.- 딱 한번만 넣을 수 있도록 한다.
 
- instance의 Getter함수로서- instance을 리턴한다.- 즉 모두가 공유하는 단 하나의 사운드객체의 포인터를 리턴해주는 것이다.
 
- 외부에서 이 함수를 통해 instance을 전달받아야 하므로 public
 
- 생성자는 private이다
    - 외부에서 사운드엔진 객체 마구 찍어내지 못하도록
        - 사운드엔진 객체가 단 하나만 존재할 수 있도록 하는 장치1️⃣
 
 
- 외부에서 사운드엔진 객체 마구 찍어내지 못하도록
        
#pragma once
#include "fmod.hpp"
#include <iostream>
#include <map>
#include <string>
namespace jm
{
	class SoundEngine_Singleton
	{
	private:
		FMOD::System  *system = nullptr;
		std::map<std::string, FMOD::Sound*> sound_map;
		std::map<FMOD::Sound*, FMOD::Channel*> channel_map;
		
		FMOD_RESULT   result;
		unsigned int  version;
		void          *extradriverdata = nullptr;
		static SoundEngine_Singleton * instance;  
	public:
		static SoundEngine_Singleton * getInstance(); 
	private:
		SoundEngine_Singleton()     
		{
			using namespace std;
			result = FMOD::System_Create(&system);
			if (result != FMOD_OK) {
				cout << "FMOD::System_Create() fail" << endl;
				exit(-1);
			}
			result = system->getVersion(&version);
			if (result != FMOD_OK) {
				cout << "getVersion() fail" << endl;
				exit(-1);
			}
			else printf("FMOD version %08x\n", version);
			result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
			if (result != FMOD_OK) {
				cout << "system->init() fail" << endl;
				exit(-1);
			}
		}
	public:
		~SoundEngine_Singleton()
		{
			system->release();
		}
		void createSound(const std::string & filename, const std::string & sound_name, const bool & use_loop)
		{
			sound_map[sound_name] = nullptr;
			auto & sound_ptr = sound_map[sound_name];
			const int loop_flag = use_loop ? FMOD_LOOP_NORMAL : FMOD_LOOP_OFF;
			result = system->createSound(filename.c_str(), loop_flag, 0, &sound_ptr);
			
			if (result != FMOD_OK) {
				std::cout << "system->createSound() fail" << std::endl;
				exit(-1);
			}
		}
		void playSound(const std::string & sound_name)
		{
			if (sound_map.count(sound_name) <= 0) {
				std::cout << sound_name << " isn't initialized." << std::endl;
				exit(-1);
			}
			const auto & sound_ptr = sound_map[sound_name];
			auto & channel_ptr = channel_map[sound_ptr];
			bool is_playing = false;
			result = channel_ptr->isPlaying(&is_playing);
			if (is_playing) return; // don't play if this is already playing
			result = system->playSound(sound_ptr, 0, false, &channel_ptr);
			if (result != FMOD_OK) {
				std::cout << "system->playSound() fail" << std::endl;
				exit(-1);
			}
		}
		void stopSound(const std::string & sound_name)
		{
			if (sound_map.count(sound_name) <= 0) {
				std::cout << sound_name << " isn't initialized." << std::endl;
				exit(-1);
			}
			const auto & sound_ptr = sound_map[sound_name];
			auto & channel_ptr = channel_map[sound_ptr];
			bool is_playing = false;
			result = channel_ptr->isPlaying(&is_playing);
			if (is_playing == false) return; // don't stop playing if this is not playing
			result = channel_ptr->stop();
			if (result != FMOD_OK) {
				std::cout << "system->playSound() fail" << std::endl;
				exit(-1);
			}
		}
	};
}
📜SoundEngine_Singleton.cpp
- static SoundEngine_Singleton * getInstance(); 바디 구현
    - if (instance == nullptr)
        - 사운드엔진 객체가 단 하나만 존재할 수 있도록 하는 장치2️⃣
- 이 조건 덕분에 instance = new SoundEngine_Singleton(); 는 딱 한번만 실행된다.
 
- instance에 유일무이하며 모두가 공유할 수 있는 사운드엔진 객체를 넣어준다.
- 그리고 instance을 가리키는 포인터를 리턴해준다.
 
- if (instance == nullptr)
        
#include "SoundEngine_Singleton.h"
namespace jm
{
	SoundEngine_Singleton * SoundEngine_Singleton::instance = nullptr; 
	SoundEngine_Singleton * SoundEngine_Singleton::getInstance() 
	{
		if (instance == nullptr)  
		{
			instance = new SoundEngine_Singleton();  
		}
		return instance;
	}
}
📜TankExample.h
auto & sound_engine = *SoundEngine_Singleton::getInstance();  
- 사운드 엔진 객체를 생성할 필요가 없다. 그냥 메모리 공유하기 위해 가져오면 됨.
    - 싱글톤인 사운드엔진 객체의 주소를 받아와 간접참조(*) 한다.
- 사운드 엔진 객체.
- sound_engine이 이를 참조한다.
 
#pragma once
#include "Game2D.h"
#include "MyBullet.h"
#include "MyTank.h"
// #include "SoundEngine.h"
#include "SoundEngine_Singleton.h"
namespace jm
{
	class TankExample : public Game2D
	{
	public:
		MyTank tank;
		MyBullet *bullet = nullptr;
	
	public:
		TankExample()
			: Game2D("This is my digital canvas!", 1024, 768, false, 2)
		{
			auto & sound_engine = *SoundEngine_Singleton::getInstance();  
			sound_engine.createSound("drumloop.wav", "background_music", true);
			sound_engine.createSound("truck_idle_off_02.wav", "tank_move", true);
			sound_engine.createSound("cannon1.wav", "cannon", false);
			sound_engine.createSound("missile.mp3", "missile", false);
			sound_engine.playSound("background_music");
		}
		~TankExample()
		{
			if(bullet != nullptr) delete bullet;
		}
		void update() override
		{
			//auto & sound_engine = *SoundEngine_Singleton::getInstance();
			// move tank
			bool is_moving = false;
			if (isKeyPressed(GLFW_KEY_LEFT)) {
				tank.moveLeft(getTimeStep());
				is_moving = true;
			}
			if (isKeyPressed(GLFW_KEY_RIGHT)) {
				tank.moveRight(getTimeStep());
				is_moving = true;
			}
			if (isKeyPressed(GLFW_KEY_UP)) {
				tank.moveUp(getTimeStep());
				is_moving = true;
			}
			if (isKeyPressed(GLFW_KEY_DOWN)) {
				tank.moveDown(getTimeStep());
				is_moving = true;
			}
			if(is_moving)
				sound_engine.playSound("tank_move");
			else
				sound_engine.stopSound("tank_move");
			// shoot a cannon ball
			if (isKeyPressedAndReleased(GLFW_KEY_SPACE))
			{
				if (bullet != nullptr) delete bullet;
				bullet = new MyBullet;
				bullet->center = tank.center;
				bullet->center.x += 0.2f;
				bullet->center.y += 0.1f;
				bullet->velocity = vec2(2.0f, 0.0f);
				sound_engine.stopSound("cannon");
				sound_engine.playSound("cannon");				
			}
			if (bullet != nullptr) bullet->update(getTimeStep());
			// rendering
			tank.draw();
			if (bullet != nullptr) bullet->draw();
		}
	};
}
📜MyBullet.h
- 총알이 날아갈 때마다 사운드를 재생시키고 싶다면
    - 싱글톤을 안쓴다면
        - 사운드엔진 객체를 생성해주는 것은 컴퓨터 사운드 카드에게 너무 부담 되는 일
            - 총알은 엄청 생성되기 때문에 그때마다 게속 사운드엔진 객체가 또 생기고 또 생기고 헤애히니까.
 
- 그래서 사운드 엔진 객체를 생성하지 않고 MyBullet 객체를 생성하는 TankExample로부터 사운드 엔진 객체를 파라미터로 받아온다.
            - bullet = new myBullet(sound_engine) 이렇게.
- 여기저기 사운드 엔진 객체를 계속 파라미터로 전달하고 다녀야해서 불편.
 
 
- 사운드엔진 객체를 생성해주는 것은 컴퓨터 사운드 카드에게 너무 부담 되는 일
            
- 싱글톤을 쓴다면
        - 사운드 엔진 객체 생성 혹은 파라미터 전달필요 없이 그냥 간단하게 bullet = new myBullet; 만 해주어도 된다.
- 객체 생성 필요 없이 바로 *SoundEngine_Singleton::getInstance();로 받아오면 되기 때문에.
 
 
- 싱글톤을 안쓴다면
        
#pragma once
#include "Game2D.h"
#include "SoundEngine.h"
#include "SoundEngine_Singleton.h"
namespace jm
{
	class MyBullet
	{
	public:
		vec2 center = vec2(0.0f, 0.0f);
		vec2 velocity = vec2(0.0f, 0.0f);
			
		MyBullet()
		{
			SoundEngine_Singleton::getInstance()->playSound("missile"); 
		}
		~MyBullet()
		{
			SoundEngine_Singleton::getInstance()->stopSound("missile"); 
		}
		void draw()
		{
			beginTransformation();
			translate(center);
			drawFilledRegularConvexPolygon(Colors::yellow, 0.02f, 8);
			drawWiredRegularConvexPolygon(Colors::gray, 0.02f, 8);
			endTransformation();
		}
		void update(const float& dt)
		{
			center += velocity * dt;
		}
	};
}
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
 
      
    
댓글 남기기