【Cocos Creator】アタッチするだけ!DistanceActivator (距離アクティブ)の実装方法【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】DistanceActivator の実装:アタッチするだけで「プレイヤーが近づくまでノードを眠らせる」汎用スクリプト

このガイドでは、Cocos Creator 3.8.7 + TypeScript で、プレイヤーが一定距離まで近づくまでノードの処理や物理演算を無効化しておき、近づいたらまとめて有効化するための汎用コンポーネント DistanceActivator を実装します。

敵やギミック、パーティクルなどを「プレイヤーの近くに来たときだけ動かしたい」「遠くにあるオブジェクトは処理を止めて軽くしたい」といった場面で、このコンポーネントをアタッチして距離しきい値とプレイヤーノードを指定するだけで使えるようにします。


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

実現したい機能

  • このコンポーネントをアタッチしたノード(以下「対象ノード」)は、最初は非アクティブ状態(処理・物理演算などを停止した状態)として扱う。
  • 指定した「プレイヤーノード」が対象ノードに一定距離まで近づいたら、対象ノードの各種コンポーネントを有効化する。
  • 一度有効化したら、その後は再び距離が離れても無効化には戻さない(デフォルト)
  • 距離チェックの対象は 2D / 3D を問わず、ワールド座標ベースの 3D 距離で行う。
  • 処理負荷を抑えるため、距離チェックは毎フレームではなく、任意の間隔(秒)ごとに行えるようにする

外部依存をなくすための設計

  • プレイヤーノードは Inspector から指定する @property とし、GameManager やシングルトンには依存しない。
  • 有効化・無効化の対象とするコンポーネント(RigidBody2D / RigidBody / Collider2D / Collider / Animation / SkeletalAnimation / ParticleSystem2D / ParticleSystem / AudioSource など)は、自動で探して制御する。
  • 存在しないコンポーネントは単にスキップし、ログエラーにはしない(任意で利用できるようにするため)。
  • 開始時に「無効化フラグ」がオンの場合は、対象コンポーネントを強制的に停止・無効化してから距離監視を開始する。

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

  • playerNode: Node | null
    • プレイヤー(または距離の基準とする)ノード。
    • Hierarchy からドラッグ&ドロップで指定。
    • 未設定の場合は警告ログを出し、距離チェックを行わない。
  • activateDistance: number
    • プレイヤーノードがこの距離以下まで近づいたらアクティブ化を行う。
    • 単位はワールド座標系での距離(メートル)。
    • 例: 10 にすると、10m 以内に入った時点で有効化。
  • checkInterval: number
    • 距離をチェックする間隔(秒)。
    • 0 以下にすると毎フレーム update でチェック。
    • 例: 0.2 にすると、0.2 秒ごとに距離を測定して判定。
  • deactivateOnStart: boolean
    • true の場合、開始時に対象ノードの各種コンポーネントを停止/無効化状態にしておく
    • 敵 AI やパーティクルを「プレイヤーが近づくまで完全に止めておく」用途に便利。
  • reactivateWhenFar: boolean
    • true の場合、距離がしきい値より遠くなったら再び無効化する
    • false の場合、一度有効化したらそのまま有効のまま(デフォルト挙動)。
  • hysteresisDistance: number
    • reactivateWhenFar が true のときだけ使用。
    • 「再び無効化する距離」のしきい値。
    • 例: activateDistance = 10hysteresisDistance = 15 とすると、10m 以内で有効化し、15m 以上離れたら無効化に戻る。
    • 0 以下の場合は activateDistance と同じ値として扱う。
  • affectChildren: boolean
    • true の場合、子ノード以下も含めて各種コンポーネントの有効・無効を切り替える。
    • false の場合、このノード自身に付いているコンポーネントのみ制御する。
  • logDebug: boolean
    • true の場合、距離判定や状態変更のタイミングで console.log を出力する。
    • 挙動確認やデバッグに便利。リリースビルドでは false 推奨。

TypeScriptコードの実装


import {
    _decorator,
    Component,
    Node,
    Vec3,
    RigidBody2D,
    RigidBody,
    Collider2D,
    Collider,
    Animation,
    SkeletalAnimation,
    ParticleSystem2D,
    ParticleSystem,
    AudioSource,
    game,
} from 'cc';
const { ccclass, property } = _decorator;

/**
 * DistanceActivator
 * プレイヤーが一定距離に近づくまで、対象ノードの処理や物理演算を無効化しておき、
 * 近づいたらまとめて有効化する汎用コンポーネント。
 */
@ccclass('DistanceActivator')
export class DistanceActivator extends Component {

    @property({
        tooltip: '距離判定の基準となるプレイヤーノード。\nここで指定したノードとの距離を監視します。'
    })
    public playerNode: Node | null = null;

    @property({
        tooltip: 'プレイヤーがこの距離(メートル)以内に近づいたら有効化します。'
    })
    public activateDistance: number = 10;

    @property({
        tooltip: '距離チェックを行う間隔(秒)。\n0 以下にすると毎フレームチェックします。'
    })
    public checkInterval: number = 0.2;

    @property({
        tooltip: '開始時に対象ノードの処理や物理演算を無効化しておくかどうか。'
    })
    public deactivateOnStart: boolean = true;

    @property({
        tooltip: 'プレイヤーが離れたときに再び無効化するかどうか。\ntrue の場合はヒステリシス距離も利用します。'
    })
    public reactivateWhenFar: boolean = false;

    @property({
        tooltip: '再び無効化する距離(メートル)。\n0 以下の場合は activateDistance と同じ値として扱います。'
    })
    public hysteresisDistance: number = 0;

    @property({
        tooltip: 'true の場合、子ノード以下も含めてコンポーネントの有効・無効を切り替えます。'
    })
    public affectChildren: boolean = true;

    @property({
        tooltip: 'デバッグ用ログを出力するかどうか。'
    })
    public logDebug: boolean = false;

    /** 現在アクティブ化されているかどうか */
    private _isActive: boolean = false;

    /** 距離チェック用の経過時間カウンタ */
    private _timeAccumulator: number = 0;

    /** 有効化・無効化の対象とするノード一覧(自身+必要なら子孫) */
    private _targetNodes: Node[] = [];

    onLoad() {
        // 対象ノード一覧を構築
        this._buildTargetNodeList();

        // 起動時に無効化しておく設定なら、ここで一括停止
        if (this.deactivateOnStart) {
            this._applyActiveState(false);
            this._isActive = false;
            if (this.logDebug) {
                console.log(`[DistanceActivator] Deactivated on start for node: ${this.node.name}`);
            }
        } else {
            // 無効化しない場合は、現在の状態を「アクティブ」として扱う
            this._isActive = true;
        }

        // プレイヤーノード未設定の場合は警告のみ出す
        if (!this.playerNode) {
            console.warn(`[DistanceActivator] playerNode is not assigned on node: ${this.node.name}. Distance check will be skipped.`);
        }

        // checkInterval が負の場合の防御
        if (this.checkInterval < 0) {
            console.warn(`[DistanceActivator] checkInterval is negative on node: ${this.node.name}. Using 0 (every frame).`);
            this.checkInterval = 0;
        }

        // hysteresisDistance の補正
        if (this.hysteresisDistance <= 0) {
            this.hysteresisDistance = this.activateDistance;
        }
    }

    start() {
        // 特に処理は不要だが、ライフサイクルの流れを明示
        if (this.logDebug) {
            console.log(`[DistanceActivator] start on node: ${this.node.name}`);
        }
    }

    update(deltaTime: number) {
        // プレイヤーノードが設定されていなければ何もしない
        if (!this.playerNode) {
            return;
        }

        // ゲームが一時停止中の場合は処理をスキップ(任意)
        if (game.isPaused()) {
            return;
        }

        // 距離チェックの間隔制御
        if (this.checkInterval > 0) {
            this._timeAccumulator += deltaTime;
            if (this._timeAccumulator < this.checkInterval) {
                return;
            }
            this._timeAccumulator = 0;
        }

        // 実際の距離チェック
        this._checkDistanceAndUpdateState();
    }

    /**
     * 自身および(必要なら)子孫ノードの一覧を構築。
     */
    private _buildTargetNodeList() {
        this._targetNodes.length = 0;
        this._targetNodes.push(this.node);

        if (this.affectChildren) {
            this._collectChildrenRecursive(this.node);
        }
    }

    private _collectChildrenRecursive(node: Node) {
        for (const child of node.children) {
            this._targetNodes.push(child);
            this._collectChildrenRecursive(child);
        }
    }

    /**
     * プレイヤーとの距離を計算して、有効/無効状態を更新する。
     */
    private _checkDistanceAndUpdateState() {
        const selfWorldPos = new Vec3();
        const playerWorldPos = new Vec3();

        this.node.getWorldPosition(selfWorldPos);
        this.playerNode!.getWorldPosition(playerWorldPos);

        const distance = Vec3.distance(selfWorldPos, playerWorldPos);

        if (this.logDebug) {
            console.log(`[DistanceActivator] distance = ${distance.toFixed(2)} (node: ${this.node.name})`);
        }

        // まだアクティブでない場合、近づいたら有効化
        if (!this._isActive && distance <= this.activateDistance) {
            this._isActive = true;
            this._applyActiveState(true);
            if (this.logDebug) {
                console.log(`[DistanceActivator] Activated (distance ${distance.toFixed(2)}) on node: ${this.node.name}`);
            }
            return;
        }

        // すでにアクティブで、離れたら無効化する設定の場合
        if (this._isActive && this.reactivateWhenFar && distance >= this.hysteresisDistance) {
            this._isActive = false;
            this._applyActiveState(false);
            if (this.logDebug) {
                console.log(`[DistanceActivator] Deactivated (distance ${distance.toFixed(2)}) on node: ${this.node.name}`);
            }
        }
    }

    /**
     * 対象ノード群のコンポーネントを有効/無効にする。
     * - 物理ボディ(2D/3D)の有効化・無効化
     * - コライダー(2D/3D)の有効化・無効化
     * - アニメーションの再生/停止
     * - パーティクルの再生/停止
     * - オーディオの再生/停止
     */
    private _applyActiveState(activate: boolean) {
        for (const node of this._targetNodes) {
            // RigidBody2D
            const rb2d = node.getComponent(RigidBody2D);
            if (rb2d) {
                rb2d.enabled = activate;
            }

            // RigidBody (3D)
            const rb3d = node.getComponent(RigidBody);
            if (rb3d) {
                rb3d.enabled = activate;
            }

            // Collider2D
            const col2d = node.getComponent(Collider2D);
            if (col2d) {
                col2d.enabled = activate;
            }

            // Collider (3D)
            const col3d = node.getComponent(Collider);
            if (col3d) {
                col3d.enabled = activate;
            }

            // Animation
            const anim = node.getComponent(Animation);
            if (anim) {
                if (activate) {
                    // すでに再生中でなければ再生
                    if (!anim.getState(anim.defaultClip?.name || '')) {
                        anim.play();
                    } else if (!anim.getState(anim.defaultClip!.name).isPlaying) {
                        anim.play();
                    }
                } else {
                    anim.stop();
                }
            }

            // SkeletalAnimation
            const skelAnim = node.getComponent(SkeletalAnimation);
            if (skelAnim) {
                if (activate) {
                    if (skelAnim.defaultClip) {
                        skelAnim.play(skelAnim.defaultClip.name);
                    } else {
                        skelAnim.play();
                    }
                } else {
                    skelAnim.stop();
                }
            }

            // ParticleSystem2D
            const ps2d = node.getComponent(ParticleSystem2D);
            if (ps2d) {
                if (activate) {
                    ps2d.resetSystem();
                } else {
                    ps2d.stopSystem();
                }
            }

            // ParticleSystem (3D)
            const ps3d = node.getComponent(ParticleSystem);
            if (ps3d) {
                if (activate) {
                    ps3d.play();
                } else {
                    ps3d.stop();
                }
            }

            // AudioSource
            const audio = node.getComponent(AudioSource);
            if (audio) {
                if (activate) {
                    if (!audio.playing) {
                        audio.play();
                    }
                } else {
                    audio.stop();
                }
            }

            // ここで必要に応じて、他のコンポーネントの有効/無効も追加可能
            // 例: CustomAI や ScriptComponent など
            // const custom = node.getComponent(CustomAI);
            // if (custom) custom.enabled = activate;
        }
    }
}

コードの要点解説

  • onLoad
    • _buildTargetNodeList() で、制御対象とするノード一覧(自身+必要なら子孫)を構築。
    • deactivateOnStart が true の場合、_applyActiveState(false) で各種コンポーネントを停止・無効化。
    • playerNode が未設定なら警告ログを出し、距離チェックはスキップされる。
    • checkIntervalhysteresisDistance の値を防御的に補正。
  • update
    • checkInterval に応じて距離チェックの頻度を制御。
    • 間隔が経過したタイミングで _checkDistanceAndUpdateState() を呼び出す。
  • _checkDistanceAndUpdateState
    • プレイヤーと対象ノードのワールド座標を取得し、Vec3.distance で距離を算出。
    • 距離が activateDistance 以下で、まだアクティブでない場合 → 有効化。
    • reactivateWhenFar が true の場合、距離が hysteresisDistance 以上で、アクティブ中 → 無効化。
  • _applyActiveState
    • 対象ノード群をループし、代表的なコンポーネントをまとめて有効/無効にする。
    • 物理ボディ・コライダーは enabled フラグを切り替え。
    • アニメーション・パーティクル・オーディオは play()/stop() や相当のメソッドで制御。
    • 存在しないコンポーネントは無視するため、どのノードにアタッチしても安全に動作。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで任意のフォルダ(例: assets/scripts)を右クリック。
  2. Create → TypeScript を選択します。
  3. ファイル名を DistanceActivator.ts に変更します。
  4. 作成した DistanceActivator.ts をダブルクリックし、エディタ(VSCode など)で開きます。
  5. 中身をすべて削除し、本記事の DistanceActivator のコードをそのまま貼り付けて保存します。

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

ここでは 2D シーンを例にしますが、3D でも同様です。

  1. Scene を開き、まだカメラや Canvas がない場合は 2D シーンテンプレートを使用して作成します。
  2. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選び、Player という名前に変更します。
    • これが playerNode に指定するプレイヤー役のノードになります。
  3. 同様に、右クリック → Create → 2D Object → Sprite で別のスプライトを作成し、Enemy という名前に変更します。
    • この Enemy ノードに DistanceActivator をアタッチします。

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

  1. Hierarchy で Enemy ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリック。
  3. Custom カテゴリの中から DistanceActivator を選択します。
    • 見つからない場合は、スクリプトを保存した後に Cocos Creator を一度フォーカスし直して再インポートさせてください。

4. プロパティの設定

Enemy ノードに追加された DistanceActivator の各プロパティを設定します。

  1. Player NodeplayerNode
    • Hierarchy から Player ノードをドラッグして、このフィールドにドロップします。
  2. Activate DistanceactivateDistance
    • 例として 5 と入力します。
    • これで、プレイヤーが Enemy から 5m 以内に近づいたときに有効化されます。
  3. Check IntervalcheckInterval
    • 例として 0.2 と入力します。
    • 0.2 秒ごとに距離チェックを行うため、負荷を抑えつつ十分な反応速度が得られます。
  4. Deactivate On StartdeactivateOnStart
    • 敵の動きや物理演算を最初は止めておきたいので、チェックを入れたまま(true)にします。
  5. Reactivate When FarreactivateWhenFar
    • 一度有効化したらそのままにしたい場合は チェックを外す(false)
    • プレイヤーが離れたら再び止めたい場合は チェックを入れる(true)
  6. Hysteresis DistancehysteresisDistance
    • reactivateWhenFar を true にした場合のみ意味があります。
    • 例: activateDistance = 5hysteresisDistance = 8 とすると、5m 以内で有効化し、8m 以上離れたら再び無効化されます。
  7. Affect ChildrenaffectChildren
    • Enemy に子ノード(当たり判定、エフェクトなど)がある場合は チェックを入れる(true) ことで、まとめて制御できます。
    • Enemy 自身のコンポーネントだけを制御したい場合はチェックを外します。
  8. Log DebuglogDebug
    • 挙動確認のために チェックを入れておくと、コンソールに距離と状態変化が表示されます。
    • 問題がなければ最終的にはオフにしておくと良いでしょう。

5. 動作確認

  1. シーンを保存し、エディタ右上の Play ボタンで実行します。
  2. ゲームビュー上で Player ノードをドラッグして移動させ、Enemy に近づけてみます。
    • 開始時点では、Enemy に付いている RigidBody2DAnimation などが停止しているはずです(deactivateOnStart = true の場合)。
    • プレイヤーが activateDistance 以内に入ると、Enemy のコンポーネントが有効化され、物理挙動やアニメーションが動き始めます。
  3. reactivateWhenFar = truehysteresisDistance > activateDistance に設定している場合は、プレイヤーを遠ざけて しきい値より離れると再び無効化されることも確認できます。
  4. logDebug = true にしている場合は、Console パネルに
    • [DistanceActivator] distance = ...
    • [DistanceActivator] Activated ...
    • [DistanceActivator] Deactivated ...

    といったログが出力され、距離判定のタイミングが分かります。


まとめ

DistanceActivator は、

  • プレイヤーが近づくまで敵やギミックを「眠らせておく」
  • 画面外や遠方のオブジェクトの物理演算・アニメーション・パーティクル・サウンドを止めておき、パフォーマンスを向上させる
  • プレイヤーとの距離ベースで「出現」「消滅」「起動」「休止」を制御する

といった用途にそのまま使える、汎用性の高いコンポーネントです。

本コンポーネントは、

  • 外部の GameManager やシングルトンに一切依存せず、必要な情報はすべて Inspector の @property から受け取る設計
  • 物理・アニメーション・パーティクル・オーディオなど、代表的なコンポーネントをまとめて制御
  • 子ノードも含めた一括制御や、チェック間隔・ヒステリシスなどの細かい調整が可能

という特徴を持っているため、任意のノードにアタッチするだけで簡単に「距離アクティブ」機構を導入できます。

応用としては、

  • DistanceActivator を複数の敵・オブジェクトに付けて、プレイヤー周辺だけが動くワールドを構築する
  • プレイヤーではなくカメラノードを playerNode に指定して、画面外のオブジェクトを自動で休止させる
  • 特定のチェックポイントノードを基準に、ステージの一部を段階的に起動していく

など、さまざまなゲーム設計にそのまま組み込めます。まずはシンプルな敵やギミックに試しにアタッチして、距離しきい値やチェック間隔を調整しながら、自分のプロジェクトに最適な設定を見つけてみてください。

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