【Cocos Creator 3.8】TeleportPortal の実装:アタッチするだけで「ペアポータル間ワープ」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで、入るとペアになっている別のポータルへ瞬時に移動させる汎用コンポーネント TeleportPortal を実装します。
プレイヤーや敵キャラなど、特定のノードがポータルの当たり判定に入った瞬間に、対応するポータル位置へテレポートさせることができます。
外部の GameManager やシングルトンには一切依存せず、インスペクタでペアポータルや対象ノードを設定するだけで使えるように設計します。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントをアタッチしたノードを「ポータル」と見なす。
- ポータルには「ペア」となる別ポータルを 1 つ指定できる。
- 指定した「対象ノード」がこのポータルの当たり判定(トリガー)に入ったとき、そのノードをペアポータルの位置へ瞬時に移動させる。
- 連続テレポート(ポータル A → B → A → … の無限ループ)を防ぐため、テレポート後に一定時間クールダウンを設ける。
- ポータルごとに「対象ノード」を個別に設定できる(プレイヤー専用ポータル、敵専用ポータルなど)。
- 物理エンジン 2D を利用し、Collider2D をトリガーとして使用する。
外部依存をなくすための設計アプローチ
- ポータル同士のペアリングは インスペクタで直接 Node 参照を設定する。
- テレポート対象も インスペクタで Node を指定し、他スクリプトのシングルトンなどに依存しない。
- 物理コンポーネント(
Collider2D)は、存在チェックと警告ログで防御的に扱う。 - 2D 物理システムの有効化やイベント登録も、このコンポーネント内で完結させる。
インスペクタで設定可能なプロパティ
今回の TeleportPortal コンポーネントでは、以下のプロパティを用意します。
-
pairedPortal: Node | null
ペアになる別ポータルのノード参照。
このノードの位置へテレポートします。
注意: ペア側にもTeleportPortalをアタッチしておくと双方向ワープなどが可能です。 -
targetNode: Node | null
このポータルに入ったときにテレポートさせたい対象ノード。
代表例: プレイヤーのキャラノード、敵キャラノードなど。
未設定の場合は、衝突してきたノード自身をテレポート対象として扱うオプションも用意します。 -
useCollidingNodeAsTarget: boolean
true:targetNodeが未指定、もしくは無効な場合に、ポータルに入ってきたノード自身をテレポート対象とする。
false: 常にtargetNodeのみを対象とし、それ以外のノードは無視する。 -
cooldownTime: number
テレポート後に再度テレポート可能になるまでのクールダウン時間(秒)。
例:0.5にすると、0.5 秒の間は同じポータルから再テレポートされません。 -
offsetFromPortal: Vec3
テレポート先のペアポータル位置からのオフセット。
例:(0, 50, 0)とすると、ペアポータルの少し上に出現させることができます。 -
enableDebugLog: boolean
trueにすると、テレポート時やエラー時にconsole.logで詳細ログを出力します。
開発・デバッグ用に便利です。
さらに、物理判定用として Collider2D を使用しますが、これはインスペクタで別途アタッチする前提とし、スクリプト側で存在チェック & 警告ログを行います。
TypeScriptコードの実装
以下が完成した TeleportPortal.ts の全コードです。
import { _decorator, Component, Node, Vec3, Collider2D, IPhysics2DContact, Contact2DType, PhysicsSystem2D, EPhysics2DDrawFlags, warn, log } from 'cc';
const { ccclass, property } = _decorator;
/**
* TeleportPortal
*
* このコンポーネントを持つノードを「ポータル」として扱い、
* 対象ノードがポータルの Collider2D トリガーに入ったときに、
* ペアポータルの位置へ瞬時に移動させます。
*/
@ccclass('TeleportPortal')
export class TeleportPortal extends Component {
@property({
tooltip: 'ペアになる別ポータルのノード。\nテレポート先として、このノードの位置が使用されます。'
})
public pairedPortal: Node | null = null;
@property({
tooltip: 'テレポート対象となるノード。\n未設定か無効な場合、下の「衝突ノードを対象にする」が true なら\nポータルに入ってきたノード自身をテレポート対象とします。'
})
public targetNode: Node | null = null;
@property({
tooltip: 'true の場合、targetNode が未指定または無効のとき、\nポータルに入ってきたノード自身をテレポート対象とします。'
})
public useCollidingNodeAsTarget: boolean = true;
@property({
tooltip: 'テレポート後に再度テレポート可能になるまでのクールダウン時間(秒)。\n無限ループ防止のため、0.1 以上を推奨します。'
})
public cooldownTime: number = 0.5;
@property({
tooltip: 'テレポート先ポータルの位置からのオフセット。\n例: (0, 50, 0) でペアポータルの少し上に出現させます。'
})
public offsetFromPortal: Vec3 = new Vec3(0, 0, 0);
@property({
tooltip: 'true の場合、テレポート時やエラー時にログを出力します。'
})
public enableDebugLog: boolean = false;
// 内部状態:クールダウン管理
private _isOnCooldown: boolean = false;
private _cooldownTimer: number = 0;
// 内部参照:ポータル自身の Collider2D
private _collider: Collider2D | null = null;
onLoad() {
// 2D 物理システムを有効化(プロジェクト設定で無効の場合でもここでオンにする)
const physics2D = PhysicsSystem2D.instance;
if (!physics2D.enable) {
physics2D.enable = true;
// デバッグ用の描画はデフォルトではオフ。
physics2D.debugDrawFlags = EPhysics2DDrawFlags.None;
}
// Collider2D の取得
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
warn('[TeleportPortal] Collider2D が見つかりません。このノードに Collider2D を追加してください。 ノード名:', this.node.name);
} else {
// トリガーイベントを受け取るために、isTrigger を true にしておくことを推奨
if (!this._collider.sensor) {
warn('[TeleportPortal] Collider2D.sensor が false です。トリガーとして使う場合は true に設定してください。 ノード名:', this.node.name);
}
}
}
start() {
// Collider2D のトリガーイベント登録
if (this._collider) {
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
}
}
onDestroy() {
// イベント登録解除
if (this._collider) {
this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
}
}
update(dt: number) {
// クールダウンタイマーの更新
if (this._isOnCooldown) {
this._cooldownTimer -= dt;
if (this._cooldownTimer <= 0) {
this._isOnCooldown = false;
this._cooldownTimer = 0;
if (this.enableDebugLog) {
log('[TeleportPortal] クールダウン終了。再度テレポート可能になりました。ノード名:', this.node.name);
}
}
}
}
/**
* 衝突開始時のコールバック
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
// すでにクールダウン中なら何もしない
if (this._isOnCooldown) {
if (this.enableDebugLog) {
log('[TeleportPortal] クールダウン中のためテレポートしません。ノード名:', this.node.name);
}
return;
}
// ペアポータルが設定されているかチェック
if (!this.pairedPortal) {
warn('[TeleportPortal] pairedPortal が設定されていません。テレポートできません。ノード名:', this.node.name);
return;
}
// テレポート対象ノードを決定
let target: Node | null = null;
// targetNode が有効ならそれを使う
if (this.targetNode && this.targetNode.isValid) {
target = this.targetNode;
} else if (this.useCollidingNodeAsTarget) {
// 衝突してきたノードを対象にする
target = otherCollider.node;
} else {
// どちらも使えない場合は何もしない
if (this.enableDebugLog) {
log('[TeleportPortal] 対象ノードが設定されておらず、衝突ノードを対象にする設定も無効のため、テレポートしません。ノード名:', this.node.name);
}
return;
}
// 実際のテレポート処理
this._teleportNodeToPairedPortal(target);
}
/**
* ノードをペアポータルの位置へテレポートさせる
*/
private _teleportNodeToPairedPortal(target: Node) {
if (!this.pairedPortal) {
return;
}
// ワールド座標でのペアポータル位置を取得
const portalWorldPos = new Vec3();
this.pairedPortal.getWorldPosition(portalWorldPos);
// オフセットを加算
const finalWorldPos = new Vec3(
portalWorldPos.x + this.offsetFromPortal.x,
portalWorldPos.y + this.offsetFromPortal.y,
portalWorldPos.z + this.offsetFromPortal.z
);
// 対象ノードのワールド座標を設定
target.setWorldPosition(finalWorldPos);
if (this.enableDebugLog) {
log(
`[TeleportPortal] テレポート実行: ${target.name} を ${this.node.name} から ${this.pairedPortal.name} へ移動しました。`
);
}
// クールダウン開始
if (this.cooldownTime > 0) {
this._isOnCooldown = true;
this._cooldownTimer = this.cooldownTime;
}
}
}
コードのポイント解説
-
onLoad()
- 2D 物理システム (
PhysicsSystem2D) を有効化。 - 自身のノードに
Collider2Dがアタッチされているかチェック。なければwarnで警告。 Collider2D.sensor(トリガー設定)がfalseの場合も警告を出し、エディタでの設定を促します。
- 2D 物理システム (
-
start()
Collider2Dに対してContact2DType.BEGIN_CONTACTイベントを登録し、_onBeginContactを呼び出すようにします。
-
update(dt)
- クールダウン中であればタイマーを減算し、0 以下になったらクールダウンを解除します。
- デバッグログが有効な場合、クールダウン終了時にログを出力します。
-
_onBeginContact()
- クールダウン中であれば即 return。
pairedPortalが設定されていない場合は警告を出して終了。targetNodeが有効ならそれを対象に、無効でuseCollidingNodeAsTarget === trueの場合は衝突してきたノードを対象にします。- 決定した対象ノードに対して
_teleportNodeToPairedPortalを呼び出します。
-
_teleportNodeToPairedPortal()
- ペアポータルのワールド座標を取得し、オフセットを加えた位置に対象ノードのワールド座標を設定します。
- テレポート成功時にデバッグログを出力(有効な場合)。
cooldownTimeが 0 より大きい場合はクールダウン状態に移行します。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を TeleportPortal.ts にします。
- 作成された
TeleportPortal.tsをダブルクリックして開き、本文をすべて削除して、前述のコードを丸ごと貼り付けて保存します。
2. テスト用シーンとノードの準備
プレイヤー(テレポート対象)ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を Player に変更します。
- Inspector で Sprite の画像を適当に設定します(任意)。
- Player に
RigidBody2DとCollider2D(例:BoxCollider2D)を追加しておくと、移動や衝突確認がしやすくなります。- Player を選択 → Inspector で Add Component → Physics 2D → RigidBody2D を追加。
- 同様に Add Component → Physics 2D → BoxCollider2D を追加。
ポータルノード A / B の作成
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を PortalA に変更します。
- 同様にもう一つ Sprite を作成し、名前を PortalB に変更します。
- PortalA, PortalB それぞれに見た目用の画像を設定します(任意)。
-
PortalA に
Collider2Dを追加します。- PortalA を選択 → Inspector → Add Component → Physics 2D → BoxCollider2D(形は任意)。
- 追加した
BoxCollider2Dの Sensor(または Is Trigger) にチェックを入れて、トリガーとして動作させます。
-
PortalB にも同様に
Collider2Dを追加し、Sensor にチェックを入れます。
3. TeleportPortal コンポーネントのアタッチと設定
PortalA にアタッチ
- Hierarchy で PortalA を選択します。
- Inspector で Add Component → Custom → TeleportPortal を選択してアタッチします。
- Inspector の
TeleportPortalセクションで以下を設定します。- Paired Portal:
Hierarchy から PortalB をドラッグ&ドロップ。 - Target Node:
Hierarchy から Player をドラッグ&ドロップ。 - Use Colliding Node As Target:
プレイヤー専用ポータルにしたい場合はfalseでも構いませんが、
ここではtrueのままでも問題ありません(targetNode が優先されます)。 - Cooldown Time:
0.5〜1.0秒程度を推奨。 - Offset From Portal:
(0, 50, 0)などにすると、ポータルの少し上に出現して見やすくなります。 - Enable Debug Log: 開発中は
trueにしてログを確認すると挙動が分かりやすいです。
- Paired Portal:
PortalB にアタッチ(双方向テレポートにする場合)
- Hierarchy で PortalB を選択します。
- PortalB にも同じように Add Component → Custom → TeleportPortal を追加します。
- Inspector で以下のように設定します。
- Paired Portal: PortalA をドラッグ&ドロップ。
- Target Node: Player をドラッグ&ドロップ。
- その他の設定(Cooldown, Offset, Debug Log)は PortalA と同様でOKです。
これで、PortalA → PortalB、PortalB → PortalA の双方向テレポートが実現できます。
4. 動作確認
- シーン内で Player を PortalA から少し離れた位置に配置します。
- ゲームを再生します(上部の再生ボタンをクリック)。
- Player をキーボード操作で動かせるようにしている場合は、PortalA に向かって移動させます。
操作スクリプトがない場合は、エディタの Scene ビューで Player をドラッグして PortalA のコライダー範囲内に移動してもテストできます。 - Player が PortalA の当たり判定に入ると、瞬時に PortalB の位置(+オフセット)へ移動していることを確認します。
- 双方向設定をしている場合は、PortalB に入ると PortalA 側へ戻ることも確認します。
- クールダウン時間を短くすると連続テレポートしやすく、長くすると一度だけワープしてしばらく再ワープしなくなります。
- Enable Debug Log を
trueにしている場合は、Console パネルに
テレポート実行やクールダウン終了のログが出ているか確認してください。
まとめ
この TeleportPortal コンポーネントは、インスペクタでペアポータルと対象ノードを設定するだけで、シーン上の任意の場所へ瞬時にワープさせる仕組みを提供します。外部の GameManager やシングルトンに依存しておらず、このスクリプト単体で完結しているため、どのプロジェクトにも簡単に持ち込んで再利用できます。
応用例としては、
- ステージ内のショートカット用ワープゾーン
- 敵専用ポータル(敵だけがワープする)
- 隠しエリアへの入口・出口ポータル
- チェックポイント間の高速移動
など、「このエリアに入ったら別の場所へ瞬間移動させたい」という場面で幅広く利用できます。
クールダウン時間やオフセット、対象ノードの指定を変えるだけで多様な挙動を作れるので、ぜひ自分のゲームのルールに合わせてカスタマイズしてみてください。




