この記事について
DXライブラリには音声の統括管理を行う機能はありません。そのため、一般的なゲームに搭載されているサウンドオプションを自力実装する必要があります。
典型的なデザインパターン「Singletonパターン」を用いて音声管理システムを作ってみましょう。
Singletonパターンとは?
詳細は他の記事を参照してもらうものとして、大まかに説明すると「実体が一つしかないことを保証する構造」となります。
例えばクラスをまたいで作動するクラスを作りたいとき、それぞれのクラスに参照を渡したりするのは非効率です。そんな場合にSingletonパターンを用いると、クラスをまたいで色々なところで呼び出すことができるようになります。
仕様
BGM、SE、ボイスの3つに分けてそれぞれの音声調整や一括停止ができるようにします。
SEどうしの重複再生はいったん不可能とします。
1.サウンド再生のためのクラスを作成
まずは1つのサウンドハンドルを作成、保持し再生、停止、音量調整を行うクラスを作成します(これ自体はSingletonである必要はありません)。
// VisualStudioの設定からC++言語水準を17以上に上げてください
#include <algorithm>
#include <string_view>
class SoundHandle {
private://変数
static constexpr int s_InvalidID = -1; // 無効時のハンドルに代入するID
int m_SoundHandle{ s_InvalidID }; // 保持しておくサウンドハンドル
public://コンストラクタ、デストラクタ
SoundHandle() noexcept {}
~SoundHandle(void) noexcept { Dispose(); }
public://削除するコンストラクタ
SoundHandle(const SoundHandle&) = delete;
SoundHandle& operator=(const SoundHandle&) = delete;
SoundHandle(SoundHandle&&) = delete;
SoundHandle& operator=(SoundHandle&&) = delete;
public:
//生ハンドルを取得
int GetHandle(void) const noexcept { return this->m_SoundHandle; }
//ハンドルが有効な状態かどうか指定
bool IsActive(void) const noexcept { return (this->m_SoundHandle != s_InvalidID); }
//再生中かどうかを指定
bool IsPlay(void) const noexcept { return (DxLib::CheckSoundMem(m_SoundHandle) == TRUE); }
//総再生時間を取得
LONGLONG GetTotalTIme(void) const noexcept { return DxLib::GetSoundTotalTime(m_SoundHandle); }
//ボリュームを0~255で取得
int GetVol(void) const noexcept { return DxLib::GetVolumeSoundMem2(m_SoundHandle); }
public:
//読み込み、複製
void Load(std::string_view FileName, int BufferNum = 3) noexcept {
Dispose(); //すでに持っているものがないかチェック
this->m_SoundHandle = DxLib::LoadSoundMemWithStrLen(FileName.data(), FileName.length(), BufferNum);
}
void Duplicate(const SoundHandle& o) noexcept {
Dispose(); //すでに持っているものがないかチェック
this->m_SoundHandle = DxLib::DuplicateSoundMem(o.m_SoundHandle);
}
//破棄
void Dispose(void) noexcept {
if (IsActive()) {
DeleteSoundMem(this->m_SoundHandle);
this->m_SoundHandle = s_InvalidID;
}
}
//再生、停止
void Play(int type = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) const noexcept {
if (IsActive()) {
DxLib::PlaySoundMem(m_SoundHandle, type, TopPositionFlag ? 1 : 0);
}
}
void Stop(void) const noexcept {
if (IsActive()) {
DxLib::StopSoundMem(m_SoundHandle);
}
}
//ボリューム指定
void UpdateVolume(int vol) noexcept {
if (IsActive()) {
DxLib::ChangeVolumeSoundMem(std::clamp<int>(vol, 0, 255), m_SoundHandle);
}
}
};
2.音声管理システムを作成
単体の音声を管理するシステムを作成します。複数の音声をIDで管理できるようにしています(これ自体もSingletonである必要はありません)。
#include<vector>
#include<memory>
class SoundPool {
private:
std::vector<std::shared_ptr<std::pair<int, std::shared_ptr<SoundHandle>>>> m_handle;
public://コンストラクタ、デストラクタ
SoundPool(void) noexcept {}
~SoundPool(void) noexcept { DisposeAll(); }
public:
SoundPool(const SoundPool&) = delete;
SoundPool(SoundPool&& o) = delete;
SoundPool& operator=(const SoundPool&) = delete;
SoundPool& operator=(SoundPool&& o) = delete;
private:
const std::shared_ptr<std::pair<int, std::shared_ptr<SoundHandle>>>& FindByID(int ID_t) noexcept {
for (auto& h : this->m_handle) {
if (h->first == ID_t) {
return h;
}
}
return nullptr;
}
public:
void Add(int ID_t, std::string path_t = "") noexcept {
if (path_t == "") { return; }//読み込めない
//既存のもので同じIDのものがあればそちらに再設定
auto& h = FindByID(ID_t);
if (h) {
//もとからあるものを破棄して
h->second->Dispose();
//再設定
h->first = ID_t;
h->second->Load(path_t);
return;
}
//新規に作成して
this->m_handle.emplace_back(std::make_shared<std::pair<int, std::shared_ptr<SoundHandle>>>());
this->m_handle.back()->second = std::make_shared<SoundHandle>();
//設定
this->m_handle.back()->first = ID_t;
this->m_handle.back()->second->Load(path_t);
}
void Dispose(int ID_t) noexcept {
for (auto& h : this->m_handle) {
if (h->first == ID_t) {
h->second->Dispose();
//削除
std::swap(h, this->m_handle.back());
this->m_handle.pop_back();
break;
}
}
}
void DisposeAll() noexcept {
this->m_handle.clear();
}
public:
//特定のサウンドを再生
void Play(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
auto& h = FindByID(ID_t);
if (h) {
h->second->Play(type_t, TopPositionFlag);
}
}
//特定のサウンドを停止
void Stop(int ID_t) noexcept {
auto& h = FindByID(ID_t);
if (h) {
h->second->Stop();
}
}
//すべてのサウンドを止める
void StopAll() noexcept {
for (auto& h : this->m_handle) {
h->second->Stop();
}
}
//ボリュームを反映
void SetVolAll(int vol) noexcept {
for (auto& h : this->m_handle) {
h->second->UpdateVolume(vol);
}
}
};
3.Singletonパターンを付与できる基底クラスを作成
こちらは以下リンク先のものをほぼコピーしたものとなります。
https://cflat-inc.hatenablog.com/entry/2014/03/04/214608
大事なのは
・コンストラクタ、デストラクタをprivateとする
・自分のstaticポインタを宣言
・Createで作成しInstanceでポインタを取得する
点です。
InstanceをCreateする前に呼んではいけない為、ポップアップを出して警告ののち終了させています。
継承クラスでは自分のstaticポインタの実体を定義する必要がありますのでそちらも注意!
template <class T>
class SingletonBase {
private:
static const T* m_Singleton;
public:
static void Create(void) noexcept {
m_Singleton = new T();
}
static T* Instance(void) noexcept {
if (m_Singleton == nullptr) {
MessageBox(NULL, "Failed Instance Create", "", MB_OK);
exit(-1);
}
//if (m_Singleton == nullptr) { m_Singleton = new T(); }
return (T*)m_Singleton;
}
protected:
SingletonBase(void) noexcept {}
virtual ~SingletonBase(void) noexcept {}
private:
SingletonBase(const SingletonBase&) = delete;
SingletonBase& operator=(const SingletonBase&) = delete;
SingletonBase(SingletonBase&&) = delete;
SingletonBase& operator=(SingletonBase&&) = delete;
};
//子のサンプル
/*
class A : public SingletonBase<A> {
private:
friend class SingletonBase<A>;
private:
}
//*/
4.SingletonBaseを用いて外部から呼び出せるクラスを作る
BGM、SE、ボイスの3つをもちつつ、SingletonBaseを継承したクラスを作ります。
class SoundSingleton : public SingletonBase<SoundSingleton> {
private:
friend class SingletonBase<SoundSingleton>;
private:
std::shared_ptr<SoundPool> m_BGM;
std::shared_ptr<SoundPool> m_SE;
std::shared_ptr<SoundPool> m_VOICE;
private://コンストラクタ、デストラクタ
SoundSingleton(void) noexcept {
m_BGM = std::make_shared<SoundPool>();
m_SE = std::make_shared<SoundPool>();
m_VOICE = std::make_shared<SoundPool>();
}
virtual ~SoundSingleton(void) noexcept {}
private:
SoundSingleton(const SoundSingleton&) = delete;
SoundSingleton(SoundSingleton&& o) = delete;
SoundSingleton& operator=(const SoundSingleton&) = delete;
SoundSingleton& operator=(SoundSingleton&& o) = delete;
public://BGM
void AddBGM(int ID_t, std::string path_t = "") noexcept { m_BGM->Add(ID_t, path_t); }
void DisposeBGM(int ID_t) noexcept { m_BGM->Dispose(ID_t); }
void DisposeBGMAll() noexcept { m_BGM->DisposeAll(); }
//特定のサウンドを再生
void PlayBGM(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
m_BGM->Play(ID_t, type_t, TopPositionFlag);
}
//特定のサウンドを停止
void StopBGM(int ID_t) noexcept { m_BGM->Stop(ID_t); }
//すべてのサウンドを止める
void StopBGMAll() noexcept { m_BGM->StopAll(); }
//ボリュームを反映
void SetVolBGMAll(int vol) noexcept { m_BGM->SetVolAll(vol); }
public://SE
void AddSE(int ID_t, std::string path_t = "") noexcept { m_SE->Add(ID_t, path_t); }
void DisposeSE(int ID_t) noexcept { m_SE->Dispose(ID_t); }
void DisposeSEAll() noexcept { m_SE->DisposeAll(); }
//特定のサウンドを再生
void PlaySE(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
m_SE->Play(ID_t, type_t, TopPositionFlag);
}
//特定のサウンドを停止
void StopSE(int ID_t) noexcept { m_SE->Stop(ID_t); }
//すべてのサウンドを止める
void StopSEAll() noexcept { m_SE->StopAll(); }
//ボリュームを反映
void SetVolSEAll(int vol) noexcept { m_SE->SetVolAll(vol); }
public://VOICE
void AddVOICE(int ID_t, std::string path_t = "") noexcept { m_VOICE->Add(ID_t, path_t); }
void DisposeVOICE(int ID_t) noexcept { m_VOICE->Dispose(ID_t); }
void DisposeVOICEAll() noexcept { m_VOICE->DisposeAll(); }
//特定のサウンドを再生
void PlayVOICE(int ID_t, int type_t = DX_PLAYTYPE_BACK, bool TopPositionFlag = true) noexcept {
m_VOICE->Play(ID_t, type_t, TopPositionFlag);
}
//特定のサウンドを停止
void StopVOICE(int ID_t) noexcept { m_VOICE->Stop(ID_t); }
//すべてのサウンドを止める
void StopVOICEAll() noexcept { m_VOICE->StopAll(); }
//ボリュームを反映
void SetVolVOICEAll(int vol) noexcept { m_VOICE->SetVolAll(vol); }
};
const SoundSingleton* SingletonBase<SoundSingleton>::m_Singleton = nullptr;
BGMは長時間のものが主となりますので、CreateSoundDataTypeなどで別途指定が必要な場合があります。
また、重複再生ができるようにするにはまた別途追加してください。
応用すればフェードアウト、フェードインとかも実現できそうかと思いますので試してみてください。
つかいかた
Addで事前に使うSEをすべてロードした後ボリュームの設定(サウンドオプションで変更した直後とかにも設定しましょう)を行い、任意のタイミングでPlay、使わなくなったらDisposeとなります。
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
ChangeWindowMode(TRUE);
if( DxLib_Init() == -1 ) // DXライブラリ初期化処理
{
return -1; // エラーが起きたら直ちに終了
}
SoundSingleton::Create();//
SoundSingleton::Instance()->AddSE(0, "test.wav");//IDが0のSEを事前にロード
SoundSingleton::Instance()->SetVolSEAll(128);//読み込んだSEをすべて音量半分にする
SoundSingleton::Instance()->PlaySE(0, DX_PLAYTYPE_BACK, true);//IDが0のSEを再生
WaitKey();//何かキーが押されるまで待機
SoundSingleton::Instance()->DisposeSE(0);//IDが0のSEだけ削除(メインシーンでだけ使うSEだけ削除したいとき)
//これまでロードしたすべてのSEを削除
SoundSingleton::Instance()->DisposeSEAll();
DxLib_End() ; // DXライブラリ使用の終了処理
return 0 ; // ソフトの終了(終了時SoundSingletonまわりはデストラクタが呼ばれない点に注意)
}
まとめ
Singletonパターンはおんせいいがいでもいろいろな部分で使える仕組みなので覚えておいて損はないかと思います。
これをきっかけに色々調べてみて下されれば幸いです(丸投げ)