【Cocos Creator】アタッチするだけ!Ambusher (待ち伏せ)の実装方法【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】Ambusher(待ち伏せ)の実装:アタッチするだけで「近づくと擬態解除して襲いかかる敵」を実現する汎用スクリプト

このコンポーネントは、敵キャラなどにアタッチするだけで、普段は透明(または岩などに擬態)し、プレイヤーが近づくと正体を現して攻撃行動に移る挙動を実現します。

プレイヤーのノードや外部の管理スクリプトに依存せず、「プレイヤーの位置」すらInspectorからNode参照で渡すだけという完全独立設計なので、どんなプロジェクトにも簡単に組み込めます。


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

機能要件の整理

  • Ambusher をアタッチしたノードは、待機状態(Ambush)攻撃状態(Attack)の2状態を持つ。
  • 待機状態では:
    • 透明になる、または「岩」などの擬態スプライトに切り替える。
    • プレイヤーとの距離を監視し、指定距離以内に入ったら攻撃状態へ遷移。
  • 攻撃状態では:
    • 本来のスプライト(敵の姿)を表示し、透明度を戻す。
    • プレイヤーに向かって移動する(簡易なホーミング移動)。
    • 一定時間後に再び待機状態に戻る、またはプレイヤーが離れたら待機状態に戻るオプション。
  • 完全独立のため:
    • プレイヤーのノードは @property(Node) でInspectorから指定。
    • Sprite や UIOpacity が無ければ自動取得を試み、見つからない場合は警告を出す。
    • Rigidbody や Collider などの物理コンポーネントには依存しない(純粋に Transform の移動のみ)。

状態と挙動の概要

  • Ambush(待ち伏せ)状態
    • 見た目:
      • モード1:透明度を下げる(例:Alpha = 40)。
      • モード2:岩などの擬態スプライトに差し替える。
    • プレイヤーとの距離が triggerRadius 以下になったら Attack 状態へ。
  • Attack(攻撃)状態
    • 見た目:
      • 本来のスプライトに戻す。
      • 透明度を 255 に戻す。
    • 移動:
      • 毎フレーム、プレイヤーの方向へ attackMoveSpeed で移動。
    • 終了条件(オプション):
      • autoReturnToAmbush が true なら、attackDuration 秒後に自動で待機状態へ戻る。
      • または、プレイヤーが disengageRadius より遠くに離れたら待機状態へ戻るオプション。

Inspector で設定可能なプロパティ設計

コンポーネント名:Ambusher

  • target (Node)
    • プレイヤーなど、距離を監視する対象ノード。
    • 必須。未設定の場合はエラーログを出して挙動を停止。
  • triggerRadius (number)
    • この半径以内に target が入ると攻撃状態に遷移。
    • 例:2Dゲームなら 200〜400 など。
  • disengageRadius (number)
    • 攻撃状態中に target がこの距離より外へ出たら、待機状態に戻る。
    • 0 の場合は「距離では解除しない」。
    • disengageRadius <= triggerRadius だと即戻りが発生しやすいので、基本的には trigger より大きめに設定。
  • useOpacityAmbush (boolean)
    • true の場合、待機状態では透明度を ambushOpacity まで下げる。
    • UIOpacity コンポーネントを利用。無ければ自動追加する。
  • ambushOpacity (number)
    • 待機状態の透明度(0〜255)。
    • 0 に近いほど完全に見えない。40〜80 くらいがうっすら見える程度。
  • useMimicSprite (boolean)
    • true の場合、待機状態では mimicSpriteFrame にスプライトを差し替える。
    • false の場合は、元のスプライトのまま(透明度のみ制御)。
  • mimicSpriteFrame (SpriteFrame)
    • 擬態時に表示するスプライト(岩など)。
    • useMimicSprite が true のときのみ使用。
  • attackSpriteFrame (SpriteFrame)
    • 攻撃状態で表示する「本来の姿」のスプライト。
    • 未設定の場合は、開始時点の SpriteFrame を自動的に「本来の姿」として保存して利用。
  • attackMoveSpeed (number)
    • 攻撃状態中の移動速度(単位:単位距離/秒)。
    • 2Dなら 100〜400 程度が扱いやすい。
  • autoReturnToAmbush (boolean)
    • true の場合、攻撃状態になってから attackDuration 秒後に自動で待機状態に戻る。
    • false の場合は、距離による解除(disengageRadius)だけで戻るか、ずっと攻撃状態のまま。
  • attackDuration (number)
    • 攻撃状態を維持する秒数。
    • autoReturnToAmbush が true のときのみ有効。
  • debugDrawRadius (boolean)
    • true の場合、Scene ビューにトリガー半径と解除半径の簡易な目安線を Gizmo 的に描画(ラインではなく、ログや一時的なラベルにとどめる簡易実装)。
    • 本記事では Editor Gizmo のカスタムまでは行わず、ログ出力に留める軽量なデバッグ機能とする。

TypeScriptコードの実装

以下が、Cocos Creator 3.8.7 用の Ambusher.ts の完全な実装例です。


import { _decorator, Component, Node, Vec3, v3, Sprite, SpriteFrame, UIOpacity, math } from 'cc';
const { ccclass, property } = _decorator;

enum AmbushState {
    AMBUSH = 0,
    ATTACK = 1,
}

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

    @property({
        type: Node,
        tooltip: '距離を監視する対象(通常はプレイヤー)のノードを指定します。必須です。'
    })
    public target: Node | null = null;

    @property({
        tooltip: 'この半径以内に target が入ると攻撃状態に移行します。'
    })
    public triggerRadius: number = 300;

    @property({
        tooltip: '攻撃状態中に target がこの距離より外へ出ると待ち伏せ状態に戻ります。0 の場合は距離による解除を行いません。'
    })
    public disengageRadius: number = 0;

    @property({
        tooltip: '待ち伏せ状態で透明度を下げるかどうか。true の場合、UIOpacity で透過を制御します。'
    })
    public useOpacityAmbush: boolean = true;

    @property({
        tooltip: '待ち伏せ状態の透明度(0〜255)。0 に近いほど見えなくなります。',
        range: [0, 255, 1],
        slide: true
    })
    public ambushOpacity: number = 60;

    @property({
        tooltip: '待ち伏せ状態で擬態用スプライトに切り替えるかどうか。'
    })
    public useMimicSprite: boolean = false;

    @property({
        type: SpriteFrame,
        tooltip: '擬態状態(岩など)で表示する SpriteFrame。useMimicSprite が true のときのみ使用します。'
    })
    public mimicSpriteFrame: SpriteFrame | null = null;

    @property({
        type: SpriteFrame,
        tooltip: '攻撃状態で表示する本来の姿の SpriteFrame。未設定の場合は開始時の SpriteFrame を使用します。'
    })
    public attackSpriteFrame: SpriteFrame | null = null;

    @property({
        tooltip: '攻撃状態中の移動速度(単位距離/秒)。target に向かって直線的に移動します。'
    })
    public attackMoveSpeed: number = 200;

    @property({
        tooltip: 'true の場合、攻撃状態から attackDuration 秒後に自動で待ち伏せ状態に戻ります。'
    })
    public autoReturnToAmbush: boolean = true;

    @property({
        tooltip: '攻撃状態を維持する時間(秒)。autoReturnToAmbush が true のときのみ有効です。'
    })
    public attackDuration: number = 2.0;

    @property({
        tooltip: '起動時に現在の設定値や半径をログ出力して簡易デバッグを行います。'
    })
    public debugLogOnStart: boolean = false;

    // 内部状態
    private _state: AmbushState = AmbushState.AMBUSH;
    private _sprite: Sprite | null = null;
    private _uiOpacity: UIOpacity | null = null;
    private _originalSpriteFrame: SpriteFrame | null = null;
    private _attackTimer: number = 0;

    onLoad() {
        // 必要なコンポーネントの取得と防御的チェック
        this._sprite = this.getComponent(Sprite);
        if (!this._sprite) {
            console.warn('[Ambusher] Sprite コンポーネントが見つかりません。このノードに Sprite を追加してください。ノード名:', this.node.name);
        } else {
            // 元のスプライトフレームを保持
            this._originalSpriteFrame = this._sprite.spriteFrame;
        }

        // UIOpacity を取得または追加
        this._uiOpacity = this.getComponent(UIOpacity);
        if (!this._uiOpacity) {
            this._uiOpacity = this.addComponent(UIOpacity);
        }

        if (this.target == null) {
            console.error('[Ambusher] target が設定されていません。Inspector からプレイヤーなどのノードを指定してください。ノード名:', this.node.name);
        }

        // 初期状態を待ち伏せに設定
        this._setState(AmbushState.AMBUSH, true);
    }

    start() {
        if (this.debugLogOnStart) {
            console.log(`[Ambusher] 初期化完了: node=${this.node.name}, triggerRadius=${this.triggerRadius}, disengageRadius=${this.disengageRadius}, attackMoveSpeed=${this.attackMoveSpeed}`);
        }
    }

    update(deltaTime: number) {
        if (!this.target) {
            // target が無い場合は何もしない
            return;
        }

        const distance = this._getDistanceToTarget();

        switch (this._state) {
            case AmbushState.AMBUSH:
                this._updateAmbushState(distance);
                break;
            case AmbushState.ATTACK:
                this._updateAttackState(deltaTime, distance);
                break;
        }
    }

    /**
     * target までの距離を計算します(ワールド座標ベース)。
     */
    private _getDistanceToTarget(): number {
        if (!this.target) {
            return Number.MAX_VALUE;
        }
        const selfWorldPos = this.node.worldPosition;
        const targetWorldPos = this.target.worldPosition;
        return Vec3.distance(selfWorldPos, targetWorldPos);
    }

    /**
     * 待ち伏せ状態の更新処理。
     */
    private _updateAmbushState(distanceToTarget: number) {
        if (distanceToTarget = this.attackDuration) {
                this._setState(AmbushState.AMBUSH);
                return;
            }
        }

        // 距離による解除
        if (this.disengageRadius > 0 && distanceToTarget >= this.disengageRadius) {
            this._setState(AmbushState.AMBUSH);
        }
    }

    /**
     * target の方向へ攻撃移動します。
     */
    private _moveTowardsTarget(deltaTime: number) {
        if (!this.target) {
            return;
        }

        const selfWorldPos = this.node.worldPosition;
        const targetWorldPos = this.target.worldPosition;

        const dir = v3(
            targetWorldPos.x - selfWorldPos.x,
            targetWorldPos.y - selfWorldPos.y,
            targetWorldPos.z - selfWorldPos.z
        );

        if (dir.lengthSqr() < math.EPSILON) {
            return;
        }

        dir.normalize();
        const moveDistance = this.attackMoveSpeed * deltaTime;
        const moveVec = v3(
            dir.x * moveDistance,
            dir.y * moveDistance,
            dir.z * moveDistance
        );

        const newWorldPos = v3(
            selfWorldPos.x + moveVec.x,
            selfWorldPos.y + moveVec.y,
            selfWorldPos.z + moveVec.z
        );

        this.node.setWorldPosition(newWorldPos);
    }

    /**
     * 状態を変更し、見た目やタイマーを更新します。
     */
    private _setState(newState: AmbushState, force: boolean = false) {
        if (!force && this._state === newState) {
            return;
        }

        this._state = newState;

        switch (this._state) {
            case AmbushState.AMBUSH:
                this._enterAmbushState();
                break;
            case AmbushState.ATTACK:
                this._enterAttackState();
                break;
        }

        if (this.debugLogOnStart) {
            console.log(`[Ambusher] 状態遷移: node=${this.node.name}, state=${AmbushState[this._state]}`);
        }
    }

    /**
     * 待ち伏せ状態に入ったときの処理。
     */
    private _enterAmbushState() {
        // タイマーリセット
        this._attackTimer = 0;

        // 見た目の変更
        if (this._sprite) {
            if (this.useMimicSprite && this.mimicSpriteFrame) {
                this._sprite.spriteFrame = this.mimicSpriteFrame;
            } else {
                // 擬態スプライトを使わない場合は、本来の姿のまま透明度だけ変更
                this._sprite.spriteFrame = this._getAttackSpriteFrame();
            }
        }

        if (this.useOpacityAmbush && this._uiOpacity) {
            this._uiOpacity.opacity = math.clamp(this.ambushOpacity, 0, 255);
        }
    }

    /**
     * 攻撃状態に入ったときの処理。
     */
    private _enterAttackState() {
        // タイマーリセット
        this._attackTimer = 0;

        // 見た目の変更(本来の姿を表示)
        if (this._sprite) {
            this._sprite.spriteFrame = this._getAttackSpriteFrame();
        }

        // 透明度を元に戻す
        if (this._uiOpacity) {
            this._uiOpacity.opacity = 255;
        }
    }

    /**
     * 攻撃時に使用する SpriteFrame を取得します。
     * attackSpriteFrame が設定されていればそれを使い、無ければ開始時の SpriteFrame を使います。
     */
    private _getAttackSpriteFrame(): SpriteFrame | null {
        if (this.attackSpriteFrame) {
            return this.attackSpriteFrame;
        }
        return this._originalSpriteFrame;
    }
}

コードのポイント解説

  • onLoad
    • Sprite と UIOpacity を取得し、防御的にチェック。
    • Sprite が無い場合は警告を出し、スプライト関連の処理はスキップされるため、最低限「透明化なしの待ち伏せ」だけでも動作します。
    • target 未設定時は console.error を出し、update では target が無い場合は即 return して挙動を止めます。
    • 初期状態を Ambush にして、見た目を待ち伏せ状態に整えます。
  • update
    • 毎フレーム target との距離を計算し、現在の状態に応じて _updateAmbushState または _updateAttackState を呼びます。
    • target が null の場合は何もせず return するため、安全に停止します。
  • _updateAmbushState
    • 距離が triggerRadius 以下なら _setState(AmbushState.ATTACK) で攻撃状態へ遷移。
  • _updateAttackState
    • _moveTowardsTarget で target の方向に向かって移動。
    • autoReturnToAmbush が true の場合は attackDuration 秒経過で待ち伏せ状態に戻る。
    • disengageRadius が 0 より大きい場合、距離がそれ以上になったら待ち伏せ状態に戻る。
  • _moveTowardsTarget
    • ワールド座標で target への方向ベクトルを計算し、正規化して attackMoveSpeed * deltaTime だけ進めます。
    • 2D/3D どちらでも機能します(z が 0 の 2D でも問題なし)。
  • _enterAmbushState / _enterAttackState
    • 見た目の切り替え(スプライト・透明度)を一箇所に集約。
    • 攻撃状態に入るたびに透明度を 255 に戻し、待ち伏せに戻ると ambushOpacity に設定。
    • useMimicSpritemimicSpriteFrame により「岩に擬態」などを簡単に実現。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. 作成されたファイル名を Ambusher.ts に変更します。
  3. Ambusher.ts をダブルクリックして開き、内容をすべて削除して、前述のコードをそのまま貼り付けて保存します。

2. テスト用シーンの準備

ここでは 2D ゲームを想定した簡単なテスト手順を示します。

  1. Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、Player という名前に変更します。
    • Inspector の Sprite コンポーネントで、任意のプレイヤー画像(仮で OK)を設定します。
  2. 同様に、Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、Enemy_Ambusher という名前に変更します。
    • Inspector の Sprite コンポーネントに、敵の本来の姿となる画像を設定します。

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

  1. Hierarchy で Enemy_Ambusher ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom → Ambusher を選択して追加します。
    • もし Custom カテゴリに見当たらない場合は、プロジェクトを一度保存し、Cocos Creator を再インポートさせてください。

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

Enemy_Ambusher の Inspector で、Ambusher コンポーネントの各プロパティを次のように設定してみましょう。

  • Target
    • Hierarchy から Player ノードをドラッグ&ドロップして設定します。
  • Trigger Radius
    • 例:300(シーンのスケールに合わせて調整)。
  • Disengage Radius
    • 例:400(プレイヤーがある程度離れると再び擬態に戻る)。
  • Use Opacity Ambush
    • チェックを入れる(true)。
  • Ambush Opacity
    • 例:40(ほぼ見えない程度)。
  • Use Mimic Sprite
    • 岩などに擬態させたい場合はチェックを入れる。
    • チェックを入れた場合:
      • Mimic Sprite Frame に岩の画像などを設定。
    • チェックを外した場合:
      • 透明度だけ下げて、同じスプライトのまま待ち伏せします。
  • Attack Sprite Frame
    • 未設定でも構いません。その場合、Enemy_Ambusher の元の SpriteFrame が「本来の姿」として使われます。
    • 「擬態中は岩、攻撃時は別の敵画像」にしたい場合は、ここに敵画像の SpriteFrame を指定し、Use Mimic SpriteMimic Sprite Frame に岩画像を設定します。
  • Attack Move Speed
    • 例:250(プレイヤーに向かって素早く移動)。
  • Auto Return To Ambush
    • チェックを入れる(true)。
  • Attack Duration
    • 例:2.0 秒(2秒間追いかけたら再び擬態に戻る)。
  • Debug Log On Start
    • 動作確認中はチェックを入れておくと、Console に状態遷移などが表示されて挙動を追いやすくなります。

5. シーンでの動作確認

  1. Scene ビューで PlayerEnemy_Ambusher の位置を適当に離して配置します。
    • 例:Player を (0, 0)、Enemy_Ambusher を (400, 0) など。
  2. ゲームを再生(Play)します。
  3. Scene ビューまたは Game ビューで、Player をドラッグ移動できるようにしている場合は、Player を Enemy_Ambusher に近づけてみます。
    • 距離が triggerRadius 以内に入ると、Enemy_Ambusher の透明度が 255 になり、擬態スプライトから本来の姿に変わってプレイヤーに向かって動き始めます。
    • autoReturnToAmbush が true の場合、attackDuration 秒経過すると再び透明になって擬態に戻ります。
    • disengageRadius を設定している場合、プレイヤーを遠ざけると待ち伏せ状態に戻ります。

もし Enemy_Ambusher が全く動かない場合:

  • Console に [Ambusher] target が設定されていません のエラーログが出ていないか確認し、Target プロパティが正しく設定されているか確認します。
  • Trigger Radius が小さすぎないか、シーン上の距離とスケールを見直してください。
  • Sprite コンポーネントが Enemy_Ambusher ノードに付いているかを確認します(見た目の切り替えが行われない場合)。

まとめ

この Ambusher コンポーネントは、

  • プレイヤーが近づくまで姿を隠す敵
  • トラップ的に突然動き出すオブジェクト
  • 岩や木に擬態しているモンスター

といったギミックを、ノードにアタッチして Inspector で数値を調整するだけで実現できるようにする汎用スクリプトです。

特徴として:

  • 他のカスタムスクリプトやシングルトンに一切依存せず、Ambusher.ts 単体で完結している。
  • Sprite や UIOpacity がなくても安全に動作し、足りない場合は警告を出す防御的実装。
  • 「擬態スプライト」「透明度」「攻撃速度」「トリガー半径」「解除条件」など、ゲームごとに変えたいパラメータをすべて Inspector から調整可能。

このような「アタッチするだけで完結する汎用コンポーネント」を積み重ねていくと、

  • プロトタイプ制作のスピードが大きく向上する
  • シーンごとに簡単に挙動をカスタマイズできる
  • コードの再利用性が高まり、保守が楽になる

といったメリットがあります。

本コンポーネントをベースに、

  • 攻撃時にアニメーションを再生する
  • プレイヤーに到達したらダメージを与える(別コンポーネントで)
  • 待ち伏せ中は当たり判定をオフにする

などの拡張も容易に行えます。まずはこのシンプルな Ambusher から、自分のゲーム用にカスタマイズしてみてください。

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