【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_CONTACTとEND_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. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
ZoomTrigger.tsに変更します。 - 作成された
ZoomTrigger.tsをダブルクリックして開き、上記の TypeScript コードをすべて貼り付けて保存します。
2. カメラノードの準備
- Hierarchy パネルで、既に 2D カメラがあるか確認します。なければ:
- Hierarchy パネルで右クリック →
Create → 3D Object → Cameraなどでカメラを作成します。 - 2D 用に Camera2D コンポーネントを追加するか、2D 用テンプレートから作成します。
- Hierarchy パネルで右クリック →
- カメラノードを選択し、Inspector で
Camera2Dコンポーネントが付いていることを確認します。 - 初期のズームを決めたい場合は、
zoomRatioを 1.0 などに設定しておきます。
3. プレイヤー(侵入する側)の準備
ここでは例として「Player」ノードを使います。
- Hierarchy パネルで右クリック →
Create → 2D Object → Spriteなどでプレイヤーノードを作成し、Playerと名前を付けます。 - Player ノードを選択し、Inspector で以下を追加します。
- RigidBody2D(Body Type は Dynamic など)
- Collider2D(例: BoxCollider2D)
- Player 側の Collider2D は Is Trigger を OFF(通常の衝突判定)で問題ありません。
- 必要に応じて、Physics グループ(
Group)を「Player」用のグループに設定します(後で ZoomTrigger 側でグループ指定したい場合)。
4. トリガーエリアノードの作成と ZoomTrigger のアタッチ
- Hierarchy パネルで右クリック →
Create → Empty Nodeを選び、ZoomAreaなどの名前を付けます。 - ZoomArea ノードを選択し、Inspector で 2D → BoxCollider2D を追加します。
- BoxCollider2D のサイズを変更して、「この範囲に入ったらズームしたい」というエリアを作ります。
- BoxCollider2D の Is Trigger にチェックを入れます(必須)。
- 続いて ZoomArea ノードの Inspector で
Add Component → Custom → ZoomTriggerを選択し、コンポーネントをアタッチします。
5. ZoomTrigger のプロパティ設定
ZoomArea ノードの Inspector に表示される ZoomTrigger のプロパティを設定します。
- Target Camera:
- Camera2D を持つカメラノードを Hierarchy からドラッグ&ドロップします。
- 未設定でも親階層から自動検出を試みますが、明示的に設定しておくことを推奨します。
- Enter Zoom:
- トリガーに入ったときのズーム値を設定します。
- 例:
- 0.5: 少し引きで表示(広く見せたいエリア向け)
- 1.0: 等倍
- 1.5〜2.0: ズームイン(細かいギミックを見せたい場所など)
- Use Exit Zoom:
- ON にすると、エリアから出たときにズームを戻します。
- Restore Original Zoom:
- ON の場合、プレイヤーがエリアに入る前の zoomRatio を自動記録し、退出時にその値へ戻します。
- OFF の場合は、次の Exit Zoom の値に変更されます。
- Exit Zoom:
Restore Original Zoomが OFF のときに使用される値です。- 例: 1.0 に設定しておけば、「このエリアから出たら常に等倍に戻す」といった挙動になります。
- Transition Duration:
- ズームの切り替え時間(秒)です。
- 0.0: 即座に切り替え。
- 0.2〜0.5: 自然なズーム演出。
- 1.0 以上: ゆっくりとしたズーム。
- Only Affect Specific Group:
- ON にすると、指定したグループの Collider2D だけに反応します。
- プレイヤーだけに反応させたい場合に便利です。
- Target Group:
- 上記フラグが ON のときに有効です。
- Player の Collider2D の
Groupが例えば 2 であれば、ここも 2 に設定します。
- Debug Log:
- ON にすると、侵入・退出時やカメラ検出時などの詳細なログが Console に出力されます。
- 動作確認中は ON、本番では OFF にする、といった運用がしやすいです。
6. 実際に動かして確認する
- Scene ビューで Player ノードと ZoomArea ノードの位置関係を調整し、Player を動かしたときに ZoomArea のコライダーを通過するように配置します。
- Player に移動用の簡単なスクリプトがある場合はそれを使い、なければ一時的に Scene ビュー上でドラッグして動かすだけでも、物理シミュレーションを OFF にしていなければ接触イベントが発生します。
- Game タブで再生(▶ボタン)し、Player を ZoomArea のエリアに移動させます。
- 侵入したタイミングで Camera2D の zoomRatio が Enter Zoom の値に変化し、退出したときに 元の値または Exit Zoom に戻ることを確認します。
- Debug Log を ON にしている場合は、Console で
[ZoomTrigger] BEGIN_CONTACT: ...[ZoomTrigger] END_CONTACT: ...[ZoomTrigger] ズーム変更完了: ...
などのログが出ているか確認してください。
まとめ
この ZoomTrigger コンポーネントを使うことで、
- 「この部屋に入ったらカメラを引きで」「このボス戦エリアでは少しズームイン」といった カメラ演出をノード単位で簡単に追加できます。
- 外部の GameManager やシングルトンに依存せず、インスペクタのプロパティだけで完結しているため、どのプロジェクトにもそのまま持ち込んで再利用可能です。
- Collider2D のグループや
enterZoom / exitZoom / transitionDurationを変えるだけで、様々なシーン構成に応じたカメラ挙動を簡単に作れます。
応用例としては、
- ステージギミックの近くに ZoomTrigger を配置して、プレイヤーが近づくと自動的にズームインして見せる。
- マップの広いエリアに入るとズームアウトし、細い通路に入るとズームインすることで、視認性と演出を両立する。
- 複数の ZoomTrigger を連続配置して、進行に応じて徐々にズームしていく演出を作る。
いずれも、ZoomTrigger.ts をプロジェクトに追加し、トリガーエリアノードにアタッチしてプロパティを調整するだけで実現できます。
カメラ制御をシンプルに保ちながら、ゲーム全体の演出力を手軽に底上げしたいときに、ぜひ活用してみてください。




