【Cocos Creator 3.8】WindHowl の実装:アタッチするだけで「落下スピードに応じて風切り音が変化する」汎用スクリプト
プレイヤーやオブジェクトが高速で落下・移動するときに「ヒュオオオ…」という風切り音が自動で大きくなってくれたら、スピード感や迫力が一気に増します。この WindHowl コンポーネントは、Rigidbody(2D/3D どちらでも可)の速度を検知し、落下速度が速くなるほどホワイトノイズ(風切り音)の音量を自動調整する汎用スクリプトです。
対象ノードにアタッチして、オーディオクリップと速度スケールをインスペクタで設定するだけで動作します。外部の GameManager やシングルトンには一切依存しない、1ファイル完結のコンポーネントです。
コンポーネントの設計方針
1. 機能要件の整理
- ノードにアタッチされた Rigidbody(2D でも 3D でもよい)の速度を毎フレーム監視する。
- 「落下速度」=重力方向(通常は -Y)に沿った速度成分、もしくはオプションで「速度の絶対値(移動速度全般)」を使えるようにする。
- 速度が一定値以下のときは風切り音はほぼ無音、閾値を超えて速くなるほど音量を 0〜最大音量の範囲で線形に増加させる。
- 音源は 1つの AudioSource を使い、ループ再生されたホワイトノイズ(風切り音)クリップをボリュームだけ変化させる。
- Rigidbody2D / Rigidbody(3D)どちらにも対応し、どちらも無い場合は警告ログを出して動作を停止する。
- 外部スクリプトには依存せず、必要な設定はすべて @property からインスペクタで指定できるようにする。
2. インスペクタで設定可能なプロパティ設計
WindHowl コンポーネントでインスペクタから調整可能にするプロパティとその役割は次の通りです。
audioSource(AudioSource)- 風切り音を再生する AudioSource コンポーネント。
- 同じノード、または任意のノードの AudioSource をアサイン可能。
- 未設定の場合は 同じノードから自動取得を試みる。
windClip(AudioClip)- ループ再生されるホワイトノイズ/風切り音の AudioClip。
- ループ用の短いノイズ素材を推奨。
use3DVelocity(boolean)- true: Rigidbody(3D)の速度を優先して使用。
- false: Rigidbody2D の速度を優先して使用。
- 両方存在する場合は、このフラグでどちらを参照するかを明示的に制御。
useSpeedMagnitude(boolean)- true: ベクトルの長さ(移動速度の絶対値)で音量を決める。
- false: 指定した「落下方向」に沿った速度成分のみで音量を決める。
- 「落下」というより「高速移動全般」で風切り音を鳴らしたい場合は true を推奨。
fallDirection(Vec3)- 落下方向の基準ベクトル(例: 0, -1, 0 で下方向)。
useSpeedMagnitude = falseのときのみ使用。- ローカル座標ではなく ワールド座標系の方向として扱う。
minSpeed(number)- この速度未満では音量 0(無音)とする閾値。
- 例: 3.0(m/s 程度)。
maxSpeed(number)- この速度以上で音量が最大になる速度。
- 例: 20.0(m/s 程度)。
minSpeed < maxSpeedになるように設定する必要がある。
maxVolume(number)- 音量の上限(0.0〜1.0)。
- ゲーム全体の BGM / SE バランスに合わせて調整。
fadeInTime(number)- 風切り音が鳴り始めるときのフェードイン時間(秒)。
- 0 なら即時反映。0.1〜0.5 秒程度にすると自然。
fadeOutTime(number)- 速度が落ちて無音に近づくときのフェードアウト時間(秒)。
- 余韻を残したい場合は 0.3〜1.0 秒程度。
pauseWhenZero(boolean)- 音量が 0 近くになったときに AudioSource を一時停止するかどうか。
- true にすると無音時に再生を止めるので、わずかに CPU/ミキサー負荷を下げられる。
debugLog(boolean)- 速度や音量の計算結果をログ出力するデバッグモード。
- 挙動確認やチューニング時にのみ ON にする想定。
TypeScriptコードの実装
以下が WindHowl コンポーネントの完全な実装コードです。
import { _decorator, Component, Node, AudioSource, AudioClip, RigidBody, RigidBody2D, Vec2, Vec3, math, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* WindHowl
* 落下 / 高速移動の速度に応じて風切り音(ホワイトノイズ)の音量を自動調整するコンポーネント。
*
* - Rigidbody2D / Rigidbody (3D) のどちらにも対応
* - 外部スクリプトへの依存なし
* - インスペクタからしきい値や最大音量、フェード時間を調整可能
*/
@ccclass('WindHowl')
export class WindHowl extends Component {
@property({
type: AudioSource,
tooltip: '風切り音を再生する AudioSource。\n未設定の場合はこのノードから自動取得を試みます。'
})
public audioSource: AudioSource | null = null;
@property({
type: AudioClip,
tooltip: 'ループ再生されるホワイトノイズ / 風切り音の AudioClip。'
})
public windClip: AudioClip | null = null;
@property({
tooltip: 'true: Rigidbody(3D) の速度を使用。\nfalse: Rigidbody2D の速度を使用。'
})
public use3DVelocity: boolean = false;
@property({
tooltip: 'true: 速度ベクトルの長さ(移動速度全般)で音量を決定。\nfalse: fallDirection に沿った速度成分のみで音量を決定。'
})
public useSpeedMagnitude: boolean = false;
@property({
tooltip: '落下方向の基準ベクトル(ワールド座標系)。\nuseSpeedMagnitude = false のときのみ使用されます。'
})
public fallDirection: Vec3 = new Vec3(0, -1, 0);
@property({
tooltip: 'この速度未満では音量 0(無音)とする閾値。',
min: 0
})
public minSpeed: number = 3.0;
@property({
tooltip: 'この速度以上で音量が最大になる速度。\nminSpeed より大きい値を設定してください。',
min: 0
})
public maxSpeed: number = 20.0;
@property({
tooltip: '音量の上限(0.0〜1.0)。',
min: 0,
max: 1
})
public maxVolume: number = 0.8;
@property({
tooltip: '音量が上がるときのフェードイン時間(秒)。\n0 で即時反映。',
min: 0
})
public fadeInTime: number = 0.2;
@property({
tooltip: '音量が下がるときのフェードアウト時間(秒)。\n0 で即時反映。',
min: 0
})
public fadeOutTime: number = 0.4;
@property({
tooltip: '音量が 0 近くになったときに AudioSource を一時停止するかどうか。'
})
public pauseWhenZero: boolean = true;
@property({
tooltip: '速度と音量の計算結果をログ出力するデバッグモード。'
})
public debugLog: boolean = false;
// 内部用参照
private _rb3D: RigidBody | null = null;
private _rb2D: RigidBody2D | null = null;
// 現在の実音量(AudioSource に適用される値)
private _currentVolume: number = 0;
// 目標音量(速度から計算された値)
private _targetVolume: number = 0;
// 正規化された落下方向
private _fallDirNormalized: Vec3 = new Vec3(0, -1, 0);
onLoad() {
// AudioSource 自動取得
if (!this.audioSource) {
this.audioSource = this.getComponent(AudioSource);
if (!this.audioSource) {
warn('[WindHowl] AudioSource が設定されていません。このノードに AudioSource コンポーネントを追加するか、別ノードの AudioSource をアサインしてください。');
}
}
// Rigidbody 取得(両方試す)
this._rb3D = this.getComponent(RigidBody);
this._rb2D = this.getComponent(RigidBody2D);
if (!this._rb3D && !this._rb2D) {
warn('[WindHowl] Rigidbody / Rigidbody2D が見つかりません。このノードにいずれかのコンポーネントを追加してください。');
}
// 落下方向の正規化
if (!this.useSpeedMagnitude) {
if (this.fallDirection.length() === 0) {
warn('[WindHowl] fallDirection が (0,0,0) です。デフォルトの (0,-1,0) を使用します。');
this._fallDirNormalized.set(0, -1, 0);
} else {
this._fallDirNormalized = this.fallDirection.clone();
this._fallDirNormalized.normalize();
}
}
// minSpeed と maxSpeed の整合性チェック
if (this.maxSpeed <= this.minSpeed) {
warn(`[WindHowl] maxSpeed (${this.maxSpeed}) は minSpeed (${this.minSpeed}) より大きい必要があります。maxSpeed を minSpeed + 1 に補正します。`);
this.maxSpeed = this.minSpeed + 1.0;
}
// AudioSource 初期設定
if (this.audioSource) {
this.audioSource.loop = true;
if (this.windClip) {
this.audioSource.clip = this.windClip;
}
this.audioSource.volume = 0;
}
this._currentVolume = 0;
this._targetVolume = 0;
}
start() {
// 風切り音のループ再生開始(必要に応じて一時停止)
if (this.audioSource && this.windClip) {
if (!this.audioSource.playing) {
this.audioSource.play();
}
if (this.pauseWhenZero) {
// 初期状態では音量 0 なので一時停止
this.audioSource.pause();
}
} else {
if (!this.windClip) {
warn('[WindHowl] windClip が設定されていません。風切り音用の AudioClip をアサインしてください。');
}
}
}
update(deltaTime: number) {
// 必要なコンポーネントが揃っていない場合は何もしない
if (!this.audioSource || !this.windClip) {
return;
}
// 速度の取得
const speed = this._getCurrentSpeed();
// 速度から目標音量を計算
this._targetVolume = this._calculateTargetVolume(speed);
// 現在音量を目標音量に向けて補間(フェードイン/アウト)
this._updateVolume(deltaTime);
// AudioSource に反映
this.audioSource.volume = this._currentVolume;
// 無音時の一時停止処理
if (this.pauseWhenZero) {
const threshold = 0.001;
if (this._currentVolume <= threshold) {
if (this.audioSource.playing) {
this.audioSource.pause();
}
} else {
if (!this.audioSource.playing) {
this.audioSource.play();
}
}
}
if (this.debugLog) {
log(`[WindHowl] speed=${speed.toFixed(2)}, targetVolume=${this._targetVolume.toFixed(2)}, currentVolume=${this._currentVolume.toFixed(2)}`);
}
}
/**
* 現在の速度(スカラー)を取得する。
* useSpeedMagnitude が true の場合はベクトル長、
* false の場合は fallDirection に沿った速度成分(負方向も考慮)を返す。
*/
private _getCurrentSpeed(): number {
let speed = 0;
if (this.use3DVelocity) {
if (this._rb3D) {
const v = this._rb3D.linearVelocity;
if (this.useSpeedMagnitude) {
speed = v.length();
} else {
// fallDirection に沿った成分のみ(内積)
const v3 = new Vec3(v.x, v.y, v.z);
speed = Vec3.dot(v3, this._fallDirNormalized);
// 落下方向に進んでいない場合は 0 とみなす
if (speed < 0) speed = 0;
}
}
} else {
if (this._rb2D) {
const v2 = this._rb2D.linearVelocity as Vec2;
if (this.useSpeedMagnitude) {
speed = v2.length();
} else {
// 2D の場合も fallDirection の x,y を使用
const dir2 = new Vec2(this._fallDirNormalized.x, this._fallDirNormalized.y);
const dot = v2.x * dir2.x + v2.y * dir2.y;
speed = dot;
if (speed < 0) speed = 0;
}
}
}
// 対象 Rigidbody が見つからない場合は 0
return speed;
}
/**
* 速度から 0〜maxVolume の範囲で目標音量を計算する。
*/
private _calculateTargetVolume(speed: number): number {
if (speed <= this.minSpeed) {
return 0;
}
if (speed >= this.maxSpeed) {
return this.maxVolume;
}
// minSpeed〜maxSpeed の間を 0〜1 に正規化
const t = (speed - this.minSpeed) / (this.maxSpeed - this.minSpeed);
const clampedT = math.clamp01(t);
// 線形補間で 0〜maxVolume に変換
return clampedT * this.maxVolume;
}
/**
* フェードイン/アウトを考慮して _currentVolume を _targetVolume に近づける。
*/
private _updateVolume(deltaTime: number) {
if (this.fadeInTime <= 0 && this.fadeOutTime <= 0) {
// フェードなしで即時反映
this._currentVolume = this._targetVolume;
return;
}
if (this._targetVolume > this._currentVolume) {
// フェードイン(音量アップ)
if (this.fadeInTime <= 0) {
this._currentVolume = this._targetVolume;
} else {
const diff = this._targetVolume - this._currentVolume;
const step = (this.maxVolume / this.fadeInTime) * deltaTime;
this._currentVolume += Math.min(step, diff);
}
} else if (this._targetVolume < this._currentVolume) {
// フェードアウト(音量ダウン)
if (this.fadeOutTime <= 0) {
this._currentVolume = this._targetVolume;
} else {
const diff = this._currentVolume - this._targetVolume;
const step = (this.maxVolume / this.fadeOutTime) * deltaTime;
this._currentVolume -= Math.min(step, diff);
}
}
// 範囲クランプ
this._currentVolume = math.clamp(this._currentVolume, 0, this.maxVolume);
}
}
コードの要点解説
- onLoad
- AudioSource をインスペクタから受け取り、未設定なら
getComponent(AudioSource)で自動取得。 - RigidBody / RigidBody2D を取得し、どちらも無ければ警告ログを出す。
- 落下方向ベクトルの正規化と
minSpeed / maxSpeedの整合性チェックを行う。
- AudioSource をインスペクタから受け取り、未設定なら
- start
- AudioSource に windClip をセットし、ループ再生を開始。
pauseWhenZeroが true の場合は初期状態で一時停止しておき、音量が上がったタイミングで自動再生する。
- update
- Rigidbody から現在の速度を取得(2D/3D と magnitude/落下方向を切り替え可能)。
- 速度から
_calculateTargetVolumeで目標音量を計算。 _updateVolumeでフェードイン/フェードアウトしながら現在音量を目標に近づける。- AudioSource.volume に反映し、音量が 0 近くなら pause、0 より大きくなったら play する。
debugLogが true の場合は速度と音量をログ出力。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ下部の Assets パネルで、風切り音用スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新しく作成されたファイル名を WindHowl.ts に変更します。
- ダブルクリックしてエディタ(VS Code など)で開き、内容をすべて削除して、前述の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードと必須コンポーネントの準備
ここでは Rigidbody2D を使った 2D 落下オブジェクトの例で説明します。3D でも手順はほぼ同じです。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のスプライトノード(例:
Player)を作成します。 - 作成した
Playerノードを選択し、Inspector を確認します。 - Add Component ボタンをクリック → Physics 2D → RigidBody2D を追加します。
- Body Type: Dynamic に設定。
- Gravity Scale: 1(デフォルト)で OK。
- 同じく Add Component → Audio → AudioSource を追加します。
- Loop: チェックは ON でも OFF でも構いません(WindHowl 側で強制的に ON にします)。
- Play On Awake: OFF を推奨(WindHowl が制御するため)。
- Clip: ここは空のままで OK(WindHowl から設定します)。
3. WindHowl コンポーネントのアタッチ
- 再び
Playerノードを選択した状態で、Add Component をクリックします。 - Custom → WindHowl を選択してアタッチします。
4. WindHowl のプロパティ設定
Inspector の WindHowl セクションで、以下のように設定してみてください(2D 落下用の推奨値例)。
- Audio Source:
- 自動で同じノードの AudioSource がセットされていればそのままで OK。
- Wind Clip:
- 事前にインポートしておいた風切り音 / ホワイトノイズの AudioClip をドラッグ&ドロップでアサインします。
- ループしても違和感の少ない短めのノイズを推奨。
- Use 3D Velocity: OFF
- 2D の Rigidbody2D を使うので OFF にします。
- Use Speed Magnitude: OFF(純粋な落下速度に応じて鳴らしたい場合)
- 横移動ではあまり鳴らしたくない場合は OFF。
- 高速ダッシュなど横移動でも鳴らしたいなら ON に変更します。
- Fall Direction: (0, -1, 0)
- 2D の場合も Y 軸が上下方向なので (0, -1, 0) で OK。
- Min Speed: 3
- 3 m/s 未満では風切り音がほぼ鳴らないようにします。
- Max Speed: 20
- 20 m/s 以上の速度で最大音量(Max Volume)になります。
- Max Volume: 0.7
- ゲーム全体の音量バランスに応じて 0.5〜0.8 程度で調整。
- Fade In Time: 0.2
- 落下し始めてから 0.2 秒ほどかけて自然に風切り音が増えていきます。
- Fade Out Time: 0.5
- 落下終了後、0.5 秒ほど余韻を残しながら音が消えていきます。
- Pause When Zero: ON
- 無音時には AudioSource を一時停止しておきます。
- Debug Log: OFF(挙動確認時のみ ON)
- 挙動が想定通りか確認したいときは ON にしてログをチェックします。
5. シーンでの動作確認
- シーンに 2D Physics の環境(例: 下に Ground 用の BoxCollider2D 付きノード)があることを確認し、
Playerが上から落下できるように配置します。 - エディタ上部の ▶(Play) ボタンを押してゲームを再生します。
Playerが落下し始めると、速度がMin Speedを超えたあたりから徐々に風切り音が聞こえ始め、落下が速くなるほど音量が増していくことを確認します。- 地面に着地して速度が落ちると、風切り音がフェードアウトしていくことを確認します。
- 必要に応じて
Min Speed/Max Speed/Max Volume/Fade In/Out Timeを調整し、ゲームに合った気持ちよいバランスに仕上げてください。
6. 3D プロジェクトでの利用例(簡単な補足)
3D でキャラクターやオブジェクトに適用する場合は、以下の点だけ変えればほぼ同じ手順で使えます。
- ノードに RigidBody(3D) と Collider(BoxCollider など) を追加する。
- WindHowl の Use 3D Velocity を ON にする。
- 重力方向がデフォルトの -Y なら Fall Direction = (0, -1, 0) のままで OK。
- ロケットのように「前方移動時に風切り音を鳴らしたい」場合は
useSpeedMagnitude = trueにして「進行方向の速度全般」で鳴らす、- もしくは
fallDirectionを「機体の前方方向」に合わせてカスタマイズする、などで応用できます。
まとめ
WindHowl コンポーネントは、
- Rigidbody の速度を監視して風切り音の音量を自動調整する 汎用スクリプトであり、
- インスペクタから 速度しきい値・最大音量・フェード時間・2D/3D 切り替え を柔軟に設定でき、
- 外部の GameManager やシングルトンに依存せず、ノードにアタッチしてプロパティをセットするだけ で使えるように設計されています。
このコンポーネントを使えば、
- 落下するプレイヤーキャラクター
- 高速で飛行する乗り物やロケット
- スライドするオブジェクトやジェットコースター的なギミック
などに簡単に「スピード感のある風切り音」を付与できます。1 ファイル完結で再利用性も高いので、今後のプロジェクトでは 「動くものにはとりあえず WindHowl をアタッチしてみる」 くらいの感覚でライブラリ化しておくと、ゲームの臨場感アップに役立ちます。




