【Cocos Creator】アタッチするだけ!FloatingText (ポップアップ)の実装方法【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】FloatingText の実装:アタッチするだけで「ダメージ数値が頭上にふわっと浮かぶ」ポップアップを実現する汎用スクリプト

この記事では、キャラクターや敵のノードにアタッチするだけで、ダメージ数値などのテキストが頭上にポップアップして、ふわっと浮かびながらフェードアウトする汎用コンポーネント FloatingText を実装します。

外部の GameManager などには一切依存せず、このスクリプト 1 本だけで完結します。Inspector からフォントサイズ・色・浮き上がる距離・再生時間などを調整できるため、さまざまなゲームで再利用できます。


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

機能要件の整理

  • ダメージなどの数値を受け取って、親ノードの頭上に数値テキストを生成する。
  • テキストは
    • 指定した開始位置(親ノードのローカル座標+オフセット)から
    • 指定した距離だけ上方向へ移動しながら
    • 指定した時間でフェードアウトし
    • アニメーション終了後に自動で破棄される
  • どのノードにアタッチしても動作するよう、外部スクリプトには一切依存しない。
  • Canvas / UI シーン内での使用を想定し、LabelUIOpacity を利用して実装する。
  • 攻撃側のスクリプトなどから簡単に呼べるように、public メソッドを用意する:
    • showFloatingText(value: number | string, color?: Color): void
  • 防御的実装として、必要な標準コンポーネントが無い場合は自動追加 or エラーログ出力を行う。

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

FloatingText は、以下のようなプロパティを Inspector から調整できるようにします。

  • 見た目関連
    • fontSize: number
      テキストのフォントサイズ(ピクセル)。例:24。
    • defaultColor: Color
      テキストの基本色。ダメージなら赤系、回復なら緑系など。
    • bold: boolean
      太字にするかどうか。
    • outlineWidth: number
      アウトライン(縁取り)の太さ。0 でアウトライン無し。
    • outlineColor: Color
      アウトラインの色。
  • 動き・アニメーション関連
    • startOffset: Vec3
      親ノードのローカル座標からの開始オフセット。例:(0, 50, 0) で頭上に表示。
    • floatDistance: number
      上方向にどれだけ移動するか(ローカル Y 座標の距離)。例:50。
    • duration: number
      浮遊&フェードアウトにかける総時間(秒)。例:0.8。
    • useScalePop: boolean
      最初に少し大きくなってから縮む「ポップ」演出を行うか。
    • scalePopFactor: number
      ポップ時の最大スケール倍率。例:1.2。
  • 生成位置・親子関係関連
    • useWorldSpace: boolean
      親ノードのワールド座標に追従して UI 上に表示するかどうか。
      – true: 3D キャラの頭上に UI として表示したい場合など(Canvas 内に作成)。
      – false: 親ノードのローカル空間内にそのまま Label を作成。
    • uiRoot: Node | null
      useWorldSpace = true の場合に、生成する Label をぶら下げる Canvas(UI)ノード。未設定なら自動で親を使うかログを出す。
  • 挙動制御
    • autoZOffset: number
      各ポップアップごとに少し z をずらして、重なりを軽減するためのオフセット値。
    • maxConcurrent: number
      同時に存在できる最大ポップアップ数。超えた場合は一番古いものから強制削除。

これらのプロパティをすべて Inspector で設定できるようにし、このコンポーネントをアタッチしたノードに対して showFloatingText() を呼ぶだけで、ポップアップ演出が動作するようにします。


TypeScriptコードの実装

以下が、完成した FloatingText.ts の全コードです。


import { _decorator, Component, Node, Label, Color, Vec3, UITransform, UIOpacity, tween, Tween, v3, math, LabelOutline, director, Canvas } from 'cc';
const { ccclass, property } = _decorator;

/**
 * FloatingText
 * 親ノードの頭上に数値テキストをポップアップ表示する汎用コンポーネント。
 *
 * 使用例:
 *   // 攻撃スクリプトなどから
 *   const ft = this.node.getComponent(FloatingText);
 *   ft?.showFloatingText(120);                      // デフォルト色で表示
 *   ft?.showFloatingText(-30, new Color(0,255,0));  // 色を指定して表示
 */
@ccclass('FloatingText')
export class FloatingText extends Component {

    // =========================
    // 見た目関連
    // =========================

    @property({
        tooltip: 'ポップアップテキストのフォントサイズ(ピクセル)。'
    })
    public fontSize: number = 24;

    @property({
        tooltip: 'ポップアップテキストの基本色。'
    })
    public defaultColor: Color = new Color(255, 80, 80, 255);

    @property({
        tooltip: 'テキストを太字で表示するかどうか。'
    })
    public bold: boolean = true;

    @property({
        tooltip: 'アウトライン(縁取り)の太さ。0 でアウトライン無し。'
    })
    public outlineWidth: number = 2;

    @property({
        tooltip: 'アウトライン(縁取り)の色。'
    })
    public outlineColor: Color = new Color(0, 0, 0, 255);

    // =========================
    // 動き・アニメーション関連
    // =========================

    @property({
        tooltip: '親ノードのローカル座標からの開始オフセット。頭上に表示したい場合は Y を正の値にします。'
    })
    public startOffset: Vec3 = new Vec3(0, 50, 0);

    @property({
        tooltip: '上方向へ浮き上がる距離(ローカル Y 座標の距離)。'
    })
    public floatDistance: number = 50;

    @property({
        tooltip: '浮遊&フェードアウトにかける総時間(秒)。'
    })
    public duration: number = 0.8;

    @property({
        tooltip: '最初に少し大きくなってから縮む「ポップ」アニメーションを行うかどうか。'
    })
    public useScalePop: boolean = true;

    @property({
        tooltip: 'ポップ時の最大スケール倍率(1.0 で通常サイズ)。'
    })
    public scalePopFactor: number = 1.2;

    // =========================
    // 生成位置・親子関係関連
    // =========================

    @property({
        tooltip: 'ワールド座標を UI 空間に変換して Canvas 上に表示するかどうか。',
    })
    public useWorldSpace: boolean = false;

    @property({
        type: Node,
        tooltip: 'useWorldSpace = true の場合に、ポップアップをぶら下げる UI ルート(通常は Canvas ノード)。未設定の場合は自動でシーン内の Canvas を検索します。'
    })
    public uiRoot: Node | null = null;

    // =========================
    // 挙動制御
    // =========================

    @property({
        tooltip: '各ポップアップごとに z 座標へ加算するオフセット。重なり軽減用。'
    })
    public autoZOffset: number = 0.01;

    @property({
        tooltip: '同時に存在できる最大ポップアップ数。超えた場合は一番古いものから削除します。0 以下なら無制限。'
    })
    public maxConcurrent: number = 10;

    // 内部管理用
    private _activeTexts: Node[] = [];
    private _zCounter: number = 0;

    onLoad() {
        // useWorldSpace = true で uiRoot が未設定なら、シーン内の Canvas を探す
        if (this.useWorldSpace && !this.uiRoot) {
            const scene = director.getScene();
            if (scene) {
                const canvas = scene.getComponentInChildren(Canvas);
                if (canvas) {
                    this.uiRoot = canvas.node;
                    console.log('[FloatingText] uiRoot が未設定のため、シーン内の Canvas を自動で使用します。');
                } else {
                    console.warn('[FloatingText] useWorldSpace が true ですが、シーン内に Canvas が見つかりません。uiRoot を Inspector で指定してください。');
                }
            }
        }
    }

    /**
     * ダメージなどの数値をポップアップ表示する。
     * @param value 表示する数値または文字列
     * @param color 任意のテキストカラー(省略時は defaultColor)
     */
    public showFloatingText(value: number | string, color?: Color): void {
        const text = String(value);

        // ポップアップ数の上限管理
        if (this.maxConcurrent > 0 && this._activeTexts.length >= this.maxConcurrent) {
            const oldest = this._activeTexts.shift();
            if (oldest && oldest.isValid) {
                oldest.destroy();
            }
        }

        // Label ノード生成
        const labelNode = new Node('FloatingTextLabel');
        let parentForLabel: Node | null = null;

        if (this.useWorldSpace) {
            // UI 空間に表示
            if (!this.uiRoot) {
                console.error('[FloatingText] useWorldSpace が true ですが uiRoot が設定されていません。ポップアップを生成できません。');
                return;
            }
            parentForLabel = this.uiRoot;
        } else {
            // 親ノードのローカル空間に表示
            parentForLabel = this.node;
        }

        parentForLabel.addChild(labelNode);

        // 必要なコンポーネントを追加
        const uiTrans = labelNode.addComponent(UITransform);
        uiTrans.setContentSize(100, 40); // 適当なデフォルトサイズ

        const label = labelNode.addComponent(Label);
        label.string = text;
        label.fontSize = this.fontSize;
        label.color = color ? color.clone() : this.defaultColor.clone();
        label.enableBold = this.bold;
        label.horizontalAlign = Label.HorizontalAlign.CENTER;
        label.verticalAlign = Label.VerticalAlign.CENTER;

        // アウトライン
        if (this.outlineWidth > 0) {
            const outline = labelNode.addComponent(LabelOutline);
            outline.width = this.outlineWidth;
            outline.color = this.outlineColor.clone();
        }

        // 透明度管理
        const opacity = labelNode.addComponent(UIOpacity);
        opacity.opacity = 255;

        // 位置の設定
        let startPos = new Vec3();
        if (this.useWorldSpace) {
            // 親ノードのワールド座標を Canvas(UI) 空間に変換して配置
            const worldPos = this.node.worldPosition.clone();
            // UITransform を通してローカル座標に変換
            const uiRootTransform = this.uiRoot!.getComponent(UITransform);
            if (!uiRootTransform) {
                console.warn('[FloatingText] uiRoot に UITransform がありません。UI 空間での位置計算が正しく行えない可能性があります。');
                // とりあえず worldPosition をそのまま使う
                startPos = worldPos.add(this.startOffset);
            } else {
                // ワールド座標 → ローカル座標変換
                uiRootTransform.convertToNodeSpaceAR(worldPos, startPos);
                startPos.add(this.startOffset);
            }
        } else {
            // 親ノードのローカル空間で、そのまま startOffset を使う
            startPos.set(this.startOffset);
        }

        // z 方向の自動オフセット
        if (this.autoZOffset !== 0) {
            this._zCounter++;
            startPos.z += this.autoZOffset * this._zCounter;
        }

        labelNode.setPosition(startPos);

        // スケール初期値
        labelNode.setScale(v3(1, 1, 1));

        // 管理リストに追加
        this._activeTexts.push(labelNode);

        // アニメーション設定
        const endPos = new Vec3(startPos.x, startPos.y + this.floatDistance, startPos.z);
        const totalDuration = math.clamp(this.duration, 0.1, 10); // 過度な値を防ぐ

        // ポップアニメーション用のスケール
        const startScale = 1.0;
        const peakScale = this.useScalePop ? this.scalePopFactor : 1.0;
        const halfDuration = totalDuration * 0.3;
        const remainDuration = totalDuration - halfDuration;

        // 位置と透明度の tween
        const moveAndFade: Tween<any> = tween(labelNode)
            .parallel(
                tween(labelNode)
                    .to(totalDuration, { position: endPos }, { easing: 'quadOut' }),
                tween(opacity)
                    .to(totalDuration, { opacity: 0 }, { easing: 'quadIn' })
            )
            .call(() => {
                // 終了後にノードを破棄
                const idx = this._activeTexts.indexOf(labelNode);
                if (idx !== -1) {
                    this._activeTexts.splice(idx, 1);
                }
                labelNode.destroy();
            });

        // スケールポップの tween
        let scaleTween: Tween<any> | null = null;
        if (this.useScalePop && peakScale > 1.0) {
            const startScaleVec = v3(startScale, startScale, startScale);
            const peakScaleVec = v3(peakScale, peakScale, peakScale);
            const endScaleVec = v3(1, 1, 1);

            labelNode.setScale(startScaleVec);

            scaleTween = tween(labelNode)
                .to(halfDuration, { scale: peakScaleVec }, { easing: 'backOut' })
                .to(remainDuration, { scale: endScaleVec }, { easing: 'quadIn' });
        }

        // 並列再生
        if (scaleTween) {
            tween(labelNode)
                .parallel(moveAndFade, scaleTween)
                .start();
        } else {
            moveAndFade.start();
        }
    }
}

コードのポイント解説

  • onLoad
    • useWorldSpace が true かつ uiRoot 未設定の場合、シーン内の Canvas を自動で検索し、見つかればそれを uiRoot に設定します。
    • Canvas が見つからない場合は console.warn を出して、Inspector での設定を促します。
  • showFloatingText
    • 外部から呼ぶための public メソッドです。ダメージ値(number or string)と任意の色を受け取ります。
    • maxConcurrent を超える場合は、一番古いポップアップを破棄してから新しいものを生成します。
    • useWorldSpace に応じて、uiRoot(Canvas)か自ノードに子ノードとして Label を追加します。
    • 必要なコンポーネント(UITransform, Label, LabelOutline, UIOpacity)を自動で追加します。
    • 開始位置は
      • useWorldSpace = false: 親ノードのローカル座標に startOffset を足した位置。
      • useWorldSpace = true: 親ノードのワールド座標を uiRoot のローカル座標に変換し、そこに startOffset を足した位置。
    • autoZOffset により、連続で生成した場合でも少しずつ z をずらして重なりを軽減します。
    • tween を使って
      • 位置を上方向へ移動
      • 透明度を 255 → 0 に変化
      • (オプションで)スケールをポップさせる

      を並列に再生し、終了後にノードを破棄します。


使用手順と動作確認

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

  1. エディタ下部の Assets パネルで、スクリプトを置きたいフォルダを右クリックします。
  2. Create → TypeScript を選択し、ファイル名を FloatingText.ts にします。
  3. 自動生成されたファイルをダブルクリックして開き、内容をすべて削除して、前章のコードをそのまま貼り付けて保存します。

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

UI シーン(Canvas 内)での利用を例に説明します。

  1. Hierarchy パネルで右クリック → Create → UI → Canvas を選択し、Canvas がなければ作成します。
  2. Canvas の子として、Create → UI → Sprite などでテスト用のキャラクターノードを作成し、名前を Enemy などに変更します。

3. FloatingText コンポーネントをアタッチ

  1. Hierarchy で Enemy ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom カテゴリから FloatingText を選択して追加します。

4. Inspector プロパティを設定

Enemy ノードの Inspector に表示される FloatingText コンポーネントのプロパティを、例えば次のように設定します。

  • 見た目
    • Font Size: 28
    • Default Color: (255, 80, 80, 255)(赤系)
    • Bold: チェック ON
    • Outline Width: 2
    • Outline Color: 黒
  • 動き
    • Start Offset: (0, 60, 0)(頭上に少し離して表示)
    • Float Distance: 60
    • Duration: 0.8
    • Use Scale Pop: チェック ON
    • Scale Pop Factor: 1.2
  • 位置・親子関係
    • Use World Space:
      • Canvas 内の 2D UI だけで完結するなら OFF(false)
      • 3D キャラの頭上に Canvas 上の UI として表示したいなら ON(true)
    • UI Root:
      • Use World Space を ON にした場合、Canvas ノードをドラッグ&ドロップして設定
      • OFF の場合は空のままでOK
  • 挙動制御
    • Auto Z Offset: 0.01
    • Max Concurrent: 10

5. テスト用の呼び出しコードを書く

FloatingText 自体は「アタッチするだけ」で準備は完了ですが、実際にポップアップを表示するには、どこかのタイミングで showFloatingText() を呼ぶ必要があります。

簡単なテストとして、Enemy ノードに「クリックしたらダメージポップアップを出す」スクリプトを追加してみます。

  1. Assets パネルで右クリック → Create → TypeScriptEnemyTest.ts を作成します。
  2. 以下のコードを貼り付けます。

import { _decorator, Component, Node, input, Input, EventMouse, Color } from 'cc';
import { FloatingText } from './FloatingText';
const { ccclass, property } = _decorator;

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

    private _floatingText: FloatingText | null = null;

    onLoad() {
        this._floatingText = this.node.getComponent(FloatingText);
        if (!this._floatingText) {
            console.warn('[EnemyTest] FloatingText コンポーネントが同じノードに見つかりません。Inspector で追加してください。');
        }
    }

    start() {
        // マウスクリックイベントを登録(エディタプレビュー用)
        input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
    }

    private onMouseDown(event: EventMouse) {
        if (!this._floatingText) {
            return;
        }

        // 仮のダメージ値をランダムに生成
        const damage = Math.floor(50 + Math.random() * 50); // 50〜99

        // たまにクリティカル(黄色)を出す例
        if (Math.random() < 0.2) {
            this._floatingText.showFloatingText(damage, new Color(255, 230, 80, 255));
        } else {
            this._floatingText.showFloatingText(damage);
        }
    }
}
  1. Hierarchy で Enemy ノードを選択し、Add Component → Custom → EnemyTest を追加します。
  2. シーンを保存し、再生ボタン(Play) を押してゲームを実行します。
  3. Game ビュー上でクリックすると、Enemy ノードにアタッチされた FloatingText が呼び出され、頭上にダメージ値がポップアップ表示されることを確認します。

実際のゲームでは、攻撃処理やダメージ計算を行っているスクリプトから、以下のように呼び出すだけで利用できます。


// 例: ダメージ処理の中で
const ft = this.node.getComponent(FloatingText);
if (ft) {
    ft.showFloatingText(actualDamage); // 色指定なし → defaultColor
}

まとめ

  • FloatingText コンポーネントは、ノードにアタッチして showFloatingText() を呼ぶだけで、
    • 頭上に数値テキストをポップアップ表示
    • 上方向への浮遊
    • フェードアウト
    • (オプション)ポップするスケール演出
    • 終了後の自動破棄

    を一括で行ってくれます。

  • Inspector から
    • フォントサイズ・色・アウトライン
    • 開始位置・浮遊距離・時間
    • スケールポップの有無
    • UI 空間での表示(Canvas 利用)の有無
    • 同時表示数の制限

    を柔軟に調整できるため、RPG・アクション・タワーディフェンスなど、さまざまなゲームでそのまま再利用できます。

  • 外部の GameManager やシングルトンに一切依存せず、この 1 スクリプトだけをプロジェクトに追加すれば完結する設計なので、他プロジェクトへの移植も簡単です。

ダメージ表示以外にも、回復量・経験値獲得・スコア加算・コンボ数など、数値や短いメッセージをポップアップさせたい場面で幅広く活用できます。
必要に応じて、フォントや色のプリセットを増やしたり、ランダムな左右揺れを加えるなど、カスタマイズしてみてください。

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