【Cocos Creator】アタッチするだけ!EngineSound (エンジン音)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】EngineSound の実装:アタッチするだけで「速度に応じてエンジン音のピッチが変化する」汎用スクリプト

このガイドでは、車の速度(Velocity)に応じてエンジン音のピッチ(音程)を自動で上げ下げする汎用コンポーネント EngineSound を実装します。

物理挙動付きの車ノード(例:RigidBody2DRigidBody)にこのスクリプトをアタッチするだけで、移動速度に応じて AudioSource の pitch がリアルタイムに変化するようになります。外部の GameManager やシングルトンは一切不要で、インスペクタでの設定だけで完結する設計です。


コンポーネントの設計方針

1. 機能要件の整理

  • ノードの「速度(ベロシティ)」に応じて、エンジン音のピッチを変化させる。
  • 速度 0 のときは低いピッチ、最高速度付近では高いピッチになるように、線形補間(Lerp)で変化させる。
  • 最低ピッチ・最高ピッチ・想定最高速度などは、すべてインスペクタから調整可能にする。
  • 音の急激な変化を防ぐため、ピッチ変化にスムージング(追従速度)を設ける。
  • ループするエンジン音を自動再生するかどうかを選べる。

2. 外部依存をなくすための設計

完全な独立性を保つため、以下の方針で設計します。

  • 速度の取得は、以下の2パターンのどちらかを選べるようにする:
    • 2D物理RigidBody2DlinearVelocity を使用
    • 3D物理RigidBodylinearVelocity を使用
  • どちらの物理コンポーネントも無い場合は、自動でノードの位置差分から速度を推定する「Transform ベース」のモードを用意する。
  • 必要な標準コンポーネントはすべて getComponent で取得し、存在しなければ error ログを出力して防御的に動作。
  • エンジン音再生には AudioSource を使用し、これもインスペクタで指定&自動取得を試みる。

3. インスペクタで設定可能なプロパティ設計

以下のようなプロパティを用意します。

  • audioSource : AudioSource
    • エンジン音を再生する AudioSource。
    • 未指定の場合は、同じノードに付いている AudioSource を自動取得する。
  • autoPlayOnStart : boolean
    • true のとき、start() で AudioSource を自動再生する。
    • エンジン音を常時ループさせる用途を想定。
  • velocitySource : Enum
    • 速度の取得方法を選択する。
    • RigidBody2D / RigidBody3D / Transform の3種類。
  • rigidBody2D : RigidBody2D
    • 2D物理を使う場合に参照する Rigidbody2D。
    • 未指定なら、同じノードから自動取得を試みる。
  • rigidBody3D : RigidBody
    • 3D物理を使う場合に参照する Rigidbody。
    • 未指定なら、同じノードから自動取得を試みる。
  • maxSpeed : number
    • 想定する「最高速度」。
    • この値以上の速度では、ピッチは maxPitch にクランプされる。
    • 例:50100 など。
  • minPitch : number
    • 停止時(速度 0)のピッチ。
    • 通常は 0.81.0 程度。
  • maxPitch : number
    • 最高速度時のピッチ。
    • 通常は 1.52.0 程度。
  • pitchLerpSpeed : number
    • 現在のピッチを目標ピッチへ補間する速度。
    • 大きいほど追従が速く、小さいほどなめらかに変化。
    • 例:515 程度。
  • minSpeedThreshold : number
    • これ未満の速度は「ほぼ停止」とみなして 0 として扱う。
    • 小刻みなノイズでピッチが揺れないようにするためのしきい値。
  • debugLog : boolean
    • true のとき、現在速度とピッチを log 出力する。
    • 調整時のデバッグ用途。

TypeScriptコードの実装


import { _decorator, Component, Node, AudioSource, RigidBody2D, RigidBody, Vec2, Vec3, math, Enum, error, log } from 'cc';
const { ccclass, property } = _decorator;

/**
 * 速度の取得元を選択するための列挙型
 */
enum VelocitySourceType {
    RigidBody2D = 0,
    RigidBody3D = 1,
    Transform   = 2,
}
Enum(VelocitySourceType); // インスペクタで Enum として表示させる

@ccclass('EngineSound')
export class EngineSound extends Component {

    @property({
        type: AudioSource,
        tooltip: 'エンジン音を再生する AudioSource。\n未指定の場合、このノードから自動取得を試みます。',
    })
    public audioSource: AudioSource | null = null;

    @property({
        tooltip: '開始時にエンジン音を自動再生するかどうか。\n通常は ON (true) 推奨です。',
    })
    public autoPlayOnStart: boolean = true;

    @property({
        type: VelocitySourceType,
        tooltip: '速度の取得方法を選択します。\nRigidBody2D: 2D物理の linearVelocity\nRigidBody3D: 3D物理の linearVelocity\nTransform: 前フレームとの位置差分から速度を算出',
    })
    public velocitySource: VelocitySourceType = VelocitySourceType.RigidBody2D;

    @property({
        type: RigidBody2D,
        tooltip: '速度取得に使用する RigidBody2D。\n未指定の場合、このノードから自動取得を試みます。',
    })
    public rigidBody2D: RigidBody2D | null = null;

    @property({
        type: RigidBody,
        tooltip: '速度取得に使用する RigidBody (3D)。\n未指定の場合、このノードから自動取得を試みます。',
    })
    public rigidBody3D: RigidBody | null = null;

    @property({
        tooltip: '想定する最高速度。\nこの値以上の速度ではピッチは maxPitch にクランプされます。',
    })
    public maxSpeed: number = 50;

    @property({
        tooltip: '停止時 (速度0) のピッチ値。',
    })
    public minPitch: number = 0.9;

    @property({
        tooltip: '最高速度時のピッチ値。',
    })
    public maxPitch: number = 1.8;

    @property({
        tooltip: 'ピッチを目標値へ補間する速度。\n値が大きいほど目標ピッチに素早く追従します。',
    })
    public pitchLerpSpeed: number = 8;

    @property({
        tooltip: 'この速度未満は 0 とみなすしきい値。\n微小な速度ノイズでピッチが揺れないようにします。',
    })
    public minSpeedThreshold: number = 0.05;

    @property({
        tooltip: 'デバッグ用に、現在速度とピッチをコンソールに出力します。',
    })
    public debugLog: boolean = false;

    // 内部状態
    private _currentPitch: number = 1.0;
    private _lastPosition2D: Vec2 | null = null;
    private _lastPosition3D: Vec3 | null = null;

    onLoad() {
        // AudioSource 自動取得
        if (!this.audioSource) {
            this.audioSource = this.getComponent(AudioSource);
            if (!this.audioSource) {
                error('[EngineSound] AudioSource が見つかりません。このコンポーネントを使用するノードに AudioSource を追加してください。');
            }
        }

        // Rigidbody 自動取得
        if (this.velocitySource === VelocitySourceType.RigidBody2D) {
            if (!this.rigidBody2D) {
                this.rigidBody2D = this.getComponent(RigidBody2D);
                if (!this.rigidBody2D) {
                    error('[EngineSound] RigidBody2D が見つかりません。Velocity Source を Transform に変更するか、このノードに RigidBody2D を追加してください。');
                }
            }
        } else if (this.velocitySource === VelocitySourceType.RigidBody3D) {
            if (!this.rigidBody3D) {
                this.rigidBody3D = this.getComponent(RigidBody);
                if (!this.rigidBody3D) {
                    error('[EngineSound] RigidBody (3D) が見つかりません。Velocity Source を Transform に変更するか、このノードに RigidBody を追加してください。');
                }
            }
        }

        // 初期ピッチを minPitch に合わせる
        this._currentPitch = this.minPitch;
        if (this.audioSource) {
            this.audioSource.pitch = this._currentPitch;
        }

        // Transform モード用に初期位置を記録
        if (this.velocitySource === VelocitySourceType.Transform) {
            this._cacheCurrentPosition();
        }
    }

    start() {
        // 自動再生設定
        if (this.audioSource && this.autoPlayOnStart) {
            if (!this.audioSource.playing) {
                this.audioSource.play();
            }
        }
    }

    update(deltaTime: number) {
        if (!this.audioSource) {
            // AudioSource がない場合は何もしない
            return;
        }

        // 現在速度を取得
        const speed = this._getCurrentSpeed(deltaTime);

        // 速度から目標ピッチを計算
        const targetPitch = this._calculateTargetPitch(speed);

        // ピッチを補間してなめらかに変更
        this._currentPitch = this._lerp(this._currentPitch, targetPitch, math.clamp01(this.pitchLerpSpeed * deltaTime));
        this.audioSource.pitch = this._currentPitch;

        if (this.debugLog) {
            log(`[EngineSound] speed=${speed.toFixed(2)}, targetPitch=${targetPitch.toFixed(2)}, currentPitch=${this._currentPitch.toFixed(2)}`);
        }
    }

    /**
     * 現在の速度(スカラー)を取得する。
     */
    private _getCurrentSpeed(deltaTime: number): number {
        let speed = 0;

        if (this.velocitySource === VelocitySourceType.RigidBody2D && this.rigidBody2D) {
            const v = this.rigidBody2D.linearVelocity;
            speed = v.length();
        } else if (this.velocitySource === VelocitySourceType.RigidBody3D && this.rigidBody3D) {
            const v = this.rigidBody3D.linearVelocity;
            speed = v.length();
        } else if (this.velocitySource === VelocitySourceType.Transform) {
            // 前フレームとの位置差分から速度を算出
            speed = this._calculateSpeedFromTransform(deltaTime);
        }

        // ノイズをカット
        if (speed < this.minSpeedThreshold) {
            speed = 0;
        }

        return speed;
    }

    /**
     * Transform ベースで速度を算出する。
     * deltaTime 秒間の移動距離 / deltaTime = 速度
     */
    private _calculateSpeedFromTransform(deltaTime: number): number {
        if (deltaTime <= 0) {
            return 0;
        }

        const node = this.node;

        // 2D/3D 共通で position を Vec3 として扱う
        const currentPos3D = node.worldPosition.clone();

        if (!this._lastPosition3D) {
            this._lastPosition3D = currentPos3D.clone();
            return 0;
        }

        const distance = Vec3.distance(this._lastPosition3D, currentPos3D);
        const speed = distance / deltaTime;

        // 現在位置を次フレーム用に保存
        this._lastPosition3D.set(currentPos3D);

        return speed;
    }

    /**
     * Transform モード用に現在位置をキャッシュする。
     */
    private _cacheCurrentPosition() {
        const node = this.node;
        const pos3D = node.worldPosition.clone();
        this._lastPosition3D = pos3D;
    }

    /**
     * 速度から目標ピッチを計算する。
     */
    private _calculateTargetPitch(speed: number): number {
        if (this.maxSpeed <= 0) {
            return this.minPitch;
        }

        // 0〜1 に正規化してクランプ
        const t = math.clamp01(speed / this.maxSpeed);

        // 線形補間でピッチを計算
        const pitch = this.minPitch + (this.maxPitch - this.minPitch) * t;

        return pitch;
    }

    /**
     * 線形補間 (a から b へ t の割合で近づける)
     */
    private _lerp(a: number, b: number, t: number): number {
        return a + (b - a) * t;
    }
}

コードの主要ポイント解説

  • onLoad()
    • AudioSource / RigidBody2D / RigidBodygetComponent で自動取得。
    • 見つからない場合は error ログを出して、エディタ側での設定不足を気づけるようにしています。
    • 初期ピッチを minPitch に設定し、Transform モード用の初期位置をキャッシュします。
  • start()
    • autoPlayOnStarttrue のとき、AudioSource を自動再生。
    • エンジン音のループクリップを設定しておけば、そのまま走行音になります。
  • update(deltaTime)
    • 選択された velocitySource に応じて現在の速度を取得。
    • 速度から 0〜1 に正規化した値を求め、minPitchmaxPitch を線形補間して目標ピッチを算出。
    • pitchLerpSpeed を使って _currentPitch を目標ピッチへスムージングし、audioSource.pitch に反映。
    • debugLogtrue のとき、速度とピッチをコンソールに出力してチューニングしやすくしています。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタの Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を EngineSound.ts に変更します。
  4. 作成された EngineSound.ts をダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用ノード(車)の作成

2Dゲームの場合の例を示します(3Dでも基本は同じです)。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、車用のノード(例:Car)を作成します。
  2. Car ノードを選択し、Inspector で次のコンポーネントを追加します。
    1. Add Component → Physics 2D → RigidBody2D(2D物理で動かしたい場合)
    2. Add Component → Audio → AudioSource
  3. AudioSource コンポーネントの設定:
    • Clip に、ループ再生したいエンジン音の AudioClip を設定します。
    • LoopON にします。
    • Play On AwakeOFF でも構いません(EngineSound 側で autoPlayOnStarttrue なら自動再生されます)。
    • Volume は 0.5〜1.0 くらいで好みに調整します。

3. EngineSound コンポーネントのアタッチ

  1. Car ノードを選択した状態で、Inspector の下部で Add Component → Custom → EngineSound を選択します。
  2. EngineSound コンポーネントが追加されたら、プロパティを以下のように設定してみます(2D物理の例)。
  • audioSource : 自動で Car の AudioSource が入っていればそのまま。入っていない場合はドラッグ&ドロップで設定。
  • autoPlayOnStart : true
  • velocitySource : RigidBody2D
  • rigidBody2D : 自動で Car の RigidBody2D が入っていればそのまま。
  • maxSpeed : 例として 30 に設定(ゲーム内の想定最高速度に合わせて調整)。
  • minPitch : 0.9
  • maxPitch : 1.8
  • pitchLerpSpeed : 8
  • minSpeedThreshold : 0.05(微小な揺れを無視する)
  • debugLog : 初期は false。調整時に true にしてログを確認。

4. 車を動かす簡単なテスト

車に簡単な移動スクリプトをつけて動かすと、速度に応じてエンジン音のピッチが変化することを確認できます。ここでは最小限の確認方法だけ記します。

  1. Scene を再生します(上部の再生ボタン)。
  2. キーボード入力などで CarRigidBody2D に力を加えるスクリプトが既にある場合、そのまま走らせてみてください。
  3. 車が加速していくと、エンジン音のピッチが徐々に高くなり、減速するとピッチが下がるのが分かります。
  4. もし挙動が分かりにくい場合は、EngineSounddebugLogtrue にして、コンソールで速度とピッチの数値を確認しながら maxSpeed / minPitch / maxPitch を調整するとよいです。

5. 3D や Transform ベースでの利用

  • 3D物理 (RigidBody) を使う場合:
    1. 車ノードに RigidBody (3D) を追加します。
    2. EngineSoundvelocitySourceRigidBody3D に変更。
    3. rigidBody3D に該当の RigidBody を設定(自動で入っていればそのまま)。
  • Transform ベースで使う場合(物理を使っていない場合):
    1. 車ノードの位置を、独自の移動スクリプトで更新しているだけのケース。
    2. EngineSoundvelocitySourceTransform に変更。
    3. これで、前フレームとの位置差分から速度を自動計算してピッチを変化させます。

まとめ

この EngineSound コンポーネントを使うことで、

  • 任意の車ノードにアタッチするだけで、速度に応じたエンジン音のピッチ変化を簡単に実装できます。
  • 2D/3D 物理、あるいは Transform ベースの移動など、さまざまな移動方式に対応できます。
  • 最低/最高ピッチ、想定最高速度、追従速度などを インスペクタで即座に調整できるため、サウンドデザイナーやレベルデザイナーもエディタ上でチューニングしやすくなります。
  • 外部の GameManager やシングルトンに依存しない「完全に独立した汎用コンポーネント」なので、他プロジェクトへのコピペ再利用も容易です。

このコンポーネントをベースに、例えば「ブレーキ中は別のブレーキ音を鳴らす」「一定以上の速度で風切り音を追加する」といった拡張も簡単に行えます。まずはこの EngineSound をプロジェクトに組み込んで、走行感のあるサウンド演出を素早く実現してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!