[C++] 2.6 싱글톤 패턴 Singleton Pattern

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!


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개의 채널에 맵핑되어 있는 이 부분 때문에 비효율적인 코드라고 말씀하신 것.
  • 생성자
    • 시스템 생성
    • 시스템 버전 확인
    • 시스템 초기화
  • 함수
    • 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 둘 중 하나다.
      • 설명
        • 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로 작업 가능
    • 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 통해 정상 재생 !
    • stopSound(sound_name)
      • 사운드를 정지한다.
      • 과정은 playSound(sound_name)와 비슷
        • 재생 중이 아니라면 정지할 필요가 없음
        • 재생 중일 때만 정지


🔔 싱글톤 유무 비교하기

사운드 엔진을 객체로 생성하여 사용한 경우 (싱글톤X)

SoundEngine sound_engine;

사운드 엔진을 사용하려고 할 때 SoundEngine을 객체 로 찍어내 사용한 경우.

  • #include “SoundEngine.h”
  • SoundEngine sound_engine;
    • ✨✨ 사운드 엔진을 객체를 생성함 ✨✨`
      • 사운드 엔진의 함수들을 사용하기 위해서! (사운드 재생 및 정지)
      • 이 부분이 싱글톤과의 차이점이다.
  • sound_engine.playSound(“background_music”);
    • “drumloop.wav”이 무한루프로 재생될 것.

사운드 엔진을 싱글톤으로 사용한 경우

싱글톤을 사용하는 이유

  1. 사운드 엔진은 여러가지 많은 클래스, 객체에서 많이 사용된다.
    • 사운드 기능을 쓰는 객체마다 계속 사운드 엔진 객체를 생성해내고 매개변수로 넘기고 하는 것 너무 낭비.
  2. 사운드 엔진은 객체가 여러개일 필요가 없다. 단 하나면 된다.
    • 사운드 엔진의 기능들만 가져다 쓰면 된다.
    • 사운드 엔진의 map 에 저장되어있는 사운드들 굳이 다 객체로 찍어낼 필요 없이 기존에 만들어진 사운드 엔진 객체를 공유하는게 더 효율적이다.

따라서 사운드 엔진이 1️⃣객체 생성 없이 사용될 수 있게끔 한다. 2️⃣시운드 엔진 객체 딱 하나를 여러군데서 공유할 수 있게 한다. 이와 같은 디자인 패턴을 싱글톤이라고 한다.

  1. ✨사운드 엔진 클래스에서 자기 자신(사운드 엔진 객체)static 변수에 넣어두면 된다.
  2. ✨그리고 사운드 엔진 객체는 딱 1개만 생성될 수 있게 여러 장치를 통하여 제어한다.
    • 생성자를 private으로 하여 객체로 찍어내는 것을 막는다.
    • static변수에 사운드 엔진 객체를 넣어줄 때 변수 값이 null일때만 넣는다.

static 멤버 변수 특징

  1. static 멤버 변수의 값을 모든 인스턴스들이 공유할 수 있다.
  2. 딱 한번만 생성된다.
  3. 인스턴스 생성 없이도 클래스 이름으로 접근하여 사용할 수 있다.
  4. 초기화는 반드시 클래스 밖에서 해준다.
    • 공유되는 변수이기 때문에 객체 생성시마다 변수가 중복 선언 되는것을 방지 하기 위해
    • 초기화는 보통 cpp 파일에서 따로 초기화 1회 해주는 함수를 만들어서 해준다.

static 멤버 함수 특징

  1. 인스턴스 생성 없이도 클래스 이름으로 접근하여 사용 가능
  2. 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을 가리키는 포인터를 리턴해준다.
#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;
		}
	};

}


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

맨 위로 이동하기

C++ games 카테고리 내 다른 글 보러가기

댓글 남기기