【Cocos Creator】アタッチするだけ!RoomTransition (部屋切り替え)の実装方法【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】RoomTransition の実装:アタッチするだけで「画面端に来たら隣の部屋へカメラをスライド移動」させる汎用スクリプト

このガイドでは、2Dゲームでよくある「部屋制・画面切り替え」演出を、1つのコンポーネントをメインカメラにアタッチするだけで実現できる RoomTransition を実装します。
プレイヤーが画面端まで移動したときに、カメラが隣の部屋(一定サイズのグリッド)へスムーズにスライドし、プレイヤーも次の部屋の端にワープさせる仕組みです。

シーン内に「GameManager」などの外部スクリプトを一切用意せず、このコンポーネント単体で完結するように設計します。


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

1. 基本コンセプト

  • アタッチ先:2Dカメラの Node(通常は Main Camera
  • 部屋の概念:
    • 部屋は「カメラが表示する範囲」と同じ大きさの矩形とみなす
    • 部屋は横方向・縦方向に等間隔で並ぶグリッド(タイルマップのようなイメージ)
    • カメラは「部屋の中央」にスナップされて止まる
  • トリガー条件:
    • プレイヤーが現在のカメラ表示範囲の端(境界)を一定距離超えたら「隣の部屋」へ遷移
    • 遷移中はカメラとプレイヤーの操作をロック(多重遷移防止)
  • 移動方法:
    • カメラ:現在位置から目標部屋位置まで、一定速度 or 一定時間で補間移動(線形補間)
    • プレイヤー:カメラの移動に合わせて、次の部屋の端に座標をスナップ

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

このコンポーネントは、以下の点で完全に独立しています。

  • プレイヤーは @property で Node を指定するだけ(専用スクリプト不要)
  • カメラ Node にアタッチするだけで動作(カメラコンポーネントは自動取得し、なければ警告表示)
  • 部屋サイズ・遷移方向・遷移速度はすべて インスペクタから設定
  • 他のスクリプト(GameManager, PlayerController など)への依存は一切なし

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

以下のプロパティを用意します。

  • playerNode: Node
    • プレイヤーキャラクターの Node
    • この Node の worldPosition を監視して、画面端に到達したかを判定
  • roomWidth: number
    • 1部屋の横幅(世界座標系の単位)
    • カメラはこの幅ごとに横方向へスナップ移動する
    • 例:タイルマップ1部屋が 640px 幅なら 640
  • roomHeight: number
    • 1部屋の縦幅(世界座標系の単位)
    • カメラはこの高さごとに縦方向へスナップ移動する
  • horizontalRooms: number
    • 横方向に何部屋並んでいるか
    • 左端を 0、右端を horizontalRooms - 1 としてインデックス管理
    • 右端の部屋からさらに右へはみ出した場合は、それ以上遷移しない(任意でクランプ)
  • verticalRooms: number
    • 縦方向に何部屋並んでいるか
    • 下端を 0、上端を verticalRooms - 1 としてインデックス管理
  • startRoomX: number
    • ゲーム開始時にカメラがいる部屋の X インデックス(0〜horizontalRooms-1)
  • startRoomY: number
    • ゲーム開始時にカメラがいる部屋の Y インデックス(0〜verticalRooms-1)
  • transitionDuration: number
    • カメラが1部屋分スライドするのにかける時間(秒)
    • 0 の場合は即座にスナップ(瞬間切り替え)
  • edgeThreshold: number
    • 「画面端に到達した」とみなす距離(世界座標)
    • カメラ表示範囲の端から、この距離だけ外側にはみ出したら次の部屋へ遷移開始
    • 例:プレイヤーが画面端ギリギリまで来なくても、少しはみ出したタイミングで切り替えたい場合に調整
  • lockPlayerDuringTransition: boolean
    • 遷移中にプレイヤー位置を固定するかどうか
    • true: 遷移開始時にプレイヤーを一時的にロックし、次の部屋の端にスナップ
    • false: プレイヤーは自由に動き続けられる(カメラだけがスライド)
  • debugDrawGizmos: boolean
    • エディタ上で部屋の境界や現在のカメラ位置を Gizmo で簡易表示するかどうか
    • 開発中のレイアウト確認用(実行には影響しない)

なお、カメラの「表示範囲」(画面の半幅・半高さ)は Camera.orthoHeight とアスペクト比から算出します。
そのため、カメラ Node に Camera コンポーネントが付いていない場合は警告を出し、処理をスキップする防御的実装にします。


TypeScriptコードの実装

以下が完成版の RoomTransition.ts です。


import { _decorator, Component, Node, Camera, Vec3, math, view, geometry, Color, gfx } from 'cc';
const { ccclass, property, executeInEditMode, requireComponent } = _decorator;

@ccclass('RoomTransition')
@executeInEditMode(true)
@requireComponent(Camera)
export class RoomTransition extends Component {

    @property({
        type: Node,
        tooltip: 'プレイヤーキャラクターの Node を指定します。この位置を監視して画面端到達を検知します。'
    })
    public playerNode: Node | null = null;

    @property({
        tooltip: '1部屋の横幅(world 座標単位)。カメラはこの幅ごとに横方向へスナップ移動します。'
    })
    public roomWidth: number = 640;

    @property({
        tooltip: '1部屋の縦幅(world 座標単位)。カメラはこの高さごとに縦方向へスナップ移動します。'
    })
    public roomHeight: number = 360;

    @property({
        tooltip: '横方向の部屋数。0 以上の整数。'
    })
    public horizontalRooms: number = 3;

    @property({
        tooltip: '縦方向の部屋数。0 以上の整数。'
    })
    public verticalRooms: number = 1;

    @property({
        tooltip: '開始時にカメラがいる部屋の X インデックス(0〜horizontalRooms-1)。'
    })
    public startRoomX: number = 0;

    @property({
        tooltip: '開始時にカメラがいる部屋の Y インデックス(0〜verticalRooms-1)。'
    })
    public startRoomY: number = 0;

    @property({
        tooltip: '1部屋分スライドするのにかける時間(秒)。0 の場合は即座にスナップします。'
    })
    public transitionDuration: number = 0.4;

    @property({
        tooltip: 'この距離だけ画面端を超えたら次の部屋へ遷移します(world 座標単位)。'
    })
    public edgeThreshold: number = 16;

    @property({
        tooltip: 'true の場合、遷移中にプレイヤーの位置をロックし、次の部屋の端にスナップします。'
    })
    public lockPlayerDuringTransition: boolean = true;

    @property({
        tooltip: 'エディタ上で部屋の境界を簡易表示するかどうか(Gizmo)。実行には影響しません。'
    })
    public debugDrawGizmos: boolean = true;

    // 内部状態
    private _camera: Camera | null = null;
    private _isTransitioning: boolean = false;
    private _currentRoomX: number = 0;
    private _currentRoomY: number = 0;

    private _transitionTime: number = 0;
    private _transitionStartPos: Vec3 = new Vec3();
    private _transitionTargetPos: Vec3 = new Vec3();

    // プレイヤーのロック用
    private _playerLocked: boolean = false;
    private _playerLockPos: Vec3 = new Vec3();

    onLoad() {
        this._camera = this.getComponent(Camera);
        if (!this._camera) {
            console.error('[RoomTransition] Camera コンポーネントが見つかりません。このスクリプトはカメラ Node にアタッチしてください。');
            return;
        }

        // 部屋インデックスをクランプ
        this._currentRoomX = math.clamp(this.startRoomX, 0, Math.max(0, this.horizontalRooms - 1));
        this._currentRoomY = math.clamp(this.startRoomY, 0, Math.max(0, this.verticalRooms - 1));

        // カメラを開始部屋の中央へスナップ
        const startPos = this._getRoomCenterWorldPosition(this._currentRoomX, this._currentRoomY);
        this.node.setWorldPosition(startPos);
    }

    start() {
        if (!this._camera) {
            this._camera = this.getComponent(Camera);
        }
        if (!this._camera) {
            console.error('[RoomTransition] Camera コンポーネントがありません。動作できません。');
        }
    }

    update(deltaTime: number) {
        if (!this._camera) {
            return;
        }

        // 遷移中のカメラ移動処理
        if (this._isTransitioning) {
            this._updateTransition(deltaTime);
            return;
        }

        // プレイヤーが未設定なら何もしない
        if (!this.playerNode) {
            return;
        }

        // プレイヤー位置から画面端到達をチェック
        this._checkPlayerEdgeAndMaybeTransition();
    }

    /**
     * プレイヤーが画面端を超えたかどうかを判定し、必要なら部屋インデックスを更新して遷移を開始します。
     */
    private _checkPlayerEdgeAndMaybeTransition() {
        if (!this._camera || !this.playerNode) {
            return;
        }

        const playerWorldPos = this.playerNode.worldPosition;
        const cameraWorldPos = this.node.worldPosition;

        // カメラの表示範囲(半幅・半高さ)を計算
        const halfSize = this._getCameraHalfSize();

        const leftEdge = cameraWorldPos.x - halfSize.x;
        const rightEdge = cameraWorldPos.x + halfSize.x;
        const bottomEdge = cameraWorldPos.y - halfSize.y;
        const topEdge = cameraWorldPos.y + halfSize.y;

        let targetRoomX = this._currentRoomX;
        let targetRoomY = this._currentRoomY;

        // 右端を超えたか
        if (playerWorldPos.x > rightEdge + this.edgeThreshold) {
            targetRoomX = this._currentRoomX + 1;
        }
        // 左端を超えたか
        else if (playerWorldPos.x < leftEdge - this.edgeThreshold) {
            targetRoomX = this._currentRoomX - 1;
        }

        // 上端を超えたか
        if (playerWorldPos.y > topEdge + this.edgeThreshold) {
            targetRoomY = this._currentRoomY + 1;
        }
        // 下端を超えたか
        else if (playerWorldPos.y < bottomEdge - this.edgeThreshold) {
            targetRoomY = this._currentRoomY - 1;
        }

        // 部屋インデックスを範囲内にクランプ
        targetRoomX = math.clamp(targetRoomX, 0, Math.max(0, this.horizontalRooms - 1));
        targetRoomY = math.clamp(targetRoomY, 0, Math.max(0, this.verticalRooms - 1));

        // 変化がなければ遷移不要
        if (targetRoomX === this._currentRoomX && targetRoomY === this._currentRoomY) {
            return;
        }

        // 遷移開始
        this._beginTransition(targetRoomX, targetRoomY);
    }

    /**
     * 遷移の初期化:目標部屋インデックスを設定し、カメラの補間開始。
     */
    private _beginTransition(targetRoomX: number, targetRoomY: number) {
        if (this._isTransitioning || !this._camera) {
            return;
        }

        this._isTransitioning = true;
        this._transitionTime = 0;

        this._transitionStartPos.set(this.node.worldPosition);
        this._transitionTargetPos.set(this._getRoomCenterWorldPosition(targetRoomX, targetRoomY));

        // プレイヤーロック処理
        if (this.lockPlayerDuringTransition && this.playerNode) {
            this._playerLocked = true;
            // 遷移先の部屋の端にプレイヤーを配置する
            const playerTargetPos = this._getPlayerSnapPositionForRoom(targetRoomX, targetRoomY);
            this._playerLockPos.set(playerTargetPos);
            this.playerNode.setWorldPosition(playerTargetPos);
        } else {
            this._playerLocked = false;
        }

        // 部屋インデックス更新
        this._currentRoomX = targetRoomX;
        this._currentRoomY = targetRoomY;
    }

    /**
     * 遷移中のカメラ位置更新(線形補間)。
     */
    private _updateTransition(deltaTime: number) {
        if (!this._camera) {
            return;
        }

        if (this.transitionDuration <= 0) {
            // 即スナップ
            this.node.setWorldPosition(this._transitionTargetPos);
            this._endTransition();
            return;
        }

        this._transitionTime += deltaTime;
        const t = math.clamp01(this._transitionTime / this.transitionDuration);
        const newPos = new Vec3();
        Vec3.lerp(newPos, this._transitionStartPos, this._transitionTargetPos, t);
        this.node.setWorldPosition(newPos);

        if (t >= 1) {
            this._endTransition();
        }
    }

    /**
     * 遷移終了処理。
     */
    private _endTransition() {
        this._isTransitioning = false;
        this._transitionTime = 0;

        if (this._playerLocked) {
            this._playerLocked = false;
        }
    }

    /**
     * 指定した部屋インデックス (roomX, roomY) の中央の world 座標を返します。
     * 部屋 (0,0) の中心を (0,0) とし、右に行くほど X+, 上に行くほど Y+ とします。
     */
    private _getRoomCenterWorldPosition(roomX: number, roomY: number): Vec3 {
        const x = (roomX + 0.5) * this.roomWidth;
        const y = (roomY + 0.5) * this.roomHeight;
        return new Vec3(x, y, this.node.worldPosition.z);
    }

    /**
     * 遷移先の部屋において、プレイヤーをどの位置にスナップさせるかを計算します。
     * ここでは「前の部屋から来た方向の反対側の端」に配置します。
     */
    private _getPlayerSnapPositionForRoom(roomX: number, roomY: number): Vec3 {
        if (!this._camera || !this.playerNode) {
            return new Vec3();
        }

        // カメラ表示範囲
        const halfSize = this._getCameraHalfSize();
        const roomCenter = this._getRoomCenterWorldPosition(roomX, roomY);

        const prevX = this._currentRoomX;
        const prevY = this._currentRoomY;

        let x = this.playerNode.worldPosition.x;
        let y = this.playerNode.worldPosition.y;

        // どの方向から入ってきたかでスナップ位置を変える
        if (roomX > prevX) {
            // 左から右の部屋へ移動した:プレイヤーは左端付近に出現
            x = roomCenter.x - halfSize.x + this.edgeThreshold;
        } else if (roomX < prevX) {
            // 右から左の部屋へ移動した:プレイヤーは右端付近に出現
            x = roomCenter.x + halfSize.x - this.edgeThreshold;
        }

        if (roomY > prevY) {
            // 下から上の部屋へ移動した:プレイヤーは下端付近に出現
            y = roomCenter.y - halfSize.y + this.edgeThreshold;
        } else if (roomY < prevY) {
            // 上から下の部屋へ移動した:プレイヤーは上端付近に出現
            y = roomCenter.y + halfSize.y - this.edgeThreshold;
        }

        return new Vec3(x, y, this.playerNode.worldPosition.z);
    }

    /**
     * カメラの表示範囲の半サイズ(半幅・半高さ)を world 座標で返します。
     * - orthoHeight: カメラの半縦幅
     * - 画面のアスペクト比から半横幅を計算
     */
    private _getCameraHalfSize(): Vec3 {
        if (!this._camera) {
            return new Vec3(0, 0, 0);
        }

        const orthoHeight = this._camera.orthoHeight;
        const frameSize = view.getVisibleSize();
        const aspect = frameSize.width / frameSize.height;
        const halfHeight = orthoHeight;
        const halfWidth = orthoHeight * aspect;

        return new Vec3(halfWidth, halfHeight, 0);
    }

    /**
     * エディタ上で部屋の境界を簡易的に可視化するための Gizmo 描画。
     * 3.8 ではカスタム Gizmo クラスを使うのが正式ですが、
     * ここでは最低限のデバッグ用途として onEnable 中のログ程度に留めます。
     * (本格的な Gizmo はプロジェクト側で別途実装を推奨)
     */
    onEnable() {
        if (!this._camera) {
            this._camera = this.getComponent(Camera);
        }
        if (!this._camera) {
            console.warn('[RoomTransition] Camera コンポーネントが見つかりません。');
        }
    }

}

コードの主要ポイント解説

  • onLoad
    • カメラコンポーネントを取得し、見つからなければ console.error でエラーを出して処理停止
    • 開始部屋インデックス (startRoomX, startRoomY) を部屋数に合わせてクランプ
    • カメラ Node を開始部屋の中央にスナップ配置
  • update
    • まず、遷移中なら _updateTransition でカメラを補間移動し、早期 return
    • プレイヤー Node が未設定なら何もしない(防御的)
    • プレイヤー位置を元に _checkPlayerEdgeAndMaybeTransition で画面端到達を判定
  • _checkPlayerEdgeAndMaybeTransition
    • カメラの表示範囲(半幅・半高さ)を _getCameraHalfSize で計算
    • プレイヤーが rightEdge + edgeThreshold などの閾値を超えたら、隣の部屋へ遷移するよう targetRoomX/Y を更新
    • 部屋インデックスを math.clamp で 0〜部屋数-1 に制限し、範囲外への遷移を防止
    • 部屋が変わる場合のみ _beginTransition を呼び出して遷移開始
  • _beginTransition
    • カメラの現在位置を _transitionStartPos、遷移先部屋の中央を _transitionTargetPos に保存
    • lockPlayerDuringTransition が true なら、_getPlayerSnapPositionForRoom で計算した位置にプレイヤーを即スナップ
    • 多重遷移防止のため _isTransitioning フラグを立てる
  • _updateTransition
    • transitionDuration が 0 以下なら、カメラを即座に目標位置へスナップ
    • それ以外は t = 経過時間 / transitionDuration を 0〜1 にクランプし、Vec3.lerp で線形補間
    • t >= 1 になったら _endTransition で遷移完了処理
  • _getRoomCenterWorldPosition
    • 部屋 (roomX, roomY) の中央を ((roomX + 0.5) * roomWidth, (roomY + 0.5) * roomHeight) で計算
    • Z はカメラの現在の z を維持
  • _getPlayerSnapPositionForRoom
    • 「どの方向から部屋に入ってきたか」に応じて、プレイヤーを部屋の端にスナップ
    • 例:右の部屋へ遷移する場合は、遷移先部屋の左端+edgeThreshold に配置
    • 縦方向も同様に処理し、上下移動に対応
  • _getCameraHalfSize
    • Camera.orthoHeight を半縦幅として使用
    • view.getVisibleSize() からアスペクト比を求め、半横幅 = orthoHeight * aspect として計算
    • これにより、画面解像度が変わっても正しく「画面端」を計算可能

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、任意のフォルダ(例:assets/scripts)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を RoomTransition.ts にします。
  3. 生成された RoomTransition.ts をダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードをそのまま貼り付けて保存します。

2. シーンにカメラとプレイヤーを用意する

  1. Hierarchy パネルで、すでに Main Camera があることを確認します。
    • ない場合:Hierarchy で右クリック → Create → 3D Object → Camera を選択し、名前を Main Camera に変更します。
  2. Main Camera を選択し、Inspector で ProjectionOrthographic になっていることを確認します。
    • 2Dゲーム想定のため、Perspective の場合は Orthographic に変更してください。
  3. プレイヤー用の Node を作成します。
    • Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を Player にします。
    • 適当な画像を Sprite に設定しておくと位置確認しやすくなります。

3. RoomTransition コンポーネントをカメラにアタッチ

  1. Hierarchy で Main Camera を選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom ScriptRoomTransition を選択して追加します。
    • リストに出てこない場合は、エディタ右上の Reload(リロード)ボタンを押してスクリプトを再読み込みしてください。

4. インスペクタプロパティの設定

Main Camera の Inspector で、RoomTransition コンポーネントの各プロパティを設定します。

  1. Player Node
    • Hierarchy から Player ノードをドラッグ&ドロップして、このフィールドにセットします。
  2. Room Width / Room Height
    • 例として、以下のように設定します:
      • Room Width: 640
      • Room Height: 360
    • これは「1部屋が 640×360 のサイズ」という意味です。あとでタイルマップなどと合わせて調整してください。
  3. Horizontal Rooms / Vertical Rooms
    • 例:
      • Horizontal Rooms: 3(横に3部屋)
      • Vertical Rooms: 1(縦は1部屋)
    • この設定だと、部屋インデックスは X: 0〜2, Y: 0 の範囲になります。
  4. Start Room X / Start Room Y
    • 例:
      • Start Room X: 0
      • Start Room Y: 0
    • ゲーム開始時は「左端の部屋 (0,0)」の中央にカメラが配置されます。
  5. Transition Duration
    • 例:0.4(0.4秒かけて滑らかにスライド)
    • 瞬間切り替えにしたい場合は 0 に設定します。
  6. Edge Threshold
    • 例:16
    • プレイヤーが画面端から 16 ユニット外側にはみ出した時に次の部屋へ遷移します。
    • もっと早く切り替えたい場合は 0〜8 程度に、小さくしたい場合は 32 などに調整してください。
  7. Lock Player During Transition
    • 最初は チェックをオン(true) にしておくことを推奨します。
    • これにより、遷移中にプレイヤーが次の部屋の端にスナップされ、複雑な挙動を避けられます。
  8. Debug Draw Gizmos
    • 本サンプルでは簡易的なログのみですが、ON にしておくと開発中の調整に役立ちます。

5. 部屋レイアウトの確認とテスト

  1. シーンビューで Player ノードを選択し、Transform の Position を (0, 0, 0) 付近に置いておきます。
  2. カメラ(Main Camera)の Position が、RoomTransition によって自動的に (roomWidth * 0.5, roomHeight * 0.5) 付近に移動していることを確認します。
  3. エディタ右上の Play ボタンを押して実行します。
  4. キーボード入力などでプレイヤーを動かすスクリプトをまだ付けていない場合は、簡易的に以下のようなテストを行います:
    • 実行中に Scene ビューに切り替え、Player ノードを選択
    • Move ツールで Player を右方向にドラッグし、画面右端を超えるまで移動させる
    • 一定距離を超えたタイミングで、カメラが右隣の部屋へスライドするのを確認
    • 同様に左へ戻したり、縦方向に複数部屋を用意した場合は上下にも動かして挙動を確認

プレイヤー移動用のシンプルなスクリプトを別途用意して WASD/矢印キーで動かせるようにしておくと、より実際のゲームに近い挙動を確認できます(ただし本ガイドの RoomTransition 自体はそのスクリプトに依存しません)。


まとめ

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

  • 「画面端に来たら隣の部屋へスライド移動」という典型的な 2D 画面切り替え演出を、カメラにアタッチするだけで実現できます。
  • 部屋のサイズ・部屋数・開始位置・スライド時間・トリガー閾値などを すべてインスペクタから調整できるため、レベルデザインの試行錯誤がしやすくなります。
  • GameManager や PlayerController といった外部スクリプトに依存していないため、どのプロジェクトにもそのまま持ち込んで再利用可能です。

応用として:

  • 部屋ごとに BGM やエフェクトを変えたい場合は、このコンポーネントに「遷移完了時にイベントを発火する機能」を追加し、別スクリプトがそれを Listen する形に拡張できます。
  • タイルマップの 1 チャンクを 1 部屋とみなし、roomWidth / roomHeight をタイル数 × タイルサイズで計算すれば、より大規模なマップにも対応できます。
  • 縦方向の部屋数を増やせば、「塔を登る」「ダンジョンを降りる」といった上下スクロール型の画面切り替えも簡単に実装できます。

まずはこのシンプルな RoomTransition をベースに、自分のゲームに合わせてカスタマイズしてみてください。カメラとプレイヤーの遷移ロジックがコンポーネント1つにまとまっていることで、ゲーム全体の設計やデバッグがぐっと楽になります。

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