【Cocos Creator 3.8】FootstepAudio の実装:アタッチするだけで「移動速度やアニメーションに応じた足音再生」を実現する汎用スクリプト
このコンポーネントは、キャラクターなどのノードにアタッチするだけで、
- ノードの移動速度に応じて自動で足音を鳴らす
- (任意で)アニメーションイベントから足音を鳴らす
といった処理を行う「汎用足音コンポーネント」です。
移動処理やアニメーション制御がどのように実装されていても、このスクリプト単体で完結し、Inspector から足音の間隔や音量、ランダムピッチなどを調整できます。
コンポーネントの設計方針
要件整理
- 親ノード(アタッチ先ノード)の「移動速度」を監視し、一定以上動いているときだけ足音を鳴らす。
- 足音の再生間隔は、移動速度に応じて変化できるようにする(速度が速いほど間隔が短くなる)。
- アニメーションを使う場合は、アニメーションイベントから直接「足音を鳴らす」メソッドを呼び出せるようにする。
- 外部の GameManager やシングルトンには一切依存しない。
- 必要なサウンド再生は、アタッチ先ノードに AudioSource を追加して行う(なければ警告ログ)。
- エディタ上で足音クリップや音量、ランダム性などを柔軟に設定できるようにする。
設計アプローチ
このコンポーネントは、以下の2つの方法で足音を鳴らせるようにします。
- 移動監視モード(自動)
- 毎フレーム、前フレームの位置との差分から「移動速度」を計算。
- 一定以上の速度(しきい値)を超えた場合のみ「歩行中」とみなし、一定間隔で足音を再生。
- しきい値や間隔は Inspector から調整可能。
- アニメーションイベントモード(手動トリガ)
- AnimationClip のイベントから public メソッド
playFootstepFromAnimation()を呼び出す。 - このメソッドは、移動速度に関係なく「そのフレームで足音を鳴らしたい」ときに使用。
- アニメーションに合わせて左右の足のタイミングで鳴らしたい場合に便利。
- AnimationClip のイベントから public メソッド
どちらのモードも同時に使えるようにし、必要に応じて Inspector から ON/OFF 切り替えできるようにします。
Inspector で設定可能なプロパティ
- enableMovementBased (移動ベース足音を有効化)
– 型:boolean
– 説明: ON の場合、ノードの移動速度を監視して自動的に足音を鳴らします。 - enableAnimationEvent (アニメーションイベント足音を有効化)
– 型:boolean
– 説明: ON の場合、AnimationClip のイベントからplayFootstepFromAnimation()を呼び出した時に足音が鳴ります。 - minSpeedForSteps (足音再生の最小移動速度)
– 型:number
– 単位: 単位時間あたりの距離(world座標ベース)
– 説明: この値以上の速度で動いているときだけ足音を鳴らします。小さすぎると微妙な揺れでも鳴ってしまうので注意。 - baseInterval (基準足音間隔)
– 型:number
– 単位: 秒
– 説明: 通常の歩行速度(後述のreferenceSpeed)で移動しているときの足音の間隔。 - referenceSpeed (基準速度)
– 型:number
– 説明: この速度で移動しているときにbaseIntervalで足音を鳴らします。速く動けば間隔が短くなり、遅ければ間隔が長くなります。 - randomIntervalOffset (間隔のランダム幅)
– 型:number
– 単位: 秒
– 説明: 足音間隔に±randomIntervalOffsetのランダム値を加算して、単調にならないようにします。0 にすると完全に一定間隔になります。 - audioClips (足音 AudioClip の配列)
– 型:AudioClip[]
– 説明: 足音として再生する AudioClip を複数登録できます。再生時にランダムで1つ選ばれます。最低1つは設定してください。 - volume (音量)
– 型:number
– 範囲: 0.0 ~ 1.0
– 説明: 足音再生時の音量。AudioSource の volume とは別に「足音用の倍率」として扱います。 - randomPitchEnabled (ランダムピッチを有効化)
– 型:boolean
– 説明: ON の場合、再生ごとに pitch をランダムに変化させて、同じ音でもバリエーションを出します。 - pitchMin / pitchMax (ピッチ範囲)
– 型:number
– 説明: ランダムピッチ有効時に、この範囲内で pitch をランダム設定します。1.0 が通常の高さです。 - use3DPosition (3D位置を使用)
– 型:boolean
– 説明: ON の場合、worldPositionを使って速度を計算します。2Dゲームでも 3D ノード構造を使っている場合はこちらを使うと安定します。OFF の場合は 2D のpositionを利用します。 - debugLog (デバッグログ出力)
– 型:boolean
– 説明: ON にすると、足音再生やエラー・警告のログをコンソールに出力します。開発中のみ ON 推奨。
また、内部的には以下の標準コンポーネントを利用します。
- AudioSource(必須)
– アタッチ先ノードにAudioSourceが存在しない場合、自動で追加しつつ警告ログを出します。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, AudioSource, AudioClip, randomRange, warn, log } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('FootstepAudio')
export class FootstepAudio extends Component {
@property({
tooltip: '移動速度に応じて自動で足音を鳴らすかどうか。ON の場合、ノードの移動を監視して一定間隔で足音を再生します。'
})
public enableMovementBased: boolean = true;
@property({
tooltip: 'アニメーションイベントから足音を鳴らす機能を有効にします。AnimationClip のイベントから playFootstepFromAnimation() を呼び出してください。'
})
public enableAnimationEvent: boolean = true;
@property({
tooltip: '足音を鳴らし始める最小移動速度。これ未満の速度では足音を鳴らしません。'
})
public minSpeedForSteps: number = 0.1;
@property({
tooltip: '基準の足音間隔(秒)。referenceSpeed で移動しているとき、この間隔で足音を鳴らします。'
})
public baseInterval: number = 0.4;
@property({
tooltip: '基準速度。baseInterval と組み合わせて、実際の移動速度に応じて足音間隔をスケーリングします。'
})
public referenceSpeed: number = 2.0;
@property({
tooltip: '足音間隔に加えるランダム幅(秒)。0 の場合は完全に一定間隔になります。'
})
public randomIntervalOffset: number = 0.05;
@property({
type: [AudioClip],
tooltip: '足音として再生する AudioClip のリストです。再生時にランダムで 1 つ選ばれます。最低 1 つは設定してください。'
})
public audioClips: AudioClip[] = [];
@property({
tooltip: '足音の音量(0.0 ~ 1.0)。AudioSource の volume と掛け合わされます。'
})
public volume: number = 1.0;
@property({
tooltip: 'ランダムピッチを有効にするかどうか。ON の場合、再生ごとに pitch をランダムに変更します。'
})
public randomPitchEnabled: boolean = true;
@property({
tooltip: 'ランダムピッチの最小値。1.0 が通常の高さです。',
visible() {
return this.randomPitchEnabled;
}
})
public pitchMin: number = 0.95;
@property({
tooltip: 'ランダムピッチの最大値。1.0 が通常の高さです。',
visible() {
return this.randomPitchEnabled;
}
})
public pitchMax: number = 1.05;
@property({
tooltip: '3D の worldPosition を使用して速度を計算するかどうか。2D でも 3D ノード構造を使っている場合はこちらを ON にします。'
})
public use3DPosition: boolean = true;
@property({
tooltip: 'デバッグログをコンソールに出力するかどうか。開発中のみ ON を推奨します。'
})
public debugLog: boolean = false;
private _audioSource: AudioSource | null = null;
private _lastPosition: Vec3 = new Vec3();
private _timer: number = 0;
private _currentInterval: number = 0.4;
private _isMoving: boolean = false;
onLoad() {
// AudioSource の取得または自動追加
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource) {
// 自動で追加するが、開発者に気づいてもらうために警告を出す
this._audioSource = this.addComponent(AudioSource);
warn('[FootstepAudio] AudioSource が見つからなかったため、自動で追加しました。必要に応じて設定を見直してください。');
}
// 初期位置を記録
if (this.use3DPosition) {
this.node.getWorldPosition(this._lastPosition);
} else {
const p = this.node.position;
this._lastPosition.set(p.x, p.y, p.z);
}
// 初期の足音間隔を設定
this._currentInterval = this.baseInterval;
if (this.debugLog) {
log('[FootstepAudio] onLoad 完了');
}
}
start() {
// 特に初期化は onLoad で済ませているが、
// 将来的に初期再生タイミングなどを制御したい場合はここで行う。
this._timer = 0;
}
update(deltaTime: number) {
if (!this.enableMovementBased) {
return;
}
// 現在位置の取得
const currentPos = new Vec3();
if (this.use3DPosition) {
this.node.getWorldPosition(currentPos);
} else {
const p = this.node.position;
currentPos.set(p.x, p.y, p.z);
}
// フレーム間の移動距離から速度を計算
const dx = currentPos.x - this._lastPosition.x;
const dy = currentPos.y - this._lastPosition.y;
const dz = currentPos.z - this._lastPosition.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
const speed = distance / deltaTime;
// 次フレームのために位置を更新
this._lastPosition.set(currentPos.x, currentPos.y, currentPos.z);
// 移動しているかどうかを判定
this._isMoving = speed >= this.minSpeedForSteps;
if (!this._isMoving) {
// 動いていないときはタイマーをリセット(好みによってはリセットしない実装もあり)
this._timer = 0;
return;
}
// 実際の速度に応じて足音間隔を計算
// 速度が速いほど間隔が短くなるようにする(下限は baseInterval * 0.3 に制限)
if (this.referenceSpeed > 0) {
const ratio = speed / this.referenceSpeed;
const clampedRatio = Math.max(0.3, Math.min(3.0, 1 / ratio)); // ざっくりとしたスケーリング
this._currentInterval = this.baseInterval * clampedRatio;
} else {
this._currentInterval = this.baseInterval;
}
// ランダム幅を加味した実際の間隔
let targetInterval = this._currentInterval;
if (this.randomIntervalOffset > 0) {
const offset = randomRange(-this.randomIntervalOffset, this.randomIntervalOffset);
targetInterval += offset;
// 負の間隔にならないように最低値を保証
targetInterval = Math.max(0.05, targetInterval);
}
// タイマーを進めて、一定時間ごとに足音を再生
this._timer += deltaTime;
if (this._timer >= targetInterval) {
this._timer = 0;
this.playFootstepInternal('movement');
}
}
/**
* アニメーションイベントから呼び出す用のメソッド。
* AnimationClip のイベントにこのメソッド名を設定することで、
* 指定フレームで足音を鳴らすことができます。
*/
public playFootstepFromAnimation() {
if (!this.enableAnimationEvent) {
if (this.debugLog) {
log('[FootstepAudio] アニメーションイベントは無効化されています。');
}
return;
}
this.playFootstepInternal('animation');
}
/**
* 足音再生の共通処理。
* triggerSource: 'movement' | 'animation' など、どこから呼ばれたかの識別用。
*/
private playFootstepInternal(triggerSource: 'movement' | 'animation') {
if (!this._audioSource) {
warn('[FootstepAudio] AudioSource が見つかりません。足音を再生できません。');
return;
}
if (!this.audioClips || this.audioClips.length === 0) {
warn('[FootstepAudio] audioClips が設定されていません。足音用の AudioClip を Inspector で設定してください。');
return;
}
// ランダムに AudioClip を選択
const index = Math.floor(Math.random() * this.audioClips.length);
const clip = this.audioClips[index];
if (!clip) {
warn('[FootstepAudio] audioClips[' + index + '] が null です。設定を確認してください。');
return;
}
// ランダムピッチ設定
if (this.randomPitchEnabled) {
const min = Math.min(this.pitchMin, this.pitchMax);
const max = Math.max(this.pitchMin, this.pitchMax);
const pitch = randomRange(min, max);
this._audioSource.pitch = pitch;
} else {
this._audioSource.pitch = 1.0;
}
// 音量設定
this._audioSource.volume = this.volume;
// 再生
this._audioSource.playOneShot(clip, 1.0);
if (this.debugLog) {
log(`[FootstepAudio] 足音再生: source=${triggerSource}, clipIndex=${index}, volume=${this.volume.toFixed(2)}, pitch=${this._audioSource.pitch.toFixed(2)}`);
}
}
}
コードのポイント解説
- onLoad()
AudioSourceをgetComponentで取得し、なければaddComponent(AudioSource)で自動追加。- 初期位置を
_lastPositionに記録し、速度計算の基準にします。 - デバッグログが有効な場合、初期化完了をコンソールに表示します。
- update(deltaTime)
enableMovementBasedが OFF のときは何もしません。- 現在位置と前フレームの位置の差から移動距離を求め、
distance / deltaTimeで速度を算出します。 - 速度が
minSpeedForSteps未満なら足音は鳴らさず、タイマーもリセット。 - 速度と
referenceSpeedから足音間隔をスケーリングし、randomIntervalOffsetで少しバラつきを出します。 - 累積タイマー
_timerが目標間隔を超えたらplayFootstepInternal('movement')を呼び出して足音を鳴らします。
- playFootstepFromAnimation()
- AnimationClip のイベントから直接呼び出すための public メソッドです。
enableAnimationEventが OFF の場合は何もせず、デバッグログのみ出力します。- ON の場合は
playFootstepInternal('animation')を呼び出します。
- playFootstepInternal()
- 足音再生の実処理を共通化したメソッドです。
AudioSourceとaudioClipsの存在をチェックし、足りない場合はwarnで警告を出して早期 return。- ランダムに AudioClip を選択し、必要なら pitch をランダム設定。
volumeを AudioSource に設定した上でplayOneShot()で再生します。debugLogが ON の場合、どのクリップをどのピッチ・音量で再生したかをコンソールに出力します。
使用手順と動作確認
1. スクリプトの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を FootstepAudio.ts として作成します。
- 自動生成されたコードをすべて削除し、本記事の
FootstepAudioコードをそのまま貼り付けて保存します。
2. テスト用ノード(キャラクター)の準備
ここでは 2D のキャラクターを例に説明しますが、3D でも手順はほぼ同じです。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のスプライトノードを作成します。
- 名前を Player など分かりやすいものに変更しておきましょう。
- 作成した Player ノードを選択し、Inspector を確認します。
3. FootstepAudio コンポーネントのアタッチ
- Inspector で Add Component ボタンをクリックします。
- 検索欄に FootstepAudio と入力し、Custom → FootstepAudio を選択して追加します。
- Inspector に FootstepAudio のプロパティが表示されることを確認します。
4. AudioSource の確認
- FootstepAudio は onLoad 時に自動で
AudioSourceを追加しますが、手動で設定を確認したい場合は以下を行います。- Player ノードの Inspector をスクロールし、AudioSource コンポーネントが追加されていることを確認します。
- 必要であれば
AudioSourceの Loop を OFF、PlayOnAwake を OFF にしておきます(足音専用なので自動再生は不要です)。
5. 足音用 AudioClip の設定
- Assets に足音用の AudioClip(例:
footstep_grass_01.mp3など)をインポートしておきます。 - Player ノードを選択し、Inspector の FootstepAudio コンポーネントを確認します。
- Audio Clips の右側にある「+」ボタンをクリックして要素数を増やし、インポートした足音 AudioClip をドラッグ&ドロップで登録します。
- 1つだけでも動作しますが、2~4個程度登録しておくとランダム再生で自然になります。
6. 基本プロパティの設定例
2D の横スクロールアクションで、Player がだいたい 2~3 の速度で歩く想定の設定例です。
- Enable Movement Based: ON
- Enable Animation Event: ON(アニメーションからも鳴らしたい場合)
- Min Speed For Steps:
0.1 - Base Interval:
0.4秒 - Reference Speed:
2.0 - Random Interval Offset:
0.05秒 - Volume:
0.8 - Random Pitch Enabled: ON
- Pitch Min:
0.95 - Pitch Max:
1.05 - Use 3D Position: 2D プロジェクトなら OFF / 3D プロジェクトなら ON
- Debug Log: 動作確認中は ON、本番ビルド前に OFF 推奨
7. 移動スクリプトを仮実装して動作確認
FootstepAudio 自体は他のスクリプトに依存しませんが、実際にノードが動かないと足音は鳴りません。
簡単なテスト用の移動スクリプトを用意して、足音が鳴くことを確認します。
- Assets パネルで右クリック → Create → TypeScript →
TestMover.tsを作成します。 - 以下のような簡易移動コードを貼り付けます。(あくまで検証用)
import { _decorator, Component, Vec3, input, Input, EventKeyboard, KeyCode } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('TestMover')
export class TestMover extends Component {
@property
public speed: number = 2.0;
private _direction: number = 0;
onLoad() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.ARROW_LEFT) {
this._direction = -1;
} else if (event.keyCode === KeyCode.ARROW_RIGHT) {
this._direction = 1;
}
}
onKeyUp(event: EventKeyboard) {
if (event.keyCode === KeyCode.ARROW_LEFT || event.keyCode === KeyCode.ARROW_RIGHT) {
this._direction = 0;
}
}
update(deltaTime: number) {
if (this._direction !== 0) {
const pos = this.node.position;
const move = this._direction * this.speed * deltaTime;
this.node.setPosition(pos.x + move, pos.y, pos.z);
}
}
}
- Hierarchy で Player ノードを選択し、Add Component → Custom → TestMover を追加します。
- TestMover の Speed を
2.0などに設定します。 - エディタ右上の Play ボタンでゲームを再生し、左右キー を押して Player を動かします。
- Player が一定以上の速度で移動している間、足音が一定間隔で鳴ることを確認します。
8. アニメーションイベントから足音を鳴らす(任意)
歩行アニメーションに合わせて、左右の足が地面に着地するタイミングで足音を鳴らしたい場合は、AnimationClip のイベント機能を使います。
- 歩行用の AnimationClip(例:
walk.anim)を用意しておきます。 - Animation パネルで該当の AnimationClip を開きます。
- タイムライン上で、足が地面に着地するフレームを選択します。
- そのフレームに イベント を追加し、以下のように設定します。
- Function:
playFootstepFromAnimation - 引数は不要です。
- Function:
- Player ノードには、先ほどの FootstepAudio がアタッチされていることを確認します。
- 再生してみると、アニメーションの足の着地タイミング で足音が鳴るようになります。
このとき、移動ベースの足音も同時に有効にしている場合は、二重に鳴ってしまわないように、どちらか片方を OFF にすることをおすすめします。
- アニメーションに合わせて正確に鳴らしたい → Enable Animation Event: ON / Enable Movement Based: OFF
- 単純な移動ベースで良い → Enable Movement Based: ON / Enable Animation Event: OFF
まとめ
今回作成した FootstepAudio コンポーネントは、
- アタッチ先ノードの移動速度を自動監視し、一定以上動いているときだけ足音を鳴らす。
- AnimationClip のイベントから直接足音を鳴らすこともできる。
- AudioSource を自動追加し、Inspector から足音クリップや音量、ピッチ、間隔などを柔軟に調整できる。
- 外部の GameManager やシングルトンに一切依存しない、完全に独立した汎用コンポーネント。
プロジェクト内のどのキャラクターにも、FootstepAudio.ts をアタッチして AudioClip を設定するだけで、統一された足音システムを簡単に導入できます。
歩行だけでなく、走り・ダッシュ・ステルス移動など、移動速度に応じて足音の間隔や音量を変えることで、プレイヤーへのフィードバックが大きく向上します。
このコンポーネントをベースに、
- 地面の種類(草・石・水たまりなど)によって AudioClip を差し替える
- 接地判定(Raycast など)と組み合わせてジャンプ着地音を鳴らす
- キャラクターごとに足音の音量やピッチを変える
といった拡張も、すべてこのスクリプト単体の範囲内で実現しやすくなります。
まずは本記事のコードをそのまま導入し、プロジェクトに合わせてパラメータを調整してみてください。




