【Cocos Creator】アタッチするだけ!TypingAudio (文字送り音)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】TypingAudio の実装:アタッチするだけで「文字送りに同期したポポポ音再生」を実現する汎用スクリプト

ノベルゲームや会話シーンで、文字が一文字ずつ表示されるタイミングに合わせて「ポポポ」と効果音を鳴らしたい場面はとても多いです。
この記事では、任意のダイアログ処理から簡単に呼び出せて、アタッチするだけで使える「文字送り音」専用コンポーネント TypingAudio を実装します。

外部の GameManager などには一切依存せず、このコンポーネント単体で完結する設計にします。


コンポーネントの設計方針

1. 機能要件の整理

  • ダイアログで文字が 1 文字出るタイミングに合わせて「ポポポ」音を再生する。
  • 再生トリガーは「外部からメソッドを呼ぶ」だけで完結する(ダイアログ側はこのメソッドを呼ぶだけ)。
  • 一定間隔よりも短い頻度では鳴らさない(高速な文字送り時に音がうるさくなりすぎないようにする)。
  • 連続で鳴らすときに音が重なりすぎないよう、必要に応じて「前の音を止めてから再生」や「同時再生数の制限」ができる。
  • インスペクタから音量や再生間隔などを調整できる。
  • 外部のカスタムスクリプトには依存せず、AudioSource などの標準コンポーネントだけを使用する。

2. 外部依存をなくすための設計アプローチ

  • 再生用メソッドを公開:
    • playOnType() を public メソッドとして用意し、ダイアログ側から 1 文字表示ごとに呼ぶだけで音が鳴るようにする。
    • TypingAudio がダイアログの状態を知る必要はない。
  • AudioSource は自ノードに持つ:
    • TypingAudio がアタッチされたノードに AudioSource を持たせる。
    • 存在しない場合は getComponent(AudioSource) で取得を試み、無ければ警告を出す。
  • インスペクタから全て設定可能:
    • 効果音クリップ、最小再生間隔、音量、ピッチ、同時再生数などを @property で公開する。

3. インスペクタで設定可能なプロパティ設計

TypingAudio コンポーネントに持たせるプロパティとその役割は以下の通りです。

  • audioSource (AudioSource)
    • このコンポーネントが利用する AudioSource への参照。
    • 未設定の場合は onLoad で自ノードから自動取得を試みる。
  • typingClip (AudioClip)
    • 文字送り時に再生する「ポポポ」音のクリップ。
    • 必須。未設定の場合はエラーログを出して再生をスキップする。
  • minInterval (number)
    • 連続して音を鳴らす際の最小間隔(秒)
    • 例:0.03〜0.08 あたりがおすすめ。高速文字送り時の「音鳴りすぎ」を防ぐ。
  • volume (number)
    • 文字送り音の音量(0〜1)。
    • AudioSource の volume を一時的に上書きして再生する。
  • pitch (number)
    • 再生ピッチ(1 が通常)。
    • 若干高め(1.05〜1.2)にすると軽い「ポポポ」感が出やすい。
  • randomizePitch (boolean)
    • 有効にすると、毎回少しだけピッチをランダムにずらして再生する。
    • 同じ音でもわずかに変化をつけて耳障りを軽減する。
  • randomPitchRange (number)
    • randomizePitch 有効時、ピッチを ±この値だけランダムに変動させる。
    • 例:0.05 なら、pitch ± 0.05 の範囲で揺らぐ。
  • interruptPrevious (boolean)
    • true: 前の再生が残っていても stop() してから再度再生する。
    • false: AudioSource の仕様に従う(通常は前の音がフェードアウトするような挙動)。
  • maxSimultaneous (number)
    • 同時再生数の上限。interruptPrevious が false の場合の安全装置。
    • 上限に達した場合は新しい再生をスキップする。
    • 1 にするとほぼ「1音だけ」を保つ挙動になる。
  • debugLog (boolean)
    • 挙動をコンソールにログ出力するかどうか。
    • 音が鳴らないときの原因調査用。

これらをまとめて、ダイアログ側からは playOnType() だけを呼べば良いように実装します。


TypeScriptコードの実装


import { _decorator, Component, AudioSource, AudioClip, sys } from 'cc';
const { ccclass, property } = _decorator;

/**
 * TypingAudio
 * 
 * ダイアログの「1文字表示」に合わせて呼び出すことで
 * 文字送り音(ポポポ)を再生する汎用コンポーネント。
 * 
 * 使い方:
 *   1. ノードにこのコンポーネントをアタッチ
 *   2. AudioSource と AudioClip を設定
 *   3. ダイアログ処理から this.typingAudio.playOnType() を呼ぶ
 */
@ccclass('TypingAudio')
export class TypingAudio extends Component {

    @property({
        type: AudioSource,
        tooltip: '文字送り音を再生する AudioSource。\n未設定の場合は、このノードから自動取得を試みます。'
    })
    public audioSource: AudioSource | null = null;

    @property({
        type: AudioClip,
        tooltip: '文字送り時に再生する AudioClip(ポポポ音など)。\n必ず設定してください。'
    })
    public typingClip: AudioClip | null = null;

    @property({
        tooltip: '連続して音を鳴らす際の最小間隔(秒)。\n高速な文字送り時に音が鳴りすぎるのを防ぎます。'
    })
    public minInterval: number = 0.05;

    @property({
        tooltip: '文字送り音の音量(0.0 ~ 1.0)。\nAudioSource の volume を一時的に上書きして使用します。'
    })
    public volume: number = 0.6;

    @property({
        tooltip: '再生ピッチ。1.0 が標準で、値を上げると高い音になります。'
    })
    public pitch: number = 1.0;

    @property({
        tooltip: '有効にすると、毎回ピッチを少しだけランダムに変動させます。'
    })
    public randomizePitch: boolean = false;

    @property({
        tooltip: 'randomizePitch が有効なとき、ピッチを ±この値の範囲でランダムに変動させます。'
    })
    public randomPitchRange: number = 0.05;

    @property({
        tooltip: 'true の場合、前回の再生が残っていても stop() してから再生します。'
    })
    public interruptPrevious: boolean = true;

    @property({
        tooltip: '同時再生数の上限。interruptPrevious が false の場合の安全装置として使います。\n1 にすると常に 1 音だけが鳴るような挙動になります。'
    })
    public maxSimultaneous: number = 3;

    @property({
        tooltip: 'デバッグログをコンソールに出力するかどうか。問題があるときだけ有効にしてください。'
    })
    public debugLog: boolean = false;

    // 内部状態管理用
    private _lastPlayTime: number = -9999;
    private _playingCount: number = 0;

    onLoad() {
        // AudioSource が未指定なら自ノードから取得を試みる
        if (!this.audioSource) {
            const found = this.getComponent(AudioSource);
            if (found) {
                this.audioSource = found;
                if (this.debugLog) {
                    console.log('[TypingAudio] AudioSource を自ノードから自動取得しました。');
                }
            } else {
                console.warn('[TypingAudio] AudioSource が見つかりません。このノードに AudioSource コンポーネントを追加してください。');
            }
        }

        // 数値プロパティの防御的クランプ
        if (this.minInterval < 0) {
            console.warn('[TypingAudio] minInterval が 0 未満だったため 0 に補正しました。');
            this.minInterval = 0;
        }
        if (this.volume < 0) {
            console.warn('[TypingAudio] volume が 0 未満だったため 0 に補正しました。');
            this.volume = 0;
        }
        if (this.volume > 1) {
            console.warn('[TypingAudio] volume が 1 より大きかったため 1 に補正しました。');
            this.volume = 1;
        }
        if (this.maxSimultaneous < 1) {
            console.warn('[TypingAudio] maxSimultaneous は 1 以上である必要があるため 1 に補正しました。');
            this.maxSimultaneous = 1;
        }
        if (this.randomPitchRange < 0) {
            console.warn('[TypingAudio] randomPitchRange が 0 未満だったため 0 に補正しました。');
            this.randomPitchRange = 0;
        }
    }

    /**
     * ダイアログ側から「1文字表示されたタイミング」で呼び出すメソッド。
     * 
     * 例:
     *   // ダイアログスクリプト側
     *   this.typingAudio.playOnType();
     */
    public playOnType() {
        // AudioSource / Clip の存在チェック
        if (!this.audioSource) {
            console.warn('[TypingAudio] AudioSource が設定されていないため、音を再生できません。');
            return;
        }
        if (!this.typingClip) {
            console.warn('[TypingAudio] typingClip が設定されていないため、音を再生できません。');
            return;
        }

        const now = this._getTime();

        // 最小再生間隔のチェック
        if (now - this._lastPlayTime < this.minInterval) {
            if (this.debugLog) {
                console.log('[TypingAudio] minInterval により再生をスキップしました。');
            }
            return;
        }

        // 同時再生数のチェック
        if (!this.interruptPrevious && this._playingCount >= this.maxSimultaneous) {
            if (this.debugLog) {
                console.log('[TypingAudio] maxSimultaneous により再生をスキップしました。');
            }
            return;
        }

        // 実際の再生処理
        this._playInternal(now);
    }

    /**
     * 実際の AudioSource 再生処理
     */
    private _playInternal(now: number) {
        if (!this.audioSource || !this.typingClip) {
            return;
        }

        // 前の音を止める設定なら stop()
        if (this.interruptPrevious) {
            this.audioSource.stop();
            this._playingCount = 0;
        }

        // ピッチの決定
        let pitchToUse = this.pitch;
        if (this.randomizePitch && this.randomPitchRange > 0) {
            const r = (Math.random() * 2 - 1) * this.randomPitchRange; // -range ~ +range
            pitchToUse += r;
        }

        // AudioSource に設定を反映
        this.audioSource.clip = this.typingClip;
        this.audioSource.volume = this.volume;
        this.audioSource.pitch = pitchToUse;

        // 再生
        this.audioSource.play();
        this._lastPlayTime = now;
        this._playingCount++;

        if (this.debugLog) {
            console.log(`[TypingAudio] 再生: volume=${this.volume.toFixed(2)}, pitch=${pitchToUse.toFixed(2)}, playingCount=${this._playingCount}`);
        }

        // 再生終了を検知して _playingCount を減らすための簡易ポーリング
        // Cocos 3.8 の AudioSource には完了コールバックがないため、
        // 次フレーム以降の update で監視する方式にしています。
    }

    update(deltaTime: number) {
        // playingCount のざっくり管理:
        // AudioSource が再生していない状態なら playingCount を 0 にリセットする。
        if (this.audioSource && !this.audioSource.playing && this._playingCount !== 0) {
            this._playingCount = 0;
            if (this.debugLog) {
                console.log('[TypingAudio] 再生終了を検知して playingCount をリセットしました。');
            }
        }
    }

    /**
     * 現在時刻を秒単位で返すユーティリティ。
     * ブラウザ・ネイティブどちらでも使えるように sys.now() を使用。
     */
    private _getTime(): number {
        // sys.now() はミリ秒なので秒に変換
        return sys.now() / 1000;
    }
}

コードのポイント解説

  • onLoad()
    • audioSource がインスペクタで未指定の場合、this.getComponent(AudioSource) で自ノードから自動取得します。
    • 数値プロパティ(minInterval, volume, maxSimultaneous, randomPitchRange)に対して防御的な補正を行います。
  • playOnType()
    • ダイアログ側から呼ぶメインの API です。
    • AudioSource / AudioClip の存在チェック、最小再生間隔チェック、同時再生数チェックを行った上で、_playInternal() を呼びます。
  • _playInternal()
    • interruptPrevious が true の場合、audioSource.stop() で前の音を止めてから再生します。
    • ピッチは pitchrandomizePitch / randomPitchRange を元に決定します。
    • AudioSource に clip, volume, pitch を設定して play() を呼びます。
    • _lastPlayTime_playingCount を更新します。
  • update()
    • Cocos 3.8 の AudioSource には「再生終了コールバック」がないため、update 内で audioSource.playing を監視しています。
    • 再生が終わっていると判断したら _playingCount を 0 にリセットします。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ下部の Assets パネルで、スクリプトを置きたいフォルダ(例:assets/scripts)を選択します。
  2. そのフォルダで右クリック → CreateTypeScript を選択します。
  3. 新しく作成されたファイル名を TypingAudio.ts に変更します。
  4. TypingAudio.ts をダブルクリックして開き、テンプレートコードをすべて削除し、先ほど紹介した 全コード を貼り付けて保存します。

2. テスト用ノードの作成

まずは単純に「キー入力で 1 文字出た」と仮定して音が鳴るかを確認します。

  1. Hierarchy パネルで右クリック → CreateEmpty Node を選択し、名前を TypingAudioTester などに変更します。
  2. TypingAudioTester ノードを選択した状態で、右側の Inspector を確認します。
  3. Add Component ボタンをクリック → AudioAudioSource を選択します。
    • これでノードに AudioSource コンポーネントが追加されます。
  4. もう一度 Add ComponentCustomTypingAudio を選択して、TypingAudio コンポーネントをアタッチします。

3. TypingAudio のプロパティ設定

Inspector 上で TypingAudio コンポーネントの各プロパティを設定します。

  1. audioSource:
    • 空欄のままでも構いません。
      同じノードに AudioSource があるため、onLoad 時に自動取得されます。
    • 明示的に設定したい場合は、ドラッグ&ドロップで同じノードの AudioSource を入れてください。
  2. typingClip:
    • 事前にインポートしておいた「ポポポ音」の AudioClip を、ここにドラッグ&ドロップします。
    • 未設定だと音は鳴らず、コンソールに警告が出ます。
  3. minInterval:
    • まずは 0.05(50ms)くらいから試すのがおすすめです。
    • 文字送りが速いのに音が追いつかないと感じたら 0.03 などに下げてみてください。
  4. volume:
    • 全体の BGM とのバランスを見て、0.4 ~ 0.7 程度に調整します。
  5. pitch:
    • 標準のままで良ければ 1.0
    • 軽めのポポポ感を出したければ 1.05 ~ 1.2 を試してみてください。
  6. randomizePitch / randomPitchRange:
    • 同じ音が続くと耳につく場合は randomizePitch = true にします。
    • randomPitchRange = 0.03 ~ 0.07 程度が自然に聞こえやすいです。
  7. interruptPrevious:
    • 通常は true のままで問題ありません。
    • もし余韻を残したい場合は false にして、maxSimultaneous を 2〜3 に設定してみてください。
  8. maxSimultaneous:
    • interruptPrevious = true の場合は 1 のままで OK です(実質使われません)。
    • interruptPrevious = false のときは、2 ~ 3 程度にしておくと音が濁りにくくなります。
  9. debugLog:
    • 最初の動作確認時に有効にすると、コンソールに「鳴った/スキップされた」理由が出るので調整しやすくなります。
    • リリース時は false に戻しておくと良いでしょう。

4. 簡易テスト(キー入力で鳴らしてみる)

ダイアログシステムとつなぐ前に、キー入力で playOnType() を呼んで音が鳴るかを確認します。
テスト用に、TypingAudioTester ノードに以下のような簡単なスクリプトを追加します。

  1. Assets パネルで右クリック → CreateTypeScript を選択し、TypingAudioTestInput.ts を作成します。
  2. 以下のコードを貼り付けます(他のカスタムスクリプトには依存しません):

import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { TypingAudio } from './TypingAudio';
const { ccclass, property } = _decorator;

@ccclass('TypingAudioTestInput')
export class TypingAudioTestInput extends Component {

    @property({
        type: TypingAudio,
        tooltip: 'テスト対象の TypingAudio コンポーネント'
    })
    public typingAudio: TypingAudio | null = null;

    onLoad() {
        if (!this.typingAudio) {
            const found = this.getComponent(TypingAudio);
            if (found) {
                this.typingAudio = found;
            } else {
                console.warn('[TypingAudioTestInput] TypingAudio が設定されていません。');
            }
        }
    }

    start() {
        input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    private onKeyDown(event: EventKeyboard) {
        if (event.keyCode === KeyCode.SPACE) {
            if (this.typingAudio) {
                this.typingAudio.playOnType();
            }
        }
    }
}
  1. TypingAudioTester ノードを選択し、Inspector から Add Component → Custom → TypingAudioTestInput を追加します。
  2. TypingAudioTestInputtypingAudio プロパティに、同じノードの TypingAudio をドラッグ&ドロップで設定します。
  3. 再生ボタン(▶)でゲームを実行し、ゲームビューがアクティブな状態で スペースキー を押すたびに「ポポポ」音が鳴ることを確認します。

ここまでで TypingAudio 自体の動作確認は完了です。

5. 実際のダイアログシステムへの組み込み例

ダイアログ側の実装はプロジェクトごとに異なりますが、基本的な使い方は以下のようになります。

  • ダイアログの 1 文字表示ごとに typingAudio.playOnType() を呼ぶ。

// ダイアログ表示を行うスクリプトの一部例
import { _decorator, Component } from 'cc';
import { TypingAudio } from './TypingAudio';
const { ccclass, property } = _decorator;

@ccclass('SimpleDialog')
export class SimpleDialog extends Component {

    @property(TypingAudio)
    public typingAudio: TypingAudio | null = null;

    private _fullText: string = 'これはサンプルのダイアログテキストです。';
    private _currentIndex: number = 0;
    private _interval: number = 0.05;
    private _timer: number = 0;

    update(deltaTime: number) {
        this._timer += deltaTime;
        if (this._timer >= this._interval && this._currentIndex < this._fullText.length) {
            this._timer = 0;
            const ch = this._fullText[this._currentIndex];
            this._currentIndex++;

            // ここで実際には Label などに文字を追加する処理を書く
            // this.label.string += ch;

            // 1文字出たタイミングで TypingAudio を呼ぶ
            if (this.typingAudio) {
                this.typingAudio.playOnType();
            }
        }
    }
}

このように、ダイアログ側は「文字が 1 文字出たときに TypingAudio.playOnType() を呼ぶ」だけで済むようになっています。


まとめ

  • TypingAudio は、文字送りに同期した効果音再生に特化した汎用コンポーネントです。
  • 外部の GameManager やシングルトンには一切依存せず、このスクリプト単体で完結しています。
  • typingClipminIntervalvolumepitchrandomizePitch などをインスペクタから調整することで、ゲームの雰囲気に合わせた「ポポポ音」を簡単にチューニングできます。
  • ダイアログシステム側は、文字を 1 文字表示するたびに playOnType() を呼ぶだけなので、既存のコードに最小限の変更で組み込めます。

このコンポーネントをベースに、例えば「キャラクターごとに違う声色のタイピング音にする」「シーンごとに音量やピッチを変える」といった拡張も、ノードごとに TypingAudio をアタッチして設定を変えるだけで実現できます。
ダイアログ演出のクオリティアップに、ぜひ活用してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!