第09回 音声の管理と再生
1 / 4 音声管理クラスの設計と所有権
このステップの目的
音声リソース(BGM・SE)の読み込み、所有、解放を一括管理するSoundManagerクラスの構造を理解します。
音声IDの定義と対応表
画像と同様に、ゲーム内のコードでは音声ファイル名を直接扱わず、列挙型SoundManager::Idを使用します。
SoundManager.hcpp
#pragma once
#include <array>
#include <cstddef>
#include <string>
namespace dxgame::shooting
{
// 音声の読み込み、ハンドルの所有、再生、停止、解放を管理する
class SoundManager
{
public:
// ゲーム内で使うBGMとSEを種類ごとに識別する
enum class Id
{
TitleBgm,
PlayingBgm,
Shot,
Hit,
Damage,
Clear,
GameOver,
Count
};
// 全ハンドルを未取得の状態で初期化する
SoundManager();
// 再生中のBGMを止め取得済みの音声ハンドルをすべて解放する
~SoundManager();
// ハンドルを二重に解放しないようコピー構築を禁止する
SoundManager(const SoundManager&) = delete;
// ハンドルを二重に解放しないようコピー代入を禁止する
SoundManager& operator=(const SoundManager&) = delete;
// 音声の読み込み結果と、失敗した場合の相対パスを表す
struct LoadResult
{
bool succeeded = true;
std::wstring failedPath;
};
// 対応表にある音声をすべて読み込んで種類に応じた音量を設定する
// 1つでも読み込めない場合は取得済み音声を解放して失敗を返す
LoadResult LoadAll();
// 現在のBGMを止めidのBGMをループ再生する
void PlayBgm(Id id);
// BGMが再生中なら停止して未再生状態へ戻す
void StopBgm();
// idのSEを最初から1回再生する
void PlaySe(Id id) const;
private:
// BGMを停止し取得済みの全音声ハンドルを解放する
void Release();
std::array<int, static_cast<std::size_t>(Id::Count)> handles = {};
int currentBgm = -1;
};
}音声IDとファイルパスの対応は、SoundManager.cppのSoundTableでまとめて管理します。
SoundManager.cppcpp
using SoundId = SoundManager::Id;
struct SoundEntry
{
SoundId id;
const wchar_t* path;
};
constexpr std::array<SoundEntry, static_cast<std::size_t>(SoundId::Count)> SoundTable =
{
SoundEntry { SoundId::TitleBgm, L"Assets/Sounds/TitleBgm.wav" },
SoundEntry { SoundId::PlayingBgm, L"Assets/Sounds/PlayingBgm.wav" },
SoundEntry { SoundId::Shot, L"Assets/Sounds/Shot.wav" },
SoundEntry { SoundId::Hit, L"Assets/Sounds/Hit.wav" },
SoundEntry { SoundId::Damage, L"Assets/Sounds/Damage.wav" },
SoundEntry { SoundId::Clear, L"Assets/Sounds/Clear.wav" },
SoundEntry { SoundId::GameOver, L"Assets/Sounds/GameOver.wav" }
};音声ハンドルの所有とRAIIによる自動解放
DXライブラリのLoadSoundMem()が返すサウンドハンドル(整数値)は、DirectSoundやOSのオーディオバッファを指す管理番号です。
SoundManagerはハンドルを配列handlesに持ち、インスタンスが破棄されるときにデストラクタでまとめて解放します。
SoundManager.cppcpp
SoundManager::SoundManager()
{
handles.fill(-1);
}
SoundManager::~SoundManager()
{
for (int handle : handles)
{
if (handle != -1)
DeleteSoundMem(handle);
}
}第08回の画像管理と同じRAIIを使うため、途中で予期せず終了しても、メモリやオーディオデバイスの解放漏れを防げます。二重解放を防ぐため、コピーコンストラクタとコピー代入演算子も= deleteで禁止しています。
チェックリスト
- 音声の種類が
SoundManager::Idとして定義されている - 音声ファイルのパスが
SoundTableに一元管理されている - DXライブラリの
LoadSoundMem()とDeleteSoundMem()がSoundManager内にのみ隠蔽されている - RAIIパターンにより、ゲーム終了時にすべてのサウンドハンドルが自動解放されることを説明できる