【Cocos Creator 3.8】EngineSound の実装:アタッチするだけで「速度に応じてエンジン音のピッチが変化する」汎用スクリプト
このガイドでは、車の速度(Velocity)に応じてエンジン音のピッチ(音程)を自動で上げ下げする汎用コンポーネント EngineSound を実装します。
物理挙動付きの車ノード(例:RigidBody2D や RigidBody)にこのスクリプトをアタッチするだけで、移動速度に応じて AudioSource の pitch がリアルタイムに変化するようになります。外部の GameManager やシングルトンは一切不要で、インスペクタでの設定だけで完結する設計です。
コンポーネントの設計方針
1. 機能要件の整理
- ノードの「速度(ベロシティ)」に応じて、エンジン音のピッチを変化させる。
- 速度 0 のときは低いピッチ、最高速度付近では高いピッチになるように、線形補間(Lerp)で変化させる。
- 最低ピッチ・最高ピッチ・想定最高速度などは、すべてインスペクタから調整可能にする。
- 音の急激な変化を防ぐため、ピッチ変化にスムージング(追従速度)を設ける。
- ループするエンジン音を自動再生するかどうかを選べる。
2. 外部依存をなくすための設計
完全な独立性を保つため、以下の方針で設計します。
- 速度の取得は、以下の2パターンのどちらかを選べるようにする:
- 2D物理:
RigidBody2DのlinearVelocityを使用 - 3D物理:
RigidBodyのlinearVelocityを使用
- 2D物理:
- どちらの物理コンポーネントも無い場合は、自動でノードの位置差分から速度を推定する「Transform ベース」のモードを用意する。
- 必要な標準コンポーネントはすべて
getComponentで取得し、存在しなければerrorログを出力して防御的に動作。 - エンジン音再生には
AudioSourceを使用し、これもインスペクタで指定&自動取得を試みる。
3. インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
- audioSource :
AudioSource- エンジン音を再生する AudioSource。
- 未指定の場合は、同じノードに付いている AudioSource を自動取得する。
- autoPlayOnStart :
booleantrueのとき、start()で AudioSource を自動再生する。- エンジン音を常時ループさせる用途を想定。
- velocitySource :
Enum- 速度の取得方法を選択する。
RigidBody2D/RigidBody3D/Transformの3種類。
- rigidBody2D :
RigidBody2D- 2D物理を使う場合に参照する Rigidbody2D。
- 未指定なら、同じノードから自動取得を試みる。
- rigidBody3D :
RigidBody- 3D物理を使う場合に参照する Rigidbody。
- 未指定なら、同じノードから自動取得を試みる。
- maxSpeed :
number- 想定する「最高速度」。
- この値以上の速度では、ピッチは
maxPitchにクランプされる。 - 例:
50や100など。
- minPitch :
number- 停止時(速度 0)のピッチ。
- 通常は
0.8〜1.0程度。
- maxPitch :
number- 最高速度時のピッチ。
- 通常は
1.5〜2.0程度。
- pitchLerpSpeed :
number- 現在のピッチを目標ピッチへ補間する速度。
- 大きいほど追従が速く、小さいほどなめらかに変化。
- 例:
5〜15程度。
- minSpeedThreshold :
number- これ未満の速度は「ほぼ停止」とみなして 0 として扱う。
- 小刻みなノイズでピッチが揺れないようにするためのしきい値。
- debugLog :
booleantrueのとき、現在速度とピッチを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/RigidBodyをgetComponentで自動取得。- 見つからない場合は
errorログを出して、エディタ側での設定不足を気づけるようにしています。 - 初期ピッチを
minPitchに設定し、Transform モード用の初期位置をキャッシュします。
- start()
autoPlayOnStartがtrueのとき、AudioSource を自動再生。- エンジン音のループクリップを設定しておけば、そのまま走行音になります。
- update(deltaTime)
- 選択された
velocitySourceに応じて現在の速度を取得。 - 速度から 0〜1 に正規化した値を求め、
minPitch〜maxPitchを線形補間して目標ピッチを算出。 pitchLerpSpeedを使って_currentPitchを目標ピッチへスムージングし、audioSource.pitchに反映。debugLogがtrueのとき、速度とピッチをコンソールに出力してチューニングしやすくしています。
- 選択された
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- ファイル名を
EngineSound.tsに変更します。 - 作成された
EngineSound.tsをダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノード(車)の作成
2Dゲームの場合の例を示します(3Dでも基本は同じです)。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、車用のノード(例:
Car)を作成します。 Carノードを選択し、Inspector で次のコンポーネントを追加します。- Add Component → Physics 2D → RigidBody2D(2D物理で動かしたい場合)
- Add Component → Audio → AudioSource
AudioSourceコンポーネントの設定:- Clip に、ループ再生したいエンジン音の AudioClip を設定します。
- Loop を
ONにします。 - Play On Awake は
OFFでも構いません(EngineSound側でautoPlayOnStartがtrueなら自動再生されます)。 - Volume は 0.5〜1.0 くらいで好みに調整します。
3. EngineSound コンポーネントのアタッチ
Carノードを選択した状態で、Inspector の下部で Add Component → Custom → EngineSound を選択します。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. 車を動かす簡単なテスト
車に簡単な移動スクリプトをつけて動かすと、速度に応じてエンジン音のピッチが変化することを確認できます。ここでは最小限の確認方法だけ記します。
- Scene を再生します(上部の再生ボタン)。
- キーボード入力などで
CarのRigidBody2Dに力を加えるスクリプトが既にある場合、そのまま走らせてみてください。 - 車が加速していくと、エンジン音のピッチが徐々に高くなり、減速するとピッチが下がるのが分かります。
- もし挙動が分かりにくい場合は、
EngineSoundの debugLog をtrueにして、コンソールで速度とピッチの数値を確認しながらmaxSpeed/minPitch/maxPitchを調整するとよいです。
5. 3D や Transform ベースでの利用
- 3D物理 (RigidBody) を使う場合:
- 車ノードに RigidBody (3D) を追加します。
EngineSoundの velocitySource をRigidBody3Dに変更。- rigidBody3D に該当の RigidBody を設定(自動で入っていればそのまま)。
- Transform ベースで使う場合(物理を使っていない場合):
- 車ノードの位置を、独自の移動スクリプトで更新しているだけのケース。
EngineSoundの velocitySource をTransformに変更。- これで、前フレームとの位置差分から速度を自動計算してピッチを変化させます。
まとめ
この EngineSound コンポーネントを使うことで、
- 任意の車ノードにアタッチするだけで、速度に応じたエンジン音のピッチ変化を簡単に実装できます。
- 2D/3D 物理、あるいは Transform ベースの移動など、さまざまな移動方式に対応できます。
- 最低/最高ピッチ、想定最高速度、追従速度などを インスペクタで即座に調整できるため、サウンドデザイナーやレベルデザイナーもエディタ上でチューニングしやすくなります。
- 外部の GameManager やシングルトンに依存しない「完全に独立した汎用コンポーネント」なので、他プロジェクトへのコピペ再利用も容易です。
このコンポーネントをベースに、例えば「ブレーキ中は別のブレーキ音を鳴らす」「一定以上の速度で風切り音を追加する」といった拡張も簡単に行えます。まずはこの EngineSound をプロジェクトに組み込んで、走行感のあるサウンド演出を素早く実現してみてください。




