Appearance
第06回 サウンドとゲームイベント
前回: 第05回 キャラクターアニメーション / 次回: 第07回 CSVによる敵の複数配置
今回の授業内容
- BGMとSEの役割と再生方法の違いを理解する
SoundIdとSoundManagerで音声の読み込み、再生、停止、解放を管理する- ジャンプ、着地、落下、ゴールが成立した瞬間にSEを再生する
- ゲームクリアとゲームオーバーでBGMを停止し、リトライ時に再開する
- 音声ファイルの欠落と重複再生を確認する
今回の配布コード
第05回のアニメーション付きゲームへ、音響管理とゲームイベントによる再生を追加したコードです
敵とタイトル画面はまだ追加せず、既存のプレイヤー操作、落下、ゴールを使って音の鳴るタイミングを確認します
配布ファイル一覧
| ファイル | 内容 |
|---|---|
| Source/SoundManager.h | 音声IDと管理クラスの宣言 |
| Source/SoundManager.cpp | 読み込み、BGM切り替え、SE再生、解放 |
| Source/Game.h | SoundManagerの所有 |
| Source/Game.cpp | ゲーム中の出来事から音声を再生 |
| Source/ImageManager.h | 画像識別子と管理クラス |
| Source/ImageManager.cpp | 画像の一括読み込み、サイズ取得、解放 |
| Source/Stage.h | ステージの宣言 |
| Source/Stage.cpp | CSVマップとスクロール描画 |
| Assets/Sounds/PlayingBgm.wav | プレイ中BGM |
| Assets/Sounds/Jump.wav | ジャンプSE |
| Assets/Sounds/Land.wav | 着地SE |
| Assets/Sounds/GameOver.wav | 落下時のSE |
| Assets/Sounds/Goal.wav | ゴールSE |
BGMとSEの違い
BGMは場面の間ループ再生し、SEは短い出来事が発生した瞬間に1回再生します
| 種類 | 再生方法 | 今回の例 |
|---|---|---|
| BGM | DX_PLAYTYPE_LOOP | プレイ中 |
| SE | DX_PLAYTYPE_BACK | ジャンプ、着地、落下、ゴール |
BGMを毎フレーム再生すると、曲が先頭から繰り返し再生されて進みません
SEを条件成立中に毎フレーム再生すると、同じ音が大量に重なります
そのため、BGMはプレイ開始時、SEは出来事が成立した瞬間に再生します
SoundManagerで音声を管理する
SoundIdで音声を指定する
画像をImageIdで扱ったのと同様に、音声もSoundIdで指定します
SoundManager.hcpp
enum class SoundId
{
PlayingBgm,
Jump,
Land,
Damage,
Goal,
Count
};ゲーム側は音声ハンドルやファイルパスを直接扱いません
音声の追加や差し替えに必要な情報はSoundManagerへ集約します
定義表から音声を一括で読み込む
SoundManager.cppcpp
constexpr SoundDefinition definitions[] =
{
{ SoundId::PlayingBgm, L"Assets/Sounds/PlayingBgm.wav", 140 },
{ SoundId::Jump, L"Assets/Sounds/Jump.wav", 200 },
{ SoundId::Land, L"Assets/Sounds/Land.wav", 180 },
{ SoundId::GameOver, L"Assets/Sounds/GameOver.wav", 220 },
{ SoundId::Goal, L"Assets/Sounds/Goal.wav", 220 }
};LoadAll()は音声を読み込み、失敗した場合は対象パスを表示してfalseを返します
必要な音声が欠けたままゲームを開始しない点は、画像やCSVのエラー処理と同じです
BGMの重複再生を防ぐ
SoundManager.cppcpp
void SoundManager::PlayBgm(SoundId id)
{
if (currentBgm == id) return;
StopBgm();
PlaySoundMem(
handles[static_cast<std::size_t>(id)],
DX_PLAYTYPE_LOOP);
currentBgm = id;
}現在再生中のBGMと同じIDが指定された場合は何もしません
別のBGMへ切り替える場合にも、現在のBGMを停止してから再生します
GameへSoundManagerを追加する
初期化時に音声を読み込む
Game.hcpp
ImageManager images;
SoundManager sounds;
Stage stage;Game.cppcpp
bool Game::Initialize()
{
if (!images.LoadAll()) return false;
if (!sounds.LoadAll()) return false;
// 画像とステージの初期化
StartPlay();
return true;
}SoundManagerのデストラクタからRelease()を呼ぶため、ゲーム終了時には読み込んだ音声を解放します
プレイ開始時にBGMを再生する
ゲーム開始時とリトライ時はどちらもStartPlay()を通ります
Game.cppcpp
void Game::StartPlay()
{
ResetPlay();
gameState = Playing;
sounds.PlayBgm(SoundId::PlayingBgm);
}再生中のBGMと同じIDならPlayBgm()が何もしないため、重複再生されません
出来事が成立した瞬間にSEを再生する
ジャンプが成立した場所で再生する
キーが押されているだけでなく、接地中で実際にジャンプを開始した場所へ再生処理を置きます
Game.cppcpp
if (player.onGround && CheckHitKey(KEY_INPUT_Z))
{
player.vy = jumpPower;
player.onGround = false;
sounds.PlaySe(SoundId::Jump);
}これにより、空中でキーを押してもジャンプSEは鳴りません
接地状態の変化から着地を検出する
着地はonGroundがfalseからtrueへ変わった瞬間です
Game.cppcpp
const bool wasOnGround = player.onGround;
// 移動と接地判定
if (!wasOnGround && player.onGround)
{
sounds.PlaySe(SoundId::Land);
}現在値だけを見てplayer.onGroundがtrueの間ずっと再生すると、着地SEが毎フレーム鳴ります
更新前の値を保存し、更新後の値と比較します
結果が決まった瞬間にBGMを止める
Game.cppcpp
if (player.y > Stage::mapHeight * Stage::tileSize + 120)
{
gameState = GameOver;
sounds.StopBgm();
sounds.PlaySe(SoundId::GameOver);
return;
}
if (IsHitRect(PlayerRect(), stage.GetGoal()))
{
gameState = GameClear;
sounds.StopBgm();
sounds.PlaySe(SoundId::Goal);
}ゲームオーバーまたはクリアが成立した瞬間にBGMを停止し、対応するSEを1回再生します
リトライするとStartPlay()からBGMが再開されます
実習
実習1: SoundManagerを追加する
SoundManager.hとSoundManager.cppをプロジェクトへ追加するAssets/Soundsをプロジェクト直下へ配置するGameへSoundManagerを追加するInitialize()からLoadAll()を呼ぶ- 音声ファイル名を一時的に変更し、読み込みエラーへ対象パスが表示されることを確認する
実習2: BGMとSEを再生する
StartPlay()でプレイ中BGMを再生する- ジャンプ成立時にジャンプSEを再生する
- 接地状態の変化から着地SEを再生する
- 落下とゴール時にBGMを停止し、対応するSEを再生する
- リトライ後にBGMが再開されることを確認する
実習3: 音量を比較する
- 定義表の音量を1つだけ変更する
- 同じ操作で変更前後を聞き比べる
- BGMがSEを聞き取りにくくしていないか確認する
- 採用した値と理由を記録する
今回の完成条件
SoundManagerで音声を読み込み、終了時に解放できる- プレイ中BGMが重複再生されない
- ジャンプと着地のSEが1回の出来事につき1回だけ鳴る
- 落下とゴール時にBGMが停止する
- リトライ後にBGMが再開する
- 読み込み失敗時に対象ファイルを確認できる
よくある問題
BGMが先頭から進まない
UpdatePlaying()など、毎フレーム呼ばれる場所からBGMを再生していないか確認します
BGMはStartPlay()から再生します
着地SEが連続して鳴る
player.onGroundの現在値だけで再生していないか確認します
更新前のwasOnGroundと比較します
リトライ後にBGMが鳴らない
UpdateResult()から状態だけを変更せず、StartPlay()を呼んでいるか確認します
音声ファイルを読み込めない
Assets/Soundsの配置、Visual Studioの作業ディレクトリ、ファイル名の大文字小文字を確認します
まとめ
- BGMとSEの役割を分けました
- 音声の読み込み、再生、停止、解放を
SoundManagerへ集約しました - 入力中や状態中ではなく、出来事が成立した瞬間にSEを再生しました
- ゲームクリアとゲームオーバーでBGMを停止し、リトライ時に再開しました
- 次回は敵を追加し、敵との接触からゲームオーバーになる流れを実装します