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