【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 (任意)
autoPlayOnEnableがtrueのときに、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. スクリプトファイルの作成
- エディタ上部の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択します。
- 作成されたファイル名を
Typewriter.tsに変更します。 - ダブルクリックしてエディタで開き、上記の TypeScript コードを 全て貼り付けて保存します。
2. テスト用ノード(Label)の作成
- Hierarchy パネルで右クリック → Create → UI → Label を選択し、Label ノードを作成します。
- 作成された Label ノードを選択し、Inspector を確認します。
- Label コンポーネントの String 欄に、テスト用のテキストを入力します。例:
こんにちは!これはタイプライター演出のテストです。
3. Typewriter コンポーネントのアタッチ
- 先ほど作成した Label ノードを選択した状態で、Inspector 下部の Add Component ボタンをクリックします。
- Custom カテゴリから Typewriter を選択して追加します。
- もし Custom 内に見つからない場合は、スクリプト名と
@ccclass('Typewriter')のクラス名が一致しているか確認してください。
- もし Custom 内に見つからない場合は、スクリプト名と
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. シーンを再生して動作確認
- エディタ上部の ▶(Play)ボタン をクリックしてシーンを再生します。
- ゲームビューで、Label のテキストが 一文字ずつ表示されていくことを確認します。
- 速度が速すぎる/遅すぎると感じたら、再生を止めて 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 を鳴らす
- 特定の文字(句読点など)で一時停止を入れる
- リッチテキスト対応(色付き文字など)に拡張する
といった発展も可能です。まずはこのベースとなる汎用コンポーネントをプロジェクトに取り入れ、文字演出の実装負荷を大きく下げてみてください。




