【Cocos Creator 3.8】Kamikaze(特攻AI)の実装:アタッチするだけで「発見時に加速し、接触した瞬間に自爆ダメージ」を行う汎用スクリプト

このガイドでは、任意の敵キャラクターにアタッチするだけで、

  • 一定距離内にターゲットを発見すると移動速度が上がり
  • ターゲットに接触した瞬間に自爆ダメージを与え、自身は消滅する

という挙動を実現する汎用コンポーネント Kamikaze を、Cocos Creator 3.8.7 + TypeScript で実装します。

ターゲットの指定・移動速度・発見距離・ダメージ量などはすべてインスペクタから調整可能で、外部の GameManager やシングルトンに依存しない「単体で完結する」設計になっています。


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

1. 機能要件の整理

  • ターゲット(通常はプレイヤー)に向かって移動する。
  • ターゲットとの距離が「発見距離」より小さくなったら「発見状態」になり、移動速度を上げる。
  • ターゲットに接触した瞬間に「自爆」し、ターゲットにダメージを与える。
  • 自爆後は自分自身のノードを破棄する。
  • ターゲットへのダメージ通知も、このコンポーネント単体で完結させる。

ここで「他のノードに依存しない」ことが条件なので、

  • ターゲットの参照はインスペクタで直接設定できるようにする(@property(Node))。
  • ダメージ通知は「ターゲットに receiveDamage(damage: number) というメソッドがあれば呼び出す」という汎用的な形にし、存在しなければ警告ログを出すだけにする。
  • 移動は Transform を直接更新する(物理挙動に依存しない)。
  • 接触判定はシンプルに「距離が contactRadius 以下になったら接触とみなす」という距離ベースの判定にする(Collider や Rigidbody への依存を避ける)。

これにより、どんな 2D/3D ゲームでも「とりあえずターゲットに向かって突撃して爆発する敵」を簡単に作ることができます。

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

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

  • targetNode | null
    • 説明: 追いかけるターゲットノード(通常はプレイヤー)。
    • 設定方法: Inspector でシーン内のノードをドラッグ&ドロップ。
    • 未設定の場合は自動でターゲットを探さず、警告ログを出して動作を停止する。
  • normalSpeednumber
    • 説明: 未発見時(通常巡回状態)の移動速度(単位: ユニット/秒)。
    • 例: 1.0〜3.0 程度。
  • rushSpeednumber
    • 説明: ターゲット発見後(特攻状態)の移動速度(ユニット/秒)。
    • 例: 5.0〜10.0 程度。normalSpeed より大きくする。
  • detectRadiusnumber
    • 説明: ターゲットを「発見」したとみなす距離(ユニット)。
    • この距離より近づくと、速度が rushSpeed に変化する。
  • contactRadiusnumber
    • 説明: ターゲットに「接触」したとみなす距離(ユニット)。
    • この距離より近づいた瞬間に自爆ダメージ処理を行う。
    • detectRadius よりも小さく設定するのが一般的(例: detect=5, contact=0.5)。
  • damagenumber
    • 説明: 接触時にターゲットへ与えるダメージ量。
    • ターゲット側が任意の HP 管理ロジックを持っている前提で、receiveDamage(damage: number) を呼び出す。
  • destroySelfOnExplodeboolean
    • 説明: 自爆時に自分自身のノードを破棄するかどうか。
    • 通常は true。テスト用に false にして挙動を確認することも可能。
  • debugDrawboolean
    • 説明: エディタまたはゲーム実行中に、発見範囲/接触範囲を Gizmo で可視化するかどうか。
    • 有効にすると Scene ビュー上で円が描画され、調整がしやすくなる。

なお、今回は「完全に独立したコンポーネント」とするため、

  • GameManager などの外部スクリプトへの参照は一切持たない。
  • ターゲットへのダメージ通知も「メソッドがあれば呼ぶ」だけにとどめ、存在しない場合はログを出すだけでゲーム進行を止めない。

TypeScriptコードの実装

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


import { _decorator, Component, Node, Vec3, math, director, Color, Gizmo, geometry, log, warn } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * Kamikaze
 * ターゲットを発見すると加速し、接触した瞬間に自爆ダメージを与える汎用特攻AIコンポーネント。
 *
 * 想定使用例:
 * - 敵キャラクターのノードにアタッチし、target にプレイヤーノードを指定するだけで動作。
 * - ターゲット側に receiveDamage(damage: number) があれば自動で呼び出される。
 */
@ccclass('Kamikaze')
@executeInEditMode(true)
@menu('Custom/Kamikaze')
export class Kamikaze extends Component {

    @property({
        type: Node,
        tooltip: '追いかけるターゲットノード(通常はプレイヤー)。\n未設定の場合、このコンポーネントは動作しません。'
    })
    public target: Node | null = null;

    @property({
        tooltip: '通常時の移動速度(ユニット/秒)。'
    })
    public normalSpeed: number = 2.0;

    @property({
        tooltip: 'ターゲット発見後(特攻状態)の移動速度(ユニット/秒)。'
    })
    public rushSpeed: number = 6.0;

    @property({
        tooltip: 'ターゲットを「発見」したとみなす距離(ユニット)。\nこの距離より近づくと速度が rushSpeed に切り替わります。'
    })
    public detectRadius: number = 5.0;

    @property({
        tooltip: 'ターゲットに「接触」したとみなす距離(ユニット)。\nこの距離より近づいた瞬間に自爆ダメージ処理を行います。'
    })
    public contactRadius: number = 0.5;

    @property({
        tooltip: '接触時にターゲットへ与えるダメージ量。\nターゲットに receiveDamage(damage: number) があれば呼び出します。'
    })
    public damage: number = 10;

    @property({
        tooltip: '自爆時にこのノードを自動的に破棄するかどうか。'
    })
    public destroySelfOnExplode: boolean = true;

    @property({
        tooltip: 'Sceneビュー上で発見範囲/接触範囲を可視化するかどうか(Gizmo描画)。'
    })
    public debugDraw: boolean = true;

    // 内部状態
    private _isRushing: boolean = false;
    private _hasExploded: boolean = false;
    private _tempVec: Vec3 = new Vec3();

    onLoad() {
        // 防御的チェック:ターゲット未設定の場合は警告を出す
        if (!this.target) {
            warn('[Kamikaze] target が設定されていません。このコンポーネントは動作しません。 Node:', this.node.name);
        }

        // contactRadius は detectRadius より小さい方が自然なので、逆転していたら警告
        if (this.contactRadius >= this.detectRadius) {
            warn('[Kamikaze] contactRadius が detectRadius 以上に設定されています。意図した挙動にならない可能性があります。');
        }
    }

    start() {
        // 実行時に初期状態をリセット
        this._isRushing = false;
        this._hasExploded = false;
    }

    update(deltaTime: number) {
        // エディタ上でプレビュー中に動かしたくない場合は、実行中かどうかチェックしてもよい
        if (!director.isPaused() && director.getScene()) {
            this._updateLogic(deltaTime);
        }
    }

    /**
     * メインロジック更新
     */
    private _updateLogic(deltaTime: number) {
        if (this._hasExploded) {
            return;
        }

        if (!this.target) {
            // ターゲット未設定なら何もしない
            return;
        }

        // ターゲットとの距離を計算
        const selfPos = this.node.worldPosition;
        const targetPos = this.target.worldPosition;

        Vec3.subtract(this._tempVec, targetPos, selfPos);
        const distance = this._tempVec.length();

        // 発見状態の切り替え
        if (distance <= this.detectRadius) {
            if (!this._isRushing) {
                this._isRushing = true;
                log(`[Kamikaze] ターゲットを発見しました。特攻モードに移行します。 Node: ${this.node.name}`);
            }
        }

        // 接触判定
        if (distance <= this.contactRadius) {
            this._explode();
            return;
        }

        // ターゲットへ移動
        if (distance > 0.0001) {
            // 正規化して方向ベクトルを得る
            const dir = this._tempVec;
            dir.normalize();

            // 現在の速度を決定
            const speed = this._isRushing ? this.rushSpeed : this.normalSpeed;

            // 1フレーム分の移動量を計算
            const move = dir.multiplyScalar(speed * deltaTime);

            // ワールド座標で移動
            const newPos = new Vec3();
            Vec3.add(newPos, selfPos, move);
            this.node.setWorldPosition(newPos);
        }
    }

    /**
     * 自爆処理
     */
    private _explode() {
        if (this._hasExploded) {
            return;
        }
        this._hasExploded = true;

        log(`[Kamikaze] 自爆しました。ターゲットにダメージを与えます。 Damage: ${this.damage}, Node: ${this.node.name}`);

        // ターゲットにダメージ通知
        if (this.target) {
            // any キャストで汎用的にメソッド存在チェック
            const anyTarget: any = this.target.getComponent(Component) || this.target;
            const receiver: any = anyTarget;

            if (receiver && typeof receiver.receiveDamage === 'function') {
                try {
                    receiver.receiveDamage(this.damage);
                } catch (e) {
                    warn('[Kamikaze] ターゲットの receiveDamage 呼び出し中に例外が発生しました:', e);
                }
            } else {
                warn('[Kamikaze] ターゲットに receiveDamage(damage: number) メソッドが見つかりませんでした。ダメージは通知されません。');
            }
        }

        // 自身を破棄
        if (this.destroySelfOnExplode) {
            this.node.destroy();
        }
    }

    /**
     * SceneビューでのGizmo描画(発見範囲と接触範囲の可視化)
     * Cocos Creator 3.8 ではカスタムGizmoのAPIが限定的なため、
     * ここでは簡易的な疑似コード的実装例として残しています。
     *
     * 実際のプロジェクトでは、Editor用拡張やデバッグ用の描画コンポーネントと組み合わせてください。
     */
    // エディタ専用の簡易的な可視化(実行環境によっては無視されます)
    onDrawGizmos?(gizmo: Gizmo) {
        if (!this.debugDraw) {
            return;
        }

        const pos = this.node.worldPosition;
        const detectColor = new Color(0, 255, 0, 128); // 緑
        const contactColor = new Color(255, 0, 0, 128); // 赤

        // 発見範囲
        gizmo.addCircle(new geometry.Circle(pos.x, pos.y, this.detectRadius), detectColor);
        // 接触範囲
        gizmo.addCircle(new geometry.Circle(pos.x, pos.y, this.contactRadius), contactColor);
    }
}

コードのポイント解説

  • onLoad
    • ターゲット未設定時に warn を出力し、防御的に動作を抑制。
    • contactRadius >= detectRadius の場合に警告を出し、設定ミスに気づきやすくしている。
  • start
    • 実行開始時に内部フラグ _isRushing / _hasExploded をリセット。
  • update
    • 毎フレーム _updateLogic を呼び出し、特攻AIのメイン処理を実行。
    • ターゲットとの距離をもとに「発見」「接触」「移動」を制御。
  • _updateLogic
    • ターゲット未設定・既に自爆済みの場合は即 return して無駄な処理をしない。
    • 距離が detectRadius 以下になったら _isRushing = true にし、ログを出力。
    • 距離が contactRadius 以下になったら _explode() を呼び出して自爆。
    • それ以外の場合は、ターゲットへの方向ベクトルを正規化し、速度に応じた移動量を加算。
  • _explode
    • 二重呼び出し防止のため _hasExploded フラグを使用。
    • ターゲットに receiveDamage(damage: number) メソッドがあれば呼び出し、なければ警告ログのみ。
    • destroySelfOnExplode が true の場合、自身のノードを this.node.destroy() で破棄。
  • onDrawGizmos(任意)
    • Scene ビュー上で発見範囲/接触範囲の円を描画するためのフック。
    • 実際の挙動はエディタバージョンや設定に依存するため、プロジェクトに合わせて調整してください。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. 新しく作成されたスクリプトに Kamikaze.ts という名前を付けます。
  3. ダブルクリックしてエディタ(VSCode など)で開き、先ほどのコードをすべて貼り付けて保存します。

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

ここでは 2D ゲームを想定した簡単なテスト手順を紹介します(3D でも同様の手順で構いません)。

  1. プレイヤーノードの作成
    1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択します。
    2. 作成されたノードの名前を Player に変更します。
    3. Sprite コンポーネントに適当な画像を設定し、見た目を確認しやすくします。
  2. 簡易的な HP / ダメージ受け取り処理の追加(任意)

    ダメージ通知を確認したい場合は、Player に以下のような簡易コンポーネントを追加しておくと分かりやすいです(任意)。

    1. Assets パネルで右クリック → Create → TypeScript を選択し、DummyHealth.ts などの名前を付けます。
    2. 以下のようなシンプルなコードを貼り付けます(完全に任意、Kamikaze 本体とは独立):
    
    import { _decorator, Component, log } from 'cc';
    const { ccclass, property } = _decorator;
    
    @ccclass('DummyHealth')
    export class DummyHealth extends Component {
        @property
        public hp: number = 100;
    
        public receiveDamage(damage: number) {
            this.hp -= damage;
            log(`[DummyHealth] ダメージを受けました: ${damage}, 残りHP: ${this.hp}`);
            if (this.hp <= 0) {
                log('[DummyHealth] プレイヤーは倒れました。');
            }
        }
    }
    
    1. Hierarchy の Player ノードを選択し、Inspector で Add Component → Custom → DummyHealth を追加します。
  3. Kamikaze 敵ノードの作成
    1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択します。
    2. 作成されたノードの名前を KamikazeEnemy に変更します。
    3. Sprite に敵っぽい画像を設定し、位置を Player から少し離れた場所(例: X= -5, Y=0)に配置します。

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

  1. Hierarchy で KamikazeEnemy ノードを選択します。
  2. Inspector で Add Component → Custom → Kamikaze を選択し、コンポーネントを追加します。
  3. Inspector 上で Kamikaze の各プロパティを設定します(例):
  • Target: Player ノードをドラッグ&ドロップ
  • Normal Speed: 2.0
  • Rush Speed: 7.0
  • Detect Radius: 5.0
  • Contact Radius: 0.5
  • Damage: 20
  • Destroy Self On Explode: チェックをオン(true)
  • Debug Draw: 必要に応じてオン(true)

4. 動作確認

  1. Scene ビューで KamikazeEnemyPlayer の位置関係を確認し、KamikazeEnemy が detectRadius より遠くにいる場合は、ゆっくりと Player に近づいてくることを確認します。
  2. ゲームを再生(Play)します。
  3. コンソールログ(Console)を開き、以下のようなログが出ることを確認します。
    • 敵が detectRadius 内に入った瞬間:
      • [Kamikaze] ターゲットを発見しました。特攻モードに移行します。
    • 敵が contactRadius 内に入り、自爆した瞬間:
      • [Kamikaze] 自爆しました。ターゲットにダメージを与えます。 Damage: 20, Node: KamikazeEnemy
    • Player に DummyHealth を付けている場合:
      • [DummyHealth] ダメージを受けました: 20, 残りHP: 80 など
  4. Destroy Self On Explode が true の場合、自爆後に KamikazeEnemy ノードが Hierarchy から消えることも確認します。

5. よくある調整ポイント

  • 動きが速すぎる/遅すぎる
    • normalSpeed / rushSpeed を調整してください。
    • 2D/3D のスケールやカメラ距離によって「体感速度」が変わるので、実際に再生して調整すると良いです。
  • すぐに自爆してしまう/なかなか自爆しない
    • contactRadius を調整してください。
    • プレイヤーのスプライトサイズや当たり判定を考慮して、0.3〜1.0 くらいの範囲で試してみると良いです。
  • 発見が早すぎる/遅すぎる
    • detectRadius を調整してください。
    • 画面に入ったらすぐ特攻させたい場合は、カメラ範囲に合わせて大きめの値を設定します。
  • ダメージが適用されない
    • ターゲット側に receiveDamage(damage: number) メソッドを持つコンポーネントがアタッチされているか確認してください。
    • メソッド名・引数の型が一致しているかも確認します。
    • 存在しない場合、コンソールに receiveDamage が見つかりませんでした の警告が出ます。

まとめ

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

  • ターゲットノードを Inspector で指定するだけで
  • 発見距離・接触距離・通常速度・特攻速度・ダメージ量を細かく調整できる
  • 完全に独立した「特攻AI」を実現する汎用スクリプト

として設計されています。

GameManager や外部シングルトンに依存せず、

  • 敵キャラクターのバリエーションごとに normalSpeed / rushSpeed / damage を変えるだけで、
  • 軽い特攻兵・重い自爆ロボット・高速ドローンなど、様々な敵キャラクターを簡単に作成できます。

また、距離ベースの接触判定を使っているため、Collider/Physics の設定に悩まされることなく、プロトタイプ段階から素早く「突撃して爆発する敵」をシーンに追加できます。

このコンポーネントをベースに、

  • 自爆時にパーティクルエフェクトや SE を再生する
  • 特攻開始前にチャージ演出を入れる
  • ターゲットを見失ったら元の位置に戻る

といった拡張を行うことで、よりリッチな敵 AI を構築していくことも容易です。

まずは本記事のコードをそのままコピーして、シーンに 1 体だけ Kamikaze 敵を配置し、挙動を確認してみてください。そこからパラメータをいじるだけで、多様な特攻キャラクターを素早く量産できるようになります。