【Cocos Creator】アタッチするだけ!TeleportPortal (ポータル)の実装方法【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】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 の場合も警告を出し、エディタでの設定を促します。
  • start()
    • Collider2D に対して Contact2DType.BEGIN_CONTACT イベントを登録し、_onBeginContact を呼び出すようにします。
  • update(dt)
    • クールダウン中であればタイマーを減算し、0 以下になったらクールダウンを解除します。
    • デバッグログが有効な場合、クールダウン終了時にログを出力します。
  • _onBeginContact()
    • クールダウン中であれば即 return。
    • pairedPortal が設定されていない場合は警告を出して終了。
    • targetNode が有効ならそれを対象に、無効で useCollidingNodeAsTarget === true の場合は衝突してきたノードを対象にします。
    • 決定した対象ノードに対して _teleportNodeToPairedPortal を呼び出します。
  • _teleportNodeToPairedPortal()
    • ペアポータルのワールド座標を取得し、オフセットを加えた位置に対象ノードのワールド座標を設定します。
    • テレポート成功時にデバッグログを出力(有効な場合)。
    • cooldownTime が 0 より大きい場合はクールダウン状態に移行します。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を TeleportPortal.ts にします。
  3. 作成された TeleportPortal.ts をダブルクリックして開き、本文をすべて削除して、前述のコードを丸ごと貼り付けて保存します。

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

プレイヤー(テレポート対象)ノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を Player に変更します。
  2. Inspector で Sprite の画像を適当に設定します(任意)。
  3. PlayerRigidBody2DCollider2D(例: BoxCollider2D)を追加しておくと、移動や衝突確認がしやすくなります。
    • Player を選択 → Inspector で Add Component → Physics 2D → RigidBody2D を追加。
    • 同様に Add Component → Physics 2D → BoxCollider2D を追加。

ポータルノード A / B の作成

  1. Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を PortalA に変更します。
  2. 同様にもう一つ Sprite を作成し、名前を PortalB に変更します。
  3. PortalA, PortalB それぞれに見た目用の画像を設定します(任意)。
  4. PortalA に Collider2D を追加します。
    • PortalA を選択 → Inspector → Add Component → Physics 2D → BoxCollider2D(形は任意)。
    • 追加した BoxCollider2DSensor(または Is Trigger) にチェックを入れて、トリガーとして動作させます。
  5. PortalB にも同様に Collider2D を追加し、Sensor にチェックを入れます。

3. TeleportPortal コンポーネントのアタッチと設定

PortalA にアタッチ

  1. Hierarchy で PortalA を選択します。
  2. Inspector で Add Component → Custom → TeleportPortal を選択してアタッチします。
  3. Inspector の TeleportPortal セクションで以下を設定します。
    • Paired Portal:
      Hierarchy から PortalB をドラッグ&ドロップ。
    • Target Node:
      Hierarchy から Player をドラッグ&ドロップ。
    • Use Colliding Node As Target:
      プレイヤー専用ポータルにしたい場合は false でも構いませんが、
      ここでは true のままでも問題ありません(targetNode が優先されます)。
    • Cooldown Time: 0.51.0 秒程度を推奨。
    • Offset From Portal: (0, 50, 0) などにすると、ポータルの少し上に出現して見やすくなります。
    • Enable Debug Log: 開発中は true にしてログを確認すると挙動が分かりやすいです。

PortalB にアタッチ(双方向テレポートにする場合)

  1. Hierarchy で PortalB を選択します。
  2. PortalB にも同じように Add Component → Custom → TeleportPortal を追加します。
  3. Inspector で以下のように設定します。
    • Paired Portal: PortalA をドラッグ&ドロップ。
    • Target Node: Player をドラッグ&ドロップ。
    • その他の設定(Cooldown, Offset, Debug Log)は PortalA と同様でOKです。

これで、PortalA → PortalB、PortalB → PortalA の双方向テレポートが実現できます。

4. 動作確認

  1. シーン内で Player を PortalA から少し離れた位置に配置します。
  2. ゲームを再生します(上部の再生ボタンをクリック)。
  3. Player をキーボード操作で動かせるようにしている場合は、PortalA に向かって移動させます。
    操作スクリプトがない場合は、エディタの Scene ビューで Player をドラッグして PortalA のコライダー範囲内に移動してもテストできます。
  4. Player が PortalA の当たり判定に入ると、瞬時に PortalB の位置(+オフセット)へ移動していることを確認します。
  5. 双方向設定をしている場合は、PortalB に入ると PortalA 側へ戻ることも確認します。
  6. クールダウン時間を短くすると連続テレポートしやすく、長くすると一度だけワープしてしばらく再ワープしなくなります。
  7. Enable Debug Logtrue にしている場合は、Console パネルに
    テレポート実行クールダウン終了 のログが出ているか確認してください。

まとめ

この TeleportPortal コンポーネントは、インスペクタでペアポータルと対象ノードを設定するだけで、シーン上の任意の場所へ瞬時にワープさせる仕組みを提供します。外部の GameManager やシングルトンに依存しておらず、このスクリプト単体で完結しているため、どのプロジェクトにも簡単に持ち込んで再利用できます。

応用例としては、

  • ステージ内のショートカット用ワープゾーン
  • 敵専用ポータル(敵だけがワープする)
  • 隠しエリアへの入口・出口ポータル
  • チェックポイント間の高速移動

など、「このエリアに入ったら別の場所へ瞬間移動させたい」という場面で幅広く利用できます。
クールダウン時間やオフセット、対象ノードの指定を変えるだけで多様な挙動を作れるので、ぜひ自分のゲームのルールに合わせてカスタマイズしてみてください。

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