【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 を維持
- 部屋 (roomX, roomY) の中央を
- _getPlayerSnapPositionForRoom
- 「どの方向から部屋に入ってきたか」に応じて、プレイヤーを部屋の端にスナップ
- 例:右の部屋へ遷移する場合は、遷移先部屋の左端+
edgeThresholdに配置 - 縦方向も同様に処理し、上下移動に対応
- _getCameraHalfSize
Camera.orthoHeightを半縦幅として使用view.getVisibleSize()からアスペクト比を求め、半横幅 = orthoHeight * aspect として計算- これにより、画面解像度が変わっても正しく「画面端」を計算可能
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
RoomTransition.tsにします。 - 生成された
RoomTransition.tsをダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードをそのまま貼り付けて保存します。
2. シーンにカメラとプレイヤーを用意する
- Hierarchy パネルで、すでに
Main Cameraがあることを確認します。- ない場合:Hierarchy で右クリック → Create → 3D Object → Camera を選択し、名前を
Main Cameraに変更します。
- ない場合:Hierarchy で右クリック → Create → 3D Object → Camera を選択し、名前を
Main Cameraを選択し、Inspector で Projection が Orthographic になっていることを確認します。- 2Dゲーム想定のため、Perspective の場合は Orthographic に変更してください。
- プレイヤー用の Node を作成します。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を
Playerにします。 - 適当な画像を Sprite に設定しておくと位置確認しやすくなります。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を
3. RoomTransition コンポーネントをカメラにアタッチ
- Hierarchy で
Main Cameraを選択します。 - Inspector の下部にある Add Component ボタンをクリックします。
- Custom Script → RoomTransition を選択して追加します。
- リストに出てこない場合は、エディタ右上の Reload(リロード)ボタンを押してスクリプトを再読み込みしてください。
4. インスペクタプロパティの設定
Main Camera の Inspector で、RoomTransition コンポーネントの各プロパティを設定します。
- Player Node
- Hierarchy から
Playerノードをドラッグ&ドロップして、このフィールドにセットします。
- Hierarchy から
- Room Width / Room Height
- 例として、以下のように設定します:
- Room Width: 640
- Room Height: 360
- これは「1部屋が 640×360 のサイズ」という意味です。あとでタイルマップなどと合わせて調整してください。
- 例として、以下のように設定します:
- Horizontal Rooms / Vertical Rooms
- 例:
- Horizontal Rooms: 3(横に3部屋)
- Vertical Rooms: 1(縦は1部屋)
- この設定だと、部屋インデックスは X: 0〜2, Y: 0 の範囲になります。
- 例:
- Start Room X / Start Room Y
- 例:
- Start Room X: 0
- Start Room Y: 0
- ゲーム開始時は「左端の部屋 (0,0)」の中央にカメラが配置されます。
- 例:
- Transition Duration
- 例:0.4(0.4秒かけて滑らかにスライド)
- 瞬間切り替えにしたい場合は 0 に設定します。
- Edge Threshold
- 例:16
- プレイヤーが画面端から 16 ユニット外側にはみ出した時に次の部屋へ遷移します。
- もっと早く切り替えたい場合は 0〜8 程度に、小さくしたい場合は 32 などに調整してください。
- Lock Player During Transition
- 最初は チェックをオン(true) にしておくことを推奨します。
- これにより、遷移中にプレイヤーが次の部屋の端にスナップされ、複雑な挙動を避けられます。
- Debug Draw Gizmos
- 本サンプルでは簡易的なログのみですが、ON にしておくと開発中の調整に役立ちます。
5. 部屋レイアウトの確認とテスト
- シーンビューで
Playerノードを選択し、Transform の Position を(0, 0, 0)付近に置いておきます。 - カメラ(
Main Camera)の Position が、RoomTransitionによって自動的に(roomWidth * 0.5, roomHeight * 0.5)付近に移動していることを確認します。 - エディタ右上の Play ボタンを押して実行します。
- キーボード入力などでプレイヤーを動かすスクリプトをまだ付けていない場合は、簡易的に以下のようなテストを行います:
- 実行中に Scene ビューに切り替え、
Playerノードを選択 - Move ツールで Player を右方向にドラッグし、画面右端を超えるまで移動させる
- 一定距離を超えたタイミングで、カメラが右隣の部屋へスライドするのを確認
- 同様に左へ戻したり、縦方向に複数部屋を用意した場合は上下にも動かして挙動を確認
- 実行中に Scene ビューに切り替え、
プレイヤー移動用のシンプルなスクリプトを別途用意して WASD/矢印キーで動かせるようにしておくと、より実際のゲームに近い挙動を確認できます(ただし本ガイドの RoomTransition 自体はそのスクリプトに依存しません)。
まとめ
この RoomTransition コンポーネントを使うことで、
- 「画面端に来たら隣の部屋へスライド移動」という典型的な 2D 画面切り替え演出を、カメラにアタッチするだけで実現できます。
- 部屋のサイズ・部屋数・開始位置・スライド時間・トリガー閾値などを すべてインスペクタから調整できるため、レベルデザインの試行錯誤がしやすくなります。
- GameManager や PlayerController といった外部スクリプトに依存していないため、どのプロジェクトにもそのまま持ち込んで再利用可能です。
応用として:
- 部屋ごとに BGM やエフェクトを変えたい場合は、このコンポーネントに「遷移完了時にイベントを発火する機能」を追加し、別スクリプトがそれを Listen する形に拡張できます。
- タイルマップの 1 チャンクを 1 部屋とみなし、
roomWidth / roomHeightをタイル数 × タイルサイズで計算すれば、より大規模なマップにも対応できます。 - 縦方向の部屋数を増やせば、「塔を登る」「ダンジョンを降りる」といった上下スクロール型の画面切り替えも簡単に実装できます。
まずはこのシンプルな RoomTransition をベースに、自分のゲームに合わせてカスタマイズしてみてください。カメラとプレイヤーの遷移ロジックがコンポーネント1つにまとまっていることで、ゲーム全体の設計やデバッグがぐっと楽になります。




