【Cocos Creator】アタッチするだけ!StunEffect (スタン状態)の実装方法【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】StunEffectの実装:アタッチするだけで「スタン状態で物理挙動を一時停止し、頭上にピヨりマークを表示する」汎用スクリプト

このガイドでは、任意のキャラクターノードにアタッチするだけで、一定時間だけ物理挙動を止めて(スタン状態)、頭上にピヨりマークを表示する汎用コンポーネント StunEffect を実装します。

外部の GameManager やシングルトンには一切依存せず、このスクリプト単体で完結するように設計します。スタン時間やピヨりマークの見た目・位置はすべてインスペクタから調整できます。


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

機能要件の整理

  • このコンポーネントをアタッチしたノード(親)に対して、スタン状態を付与できる。
  • スタン中は、以下のような「物理挙動」を一時停止する:
    • RigidBody2D / RigidBody があれば、速度を 0 にしてシミュレーションを停止。
    • 物理ボディが無い場合でも、最低限として Node の移動を止めるために「スタン中は update 内で位置を固定」する。
  • スタン中はノードの頭上に「ピヨりマーク」を表示し、スタン終了時に自動で消す。
  • スタンの開始・終了は以下のメソッドで制御:
    • stun(duration?: number): void … 指定秒数スタン。引数省略時はデフォルト時間。
    • clearStun(): void … 即座にスタン解除。
    • isStunned(): boolean … 現在スタン中かどうか。
  • 外部スクリプトに依存せず、インスペクタのプロパティだけで設定できる。
  • ピヨりマークは Prefab または Node を指定できるようにする。

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

  • 物理制御は、存在する場合のみ RigidBody2D / RigidBody を取得し、無ければ「最低限のスタン」だけ行う。
  • ピヨりマークも、インスペクタから Prefab または既存の Node をアサインする方式にし、自動生成や他スクリプトへの問い合わせは行わない
  • 必須ではないコンポーネントは、getComponent で取得を試みて、存在しなければ console.warn でログを出すだけにする。

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

以下のようなプロパティを用意します。

  • defaultStunDuration: number
    • デフォルトのスタン時間(秒)。
    • stun() の引数を省略したときに使われる。
    • 例:1.0~3.0 秒程度。
  • allowOverrideDuration: boolean
    • stun(duration) で引数を渡したときに、その値を許可するかどうか。
    • false の場合、常に defaultStunDuration が使われる。
  • freezePhysics2D: boolean
    • RigidBody2D を持つ場合に、スタン中は物理シミュレーションを停止するかどうか。
    • ON の場合:スタン開始時に速度を 0 にし、enabled = false で一時停止、解除時に元の状態へ戻す。
  • freezePhysics3D: boolean
    • RigidBody(3D)に対して同様に制御するかどうか。
  • freezeTransform: boolean
    • 物理ボディがない場合でも、スタン中はノードの位置を固定するかどうか。
    • ON の場合:スタン開始時の位置を記録し、update でその位置に戻す。
  • stunIconPrefab: Prefab | null
    • 頭上に表示するピヨりマークの Prefab
    • 未設定でも動作はするが、スタン中の視覚効果は出ない(ログで警告)。
  • stunIconNode: Node | null
    • 既にシーン上に存在するピヨりマーク用ノードを直接指定したい場合に使用。
    • stunIconPrefab よりも優先される。
    • スタン中のみ active = true にし、解除時に active = false に戻す。
  • iconOffsetY: number
    • ピヨりマークをノードの頭上にどれだけオフセットして表示するか(ローカルY座標)。
    • 例:1.5 ~ 2.0 など。
  • destroyIconOnEnd: boolean
    • スタン終了時に、生成したアイコンを破棄するかどうか。
    • ON:毎回インスタンスを生成・破棄する。
    • OFF:生成したアイコンを再利用し、active の切り替えだけ行う。
  • autoStunOnStart: boolean
    • ゲーム開始時(start())に自動的にスタンさせるかどうか(テスト用にも便利)。
  • autoStunDuration: number
    • autoStunOnStart が ON のときに使うスタン時間。

TypeScriptコードの実装

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


import {
    _decorator,
    Component,
    Node,
    Prefab,
    instantiate,
    Vec3,
    RigidBody2D,
    RigidBody,
    v3,
} from 'cc';
const { ccclass, property } = _decorator;

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

    @property({
        tooltip: 'stun() の引数を省略したときに使われるデフォルトのスタン時間(秒)。'
    })
    public defaultStunDuration: number = 2.0;

    @property({
        tooltip: 'stun(duration) で渡された時間を使用するかどうか。false の場合は常に defaultStunDuration が使用されます。'
    })
    public allowOverrideDuration: boolean = true;

    @property({
        tooltip: '2D物理 (RigidBody2D) を持つ場合、スタン中に物理シミュレーションを停止するかどうか。'
    })
    public freezePhysics2D: boolean = true;

    @property({
        tooltip: '3D物理 (RigidBody) を持つ場合、スタン中に物理シミュレーションを停止するかどうか。'
    })
    public freezePhysics3D: boolean = true;

    @property({
        tooltip: '物理ボディが無い場合でも、スタン中はノードの位置を固定するかどうか。'
    })
    public freezeTransform: boolean = true;

    @property({
        type: Prefab,
        tooltip: '頭上に表示するピヨりマークの Prefab。未設定の場合は stunIconNode が使用されます。'
    })
    public stunIconPrefab: Prefab | null = null;

    @property({
        type: Node,
        tooltip: '既にシーン上に存在するピヨりマーク用ノード。設定されている場合はこちらが優先されます。'
    })
    public stunIconNode: Node | null = null;

    @property({
        tooltip: 'ピヨりマークを親ノードの頭上に表示するためのローカルYオフセット。'
    })
    public iconOffsetY: number = 1.5;

    @property({
        tooltip: 'スタン終了時に生成したピヨりマークを破棄するかどうか。false の場合は非表示にして再利用します。'
    })
    public destroyIconOnEnd: boolean = false;

    @property({
        tooltip: 'ゲーム開始時 (start) に自動的にスタンさせるかどうか(テスト用)。'
    })
    public autoStunOnStart: boolean = false;

    @property({
        tooltip: 'autoStunOnStart が true のときに使用されるスタン時間(秒)。'
    })
    public autoStunDuration: number = 1.0;

    // 内部状態
    private _isStunned: boolean = false;
    private _stunTimer: number = 0;

    // 物理関連のキャッシュ
    private _rb2d: RigidBody2D | null = null;
    private _rb3d: RigidBody | null = null;
    private _rb2dWasEnabled: boolean = false;
    private _rb3dWasEnabled: boolean = false;

    // 位置固定用
    private _frozenPosition: Vec3 | null = null;

    // アイコン管理用
    private _iconInstance: Node | null = null;
    private _iconInitialParent: Node | null = null;

    onLoad() {
        // 可能なら物理ボディを取得(存在しない場合は null のまま)
        this._rb2d = this.getComponent(RigidBody2D);
        this._rb3d = this.getComponent(RigidBody);

        if (!this._rb2d && !this._rb3d) {
            console.warn(
                `[StunEffect] Node "${this.node.name}" に RigidBody2D / RigidBody が見つかりません。` +
                'freezeTransform を有効にしている場合は、スタン中に位置のみ固定されます。'
            );
        }

        // 既存の stunIconNode が指定されていれば、最初は非表示にしておく
        if (this.stunIconNode) {
            this._iconInstance = this.stunIconNode;
            this._iconInitialParent = this.stunIconNode.parent;
            this.stunIconNode.active = false;
        }
    }

    start() {
        // 自動スタンが有効なら開始時にスタンをかける
        if (this.autoStunOnStart) {
            const duration = this.autoStunDuration > 0 ? this.autoStunDuration : this.defaultStunDuration;
            this.stun(duration);
        }
    }

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

        // タイマー更新
        this._stunTimer -= deltaTime;
        if (this._stunTimer <= 0) {
            this.clearStun();
            return;
        }

        // 位置固定が有効なら、毎フレーム位置を固定
        if (this.freezeTransform && this._frozenPosition) {
            this.node.setPosition(this._frozenPosition);
        }

        // アイコンが親ノードの頭上に追従するように位置を更新
        if (this._iconInstance && this._iconInstance.parent === this.node) {
            const pos = this.node.position;
            this._iconInstance.setPosition(pos.x, pos.y + this.iconOffsetY, pos.z);
        }
    }

    /**
     * 指定秒数だけスタン状態にする。
     * duration を省略した場合は defaultStunDuration が使用されます。
     */
    public stun(duration?: number): void {
        let stunDuration = this.defaultStunDuration;
        if (this.allowOverrideDuration && duration !== undefined && duration > 0) {
            stunDuration = duration;
        }

        if (stunDuration <= 0) {
            console.warn('[StunEffect] スタン時間が 0 以下のため、stun() は無視されました。');
            return;
        }

        // すでにスタン中の場合は、タイマーだけ延長する
        if (this._isStunned) {
            this._stunTimer = stunDuration;
            return;
        }

        this._isStunned = true;
        this._stunTimer = stunDuration;

        // 位置固定用の座標を記録
        if (this.freezeTransform) {
            this._frozenPosition = v3(this.node.position);
        } else {
            this._frozenPosition = null;
        }

        // 2D物理の停止
        if (this.freezePhysics2D && this._rb2d) {
            this._rb2dWasEnabled = this._rb2d.enabled;
            // 速度をリセットしてから無効化
            this._rb2d.linearVelocity = v3(0, 0, 0);
            this._rb2d.angularVelocity = 0;
            this._rb2d.enabled = false;
        }

        // 3D物理の停止
        if (this.freezePhysics3D && this._rb3d) {
            this._rb3dWasEnabled = this._rb3d.enabled;
            this._rb3d.setLinearVelocity(v3(0, 0, 0));
            this._rb3d.setAngularVelocity(v3(0, 0, 0));
            this._rb3d.enabled = false;
        }

        // ピヨりマークの表示
        this.showStunIcon();
    }

    /**
     * 即座にスタン状態を解除します。
     */
    public clearStun(): void {
        if (!this._isStunned) {
            return;
        }

        this._isStunned = false;
        this._stunTimer = 0;

        // 位置固定を解除
        this._frozenPosition = null;

        // 2D物理を元に戻す
        if (this.freezePhysics2D && this._rb2d) {
            this._rb2d.enabled = this._rb2dWasEnabled;
        }

        // 3D物理を元に戻す
        if (this.freezePhysics3D && this._rb3d) {
            this._rb3d.enabled = this._rb3dWasEnabled;
        }

        // ピヨりマークを非表示または破棄
        this.hideStunIcon();
    }

    /**
     * 現在スタン中かどうかを返します。
     */
    public isStunned(): boolean {
        return this._isStunned;
    }

    // ピヨりマーク表示処理
    private showStunIcon(): void {
        // 既存ノードが指定されている場合はそれを使用
        if (this._iconInstance) {
            this.attachIconToOwner();
            this._iconInstance.active = true;
            return;
        }

        // Prefab が設定されていない場合は警告を出して終了
        if (!this.stunIconPrefab) {
            console.warn(
                `[StunEffect] Node "${this.node.name}" に stunIconPrefab / stunIconNode が設定されていないため、` +
                'スタン中のピヨりマークは表示されません。'
            );
            return;
        }

        // Prefab からインスタンスを生成
        const icon = instantiate(this.stunIconPrefab);
        if (!icon) {
            console.error('[StunEffect] stunIconPrefab のインスタンス生成に失敗しました。');
            return;
        }

        this._iconInstance = icon;
        this._iconInitialParent = icon.parent;

        this.attachIconToOwner();
        this._iconInstance.active = true;
    }

    // ピヨりマーク非表示処理
    private hideStunIcon(): void {
        if (!this._iconInstance) {
            return;
        }

        if (this.destroyIconOnEnd) {
            // 完全に破棄
            this._iconInstance.destroy();
            this._iconInstance = null;
            this._iconInitialParent = null;
        } else {
            // 非表示にして、元の親に戻す(存在する場合)
            this._iconInstance.active = false;
            if (this._iconInitialParent && this._iconInstance.parent !== this._iconInitialParent) {
                this._iconInstance.parent = this._iconInitialParent;
            }
        }
    }

    // ピヨりマークを親ノードの頭上にアタッチ
    private attachIconToOwner(): void {
        if (!this._iconInstance) {
            return;
        }

        // 親をこのコンポーネントのノードに変更
        this._iconInstance.parent = this.node;

        const pos = this.node.position;
        this._iconInstance.setPosition(pos.x, pos.y + this.iconOffsetY, pos.z);
    }
}

コードの主要ポイント解説

  • onLoad()
    • RigidBody2D / RigidBody を取得し、存在しない場合は console.warn で通知。
    • インスペクタで stunIconNode が指定されていれば、それを内部インスタンスとしてキャッシュし、最初は非表示にする。
  • start()
    • autoStunOnStart が ON の場合、自動で stun() を呼び出してスタンテストを行う。
  • update(deltaTime)
    • スタンタイマーを減算し、0 以下になったら clearStun() で解除。
    • freezeTransform が ON の場合は、毎フレーム位置を固定。
    • ピヨりマークが親ノードに追従するよう、頭上位置を毎フレーム更新。
  • stun(duration?)
    • スタン時間を決定し、すでにスタン中ならタイマーだけ延長。
    • 初回スタン時は、物理ボディの enabled 状態を保存し、速度を 0 にしてから無効化。
    • 位置固定用に現在座標を保存。
    • showStunIcon() でピヨりマークを表示。
  • clearStun()
    • スタンフラグとタイマーをリセットし、位置固定を解除。
    • 保存しておいた enabled 状態を使って物理ボディを元に戻す。
    • hideStunIcon() でアイコンを非表示または破棄。
  • アイコン関連メソッド
    • showStunIcon():既存ノード優先 → Prefab インスタンス生成 → 警告ログの順で処理。
    • hideStunIcon()destroyIconOnEnd に応じて破棄 or 非表示+元親へ戻す。
    • attachIconToOwner():親ノードを this.node に変更し、頭上座標に配置。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を StunEffect.ts にします。
  3. 自動生成されたファイルを開き、内容をすべて削除して、前節の TypeScript コードを丸ごと貼り付けて保存します。

2. ピヨりマーク用 Prefab の用意(推奨)

すでにピヨりマーク用のノードがある場合はこの手順はスキップして構いません。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite などで、新しいノードを作成します。
  2. Inspector で Sprite コンポーネントの SpriteFrame に、ピヨりマーク用の画像を設定します。
  3. サイズや位置を調整し、ピヨりマークとしてちょうど良い見た目にします。
  4. 作成したノードを Assets パネルにドラッグ&ドロップして Prefab にします(例:StunIcon.prefab)。
  5. Hierarchy 上の元ノードは不要であれば削除して構いません。

3. テスト用キャラクターノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のキャラクターノードを作成します。
    • 名前の例:TestCharacter
  2. 物理挙動を試したい場合は、TestCharacter を選択した状態で Inspector の Add Component から
    • Physics 2D → RigidBody2D を追加(2D の場合)
    • または Physics → RigidBody を追加(3D の場合)
  3. 2D 物理の場合は、さらに Collider2D(BoxCollider2D など)も追加しておくと、物理挙動が確認しやすくなります。

4. StunEffect コンポーネントをアタッチ

  1. Hierarchy で TestCharacter ノードを選択します。
  2. Inspector の一番下の Add Component ボタンをクリックします。
  3. Custom → StunEffect を選択して追加します。

5. プロパティの設定

TestCharacter にアタッチされた StunEffect コンポーネントの各プロパティを設定します。

  • Default Stun Duration2.0 など(2 秒スタン)。
  • Allow Override Duration:チェック ON(任意)。
  • Freeze Physics 2DRigidBody2D を使う場合は ON にします。
  • Freeze Physics 3D:3D 物理を使わない場合は OFF のままで構いません。
  • Freeze Transform:物理ボディが無い場合にスタン中も止めたい場合は ON。
  • Stun Icon Prefab:先ほど作成した StunIcon.prefab をドラッグ&ドロップで設定します。
  • Stun Icon Node:既存ノードを使う場合のみ設定(今回は空のままで OK)。
  • Icon Offset Y1.52.0 など、キャラクターの頭上にちょうど良い値を設定します。
  • Destroy Icon On End:ON にするとスタン終了ごとにアイコンを破棄します。OFF の場合は再利用(パフォーマンス的には OFF 推奨)。
  • Auto Stun On Start:テストしやすいように ON にします。
  • Auto Stun Duration2.0 など。

6. 再生して動作確認

  1. エディタ右上の Play ボタンを押してゲームを再生します。
  2. ゲーム開始直後に、TestCharacter の頭上にピヨりマークが表示され、物理挙動(落下や移動)が一時停止していることを確認します。
  3. 設定した Auto Stun Duration 秒が経過すると、ピヨりマークが消え、物理挙動が再開されることを確認します。

7. スクリプトから手動でスタンさせる(任意)

他のスクリプトからスタンを発動したい場合は、次のように呼び出せます。


// 例: 攻撃ヒット時に相手を 3 秒スタンさせる
const targetNode = someTargetNode;
const stun = targetNode.getComponent(StunEffect);
if (stun) {
    stun.stun(3.0); // 3 秒スタン
}

引数を省略すると、defaultStunDuration の値が使われます。


まとめ

  • StunEffect を任意のノードにアタッチするだけで、
    • スタン中の物理挙動の一時停止(2D / 3D 対応)
    • 位置固定による簡易スタン
    • 頭上のピヨりマーク表示

    を一括管理できるようになりました。

  • 外部の GameManager やシングルトンに依存せず、すべてインスペクタのプロパティで完結しているため、どのプロジェクトにも簡単に持ち込んで再利用できます。
  • 応用例:
    • 敵 AI の被弾時に stun() を呼び出して行動停止。
    • プレイヤーがトラップにかかったときの硬直演出。
    • オンライン対戦ゲームで、スキルによるスタンデバフの表現。

このコンポーネントをベースに、スタン中のアニメーション切り替えや、スタン終了時のエフェクト再生などを追加すれば、よりリッチな状態異常システムを、依然として単一コンポーネントとして拡張していくことができます。

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