Skip to content

第06回 サウンドとゲームイベント

前回: 第05回 キャラクターアニメーション / 次回: 第07回 CSVによる敵の複数配置

今回の授業内容

  • BGMとSEの役割と再生方法の違いを理解する
  • SoundIdSoundManagerで音声の読み込み、再生、停止、解放を管理する
  • ジャンプ、着地、落下、ゴールが成立した瞬間にSEを再生する
  • ゲームクリアとゲームオーバーでBGMを停止し、リトライ時に再開する
  • 音声ファイルの欠落と重複再生を確認する

今回の配布コード

第05回のアニメーション付きゲームへ、音響管理とゲームイベントによる再生を追加したコードです
敵とタイトル画面はまだ追加せず、既存のプレイヤー操作、落下、ゴールを使って音の鳴るタイミングを確認します

配布ファイル一覧
ファイル内容
Source/SoundManager.h音声IDと管理クラスの宣言
Source/SoundManager.cpp読み込み、BGM切り替え、SE再生、解放
Source/Game.hSoundManagerの所有
Source/Game.cppゲーム中の出来事から音声を再生
Source/ImageManager.h画像識別子と管理クラス
Source/ImageManager.cpp画像の一括読み込み、サイズ取得、解放
Source/Stage.hステージの宣言
Source/Stage.cppCSVマップとスクロール描画
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回再生します

種類再生方法今回の例
BGMDX_PLAYTYPE_LOOPプレイ中
SEDX_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は鳴りません

接地状態の変化から着地を検出する

着地はonGroundfalseからtrueへ変わった瞬間です

Game.cppcpp
const bool wasOnGround = player.onGround;

// 移動と接地判定

if (!wasOnGround && player.onGround)
{
    sounds.PlaySe(SoundId::Land);
}

現在値だけを見てplayer.onGroundtrueの間ずっと再生すると、着地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を追加する

  1. SoundManager.hSoundManager.cppをプロジェクトへ追加する
  2. Assets/Soundsをプロジェクト直下へ配置する
  3. GameSoundManagerを追加する
  4. Initialize()からLoadAll()を呼ぶ
  5. 音声ファイル名を一時的に変更し、読み込みエラーへ対象パスが表示されることを確認する

実習2: BGMとSEを再生する

  1. StartPlay()でプレイ中BGMを再生する
  2. ジャンプ成立時にジャンプSEを再生する
  3. 接地状態の変化から着地SEを再生する
  4. 落下とゴール時にBGMを停止し、対応するSEを再生する
  5. リトライ後にBGMが再開されることを確認する

実習3: 音量を比較する

  1. 定義表の音量を1つだけ変更する
  2. 同じ操作で変更前後を聞き比べる
  3. BGMがSEを聞き取りにくくしていないか確認する
  4. 採用した値と理由を記録する

今回の完成条件

  • 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を停止し、リトライ時に再開しました
  • 次回は敵を追加し、敵との接触からゲームオーバーになる流れを実装します