【Cocos Creator】アタッチするだけ!Typewriter (文字送り)の実装方法【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】Typewriter(文字送り)の実装:アタッチするだけで会話風の「一文字ずつ表示」を実現する汎用スクリプト

このガイドでは、Label にアタッチするだけで文字が一文字ずつ表示される「タイプライター演出」を実現する汎用コンポーネント Typewriter を実装します。
会話シーンのセリフ表示、チュートリアルの説明テキスト、RPG のメッセージウィンドウなど、どこにでも使い回せるよう、外部スクリプトに一切依存しない設計にします。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードに Label が存在する前提。
  • Label の string 全体を対象に、visibleCharacters を 0 から 1 文字ずつ増やしていく。
  • 文字送りの速度(1文字ごとの時間)をインスペクタから調整可能にする。
  • 開始タイミング:
    • ノードが有効化されたときに自動で開始(オプション)。
    • API(play(), restart())を呼んで手動開始も可能。
  • 一度表示し終わったあとに、再度同じテキストをタイプライター演出で再生できる。
  • 途中でスキップ(残りを一気に表示)する機能を用意する。
  • テキストの内容は Label 側で自由に変更可能(Typewriter はそれをそのまま使う)。

また、外部依存を完全になくすために、

  • 他の GameManager やシングルトンには一切依存しない。
  • 必要な挙動の ON/OFF や速度などは @property で全て Inspector から設定できるようにする。
  • Label が付いていない場合は エラーログを出して何もしない(ゲームが落ちないよう防御的に実装)。

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

今回の Typewriter コンポーネントで用意するプロパティと役割は以下の通りです。

  • autoPlayOnEnable: boolean
    • ノードが有効化されたとき(onEnable)に自動でタイプライターを開始するかどうか。
    • true:有効化されるたびに先頭から再生。
    • false:自動再生しない。スクリプトから play() / restart() を呼び出して制御。
  • charactersPerSecond: number
    • 1秒あたりに表示する文字数。
    • 例:10 にすると、1秒で約10文字進む速度。
    • 0 以下の値が設定された場合は 自動的にデフォルト値(例:20)に補正する。
  • startDelay: number
    • 再生開始までの待機時間(秒)。
    • 0 ならすぐ開始。0.5 なら 0.5 秒待ってから文字送り開始。
  • playOnAwakeText: string (任意)
    • autoPlayOnEnabletrue のときに、Label にこのテキストをセットしてから再生する。
    • 空文字列の場合は、既に Label に設定されている string をそのまま使用する。
  • loop: boolean
    • 最後まで表示し終えたら、再度先頭から自動的に再生するかどうか。
    • テスト用やデモ用に便利。
  • skipOnComplete: boolean
    • 文字送りが完了したタイミングで、visibleCharacters を -1(全表示)にするかどうか
    • 通常は true で問題ないが、表示文字数を厳密に管理したい用途では false にもできる。

さらに、コードから呼び出せるメソッドとして以下を用意します。

  • play():現在の Label.string を対象に、途中からでも再生開始。
  • restart():Label.string を先頭から再生し直す。
  • skip():残りの文字を一気に表示する。
  • isPlaying():現在タイプライター再生中かどうかを返す。

TypeScriptコードの実装

以下が完成した Typewriter コンポーネントの全コードです。


import { _decorator, Component, Label, log, warn } from 'cc';
const { ccclass, property } = _decorator;

/**
 * Typewriter
 * Label の visibleCharacters を増やしていくことで
 * 一文字ずつテキストを表示するタイプライター演出を行うコンポーネント。
 *
 * 使用手順:
 * 1. Label を持つノードにこのコンポーネントをアタッチする。
 * 2. Inspector で速度や自動再生などを設定する。
 */
@ccclass('Typewriter')
export class Typewriter extends Component {

    @property({
        tooltip: 'ノードが有効化されたときに自動でタイプライター再生を開始するかどうか。',
    })
    public autoPlayOnEnable: boolean = true;

    @property({
        tooltip: '1秒あたりに表示する文字数。\n例: 10 にすると 1秒で約10文字進みます。\n0 以下の値は自動的に 20 に補正されます。',
    })
    public charactersPerSecond: number = 20;

    @property({
        tooltip: '再生開始までの待機時間(秒)。\n0 ならすぐに開始します。',
    })
    public startDelay: number = 0;

    @property({
        tooltip: 'autoPlayOnEnable が true のときに使用するテキスト。\n空文字の場合、既に Label に設定されている文字列をそのまま使用します。',
        multiline: true,
    })
    public playOnAwakeText: string = '';

    @property({
        tooltip: '最後まで表示し終えたら、先頭から自動的に繰り返し再生するかどうか。',
    })
    public loop: boolean = false;

    @property({
        tooltip: '文字送り完了時に visibleCharacters を -1(全表示)にするかどうか。',
    })
    public skipOnComplete: boolean = true;

    // 内部状態
    private _label: Label | null = null;
    private _elapsed: number = 0;           // 再生開始からの経過時間
    private _delayTimer: number = 0;        // startDelay 用タイマー
    private _isPlaying: boolean = false;    // 現在再生中かどうか
    private _totalChars: number = 0;        // 対象テキストの総文字数
    private _currentVisible: number = 0;    // 現在表示中の文字数
    private _originalText: string = '';     // 再生対象のテキスト

    onLoad() {
        // Label コンポーネントを取得
        this._label = this.getComponent(Label);
        if (!this._label) {
            warn('[Typewriter] Label コンポーネントが見つかりません。Typewriter を使用するノードには Label を追加してください。');
            return;
        }

        // visibleCharacters を 0 に初期化(自動再生が無効な場合でも安全)
        this._label.visibleCharacters = 0;

        // デフォルトテキストを保存
        this._originalText = this._label.string;
        this._totalChars = this._originalText.length;
    }

    onEnable() {
        // onLoad の時点で Label がなければ何もしない
        if (!this._label) {
            return;
        }

        // 自動再生が有効ならここで開始
        if (this.autoPlayOnEnable) {
            // playOnAwakeText が設定されていればそれを使用
            if (this.playOnAwakeText && this.playOnAwakeText.length > 0) {
                this._label.string = this.playOnAwakeText;
            }
            this.restart();
        }
    }

    update(deltaTime: number) {
        if (!this._label) {
            return;
        }

        // 再生中でなければ何もしない
        if (!this._isPlaying) {
            return;
        }

        // 開始ディレイ処理
        if (this._delayTimer < this.startDelay) {
            this._delayTimer += deltaTime;
            return;
        }

        // 速度が 0 以下の場合は安全のため補正
        if (this.charactersPerSecond <= 0) {
            this.charactersPerSecond = 20;
            warn('[Typewriter] charactersPerSecond が 0 以下だったため、20 に補正しました。');
        }

        // 経過時間を更新
        this._elapsed += deltaTime;

        // 経過時間から表示すべき文字数を計算
        const charsToShow = Math.floor(this._elapsed * this.charactersPerSecond);

        // 既に表示している文字数より多ければ更新
        if (charsToShow > this._currentVisible) {
            this._currentVisible = charsToShow;
            if (this._currentVisible > this._totalChars) {
                this._currentVisible = this._totalChars;
            }
            this._label.visibleCharacters = this._currentVisible;
        }

        // 最後まで表示し終えたかチェック
        if (this._currentVisible >= this._totalChars) {
            this._onComplete();
        }
    }

    /**
     * 現在設定されている Label.string を対象にタイプライター再生を開始します。
     * 途中から呼び出した場合は、現在のテキストを先頭から再生し直します。
     */
    public play(): void {
        if (!this._label) {
            warn('[Typewriter] Label コンポーネントが見つかりません。play() は無視されます。');
            return;
        }

        this._originalText = this._label.string;
        this._totalChars = this._originalText.length;

        if (this._totalChars === 0) {
            log('[Typewriter] 再生対象のテキストが空です。');
            this._label.visibleCharacters = 0;
            this._isPlaying = false;
            return;
        }

        // 状態リセット
        this._elapsed = 0;
        this._delayTimer = 0;
        this._currentVisible = 0;
        this._label.visibleCharacters = 0;
        this._isPlaying = true;
    }

    /**
     * Label.string を先頭から再生し直します。
     * playOnAwakeText などでテキストを更新したあとに呼び出すことを想定しています。
     */
    public restart(): void {
        this.play();
    }

    /**
     * 残りの文字を一気に表示します。
     * すでに全て表示されている場合は何もしません。
     */
    public skip(): void {
        if (!this._label) {
            return;
        }
        if (!this._isPlaying) {
            return;
        }

        this._currentVisible = this._totalChars;
        this._label.visibleCharacters = this.skipOnComplete ? -1 : this._totalChars;
        this._isPlaying = false;
    }

    /**
     * 現在タイプライター再生中かどうかを返します。
     */
    public isPlaying(): boolean {
        return this._isPlaying;
    }

    /**
     * 再生完了時の処理。
     * loop が true の場合は自動的に再スタートします。
     */
    private _onComplete(): void {
        if (!this._label) {
            return;
        }

        // 完了時の最終表示状態
        if (this.skipOnComplete) {
            // -1 にすると全ての文字が表示される
            this._label.visibleCharacters = -1;
        } else {
            this._label.visibleCharacters = this._totalChars;
        }

        this._isPlaying = false;

        if (this.loop) {
            // ループ再生の場合は再スタート
            this.play();
        }
    }
}

コードのポイント解説

  • onLoad
    • 同じノード上の Label を取得し、存在しなければ warn を出して以降の処理を安全にスキップ。
    • 初期状態として visibleCharacters = 0 にして、テキストを隠した状態から始める。
    • Label.string を保存して、総文字数をカウント。
  • onEnable
    • autoPlayOnEnable が true の場合だけ自動再生。
    • playOnAwakeText が設定されていれば、それを Label.string にセットしてから restart()
  • update
    • _isPlaying が false のときは即 return。
    • startDelay の秒数だけ待機してから文字送りを始める。
    • charactersPerSecond が 0 以下なら 20 に補正し、警告ログを出す。
    • 経過時間 × 速度 から表示すべき文字数を算出し、Label.visibleCharacters に反映。
    • 全ての文字を表示し終えたら _onComplete() を呼ぶ。
  • play / restart
    • 現在の Label.string を対象に、先頭から文字送りを開始する。
    • 内部タイマーと表示文字数をリセットして、_isPlaying = true にする。
  • skip
    • 再生中のみ有効。
    • 残りの文字を一気に表示し、_isPlaying = false にして完了状態にする。
    • skipOnComplete が true の場合は visibleCharacters を -1 にして完全表示。
  • _onComplete
    • 最後まで到達したときに呼ばれる内部メソッド。
    • 最終的な visibleCharacters を設定し、loop が true なら自動的に play() で再スタート。

使用手順と動作確認

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

  1. エディタ上部の Assets パネルで、任意のフォルダ(例:assets/scripts)を右クリックします。
  2. Create → TypeScript を選択します。
  3. 作成されたファイル名を Typewriter.ts に変更します。
  4. ダブルクリックしてエディタで開き、上記の TypeScript コードを 全て貼り付けて保存します。

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

  1. Hierarchy パネルで右クリック → Create → UI → Label を選択し、Label ノードを作成します。
  2. 作成された Label ノードを選択し、Inspector を確認します。
  3. Label コンポーネントの String 欄に、テスト用のテキストを入力します。例:
    • こんにちは!これはタイプライター演出のテストです。

3. Typewriter コンポーネントのアタッチ

  1. 先ほど作成した Label ノードを選択した状態で、Inspector 下部の Add Component ボタンをクリックします。
  2. Custom カテゴリから Typewriter を選択して追加します。
    • もし Custom 内に見つからない場合は、スクリプト名と @ccclass('Typewriter') のクラス名が一致しているか確認してください。

4. Inspector でプロパティを設定

Typewriter コンポーネントを選択すると、以下のプロパティが表示されます。用途に応じて設定してください。

  • Auto Play On Enable(autoPlayOnEnable)
    • テストしやすいように、まずは チェック ON(true) にしておきます。
  • Characters Per Second(charactersPerSecond)
    • 例:20 と入力すると、1秒に約20文字のペースで表示されます。
    • ゆっくり見せたい場合は 5~10、早く見せたい場合は 30~60 などに調整してください。
  • Start Delay(startDelay)
    • 例:0.5 とすると、シーン開始から 0.5 秒後に文字送りが始まります。
    • 不要であれば 0 のままで構いません。
  • Play On Awake Text(playOnAwakeText)
    • 空欄のままにすると、Label に既に設定されているテキストがそのまま使われます。
    • ここにテキストを入力すると、Label.string を上書きしてからタイプライター再生します。
    • 例:
      • 勇者よ、よくぞここまで来た。\nこれから旅の説明をしよう。
  • Loop(loop)
    • デモ用途で ずっと繰り返し表示したい場合はチェック ON。
    • 通常の会話シーンでは OFF のままで問題ありません。
  • Skip On Complete(skipOnComplete)
    • 通常は ON(true)のままで構いません。
    • OFF にすると、visibleCharacters が総文字数で止まり、-1 にはなりません。

5. シーンを再生して動作確認

  1. エディタ上部の ▶(Play)ボタン をクリックしてシーンを再生します。
  2. ゲームビューで、Label のテキストが 一文字ずつ表示されていくことを確認します。
  3. 速度が速すぎる/遅すぎると感じたら、再生を止めて Characters Per Second の値を調整し、再度再生してみてください。

6. コードからの制御(任意)

他のスクリプトから再生開始・スキップなどを制御したい場合も、このコンポーネントだけで完結します。例えば:


// 例: 同じノード上の Typewriter を取得して制御する
import { _decorator, Component } from 'cc';
import { Typewriter } from './Typewriter';
const { ccclass } = _decorator;

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

    start() {
        const typewriter = this.getComponent(Typewriter);
        if (!typewriter) {
            return;
        }

        // 手動で再生開始
        typewriter.play();

        // 何かの条件でスキップしたい場合
        // typewriter.skip();
    }
}

このように、Typewriter 自体は他のスクリプトに依存せず、必要であれば他のスクリプト側から利用することもできます。


まとめ

このガイドでは、Cocos Creator 3.8.7 と TypeScript を使って、

  • Label にアタッチするだけで動く、汎用的なタイプライター演出コンポーネント Typewriter を実装しました。
  • 速度・開始ディレイ・自動再生・ループ・完了時の挙動などを すべて Inspector から調整可能にしました。
  • Label が存在しない場合のエラーハンドリングなど、防御的な実装も行いました。

このコンポーネントは、

  • 会話シーンのセリフ表示
  • チュートリアルの説明テキスト
  • タイトル画面のキャッチコピー演出
  • ログ・通知メッセージの演出

など、テキストを見せるあらゆる場面で再利用できます。
プロジェクトごとに GameManager を書き換えたり、毎回コピペする必要はなく、この Typewriter.ts を 1 ファイル追加してアタッチするだけで同じ体験を提供できます。

ここからさらに、

  • 1文字ごとに SE を鳴らす
  • 特定の文字(句読点など)で一時停止を入れる
  • リッチテキスト対応(色付き文字など)に拡張する

といった発展も可能です。まずはこのベースとなる汎用コンポーネントをプロジェクトに取り入れ、文字演出の実装負荷を大きく下げてみてください。

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をコピーしました!