【Cocos Creator 3.8】CoinJingle の実装:アタッチするだけで「コイン取得時のピッチが半音ずつ上がる爽快サウンド」を実現する汎用スクリプト
コインを連続で取得したとき、効果音のピッチが少しずつ高くなっていくと、プレイヤーは「コンボしている」感覚を得やすくなります。このガイドでは、任意のノードにアタッチして 「コイン取得時に呼び出すだけ」 で、ピッチが半音ずつ上昇していくコイン音を再生できる汎用コンポーネント CoinJingle を実装します。
外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結 する設計にしているため、どのプロジェクトでも再利用しやすい構成になっています。
コンポーネントの設計方針
1. 機能要件の整理
- コイン取得時に 効果音を再生 する。
- 短時間に連続で取得した場合、ピッチを半音ずつ上げていく。
- 一定時間コインを取らなかったら、ピッチを初期値にリセット する。
- 最大ピッチに達したら、それ以上は上げずに 上限で固定 する。
- 外部スクリプトに依存せず、Inspector から音源やパラメータを指定するだけ で動作する。
- 利用側は
playCoin()を呼ぶだけ でよい API にする。
2. 外部依存をなくす設計アプローチ
- 効果音の再生には Cocos Creator 標準の
AudioSourceコンポーネントを利用。 CoinJingleは自ノードのAudioSourceをgetComponent(AudioSource)で取得し、存在しない場合は警告ログを出す。- Inspector から
AudioClipを指定しておき、毎回それを再生する。 - ピッチ制御は
AudioSource.pitchを直接操作する。 - コイン取得間隔の判定は
lastPlayTimeを保持し、game.totalTime(またはdirector.getTotalTime())で経過時間を確認する。
3. インスペクタで設定可能なプロパティ
CoinJingle コンポーネントのプロパティ設計
coinClip: AudioClip | null- コイン取得時に再生する効果音。
- Inspector で
.mp3 / .wav / .oggなどの AudioClip を指定。 - 未指定の場合はエラーログを出して再生しない。
basePitch: number- ピッチの基準値(通常は
1.0)。 - この値から半音ステップで上下する。
- 例:0.9 にすると、全体的に少し低めの音程からスタート。
- ピッチの基準値(通常は
semitoneStep: number- 1回の連続取得ごとに上げる半音数。
- 通常は
1(半音ずつ)。 - 2 にすると全音(2半音)ずつ上昇するなど、好みに応じて調整可能。
maxSemitoneOffset: number- 基準ピッチから何半音分まで上げるかの上限。
- 例:
12なら 1オクターブ上まで。 - 上限を超えると、それ以上はピッチを上げない。
comboResetTime: number- コンボ継続とみなす最大間隔(秒)。
- 最後にコイン音を鳴らしてから、この秒数を超えるとコンボリセット(ピッチもリセット)。
- 例:
0.4秒にすると、0.4秒以内の連続取得でピッチ上昇。
autoResetOnDisable: boolean- コンポーネントが
onDisableされたときにピッチとコンボをリセットするか。 - シーン遷移やノードの一時無効化で状態を引き継ぎたくない場合は
true推奨。
- コンポーネントが
logDebug: boolean- デバッグ用ログ出力の ON/OFF。
- 有効にすると、コンボ数や現在ピッチなどを
console.logに出力。
半音ステップの計算には音楽理論でよく使われる式を用います:
pitch = basePitch * 2^(semitoneOffset / 12)
例:basePitch = 1.0、semitoneOffset = 12 のとき、pitch = 2.0(1オクターブ上)。
TypeScriptコードの実装
以下が完成した CoinJingle.ts の全コードです。
import { _decorator, Component, AudioSource, AudioClip, game, clamp, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* CoinJingle
* コイン取得時に呼び出すと、ピッチが半音ずつ上昇していく効果音コンポーネント。
* - 他のスクリプトに依存せず、このコンポーネント単体で完結。
* - 任意のノードにアタッチし、playCoin() を呼び出すだけで使用可能。
*/
@ccclass('CoinJingle')
export class CoinJingle extends Component {
@property({
type: AudioClip,
tooltip: 'コイン取得時に再生する効果音。\n未指定の場合はエラーを出力し、再生しません。'
})
public coinClip: AudioClip | null = null;
@property({
tooltip: '基準となるピッチ値。\n通常は 1.0(原音の高さ)。\n全体的に低め/高めにしたい場合に調整します。'
})
public basePitch: number = 1.0;
@property({
tooltip: '1回の連続取得ごとに上昇させる半音数。\n1 = 半音ずつ、2 = 全音(2半音)ずつ上昇。'
})
public semitoneStep: number = 1;
@property({
tooltip: '基準ピッチから上昇させる最大半音数。\n例: 12 なら 1オクターブ上まで。'
})
public maxSemitoneOffset: number = 12;
@property({
tooltip: '最後にコイン音を再生してから、コンボ継続とみなす最大時間(秒)。\nこの時間を超えるとコンボとピッチをリセットします。'
})
public comboResetTime: number = 0.4;
@property({
tooltip: 'コンポーネントが無効化されたときに、コンボ状態とピッチをリセットするかどうか。'
})
public autoResetOnDisable: boolean = true;
@property({
tooltip: 'デバッグ用ログを出力するかどうか。開発中のみ有効にすることを推奨します。'
})
public logDebug: boolean = false;
private _audioSource: AudioSource | null = null;
private _currentSemitoneOffset: number = 0;
private _lastPlayTime: number = -1;
onLoad() {
// AudioSource を取得(存在しない場合は自動追加してもよいが、ここでは警告のみにする)
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource) {
console.warn(
'[CoinJingle] AudioSource コンポーネントが見つかりません。' +
'ノードに AudioSource を追加してください。ノード名:',
this.node.name
);
}
// 初期状態リセット
this._resetComboInternal();
}
onEnable() {
// 有効化時に必要であれば状態を初期化
// 今回は特に何もしない
}
onDisable() {
if (this.autoResetOnDisable) {
this._resetComboInternal();
}
}
/**
* コイン取得時に呼び出す公開メソッド。
* - ピッチを計算して AudioSource から coinClip を再生します。
*/
public playCoin(): void {
if (!this._audioSource) {
console.error(
'[CoinJingle] AudioSource が存在しないため、コイン音を再生できません。' +
'ノードに AudioSource を追加してください。ノード名:',
this.node.name
);
return;
}
if (!this.coinClip) {
console.error('[CoinJingle] coinClip が設定されていません。Inspector で AudioClip を指定してください。');
return;
}
const now = game.totalTime / 1000; // ミリ秒 → 秒
// 前回再生からの経過時間をチェックして、コンボ継続かどうか判定
if (this._lastPlayTime < 0) {
// 初回再生
this._currentSemitoneOffset = 0;
} else {
const delta = now - this._lastPlayTime;
if (delta > this.comboResetTime) {
// 一定時間以上空いたのでコンボリセット
this._currentSemitoneOffset = 0;
} else {
// コンボ継続中なので半音数を増加
this._currentSemitoneOffset += this.semitoneStep;
// 上限クランプ
this._currentSemitoneOffset = clamp(
this._currentSemitoneOffset,
0,
this.maxSemitoneOffset
);
}
}
this._lastPlayTime = now;
// 半音数からピッチ値を計算
const pitchMultiplier = Math.pow(2, this._currentSemitoneOffset / 12);
const finalPitch = this.basePitch * pitchMultiplier;
// AudioSource に設定
this._audioSource.pitch = finalPitch;
this._audioSource.clip = this.coinClip;
// すでに再生中の場合は一度停止してから再生することで、連打時も頭から鳴らす
if (this._audioSource.playing) {
this._audioSource.stop();
}
this._audioSource.play();
if (this.logDebug) {
console.log(
`[CoinJingle] playCoin(): semitoneOffset=${this._currentSemitoneOffset}, ` +
`pitch=${finalPitch.toFixed(3)}, time=${now.toFixed(3)}`
);
}
}
/**
* コンボ状態とピッチをリセットする公開メソッド。
* 外部から明示的にリセットしたい場合に使用できます。
*/
public resetCombo(): void {
this._resetComboInternal();
if (this.logDebug) {
console.log('[CoinJingle] resetCombo() が呼び出されました。');
}
}
/**
* 内部用コンボリセット処理。
*/
private _resetComboInternal(): void {
this._currentSemitoneOffset = 0;
this._lastPlayTime = -1;
if (this._audioSource) {
this._audioSource.pitch = this.basePitch;
}
}
}
コードのポイント解説
onLoad()this.getComponent(AudioSource)で同一ノード上のAudioSourceを取得。- 見つからない場合は
console.warnで警告を出し、利用者に Inspector で追加するよう促します。 - 初期状態としてコンボとピッチをリセット。
playCoin()(メイン機能)- 公開メソッドで、コイン取得時に呼ぶ想定。
AudioSourceまたはcoinClipが未設定ならエラーログを出して処理を中断。game.totalTimeから現在時刻を取得し、前回再生時刻との差分でコンボ継続かどうかを判定。- コンボ継続時は
semitoneStep分だけ半音数を加算し、maxSemitoneOffsetでクランプ。 - 半音数から
pitch = basePitch * 2^(semitone/12)を計算し、AudioSource.pitchに適用。 - 連打時も音の頭から鳴らすため、再生中なら
stop()してからplay()。 logDebugがtrueの場合は現在ピッチや半音数をログ出力。
resetCombo()- 外部から明示的にコンボ状態をリセットしたいときに使用する公開メソッド。
- 例えば「ステージクリア」「ダメージを受けた」などのタイミングでコンボを切りたい場合に呼び出せます。
onDisable()autoResetOnDisableが有効なら、ノードが無効化された際にコンボとピッチを初期化。- シーン切り替えや一時的な非表示時に状態を引き継ぎたくない場合に便利です。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
CoinJingle.tsにします。 - 自動生成されたコードをすべて削除し、本記事の 「TypeScriptコードの実装」 セクションのコードを丸ごと貼り付けて保存します。
2. テスト用ノードと AudioSource の用意
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
CoinJingleTesterなどに変更します。 - CoinJingleTester ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
- 検索欄に AudioSource と入力し、AudioSource コンポーネントを追加します。
- 同じく Inspector の Add Component → Custom → CoinJingle を選択してアタッチします。
※ AudioSource を追加し忘れると、再生時にコンソールに警告・エラーが出て音が鳴りません。
3. コイン効果音(AudioClip)の設定
- プロジェクトの assets フォルダに、コイン効果音の
.mp3 / .wav / .oggファイルをドラッグ&ドロップしてインポートします。 - CoinJingleTester ノードを選択し、Inspector の CoinJingle コンポーネントを確認します。
- Coin Clip プロパティに、先ほどインポートした効果音の AudioClip をドラッグ&ドロップして割り当てます。
4. パラメータの調整例
Inspector の CoinJingle コンポーネントで、以下のように設定してみてください。
- Base Pitch:
1.0 - Semitone Step:
1(半音ずつ上昇) - Max Semitone Offset:
12(1オクターブ上まで) - Combo Reset Time:
0.4秒 - Auto Reset On Disable:
true - Log Debug: 開発中は
true、リリース時はfalse
これで「0.4秒以内に連続してコインを取ると、半音ずつ最大1オクターブ上までピッチが上昇する」動作になります。
5. 実際に音を鳴らして確認する(簡易テスト用スクリプト)
ゲーム本体のロジックから playCoin() を呼び出せばよいですが、まずはシンプルなテスト用スクリプトで動作確認してみます。
- Assets パネルで右クリック → Create → TypeScript を選択し、
CoinJingleTestDriver.tsを作成します。 - 以下の簡易テストコードを貼り付けます(このスクリプトはあくまでテスト用で、必須ではありません)。
import { _decorator, Component, KeyCode, input, Input } from 'cc';
import { CoinJingle } from './CoinJingle';
const { ccclass, property } = _decorator;
@ccclass('CoinJingleTestDriver')
export class CoinJingleTestDriver extends Component {
@property({
type: CoinJingle,
tooltip: 'テスト対象の CoinJingle コンポーネントへの参照。\n同じノード上にある場合は自動取得も可能です。'
})
public coinJingle: CoinJingle | null = null;
onLoad() {
if (!this.coinJingle) {
this.coinJingle = this.getComponent(CoinJingle);
}
if (!this.coinJingle) {
console.error('[CoinJingleTestDriver] CoinJingle が見つかりません。Inspector で設定するか、同じノードに追加してください。');
}
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
private _onKeyDown(event: any) {
if (!this.coinJingle) return;
// スペースキーを押すたびにコイン音を再生
if (event.keyCode === KeyCode.SPACE) {
this.coinJingle.playCoin();
}
}
}
このテストドライバを使うには:
- CoinJingleTester ノードを選択し、Inspector の Add Component → Custom → CoinJingleTestDriver を追加します。
- CoinJingleTestDriver の Coin Jingle プロパティは、同じノードに
CoinJingleがあるので自動で埋まるはずです(埋まらない場合は手動でドラッグ&ドロップ)。 - プレビューを開始し、ゲーム画面がアクティブな状態で スペースキーを連打 してみてください。
- 素早く連打するとピッチがどんどん上がり、少し間を空けるとピッチがリセットされるのが分かります。
6. 実際のゲームロジックへの組み込み例
実際のゲームでは、コイン取得時のトリガーから playCoin() を呼び出すだけです。例えば:
// 例: Coin.ts (コインのスクリプト)から呼び出す場合
import { _decorator, Component, Node } from 'cc';
import { CoinJingle } from './CoinJingle';
const { ccclass, property } = _decorator;
@ccclass('Coin')
export class Coin extends Component {
@property({ type: CoinJingle })
public coinJingle: CoinJingle | null = null;
// プレイヤーがこのコインに触れたときに呼ばれる想定
public onPicked() {
if (this.coinJingle) {
this.coinJingle.playCoin();
}
// コイン削除など他の処理...
this.node.destroy();
}
}
このように、「コイン取得時に playCoin() を呼ぶ」 というシンプルな使い方で、連続取得時の爽快なピッチアップ演出を実現できます。
まとめ
- CoinJingle コンポーネント は、任意のノードにアタッチし、Inspector で AudioClip といくつかのパラメータを設定するだけで、「連続取得でピッチが半音ずつ上がるコイン音」 を実現します。
- 外部の GameManager やシングルトンに依存しない設計のため、このスクリプト単体で完結 し、どのプロジェクトにも簡単に持ち込めます。
- ピッチの上がり方(半音ステップ数・最大半音数・コンボ判定時間)を Inspector から調整できるため、ゲームのテンポや難易度に合わせたチューニング がしやすくなっています。
- 防御的な実装として
AudioSourceやcoinClip未設定時にエラーログを出すため、設定ミスも発見しやすい です。
このように、音の演出を 「アタッチしてメソッドを1つ呼ぶだけ」 に切り出した汎用コンポーネントを積み重ねていくことで、ゲーム開発のスピードと再利用性を大きく高めることができます。
プロジェクトごとに効果音やパラメータを変えながら、ぜひ自分のゲームに合った「気持ちいいコイン音」を作り込んでみてください。




