【Cocos Creator 3.8】FootstepRandomizer の実装:アタッチするだけで足音のピッチを毎回ランダム化して「同じ音の連続感」を消す汎用スクリプト
足音 SE を何度も再生していると、「同じ音が連打されている」ような単調さが気になることがあります。この記事では、AudioSource を持つノードにアタッチするだけで、再生のたびにピッチを 0.9〜1.1 の範囲でランダム変更してくれる汎用コンポーネント「FootstepRandomizer」を実装します。
足音に限らず、銃声・ボタン音・ダメージ音など「同じ SE を連打する」シーンで簡単に使い回せる設計にしています。
コンポーネントの設計方針
基本コンセプト
- このコンポーネントをアタッチしたノードに
AudioSourceがあれば、その 再生前にピッチをランダムに設定 します。 - 足音再生は、外部スクリプトから
playFootstep()を呼ぶ、または Animation Event から呼ぶ 形を想定しています。 - 外部の GameManager やシングルトンには一切依存しません。必要なのは「このノードに AudioSource を付けること」だけです。
- ピッチの最小値・最大値・音量・自動再生間隔などは すべてインスペクタから調整可能 にします。
対応するユースケース
- プレイヤーキャラの足音:移動処理側から
playFootstep()を呼ぶだけで、毎回微妙に違う音になる。 - 敵キャラの足音:アニメーションイベントや AI ロジックから同様に呼び出し。
- テスト用に「一定間隔で自動再生するモード」も用意し、シーン上での確認をしやすくします。
インスペクタで設定可能なプロパティ
FootstepRandomizer コンポーネントのプロパティ設計は以下の通りです。
minPitch: number- ピッチの最小値。
- デフォルト:
0.9 - 足音の低さをどこまで許容するかを調整できます。
maxPitch: number- ピッチの最大値。
- デフォルト:
1.1 - 高くしすぎるとコミカルになるので、1.1〜1.2 程度が現実的です。
baseVolume: number- 足音再生時の基本音量(0.0〜1.0)。
- デフォルト:
1.0 - 音量もここで一括管理できるようにします。
randomizeVolume: boolean- 音量も微妙にランダム化するかどうか。
- デフォルト:
false - ON にすると、毎回 90〜110% 程度のランダム補正が入ります。
volumeJitter: number- 音量ランダム幅(0〜1)。
- デフォルト:
0.1 - たとえば 0.1 の場合、
baseVolume × [0.9〜1.1]の範囲で音量が揺れます。
autoTestPlay: boolean- エディタやゲーム中に、一定間隔で自動的に足音を鳴らすテストモード。
- デフォルト:
false - ON にすると、
testInterval秒ごとにplayFootstep()を自動呼び出しします。
testInterval: number- 自動再生モード時の再生間隔(秒)。
- デフォルト:
0.5 - 歩行アニメのテンポに近い値を入れると確認しやすいです。
logWarningIfMissingAudio: boolean- AudioSource が見つからないときに警告ログを出すか。
- デフォルト:
true - 制作段階では ON、ビルド時に OFF にしてログを抑える、などの使い分けができます。
コンポーネントは 自身のノードにある AudioSource のみを利用 し、他ノードや他スクリプトへの依存は一切ありません。
TypeScriptコードの実装
import { _decorator, Component, AudioSource, randomRange, clamp01, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* FootstepRandomizer
* - AudioSource を持つノードにアタッチして使用します。
* - playFootstep() を呼び出すたびに、ピッチを [minPitch, maxPitch] の範囲でランダム設定してから再生します。
* - オプションで音量も微妙にランダム化できます。
*/
@ccclass('FootstepRandomizer')
export class FootstepRandomizer extends Component {
@property({
tooltip: 'ピッチの最小値。足音の低さをどこまで許容するかを指定します。'
})
public minPitch: number = 0.9;
@property({
tooltip: 'ピッチの最大値。高くしすぎるとコミカルになるので 1.1〜1.2 程度が現実的です。'
})
public maxPitch: number = 1.1;
@property({
tooltip: '足音再生時の基本音量(0.0〜1.0)。AudioSource.volume に反映されます。'
})
public baseVolume: number = 1.0;
@property({
tooltip: '音量も微妙にランダム化するかどうか。ON にすると baseVolume を中心に揺らぎます。'
})
public randomizeVolume: boolean = false;
@property({
tooltip: '音量ランダム幅(0〜1)。0.1 の場合、baseVolume × [0.9〜1.1] の範囲で変動します。'
})
public volumeJitter: number = 0.1;
@property({
tooltip: 'テスト用:ON にすると、testInterval 秒ごとに自動で足音を再生します。'
})
public autoTestPlay: boolean = false;
@property({
tooltip: '自動テスト再生モード時の再生間隔(秒)。',
min: 0.05
})
public testInterval: number = 0.5;
@property({
tooltip: 'このノードに AudioSource が見つからない場合に警告ログを出すかどうか。'
})
public logWarningIfMissingAudio: boolean = true;
private _audioSource: AudioSource | null = null;
private _testTimer: number = 0;
onLoad() {
// 必要な AudioSource コンポーネントを取得
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource) {
if (this.logWarningIfMissingAudio) {
warn('[FootstepRandomizer] このノードに AudioSource がアタッチされていません。足音は再生されません。');
}
}
// minPitch と maxPitch の整合性を確保
if (this.minPitch > this.maxPitch) {
const tmp = this.minPitch;
this.minPitch = this.maxPitch;
this.maxPitch = tmp;
warn('[FootstepRandomizer] minPitch が maxPitch より大きかったため、値を入れ替えました。');
}
// volumeJitter は 0〜1 の範囲にクランプ
this.volumeJitter = clamp01(this.volumeJitter);
// baseVolume も 0〜1 にクランプ
this.baseVolume = clamp01(this.baseVolume);
}
start() {
// AudioSource がある場合、初期音量を設定
if (this._audioSource) {
this._audioSource.volume = this.baseVolume;
}
}
update(deltaTime: number) {
// 自動テスト再生モード
if (!this.autoTestPlay) {
return;
}
if (!this._audioSource) {
// AudioSource がない場合は何もしない
return;
}
this._testTimer += deltaTime;
if (this._testTimer >= this.testInterval) {
this._testTimer = 0;
this.playFootstep();
}
}
/**
* 外部から呼び出して足音を再生するためのメソッド。
* - Animation Event からも直接呼び出せます。
*/
public playFootstep(): void {
if (!this._audioSource) {
if (this.logWarningIfMissingAudio) {
warn('[FootstepRandomizer] AudioSource が見つからないため、playFootstep() は何も行いません。');
}
return;
}
// ランダムなピッチを決定
const pitch = randomRange(this.minPitch, this.maxPitch);
this._audioSource.pitch = pitch;
// 音量の設定
let volume = this.baseVolume;
if (this.randomizeVolume && this.volumeJitter > 0) {
const minFactor = 1.0 - this.volumeJitter;
const maxFactor = 1.0 + this.volumeJitter;
const volumeFactor = randomRange(minFactor, maxFactor);
volume = this.baseVolume * volumeFactor;
volume = clamp01(volume);
}
this._audioSource.volume = volume;
// デバッグ用にログを出したい場合はここで log() を使う
// log(`[FootstepRandomizer] Play footstep: pitch=${pitch.toFixed(2)}, volume=${volume.toFixed(2)}`);
// 再生
this._audioSource.play();
}
}
コードのポイント解説
- onLoad()
this.getComponent(AudioSource)で同一ノードの AudioSource を取得します。- 見つからない場合は
warn()で警告を出し、存在しなくてもエラーで落ちない防御的実装 にしています。 minPitch > maxPitchの場合は自動で入れ替えて安全な値に補正します。volumeJitterとbaseVolumeはclamp01で 0〜1 にクランプします。
- start()
- AudioSource が存在する場合、
baseVolumeを初期音量として反映します。
- AudioSource が存在する場合、
- update(deltaTime)
autoTestPlayが ON のときだけ、testInterval秒ごとにplayFootstep()を自動呼び出しします。- 実運用では OFF にしておき、開発・調整中だけ ON にするのが便利です。
- playFootstep()
- 外部スクリプトや Animation Event から呼ぶための公開メソッドです。
- AudioSource がなければ何もせず、必要に応じて警告を出します。
randomRange(minPitch, maxPitch)でピッチをランダムに決定し、this._audioSource.pitchに設定してからplay()します。- 音量ランダム化が有効な場合は、
baseVolume × [1 - volumeJitter 〜 1 + volumeJitter]の範囲で音量を揺らし、clamp01で 0〜1 に収めます。
使用手順と動作確認
1. スクリプトファイルを作成する
- Editor の Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- 作成されたファイル名を
FootstepRandomizer.tsに変更します。 - ダブルクリックして開き、先ほどの TypeScript コードを 丸ごと貼り付け て保存します。
2. テスト用ノードを用意する
- Hierarchy パネルで右クリックし、Create → Empty Node などで新しいノードを作成します。
- 名前は分かりやすく
PlayerFootstepなどにしておきます。
- 名前は分かりやすく
- そのノードを選択し、Inspector で Add Component → Audio → AudioSource を追加します。
- AudioSource の Clip に、足音の AudioClip を設定します。
- 必要に応じて Loop のチェックを外す(足音は通常ループしないため)ようにしてください。
※ AudioSource を付け忘れると、FootstepRandomizer は警告ログを出しますが、ゲームは落ちません。
3. FootstepRandomizer をアタッチする
- 同じノード(
PlayerFootstep)を選択したまま、Inspector 内で Add Component → Custom → FootstepRandomizer を選択します。- Custom カテゴリは、プロジェクト内のユーザースクリプトが表示される場所です。
- Inspector の FootstepRandomizer セクションで、プロパティを確認します。
- Min Pitch: 0.9
- Max Pitch: 1.1
- Base Volume: 1.0
- Randomize Volume: OFF(チェックなし)
- Volume Jitter: 0.1
- Auto Test Play: OFF(チェックなし)
- Test Interval: 0.5
4. エディタ上で動作確認(自動テストモード)
まずは、外部スクリプトを用意せずに「足音が毎回微妙に違うか」を確認してみます。
- FootstepRandomizer の Auto Test Play にチェックを入れます。
- Test Interval を
0.4〜0.6秒程度に設定します。 - 画面上部の Play ボタン(▶)を押してゲームを再生します。
- シーン内で、一定間隔で足音が鳴っていることを確認します。
- ピッチが 0.9〜1.1 の範囲で毎回変化しているため、「同じ音の連打感」がかなり軽減されているはずです。
- さらに変化を確認したい場合は、Max Pitch を 1.2〜1.3 にしてみてください。
- 高くしすぎると違和感が出るので、最終的には好みの値に戻します。
5. 実運用:移動スクリプトやアニメーションから呼び出す
実際のゲームでは、自動テストモードではなく「歩いたタイミングで鳴らす」必要があります。その場合は、以下のように 外部スクリプトから playFootstep() を呼び出すだけで済みます。
例: プレイヤー移動スクリプトから呼ぶ場合(参考コード)
// PlayerMove.ts など(FootstepRandomizer には一切変更不要)
import { _decorator, Component, Node } from 'cc';
import { FootstepRandomizer } from './FootstepRandomizer';
const { ccclass, property } = _decorator;
@ccclass('PlayerMove')
export class PlayerMove extends Component {
@property({ tooltip: '足音を鳴らすノード(FootstepRandomizer + AudioSource が付いているノード)' })
public footstepNode: Node | null = null;
private _footstepRandomizer: FootstepRandomizer | null = null;
start() {
if (this.footstepNode) {
this._footstepRandomizer = this.footstepNode.getComponent(FootstepRandomizer);
}
}
// 例えば、一定距離歩くごとに呼ぶメソッド
private _onStep() {
if (this._footstepRandomizer) {
this._footstepRandomizer.playFootstep();
}
}
}
また、アニメーションイベントから直接呼び出す場合は、以下の手順で行えます。
- 足音を鳴らしたいフレームに Animation Clip のイベントを追加します。
- 関数名に
playFootstepと入力します。 - 対象ノードに FootstepRandomizer コンポーネントがアタッチされていれば、その
playFootstep()が呼び出されます。
このように、FootstepRandomizer 自体は 「AudioSource を持つノードにアタッチして、必要なタイミングで playFootstep() を呼ぶ」だけで完結する独立コンポーネントになっています。
まとめ
- FootstepRandomizer は、AudioSource を持つノードにアタッチするだけで
- 再生のたびにピッチを 0.9〜1.1 の範囲でランダム化し、
- 必要に応じて音量もランダム化できる、
- 外部スクリプトやシングルトンに一切依存しない
汎用コンポーネントです。
- 足音だけでなく、銃声・打撃音・ボタン音など、「同じ SE を連打するすべての場面」でそのまま再利用できます。
- インスペクタからピッチ・音量・自動テスト再生などを簡単に調整できるため、サウンド調整の試行錯誤がエディタ上で完結します。
- 防御的に AudioSource の有無をチェックしており、付け忘れてもゲームが落ちない設計になっています。
このような「アタッチしてメソッドを呼ぶだけで完結する小さなコンポーネント」を積み重ねていくと、プロジェクト全体の再利用性と保守性が大きく向上します。まずは足音から導入し、気に入ったら他の効果音にも同じコンポーネントを使い回してみてください。
