【Cocos Creator】アタッチするだけ!ZoomTrigger (ズーム制御)の実装方法【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】ZoomTrigger の実装:アタッチするだけで「指定エリアに入ったらカメラのズームを自動切り替え」できる汎用スクリプト

このコンポーネントは、親ノードが特定のエリア(コライダー)に入った時に Camera2D の zoomRatio を自動で変更するための汎用トリガーです。
2D アクションや探索ゲームで、「この部屋に入ったらカメラを引きで」「この通路では少しズームイン」といった演出を、ノードにアタッチしてプロパティを設定するだけで実現できます。


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

基本コンセプト

  • ZoomTrigger は「トリガー用のエリアノード」にアタッチして使います。
  • このトリガーエリアに 親ノード(プレイヤーなど)が侵入したときに、指定した Camera2D の zoomRatio を変更します。
  • 外部の GameManager やシングルトンには一切依存せず、インスペクタのプロパティだけで完結します。
  • トリガーエリアには Collider2D(例: BoxCollider2D) を利用し、「IsTrigger(トリガー)」として扱います。
  • 親ノード側にも Collider2D + RigidBody2D を付与し、物理イベントを発生させます。

動作要件

  • ZoomTrigger をアタッチしたノードに Collider2D コンポーネント(BoxCollider2D など)が必要です。
  • その Collider2D は 「Is Trigger」フラグを ON にしておく必要があります。
  • 親ノード側(プレイヤーなど)にも Collider2D と RigidBody2D が必要です(Trigger イベントを発火させるため)。
  • ズームを変更したい Camera2D コンポーネント への参照を、Inspector から指定します。

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

ZoomTrigger コンポーネントに用意するプロパティと役割は以下の通りです。

  • targetCamera: Camera2D | null
    ズーム値を変更したい対象の Camera2D。
    シーン上のカメラノードからドラッグ&ドロップで設定します。
    未設定の場合は、親ノード階層から自動で Camera2D を検索し、見つからなければエラーログを出します。
  • enterZoom: number
    トリガーエリアに 侵入したときに設定する zoomRatio
    例: 0.5(ズームアウト)、1.0(等倍)、2.0(ズームイン)。
  • useExitZoom: boolean
    トリガーエリアから 退出したときにズームを戻すかどうかのフラグ。
    ON の場合は、exitZoom または自動保存した元のズーム値に戻します。
  • exitZoom: number
    トリガーエリアから 退出したときに設定する zoomRatio
    restoreOriginalZoom が ON の場合はこの値は無視され、侵入前に記録したズーム値に戻します。
  • restoreOriginalZoom: boolean
    useExitZoom が ON のときに有効。
    侵入時に カメラの元々の zoomRatio を記録しておき、退出時にその値へ戻します。
    OFF の場合は exitZoom の値を使用します。
  • transitionDuration: number
    ズーム値を 補間して変更する時間(秒)
    0 の場合は即時変更。0.2〜0.5 などにすると、滑らかにズームが変化します。
  • onlyAffectSpecificGroup: boolean
    特定の物理グループのオブジェクトにのみ反応させるかどうか。
    ON の場合、targetGroup と同じグループの Collider2D だけがトリガー対象となります。
  • targetGroup: number
    反応させたい Collider2D の グループビット
    例: デフォルトグループが 1、プレイヤー用のカスタムグループが 2 など。
    onlyAffectSpecificGroup が ON のときだけ参照されます。
  • debugLog: boolean
    ON の場合、侵入・退出時やエラー時に console.log / console.warn を詳細に出力します。
    動作確認やデバッグに便利です。

TypeScriptコードの実装

以下が ZoomTrigger コンポーネントの完全な実装コードです。


import { _decorator, Component, Camera, CameraComponent, Camera2D, Collider2D, Contact2DType, IPhysics2DContact, Node, Vec3, tween, Tween, math } from 'cc';
const { ccclass, property, tooltip } = _decorator;

/**
 * ZoomTrigger
 * 親ノードがこのトリガーコライダーに侵入・退出したタイミングで、
 * 指定した Camera2D の zoomRatio を変更する汎用コンポーネント。
 */
@ccclass('ZoomTrigger')
export class ZoomTrigger extends Component {

    @property({
        type: Camera2D,
        tooltip: 'ズーム制御対象の Camera2D。\n未設定の場合、親ノード階層から自動で検索を試みます。'
    })
    public targetCamera: Camera2D | null = null;

    @property({
        tooltip: 'トリガーエリアに侵入したときに設定する zoomRatio。\n1.0 が等倍、0.5 でズームアウト、2.0 でズームイン。'
    })
    public enterZoom: number = 1.0;

    @property({
        tooltip: 'トリガーエリアから退出したときにズームを戻すかどうか。'
    })
    public useExitZoom: boolean = true;

    @property({
        tooltip: 'トリガーエリアから退出したときに設定する zoomRatio。\nrestoreOriginalZoom が ON の場合、この値は無視されます。'
    })
    public exitZoom: number = 1.0;

    @property({
        tooltip: '侵入前の zoomRatio を記録しておき、退出時にその値へ戻すかどうか。'
    })
    public restoreOriginalZoom: boolean = true;

    @property({
        tooltip: 'ズーム値を補間して変更する時間(秒)。\n0 の場合は即時に切り替えます。'
    })
    public transitionDuration: number = 0.3;

    @property({
        tooltip: '特定の物理グループのオブジェクトにのみ反応させるかどうか。'
    })
    public onlyAffectSpecificGroup: boolean = false;

    @property({
        tooltip: '反応させたい Collider2D のグループビット。\nonlyAffectSpecificGroup が ON のときのみ使用されます。'
    })
    public targetGroup: number = 1;

    @property({
        tooltip: 'デバッグログを有効にするかどうか。'
    })
    public debugLog: boolean = false;

    // 内部状態
    private _collider: Collider2D | null = null;
    private _originalZoom: number | null = null;
    private _currentTween: Tween<{ value: number }> | null = null;
    private _insideCount: number = 0; // ネストされた接触を考慮するためのカウンタ

    onLoad() {
        // Collider2D の取得とイベント登録
        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            console.error('[ZoomTrigger] Collider2D が見つかりません。このノードに BoxCollider2D などの Collider2D を追加してください。', this.node.name);
        } else {
            // Trigger イベントを購読
            this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this);
        }

        // Camera2D が未指定の場合、親階層から検索を試みる
        if (!this.targetCamera) {
            const foundCamera = this._findCameraInParents(this.node);
            if (foundCamera) {
                this.targetCamera = foundCamera;
                if (this.debugLog) {
                    console.log('[ZoomTrigger] 親階層から Camera2D を自動検出しました:', foundCamera.node.name);
                }
            } else {
                console.warn('[ZoomTrigger] targetCamera が設定されておらず、親階層からも Camera2D を見つけられませんでした。Inspector から Camera2D を指定してください。', this.node.name);
            }
        }
    }

    onDestroy() {
        // イベントの解除
        if (this._collider) {
            this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.off(Contact2DType.END_CONTACT, this._onEndContact, this);
        }
        this._stopTween();
    }

    /**
     * 親階層から Camera2D を探すユーティリティ。
     */
    private _findCameraInParents(startNode: Node): Camera2D | null {
        let current: Node | null = startNode;
        while (current) {
            const cam2d = current.getComponent(Camera2D);
            if (cam2d) {
                return cam2d;
            }
            // Camera コンポーネントから Camera2D を取得できるケースも考慮(将来拡張用)
            const cam = current.getComponent(CameraComponent) as Camera2D | null;
            if (cam) {
                return cam;
            }
            current = current.parent;
        }
        return null;
    }

    /**
     * BEGIN_CONTACT ハンドラ:侵入時
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!this.targetCamera) {
            console.warn('[ZoomTrigger] targetCamera が設定されていないため、ズーム変更を行いません。', this.node.name);
            return;
        }

        if (!this._isValidTarget(otherCollider)) {
            return;
        }

        // 最初の侵入時のみ元のズームを記録
        if (this._insideCount === 0 && this.restoreOriginalZoom) {
            this._originalZoom = this.targetCamera.zoomRatio;
            if (this.debugLog) {
                console.log('[ZoomTrigger] 元の zoomRatio を記録しました:', this._originalZoom);
            }
        }

        this._insideCount++;

        if (this.debugLog) {
            console.log('[ZoomTrigger] BEGIN_CONTACT: ズームを enterZoom に変更します。', {
                node: this.node.name,
                other: otherCollider.node.name,
                enterZoom: this.enterZoom,
                insideCount: this._insideCount,
            });
        }

        this._applyZoom(this.enterZoom);
    }

    /**
     * END_CONTACT ハンドラ:退出時
     */
    private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!this.targetCamera) {
            return;
        }

        if (!this._isValidTarget(otherCollider)) {
            return;
        }

        this._insideCount = Math.max(0, this._insideCount - 1);

        if (this.debugLog) {
            console.log('[ZoomTrigger] END_CONTACT:', {
                node: this.node.name,
                other: otherCollider.node.name,
                insideCount: this._insideCount,
            });
        }

        // まだ他のコライダーが中にいる場合はズームを戻さない
        if (this._insideCount > 0) {
            return;
        }

        if (!this.useExitZoom) {
            if (this.debugLog) {
                console.log('[ZoomTrigger] useExitZoom が false のため、退出時のズーム変更は行いません。');
            }
            return;
        }

        let targetZoom: number;
        if (this.restoreOriginalZoom && this._originalZoom !== null) {
            targetZoom = this._originalZoom;
        } else {
            targetZoom = this.exitZoom;
        }

        if (this.debugLog) {
            console.log('[ZoomTrigger] 退出時にズームを変更します。', {
                targetZoom,
                restoreOriginalZoom: this.restoreOriginalZoom,
            });
        }

        this._applyZoom(targetZoom);
    }

    /**
     * 反応させる Collider2D かどうかを判定する。
     */
    private _isValidTarget(otherCollider: Collider2D): boolean {
        if (!this.onlyAffectSpecificGroup) {
            return true;
        }
        const result = otherCollider.group === this.targetGroup;
        if (this.debugLog) {
            console.log('[ZoomTrigger] ターゲット判定:', {
                otherNode: otherCollider.node.name,
                otherGroup: otherCollider.group,
                targetGroup: this.targetGroup,
                isValid: result,
            });
        }
        return result;
    }

    /**
     * ズーム値を適用する(補間あり/なし)。
     */
    private _applyZoom(targetZoom: number) {
        if (!this.targetCamera) {
            return;
        }

        // 負値や 0 は無効なので防御的に修正
        if (targetZoom <= 0) {
            console.warn('[ZoomTrigger] targetZoom が 0 以下です。1.0 に補正します。指定値:', targetZoom);
            targetZoom = 1.0;
        }

        // 既存の Tween があれば停止
        this._stopTween();

        if (this.transitionDuration <= 0) {
            // 即時変更
            this.targetCamera.zoomRatio = targetZoom;
            return;
        }

        const startZoom = this.targetCamera.zoomRatio;
        const zoomData = { value: startZoom };

        this._currentTween = tween(zoomData)
            .to(this.transitionDuration, { value: targetZoom }, {
                onUpdate: (_target, ratio) => {
                    if (!this.targetCamera) {
                        return;
                    }
                    // 線形補間(tween がやってくれるが、念のため)
                    const v = math.lerp(startZoom, targetZoom, ratio);
                    this.targetCamera.zoomRatio = v;
                }
            })
            .call(() => {
                if (this.debugLog) {
                    console.log('[ZoomTrigger] ズーム変更完了:', targetZoom);
                }
                this._currentTween = null;
            })
            .start();
    }

    /**
     * 進行中の Tween を停止する。
     */
    private _stopTween() {
        if (this._currentTween) {
            this._currentTween.stop();
            this._currentTween = null;
        }
    }
}

コードのポイント解説

  • onLoad
    • 同じノード上の Collider2D を取得し、BEGIN_CONTACTEND_CONTACT イベントを登録します。
    • targetCamera が未設定の場合は、親ノード階層を辿って Camera2D を自動検出します。
    • Collider2D が見つからない場合は console.error で明確に警告します。
  • _onBeginContact
    • 侵入時に一度だけ、restoreOriginalZoom が ON なら 元の zoomRatio を記録します。
    • 複数のコライダーが同時に入るケースに備え、_insideCount で侵入数をカウントしています。
    • 条件を満たすターゲットだけに反応するため、_isValidTarget でグループチェックを行います。
    • 有効な侵入であれば enterZoom へズームを変更します(Tween で補間)。
  • _onEndContact
    • 退出時に _insideCount をデクリメントし、まだ中に他のコライダーがいる場合はズームを戻さないようにしています。
    • useExitZoom が OFF の場合は退出時の処理をスキップします。
    • restoreOriginalZoom が ON かつ元の値が記録されていればそれに戻し、そうでなければ exitZoom に設定します。
  • _applyZoom
    • 防御的に、targetZoom <= 0 の場合は 1.0 に補正します。
    • transitionDuration <= 0 のときは 即時に zoomRatio を変更します。
    • それ以外は Tween を使って なめらかにズームを補間します。
    • 前回の Tween が残っていると二重再生になるため、開始前に _stopTween で必ず停止します。

使用手順と動作確認

ここからは、実際に Cocos Creator 3.8.7 エディタ上で ZoomTrigger を使う手順を具体的に説明します。

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

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

2. カメラノードの準備

  1. Hierarchy パネルで、既に 2D カメラがあるか確認します。なければ:
    1. Hierarchy パネルで右クリック → Create → 3D Object → Camera などでカメラを作成します。
    2. 2D 用に Camera2D コンポーネントを追加するか、2D 用テンプレートから作成します。
  2. カメラノードを選択し、Inspector で Camera2D コンポーネントが付いていることを確認します。
  3. 初期のズームを決めたい場合は、zoomRatio を 1.0 などに設定しておきます。

3. プレイヤー(侵入する側)の準備

ここでは例として「Player」ノードを使います。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite などでプレイヤーノードを作成し、Player と名前を付けます。
  2. Player ノードを選択し、Inspector で以下を追加します。
    • RigidBody2D(Body Type は Dynamic など)
    • Collider2D(例: BoxCollider2D)
  3. Player 側の Collider2D は Is Trigger を OFF(通常の衝突判定)で問題ありません。
  4. 必要に応じて、Physics グループ(Group)を「Player」用のグループに設定します(後で ZoomTrigger 側でグループ指定したい場合)。

4. トリガーエリアノードの作成と ZoomTrigger のアタッチ

  1. Hierarchy パネルで右クリック → Create → Empty Node を選び、ZoomArea などの名前を付けます。
  2. ZoomArea ノードを選択し、Inspector で 2D → BoxCollider2D を追加します。
  3. BoxCollider2D のサイズを変更して、「この範囲に入ったらズームしたい」というエリアを作ります。
  4. BoxCollider2D の Is Trigger にチェックを入れます(必須)。
  5. 続いて ZoomArea ノードの Inspector で Add Component → Custom → ZoomTrigger を選択し、コンポーネントをアタッチします。

5. ZoomTrigger のプロパティ設定

ZoomArea ノードの Inspector に表示される ZoomTrigger のプロパティを設定します。

  1. Target Camera:
    • Camera2D を持つカメラノードを Hierarchy からドラッグ&ドロップします。
    • 未設定でも親階層から自動検出を試みますが、明示的に設定しておくことを推奨します。
  2. Enter Zoom:
    • トリガーに入ったときのズーム値を設定します。
    • 例:
      • 0.5: 少し引きで表示(広く見せたいエリア向け)
      • 1.0: 等倍
      • 1.5〜2.0: ズームイン(細かいギミックを見せたい場所など)
  3. Use Exit Zoom:
    • ON にすると、エリアから出たときにズームを戻します。
  4. Restore Original Zoom:
    • ON の場合、プレイヤーがエリアに入る前の zoomRatio を自動記録し、退出時にその値へ戻します。
    • OFF の場合は、次の Exit Zoom の値に変更されます。
  5. Exit Zoom:
    • Restore Original Zoom が OFF のときに使用される値です。
    • 例: 1.0 に設定しておけば、「このエリアから出たら常に等倍に戻す」といった挙動になります。
  6. Transition Duration:
    • ズームの切り替え時間(秒)です。
    • 0.0: 即座に切り替え。
    • 0.2〜0.5: 自然なズーム演出。
    • 1.0 以上: ゆっくりとしたズーム。
  7. Only Affect Specific Group:
    • ON にすると、指定したグループの Collider2D だけに反応します。
    • プレイヤーだけに反応させたい場合に便利です。
  8. Target Group:
    • 上記フラグが ON のときに有効です。
    • Player の Collider2D の Group が例えば 2 であれば、ここも 2 に設定します。
  9. Debug Log:
    • ON にすると、侵入・退出時やカメラ検出時などの詳細なログが Console に出力されます。
    • 動作確認中は ON、本番では OFF にする、といった運用がしやすいです。

6. 実際に動かして確認する

  1. Scene ビューで Player ノードと ZoomArea ノードの位置関係を調整し、Player を動かしたときに ZoomArea のコライダーを通過するように配置します。
  2. Player に移動用の簡単なスクリプトがある場合はそれを使い、なければ一時的に Scene ビュー上でドラッグして動かすだけでも、物理シミュレーションを OFF にしていなければ接触イベントが発生します。
  3. Game タブで再生(▶ボタン)し、Player を ZoomArea のエリアに移動させます。
  4. 侵入したタイミングで Camera2D の zoomRatio が Enter Zoom の値に変化し、退出したときに 元の値または Exit Zoom に戻ることを確認します。
  5. Debug Log を ON にしている場合は、Console で
    • [ZoomTrigger] BEGIN_CONTACT: ...
    • [ZoomTrigger] END_CONTACT: ...
    • [ZoomTrigger] ズーム変更完了: ...

    などのログが出ているか確認してください。


まとめ

この ZoomTrigger コンポーネントを使うことで、

  • 「この部屋に入ったらカメラを引きで」「このボス戦エリアでは少しズームイン」といった カメラ演出をノード単位で簡単に追加できます。
  • 外部の GameManager やシングルトンに依存せず、インスペクタのプロパティだけで完結しているため、どのプロジェクトにもそのまま持ち込んで再利用可能です。
  • Collider2D のグループや enterZoom / exitZoom / transitionDuration を変えるだけで、様々なシーン構成に応じたカメラ挙動を簡単に作れます。

応用例としては、

  • ステージギミックの近くに ZoomTrigger を配置して、プレイヤーが近づくと自動的にズームインして見せる。
  • マップの広いエリアに入るとズームアウトし、細い通路に入るとズームインすることで、視認性と演出を両立する。
  • 複数の ZoomTrigger を連続配置して、進行に応じて徐々にズームしていく演出を作る。

いずれも、ZoomTrigger.ts をプロジェクトに追加し、トリガーエリアノードにアタッチしてプロパティを調整するだけで実現できます。
カメラ制御をシンプルに保ちながら、ゲーム全体の演出力を手軽に底上げしたいときに、ぜひ活用してみてください。

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