Skip to content

第10回 音と演出

前回: 第09回 ゲーム状態 / 次回: 第11回 参照・ポインタ・寿命

今回の授業内容

  • DXライブラリで効果音を読み込んで再生する
  • SoundManagerで音声ハンドルを管理する
  • ショット、命中、ダメージ、クリア、ゲームオーバーに音を付ける
  • 敵撃破エフェクトを固定配列で管理する
  • 音と演出の有無で操作感が変わることを確認する

今回の開始状態

第09回終了時点では、タイトル、プレイ中、結果画面があります
今回の終了時点では、ショットや命中時に音が鳴り、敵撃破位置に短いエフェクトが出るようにします

変更するファイル

ファイル変更内容
SoundManager.h / SoundManager.cpp効果音の読み込み、再生、解放を追加する
Effect.h / Effect.cpp撃破エフェクトを追加する
Game.h / Game.cpp音管理とエフェクト配列を所有し、イベント時に呼ぶ
Assets/Sounds任意で効果音素材を配置する

SoundManagerを追加する

SoundManagerは、第08回のImageManagerと同じ形で作ります
効果音の種類を列挙型で定義します

cpp
enum class SoundId
{
    Shot,
    Hit,
    Damage,
    Clear,
    GameOver,
    Count
};

再生時は、読み込みに成功したハンドルだけを使います
DXライブラリの音声ハンドルは、読み込み失敗時に-1になります

cpp
void SoundManager::Play(SoundId id) const
{
    const int handle = handles[ToIndex(id)];
    if (handle != -1)
        PlaySoundMem(handle, DX_PLAYTYPE_BACK);
}

この確認は、第08回の画像と同様、音声素材がない状態でも授業を進めるために必要な確認です
DX_PLAYTYPE_BACKは、再生中でも次の処理へすぐ戻る指定です
効果音の再生でゲームの更新が止まらないようにするため、効果音は常にこの指定で再生します

音を鳴らす場所を決める

音は、処理結果が確定した場所で鳴らします
たとえばショット音は、実際に弾を生成できたときに鳴らします

cpp
if ((input.IsDown(KEY_INPUT_Z) || input.IsDown(KEY_INPUT_SPACE)) && player.CanShoot())
{
    SpawnBullet();
    player.OnShoot();
    sounds.Play(SoundId::Shot);
}

命中音とダメージ音も同じ考え方で、衝突判定が成立した場所に置きます

cpp
if (IsOverlapping(enemy.GetRect(), bullet.GetRect()))
{
    enemy.Deactivate();
    bullet.Deactivate();
    ++score;
    sounds.Play(SoundId::Hit);
}

if (enemy.IsActive() && IsOverlapping(enemy.GetRect(), player.GetRect()))
{
    enemy.Deactivate();
    player.Damage();
    sounds.Play(SoundId::Damage);
}

クリア音とゲームオーバー音は、状態が切り替わった瞬間に1回だけ鳴らしたい音です
第09回で用意したChangeState()は、まさに「状態が切り替わる瞬間」を表す場所なので、この関数の中で再生します

cpp
void Game::ChangeState(GameState nextState)
{
    if (state == nextState)
        return;

    state = nextState;

    if (state == GameState::GameClear)
        sounds.Play(SoundId::Clear);
    else if (state == GameState::GameOver)
        sounds.Play(SoundId::GameOver);
}

ChangeState()の先頭にあるif (state == nextState) return;は、同じ状態への遷移を無視する処理です
この確認があることで、ChangeState(GameState::GameClear)が誤って複数回呼ばれても、クリア音が二重に鳴ることはありません
状態遷移と、その瞬間に鳴らす音を1つの関数へまとめることで、呼び出し側(UpdatePlaying())は音のことを考えずに状態だけを切り替えられます

Effectを追加する

エフェクトも、弾や敵と同じように固定配列と有効フラグで管理します

cpp
class Effect
{
public:
    void Spawn(float startX, float startY);
    void Update();
    void Draw() const;
    bool IsActive() const;

private:
    Vec2 position = {};
    int timer = 0;
    bool active = false;
};

Game.hには、BulletEnemyと同じ形で上限数と配列を追加します

cpp
static constexpr int maxEffects = 32;
std::array<Effect, maxEffects> effects = {};

上限を32にしているのは、敵の上限(24体)と同程度か、それ以上の数の撃破が同じフレーム付近で重なっても表示が途切れないようにするためです
弾・敵と同じく、生成は空き要素を探すSpawnEffect()、更新と描画は「有効なものだけ処理する」という同じ形にそろえます

cpp
void Game::SpawnEffect(float x, float y)
{
    for (Effect& effect : effects)
    {
        if (!effect.IsActive())
        {
            effect.Spawn(x, y);
            return;
        }
    }
}

寿命が切れたら無効化します

cpp
void Effect::Update()
{
    if (!active) return;

    --timer;
    if (timer <= 0)
        active = false;
}

敵を倒した瞬間に、その敵の中心へエフェクトを出します

cpp
const Rect rect = enemy.GetRect();
SpawnEffect(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);

Update()では、他の配列と同じようにすべてのエフェクトを更新します

cpp
for (Effect& effect : effects)
    effect.Update();

第09回で追加したデバッグ表示にも、BulletsEnemiesと同じ形でEffectsの有効数を1行加えておきます

cpp
int activeEffects = 0;

for (const Effect& effect : effects)
{
    if (effect.IsActive()) ++activeEffects;
}

DrawFormatString(448, 456, white, L"Effects %d", activeEffects);

新しく配列を追加するたびに、デバッグ表示にも同じ形で1行足しておくと、後の回で不具合を調べるときに確認できる範囲が広がります

エフェクトは、プレイ中だけでなく結果画面でも表示され続けます
敵を倒した直後にゲームがクリアになった場合、その瞬間のエフェクトが表示の途中で消えてしまうと不自然に見えるためです
そのため、UpdateResult()でもエフェクトの更新だけは続けます

cpp
void Game::UpdateResult()
{
    for (Effect& effect : effects)
        effect.Update();

    if (input.IsPressed(KEY_INPUT_RETURN))
        StartGame();
}

弾と敵の更新はプレイ中にしか呼ばれないため、結果画面では止まったまま表示されます

動作確認

  1. 音声素材なしで実行してもゲームが止まらないことを確認する
  2. Assets/Sounds/Shot.wavを置き、ショット時に音が鳴ることを確認する
  3. 敵を倒した位置にエフェクトが出ることを確認する
  4. エフェクトが一定時間後に消えることを確認する
  5. 命中、ダメージ、クリア、ゲームオーバーの音を追加する
  6. ゲームクリアやゲームオーバーの直前に出たエフェクトが、結果画面でも最後まで再生されることを確認する

よくある問題

  • 音が鳴らない場合は、作業ディレクトリからAssets/Soundsが見えるか確認する
  • クリア音やゲームオーバー音が鳴らない、または鳴り続ける場合は、ChangeState()を経由せずにstateへ直接代入していないか確認する
  • エフェクトが残り続ける場合は、timerを減らしているか確認する
  • 結果画面でエフェクトが急に止まる場合は、UpdateResult()でエフェクトだけ更新しているか確認する

今回の完成条件

  • SoundManagerで効果音を管理できる
  • 必要なタイミングで効果音を鳴らせる
  • 敵撃破時のエフェクトを固定配列で管理できる
  • ChangeState()が状態遷移と効果音の再生をまとめて扱う理由を説明できる

まとめ

今回は、音とエフェクトでゲームの反応を強化しました
次回は、完成が近づいたコードを、所有、参照、寿命の観点で点検します