3Dモデルで電車が走るようになり、見た目のリアリティはかなり向上しました。しかし、実際にプレイしてみると何かが足りない。そうです、「音」です。

モーターの唸り、レールのジョイント音、ブレーキの緩解音……。これらがあるだけで没入感が段違いになります。今回は、シミュレーターに音声を実装した際のアプローチと、そのために必要だった「車両の状態(State)管理」について書き残しておきます。

なぜState管理が必要なのか?

最初は単純に「速度が出たら走行音を鳴らす」だけで良いと思っていました。しかし、鉄道模型や実車の挙動を考えると、それだけでは不十分だということに気づきます。

  • 加速中(インバータ音が鳴る)
  • 惰性走行中(ジョイント音だけが響く)
  • 減速中(回生ブレーキの音がする)
  • 停車中(エアコンやコンプレッサーの音)

このように、「同じ速度」であっても、状況によって鳴らすべき音が異なります。
そこで、車両が今どういう状態にあるかを判定する簡易的なステートマシンを導入することにしました。

状態判定のロジック

今回は、現在の速度 (`currentSpeedKmh`) と、前フレームとの速度差 (`speedDiff`)、そしてマスコンの入力値 (`currentNotch`) を監視して状態を決定しています。

// 0.5秒ごとに状態をチェック
setInterval(() => {
    let newState = '停車';
    const speedDiff = currentSpeedKmh - prevSpeedKmh;

    if (Math.abs(currentSpeedKmh) < 0.1) {
        newState = '停車';
    } else if (speedDiff > 0.1) {
        newState = '加速';
    } else if (speedDiff < -0.1) {
        newState = '減速';
        if (currentNotch < 0) newState = 'ブレーキ'; // 機械ブレーキ使用時
    } else {
        newState = '速度維持'; // 惰性走行
    }

    if (newState !== state) {
        changeState(newState); // 状態が変わった時だけ音声を切り替える
    }
    prevSpeedKmh = currentSpeedKmh;
}, 500);

自然な音の切り替え(クロスフェード)

状態が変わった瞬間に音をバツンと切り替えると、非常に違和感があります。そこで、setInterval を使った簡易的なフェードイン・フェードアウト関数を用意しました。

Web Audio APIを使えばもっと高度なミキシングができますが、今回は手軽に実装するため、HTMLAudioElementの volume プロパティを操作する方法を採用しています。

// 音声のフェードアウト処理
function fadeOut(audio, duration = 1000) {
    const step = 0.05;
    const interval = duration * step;
    const fade = setInterval(() => {
        if (audio.volume > step) {
            audio.volume -= step;
        } else {
            audio.volume = 0;
            audio.pause();
            clearInterval(fade);
        }
    }, interval);
}

この関数を使って、「加速」状態に入ったら、それまで鳴っていた「停車音」や「減速音」をフェードアウトさせつつ、同時に「加速音」をフェードインさせます。これにより、複数の音が混ざり合いながら滑らかに推移する表現が可能になりました。

速度に合わせた再生位置の調整

個人的にこだわったのが、「すでにスピードが出ている状態で加速音を再生し始める場合」の処理です。

例えば、時速60km/hで惰性走行中に再加速する場合、インバータ音の「出だし(低音)」から再生されるとおかしいですよね。そこで、現在の速度に応じて音声ファイルの再生開始位置(currentTime)を動的に決定するようにしました。

case '加速':
    fadeOutAllExcept('start');
    // 速度(0~180km/h)の割合に応じて、音声ファイル(27秒)のどこから再生するか決定
    let startPos = Math.floor(26 * (currentSpeedKmh / 180));
    fadeIn(sounds.start, 500, startPos);
    break;

地味な処理ですが、これのおかげで運転操作に対する音の追従性がグッと上がり、気持ちの良い操作感につながりました。

まとめ

音の実装は、単なる演出以上の「情報のフィードバック」として機能します。音が変わることで、ユーザーは画面を見なくても「あ、今ブレーキがかかったな」「惰性走行に入ったな」と直感的に理解できるようになりました。
今後は、トンネル内での反響音や、すれ違い時のドップラー効果などにも挑戦してみたいですね。