Skip to content

第08回 映像以外の構成要素 / BGM、効果音、エフェクト

前回: 第07回 C++振り返り / オブジェクト指向入門 / 次回: 第09回 ポインター入門 / メモリアドレスとオブジェクトの寿命

前回の振り返り

クラスの基本に触れました

  • 変数、関数、構造体、配列、enum の役割を振り返りました
  • class を使うと、値と処理を近い場所に置けます
  • メンバー変数とメンバー関数の考え方に触れました
  • Game クラスを作り、WinMain に集中していた処理を整理しました
  • 今回は、画面に表示するもの以外の要素を加えて、 ゲームの魅力を高める方法を学びます

今回の目的

音とエフェクトでよりゲームをリッチにする

  • BGMと効果音を再生する
  • 音を鳴らすタイミングをゲームの出来事に合わせる
  • 簡単なエフェクトを配列で管理する
  • 画面、音、エフェクトを組み合わせて手応えを作る

今回の授業内容

映像以外の構成要素 / BGM、効果音、エフェクト

  • DXライブラリで音声ファイルを読み込む
  • BGMと効果音の使い分けに触れる
  • 命中や発射のタイミングで音を鳴らす
  • 配列を使って小さなエフェクトを複数表示する
  • ゲームで起きたイベントを、映像以外の要素も使って伝える方法を学びます

音とエフェクトの実装

ゲームに欠くことの できないもの

絵と音が 揃ってこそゲーム

映像以外の構成要素

画面だけでは伝わりにくい、物足りない

  • 弾を撃った

  • 撃った弾が敵に当たった

  • アイテムを取得した

  • ダメージを受けた

  • 敵の攻撃で倒された

  • こうしたイベントに音やエフェクトを加えるとゲームはそれだけで楽しくなりますし、 プレイヤーに状況を伝えやすくなります

BGMと効果音

同じ音であっても異なる役割

  • BGMはバックグラウンドミュージックの略です ゲーム体験全体の雰囲気を作ります
  • 効果音は SE(サウンドエフェクト)とも呼ばれます 特定の出来事に合わせて鳴らす特別な音です 弾の発射音や命中音、敵の爆発音などが該当します
  • 多くの場合、BGMはループ再生で鳴らし続けることになります 一方で効果音は、必要なタイミングで1回だけ鳴らすことが多いです

音の管理の注意点

鳴らすタイミングと、止めるタイミング

  • BGMは、ゲーム開始やステージ開始のタイミングで鳴らし始めることが多いです ループ再生されるため、意図的に停止するまで鳴り続けてしまいます
  • 効果音は、発射や命中などのイベントが起きたタイミングで鳴らすのが一般的です 停止の管理は不要ですが、重ねて再生できる分「鳴らしすぎ」に注意が必要です
  • 音が重なると古い音の再生が中断されてぶつ切りになる場合があります かといってむやみに同時再生数を増やすと処理負荷が増えたり、 想定をはるかに超えた大音量になってしまう危険もあります

DXライブラリで音を扱う

音声ファイルを読み込んで、再生する

  • DXライブラリは、様々な形式の音声ファイルを読み込んで再生することができます 今回はメモリ上にデータを読み込んで管理する方法を扱います
  • 音声ファイルをメモリ上に読み込むとハンドルが返されます ハンドルはDXライブラリが発行する管理番号のようなものです 音を再生したり、停止したり、音量変更などの操作はハンドル経由で行います
  • 音声データの扱いも画像データと変わりません ハンドルも画像のものと同様に変数に保存して管理します 音声ファイルの配置やファイル名にも、画像と同じ注意が必要です

DX ライブラリが扱える音声ファイルの形式

以下の形式の音声ファイルを読み込むことができます

  • WAV: Windows でよく使用される音声ファイルです 非圧縮で運用される場合は品質が高いですが ファイルサイズも大きくなります
  • MP3: 圧縮された音声ファイルです 品質とファイルサイズのバランスが良いです
  • Ogg Vorbis: オープンな圧縮音声ファイルです MP3と同等の品質とファイルサイズを提供します
  • Opus: 比較的新しい圧縮音声ファイル形式です 特に低ビットレートでの品質が優れています

音源データを取得する

フリーの音源サイトからダウンロードする

  • 音楽や効果音は知識のある人であれば自作することも可能ですが 今回はフリーの音源サイトからダウンロードして使用します
  • 以下のサイトが有名です DOVA-SYNDROME: https://dova-s.jp/ 効果音ラボ: https://soundeffect-lab.info/ 魔王魂: https://maoudamashii.jokersounds.com/
  • 必要な音をリストアップして、ゲームの雰囲気に合うものを探しましょう 例えば、メインBGM、発射音、命中音、爆発音などが考えられます

音声ファイルを読み込む

読み込み関数 LoadSoundMem を使う

cpp
int bgmHandle = LoadSoundMem(L"Sounds/Bgm.ogg");
int shotSeHandle = LoadSoundMem(L"Sounds/Shot.wav");
  • DX ライブラリは様々な音声ファイルに対応しており、 簡単に再生することができます
  • LoadSoundMem は、音声ファイルをメモリ上に読み込んで 管理番号(ハンドル)を返します
  • ファイルパスが違うと読み込めません 画像と同じように配置を確認しましょう

BGMを再生する

再生関数 PlaySoundMem で音をループ再生する

cpp
PlaySoundMem(bgmHandle, DX_PLAYTYPE_LOOP);
  • LoadSoundMem で読み込んだハンドルを指定して音を再生します
  • DX_PLAYTYPE_LOOP を指定すると、音が終わっても繰り返し再生されます ゲーム開始などのタイミングで呼び出しましょう
  • ループ再生された音は自動で停止しません PlaySoundMem を呼ぶたびに追加で再生が始まるため、 必要以上に呼ばないよう注意してください

効果音を鳴らす

イベント発生の瞬間に再生する

cpp
// 弾を実際に発射できた場所で呼ぶ
PlaySoundMem(shotSeHandle, DX_PLAYTYPE_BACK);
  • DX_PLAYTYPE_BACK は、指定の音をバックグラウンドで再生します
  • 再生が終了すると自動で停止しますので 終了タイミングを気に掛ける必要はありません
  • 発射、命中、決定など、ゲーム内のイベントに合わせて 都度呼び出しましょう

補足:同じ効果音を重ねたい場合

LoadSoundMem で同時発生音数を指定する

cpp
int shotSeHandle = LoadSoundMem(L"Sounds/Shot.wav", 6); // 同時に6つまで鳴らせる指定
PlaySoundMem(shotSeHandle, DX_PLAYTYPE_BACK);
  • 同じ効果音を短い間隔で何度も鳴らしたい場合があります 例えば、弾を連続で撃つ音や、敵に複数回当たる音などです
  • 何も指定しない場合は、同じハンドルで同時に鳴らせる音の数は3つです 上限を超えると、古い音が止まることがあります
  • マニュアルには記載されていませんが、 LoadSoundMem の第2引数で同時に再生できる音の数を指定できます 参考: https://dxlib.xsrv.jp/cgi/patiobbs/patio.cgi?mode=view&no=3767

音量を調整する

ハンドル別で音量を変える

cpp
ChangeVolumeSoundMem(180, bgmHandle);
ChangeVolumeSoundMem(230, shotSeHandle);
  • 音量は 0 から 255 の範囲で指定します 再生中はいつでも変更可能ですが、基準音量は再生前に設定しておくのがおすすめです
  • ベースとなる音量は再生する音源次第で、都度調節が必要になります 音源そのままでは大きすぎたり小さすぎたりするかもしれません
  • BGMは少し小さめにすると、効果音が聞こえやすくなります ゲーム内容に合わせて調整しましょう

エフェクトとは

ゲームに彩りを与える特殊な効果

  • 弾が当たった場所に円を出す

  • 敵が消えるときに点を散らす

  • プレイヤーがダメージを受けたら点滅させる

  • クリアやゲームオーバーの文字を少し目立たせる

  • 今回は、命中時に短く表示される円のエフェクトを扱います

今回も class を使う

第07回からのつながり

  • 前回 class を使って、 値と処理を近い場所に置く メンバー関数の考え方に触れました
  • 今回もエフェクトを扱う Effect 型を class として定義します
  • 前回の Game クラスと同じ感覚で扱うために まずは public を指定して始めます
cpp
// エフェクト1つぶんの情報を持つクラス
class Effect
{
public:
    int x;
    int y;
    int radius;
    int timer;
    bool isActive;

    // ここにメンバー関数を追加していきます
};

Effect クラスの実装(1/3)

エフェクト1つぶんの情報と開始処理

cpp
class Effect
{
public:
    int x;
    int y;
    int radius;
    int timer;
    bool isActive;

    // エフェクトを開始する処理
    void Start(int startX, int startY)
    {
        x = startX;
        y = startY;
        radius = 6;
        timer = 20;
        isActive = true;
    }
};
  • timer で表示の残り時間を管理します
  • isActivefalse のものは、 更新や描画の対象から外します
  • 第05回の弾と同じように、 配列で複数管理していきます

Effect クラスの実装(2/3)

Update() を追加する

cpp
class Effect
{
public:
    int x;
    int y;
    int radius;
    int timer;
    bool isActive;

    // Start() は前のスライドと同じ
    void Start(int startX, int startY) { /* ... */ }

    // 毎フレーム状態を更新する処理
    void Update()
    {
        if (isActive == false)
            return;

        radius += 2;
        timer--;

        if (timer <= 0)
            isActive = false;
    }
};
  • 第07回の考え方を引き継いで、 メンバー関数をクラスに追加していきます
  • Update() は、毎フレーム呼ばれる更新処理です 半径を広げたり timer を減らしたりする処理をここへまとめます
  • エフェクトの再生が終わったら isActive = false にして停止します

Effect クラスの実装(3/3)

最後に Draw() を追加する

cpp
class Effect
{
public:
    int x;
    int y;
    int radius;
    int timer;
    bool isActive;

    // Start() と Update() は前のスライドと同じ
    void Start(int startX, int startY) { /* ... */ }
    void Update() { /* ... */ }

    // 現在の状態を描画する処理
    void Draw()
    {
        if (isActive == false)
            return;

        DrawCircle(x, y, radius, GetColor(255, 200, 80), FALSE);
    }
};
  • さらに描画機能を追加します
  • Draw() は、毎フレーム呼ばれる描画処理です 現在の状態をもとに、エフェクトを画面に 表示する処理をまとめます
  • 更新を行う Update() と描画を行う Draw() を 分けることで、ゲーム全体の更新と描画を 分けて管理できます

Game クラスへ追加するもの

音とエフェクトも Game で管理する

cpp
class Game
{
private:
    static const int effectMax = 10;

    Effect effects[effectMax] = {};

    int bgmHandle = -1;
    int shotSeHandle = -1;
    int hitSeHandle = -1;
};
  • 第07回と同じように、ゲーム中ずっと使う値は Game のメンバー変数にします
  • エフェクトは弾と同様に複数同時に表示される ため、配列で管理します
  • effectMax は、同時に表示できるエフェクトの 数の上限を示す定数です
  • 音声ハンドルも画像ハンドルと同じように メンバー変数として保存しておきます

音声を読み込む

Initialize で準備する

cpp
void Game::Initialize()
{
    // 音声を読み込む
    bgmHandle = LoadSoundMem(L"Sounds/Bgm.ogg");
    shotSeHandle = LoadSoundMem(L"Sounds/Shot.wav", 6);
    hitSeHandle = LoadSoundMem(L"Sounds/Hit.wav", 4);

    // 音量を調整する
    ChangeVolumeSoundMem(180, bgmHandle);
    ChangeVolumeSoundMem(230, shotSeHandle);
    ChangeVolumeSoundMem(230, hitSeHandle);

    // BGMをループ再生する
    PlaySoundMem(bgmHandle, DX_PLAYTYPE_LOOP);
}
  • 音声も画像と同じく、DXライブラリ初期化後に読み込みます
  • BGM は開始時にループ再生します 毎フレーム呼ぶと何度も再生されてしまいます
  • 効果音はイベント発生時に鳴らすため、 ここでは読み込みと音量調整だけ済ませます

エフェクトを発生させる

空いている場所を探して開始する

cpp
void Game::StartEffect(int x, int y)
{
    for (int i = 0; i < effectMax; i++)
    {
        if (effects[i].isActive == false)
        {
            effects[i].Start(x, y);
            break;
        }
    }
}
  • 命中した瞬間にこの関数を呼び、 短いエフェクトを発生させます
  • 空いている場所を探す流れは、 弾の発射処理と同じです
  • Start() の中に初期化処理をまとめて いるので、使う側は座標を渡すだけで済みます

エフェクトを更新して描画する

Game 側から全エフェクトを呼び出す

cpp
void Game::UpdateEffects()
{
    for (int i = 0; i < effectMax; i++)
        effects[i].Update();
}

void Game::DrawEffects()
{
    for (int i = 0; i < effectMax; i++)
        effects[i].Draw();
}
  • 1つずつの更新と描画は Effect に任せます
  • Game 側では配列を回して、 全エフェクトの Update()Draw() を 呼びます
  • 考え方は第07回の DrawBullets() と同じです

サンプルコード(1/3)

発射できた瞬間に音を鳴らす

cpp
void Game::ShootBullet()
{
    if (CheckHitKey(KEY_INPUT_Z) == false || shotTimer > 0)
        return;

    for (int i = 0; i < bulletMax; i++)
    {
        if (bullets[i].isActive == false)
        {
            bullets[i].x = player.x + 12;
            bullets[i].y = player.y;
            bullets[i].width = 8;
            bullets[i].height = 16;
            bullets[i].isActive = true;
            shotTimer = shotInterval;
            PlaySoundMem(shotSeHandle, DX_PLAYTYPE_BACK);
            break;
        }
    }
}
  • 第07回の弾発射処理に、発射音の再生を 追加します
  • キーを押しただけで弾が出なかった場合は、 音も鳴らさないようにします
  • ここでは、空いている弾を見つけた後で PlaySoundMem を呼ぶことで 弾を発射できた時だけ音を鳴らしています

サンプルコード(2/3)

命中時に音とエフェクトを出す

cpp
if (IsHit(bullets[i], enemy))
{
    int hitX = enemy.x + enemy.width / 2;
    int hitY = enemy.y + enemy.height / 2;

    PlaySoundMem(hitSeHandle, DX_PLAYTYPE_BACK);
    StartEffect(hitX, hitY);

    bullets[i].isActive = false;
    score++;
  • 弾が敵に当たった瞬間に、命中音を鳴らします
  • エフェクトは敵の中心に出すため、 enemy の座標と大きさから中心座標を 計算しています
  • 敵を再出現させると座標が変わってしまいます その前に座標を記録しているのがポイントです

サンプルコード(3/3)

クリア判定は第07回と同じ

cpp
    if (score >= clearScore)
    {
        enemy.isActive = false;
        gameState = GameClear;
    }
    else
    {
        // まだクリアでなければ敵を再出現させる
        enemy = RespawnEnemy(enemy);
    }
}
  • 音とエフェクトを足しても、 スコアやクリア判定の流れは第07回と同じです
  • 命中した後の処理を少しずつ追加していけば、 動作を保ったまま演出を増やせます
  • 今回は簡素な演出ですが、 ぜひアレンジを加えてみてください

Update と Draw への追加

毎フレーム呼び出す

cpp
void Game::Update()
{
    // switch の中で第07回と同じ更新処理を行う

    // エフェクトはクリア後も少し動かす
    UpdateEffects();
}

void Game::Draw()
{
    DrawBullets();
    DrawEffects();
}
  • UpdateEffects()switch の後で呼ぶと、 クリア後の短いエフェクトも動き続けます
  • DrawEffects() は弾の描画と同じように、 Draw() から呼び出します
  • ゲーム本体の流れは第07回のまま、 音とエフェクトだけを足しています

実習1: BGMを鳴らす

音声ファイルを読み込んで再生する

  1. BGM用の音声ファイルをプロジェクトへ置く
  2. GamebgmHandle を追加する
  3. Initialize()LoadSoundMem を呼ぶ
  4. ChangeVolumeSoundMem で音量を調整する
  5. PlaySoundMem でループ再生する
  • 音が鳴らない場合は、ファイル名と配置を確認します
  • BGM は毎フレームではなく、初期化時に1回だけ再生します

実習2: 効果音を鳴らす

発射と命中に反応を付ける

  1. GameshotSeHandlehitSeHandle を追加する
  2. Initialize() で発射音と命中音を読み込む
  3. ShootBullet() で弾を発射できたタイミングに発射音を鳴らす
  4. UpdateBullets() で弾が敵に当たったタイミングに命中音を鳴らす
  5. 音量を調整する
  • キー入力の瞬間ではなく、実際に弾が出た瞬間に鳴らすのがポイントです

実習3: 命中エフェクトを追加する

当たった場所に短いエフェクトを出す

  1. Effect クラスを定義する
  2. Gamestatic const int effectMaxEffect effects[effectMax] を追加する
  3. StartEffect() を作り、空いているエフェクトで Start() を呼ぶ
  4. UpdateEffects() から各エフェクトの Update() を呼ぶ
  5. DrawEffects() から各エフェクトの Draw() を呼ぶ
  6. 命中時に敵の中心座標を求め、StartEffect() に渡す
  7. エフェクトの大きさや色を調整する

よくあるつまずき

音やエフェクトが出ないときの確認ポイント

  • 音声ファイルのパスが正しいか
  • LoadSoundMem が成功しているか (読み込みに失敗した場合、ハンドルの値は -1 が返されます)
  • BGMを毎フレーム再生していないか
  • 効果音を鳴らすタイミングは正しいか
  • エフェクト発生のタイミングで EffectStart() が呼ばれているか

今回のまとめ

今日のポイント

  • BGMは場面全体、効果音は出来事への反応として使います
  • 音を鳴らすタイミングは、ゲーム内の出来事に合わせます
  • エフェクトは、class に処理を持たせつつ配列で管理できます
  • 次回はポインターとメモリアドレスに触れ、 オブジェクトの寿命を意識した扱い方を学びます

おつかれさまでした!

次回予告 第09回 ポインター入門

メモリとアドレスの 基礎を学ぼう