【Cocos Creator 3.8】ParallaxLayerMoverの実装:アタッチするだけで「カメラ追従の疑似パララックス背景スクロール」を実現する汎用スクリプト
2Dゲームで奥行き感を出したいときに便利なのが「パララックススクロール」です。この記事では、任意の背景ノードにアタッチするだけで、カメラの動きに合わせて少し遅れて動くパララックスレイヤーを実現する汎用コンポーネント ParallaxLayerMover を実装します。
このスクリプトは、他のゲームマネージャーやシングルトンに一切依存せず、必要な情報はすべてインスペクタから設定できるように設計します。背景SpriteやTextureRect(Spriteを貼ったNode)などにアタッチするだけで、簡単に多層のパララックス背景が構築できます。
コンポーネントの設計方針
1. 機能要件の整理
- 対象ノード(背景レイヤー)を、カメラの移動量に応じて遅れて追従させる。
- カメラは「絶対座標」ではなく「移動量(オフセット)」として扱うことで、外部のカメラスクリプトに依存しない。
- インスペクタから、以下を調整できるようにする:
- どの方向(X/Y)にパララックスさせるか
- パララックスの強さ(係数)
- カメラの移動オフセットの参照元(Node)
- レイヤーの基準位置(初期位置を基準にするか、任意のオフセットを持つか)
- スムージング(補間)で動きを滑らかにするかどうか
- カメラの位置を参照するために、カメラノードを @property で指定できるようにする。
- カメラノードが未設定の場合は、警告ログを出して自動で動作を止める(防御的実装)。
- 2Dゲームを想定し、Nodeの position (Vec3) の x/y を制御する。
2. 実装アプローチ
考え方はシンプルです:
- カメラノードのワールド座標を毎フレーム取得する。
- 初期フレームで、カメラの初期位置と、この背景レイヤーの初期位置を記録する。
- 以降は「カメラの初期位置からの差分(移動量)」に、パララックス係数を掛けた分だけ、このレイヤーを動かす。
式にすると:
layerPosition = baseLayerPosition + (cameraCurrentPos - cameraStartPos) * parallaxFactor
ただし、X/Yそれぞれに対して、パララックスの有効/無効と係数を別々に設定できるようにします。
3. インスペクタで設定可能なプロパティ
本コンポーネントで用意する @property は以下の通りです。
cameraNode: Node | null- 役割: パララックスの基準となるカメラ(またはカメラを載せたノード)。
- 設定方法: メインカメラのノード、あるいはプレイヤー等「世界を動かす基準」としたいノードをドラッグ&ドロップ。
- 未設定時: コンソールに警告を出し、Updateで処理を行わない。
parallaxFactorX: number- 役割: カメラのX方向移動に対するパララックス係数。
- 例:
- 0: カメラが動いてもこのレイヤーは動かない。
- 0.5: カメラの半分の速度で追従(遠景っぽい)。
- 1: カメラと同じ速度で動く(通常の追従)。
- >1: カメラより速く動く(前景に使える)。
parallaxFactorY: number- 役割: カメラのY方向移動に対するパララックス係数。
- 縦スクロールゲームなどで使用。横スクロールのみなら 0 に設定してもよい。
useLocalCameraPosition: boolean- 役割: カメラ位置の取得方法を切り替える。
- true:
cameraNode.position(ローカル座標)を使用。 - false:
cameraNode.worldPosition(ワールド座標)を使用。 - 通常は false(ワールド座標)推奨。カメラが別ノードの子になっている場合などで調整用。
useLocalLayerPosition: boolean- 役割: レイヤー(このノード)の座標をローカル/ワールドどちらで扱うか。
- true:
this.node.position(ローカル)を書き換える。 - false:
this.node.worldPosition(ワールド)を書き換える。 - 通常は true(ローカル)でOK。親ノードの動きに影響を受けさせたくない場合は false。
smoothFollow: boolean- 役割: カメラに対する目標位置へ、補間(Lerp)で滑らかに追従させるかどうか。
- ONにすると、急なカメラ移動でも背景がふわっと追いつくような演出になる。
smoothSpeed: number- 役割: 補間スピード(0〜10程度を想定)。
- 値が大きいほど素早く目標位置に追いつき、小さいほどゆっくり追従する。
smoothFollow = trueのときのみ有効。
debugLog: boolean- 役割: パララックス計算に関するログを一部出力するデバッグモード。
- ONにすると、初期化時やカメラ未設定時に詳細なログが出る。
- リリース時は OFF 推奨。
TypeScriptコードの実装
以下が、完成した ParallaxLayerMover.ts の全コードです。
import { _decorator, Component, Node, Vec3, math, warn, log } from 'cc';
const { ccclass, property } = _decorator;
/**
* ParallaxLayerMover
*
* カメラ(または任意の基準ノード)の移動量に応じて、
* このノードをパララックススクロールさせる汎用コンポーネント。
*
* - 他のカスタムスクリプトには依存しません。
* - 必要な情報はすべてインスペクタの @property から設定します。
*/
@ccclass('ParallaxLayerMover')
export class ParallaxLayerMover extends Component {
@property({
type: Node,
tooltip: 'パララックスの基準となるカメラ(または任意のノード)。\n' +
'このノードの位置の移動量に応じて、背景レイヤーが動きます。'
})
public cameraNode: Node | null = null;
@property({
tooltip: 'カメラのX方向移動に対するパララックス係数。\n' +
'0: 動かない / 0.5: カメラの半分の速度 / 1: 同じ速度 / >1: それ以上の速度'
})
public parallaxFactorX: number = 0.5;
@property({
tooltip: 'カメラのY方向移動に対するパララックス係数。\n' +
'横スクロールのみの場合は 0 にするとY方向は動きません。'
})
public parallaxFactorY: number = 0.0;
@property({
tooltip: 'カメラ位置をローカル座標として扱うかどうか。\n' +
'通常は OFF(ワールド座標)で問題ありません。'
})
public useLocalCameraPosition: boolean = false;
@property({
tooltip: 'このレイヤーの位置をローカル座標で制御するか、ワールド座標で制御するか。\n' +
'通常は ON(ローカル座標)推奨です。'
})
public useLocalLayerPosition: boolean = true;
@property({
tooltip: 'ONにすると、目標位置へ滑らかに補間しながら追従します。\n' +
'OFFの場合はフレーム毎に直接目標位置へ移動します。'
})
public smoothFollow: boolean = true;
@property({
tooltip: 'スムーズ追従の速度。値が大きいほど素早く追いつきます。\n' +
'おおよそ 1〜10 の範囲で調整してください。',
min: 0,
max: 20,
step: 0.1
})
public smoothSpeed: number = 5.0;
@property({
tooltip: 'ONにすると、初期化時やカメラ未設定時にデバッグログを出力します。'
})
public debugLog: boolean = false;
// 内部状態
private _cameraStartPos: Vec3 = new Vec3();
private _layerStartPos: Vec3 = new Vec3();
private _initialized: boolean = false;
onLoad() {
// ここではまだ cameraNode が設定されていない可能性があるため、
// 実際の初期化は start() で行う。
}
start() {
this._initializeParallax();
}
/**
* パララックス用の初期位置を記録する。
*/
private _initializeParallax() {
if (!this.cameraNode) {
warn('[ParallaxLayerMover] cameraNode が設定されていません。このコンポーネントは動作しません。');
this._initialized = false;
return;
}
// カメラ初期位置
if (this.useLocalCameraPosition) {
this._cameraStartPos.set(this.cameraNode.position);
} else {
this._cameraStartPos.set(this.cameraNode.worldPosition);
}
// レイヤー初期位置
if (this.useLocalLayerPosition) {
this._layerStartPos.set(this.node.position);
} else {
this._layerStartPos.set(this.node.worldPosition);
}
this._initialized = true;
if (this.debugLog) {
log('[ParallaxLayerMover] Initialized.');
log(` cameraStartPos = (${this._cameraStartPos.x.toFixed(2)}, ${this._cameraStartPos.y.toFixed(2)})`);
log(` layerStartPos = (${this._layerStartPos.x.toFixed(2)}, ${this._layerStartPos.y.toFixed(2)})`);
}
}
/**
* カメラの現在位置から移動量を計算し、
* パララックス係数に応じてこのレイヤーの位置を更新する。
*/
update(deltaTime: number) {
if (!this._initialized) {
// start() より後に cameraNode を設定した場合に備えて、毎フレームチェック。
if (this.cameraNode) {
this._initializeParallax();
} else {
// cameraNode 未設定のままなら何もしない。
return;
}
}
if (!this.cameraNode) {
// 念のための二重チェック
return;
}
// 現在のカメラ位置
const currentCameraPos = this.useLocalCameraPosition
? this.cameraNode.position
: this.cameraNode.worldPosition;
// カメラの移動量(初期位置との差分)
const cameraDeltaX = currentCameraPos.x - this._cameraStartPos.x;
const cameraDeltaY = currentCameraPos.y - this._cameraStartPos.y;
// 目標となるレイヤー位置を計算
const targetPos = new Vec3();
targetPos.x = this._layerStartPos.x + cameraDeltaX * this.parallaxFactorX;
targetPos.y = this._layerStartPos.y + cameraDeltaY * this.parallaxFactorY;
// Zは元の値を維持
if (this.useLocalLayerPosition) {
targetPos.z = this.node.position.z;
} else {
targetPos.z = this.node.worldPosition.z;
}
if (this.smoothFollow) {
// 現在位置から目標位置へ補間
const currentPos = this.useLocalLayerPosition ? this.node.position.clone() : this.node.worldPosition.clone();
// lerpFactor は deltaTime と smoothSpeed から計算
// 1フレーム内での追従割合を制御(0〜1)
let lerpFactor = this.smoothSpeed * deltaTime;
if (lerpFactor > 1) {
lerpFactor = 1;
} else if (lerpFactor < 0) {
lerpFactor = 0;
}
const newPos = new Vec3();
Vec3.lerp(newPos, currentPos, targetPos, lerpFactor);
if (this.useLocalLayerPosition) {
this.node.setPosition(newPos);
} else {
this.node.setWorldPosition(newPos);
}
} else {
// スムージングなしで直接目標位置へ
if (this.useLocalLayerPosition) {
this.node.setPosition(targetPos);
} else {
this.node.setWorldPosition(targetPos);
}
}
}
}
コードのポイント解説
start()と_initializeParallax()start()内で_initializeParallax()を呼び出し、カメラとレイヤーの初期位置を記録します。cameraNodeが未設定の場合はwarn()で警告を出し、_initialized = falseとしておきます。- 後からインスペクタで
cameraNodeをセットした場合でも、update()内で再度初期化を試みるようにしています(防御的実装)。
update(deltaTime)_initializedが false の場合、cameraNodeが設定されていれば初期化を行います。- カメラの現在位置から初期位置との差分(
cameraDeltaX,cameraDeltaY)を計算し、パララックス係数を掛けてレイヤーの目標位置を算出します。 smoothFollowが true の場合は、現在位置と目標位置の間をVec3.lerpで補間し、フレーム単位で滑らかに追従させます。smoothSpeedとdeltaTimeを掛け合わせてlerpFactorを算出し、0〜1の範囲にクランプすることで、フレームレートに依存しない追従速度を実現しています。useLocalLayerPositionによってsetPosition/setWorldPositionを切り替え、ローカル/ワールドどちらの座標系でも使えるようにしています。
- 防御的実装
cameraNodeが未設定の場合は、警告ログを出した上でupdate()内の処理をスキップします。- これにより、コンポーネントを誤ってアタッチした場合でも、ゲームがエラーで止まることを防ぎます。
使用手順と動作確認
ここからは、実際に Cocos Creator 3.8.7 のエディタ上でこのコンポーネントを使う手順を説明します。
1. スクリプトファイルの作成
- エディタの Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- ファイル名を
ParallaxLayerMover.tsに変更します。 - ダブルクリックしてエディタで開き、既存のテンプレートコードをすべて削除し、前章の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用シーンの準備
ここでは、横スクロールゲーム風の簡単なテストシーンを作る例で説明します。
- 2D シーンを用意
- Hierarchy パネルで右クリック → Create → Scene から新しいシーンを作成(任意)。
- 既にあるシーンを使う場合はそのままでOKです。
- メインカメラの確認
- Hierarchy に
Main Cameraノードがあることを確認します。 - なければ Hierarchy で右クリック → Create → Camera でカメラを作成し、名前を
Main Cameraなど分かりやすいものに変更します。
- Hierarchy に
- 背景レイヤー用ノードの作成
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、
BG_LayerFarなどの名前を付けます。 - Inspector の Sprite コンポーネントで、適当な背景用テクスチャを設定します。
- 同様に、
BG_LayerMid,BG_LayerNearなど複数のレイヤーを作っておくと、パララックス効果が分かりやすくなります。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、
3. ParallaxLayerMover のアタッチ
- Hierarchy で、先ほど作成した背景ノード(例:
BG_LayerFar)を選択します。 - Inspector 下部の Add Component ボタンをクリックします。
- Custom Script → ParallaxLayerMover を選択してアタッチします。
- もしリストに出てこない場合は、一度シーンを保存し、エディタを再読み込み(または
Ctrl+R)すると表示されます。
- もしリストに出てこない場合は、一度シーンを保存し、エディタを再読み込み(または
4. プロパティの設定
Inspector に表示された ParallaxLayerMover の各プロパティを設定します。
- cameraNode
- Hierarchy から
Main Cameraノードをドラッグし、このスロットにドロップします。
- Hierarchy から
- parallaxFactorX / parallaxFactorY
- 横スクロールのみの場合:
parallaxFactorX = 0.3(遠景)parallaxFactorY = 0
- 縦スクロールもある場合:
parallaxFactorX = 0.5parallaxFactorY = 0.2
- 横スクロールのみの場合:
- useLocalCameraPosition / useLocalLayerPosition
- 通常は両方とも ON(true) で問題ありません。
- カメラが親ノードの子として動いているなど、座標系が複雑な場合に挙動がおかしければ、
useLocalCameraPositionを OFF にして試してみてください。
- smoothFollow / smoothSpeed
- まずは
smoothFollow = true、smoothSpeed = 5あたりから試すと自然な動きになります。 - ピタッと追従させたい場合は
smoothFollow = falseにします。
- まずは
- debugLog
- 挙動を確認したいときだけ ON にして、Console に出るログをチェックします。
- 動作が確認できたら OFF に戻しておくと、不要なログ出力を防げます。
同様の手順で、BG_LayerMid, BG_LayerNear にも ParallaxLayerMover をアタッチし、係数を変えてみましょう:
BG_LayerFarparallaxFactorX = 0.2
BG_LayerMidparallaxFactorX = 0.5
BG_LayerNearparallaxFactorX = 0.8
5. カメラを動かして動作確認
実際にカメラを動かして、背景がパララックススクロールするか確認します。
- カメラ移動用の簡単なテスト
- Hierarchy で
Main Cameraを選択します。 - Inspector の Position.X をプレビュー再生中にドラッグして左右に動かしてみます。
- または、
Main Cameraに簡単な移動スクリプトを追加しても構いません(このガイドでは割愛)。
- Hierarchy で
- 期待される挙動
- カメラを右へ動かすと、遠景(parallaxFactorX が小さいレイヤー)はゆっくり右へ、近景(parallaxFactorX が大きいレイヤー)は速く右へ動きます。
smoothFollow = trueの場合、カメラを急に止めたときに、背景レイヤーが少し遅れて追いつくような滑らかな動きになります。
- 動かない場合のチェックポイント
- Inspector で cameraNode が正しく設定されているか。
- parallaxFactorX / Y が 0 になっていないか。
- Console に
[ParallaxLayerMover] cameraNode が設定されていません。という警告が出ていないか。 - 背景ノードがカメラの視界に入る位置にあるか。
まとめ
この記事では、ParallaxLayerMover という汎用コンポーネントを実装し、
- カメラ(または任意のノード)の移動量に応じて背景レイヤーを動かす
- パララックス係数をインスペクタから簡単に調整できる
- スムーズな追従・ローカル/ワールド座標の切り替えにも対応
- 外部の GameManager やシングルトンに一切依存しない
という要件を満たす、アタッチするだけで使える疑似パララックス背景スクロールを実現しました。
このコンポーネント単体をプロジェクトに追加しておけば、
- タイトル画面の背景をゆっくり動かす
- ステージ中の遠景・中景・近景をレイヤーごとにパララックスさせる
- UIの背景にわずかなパララックスを加えて高級感を出す
といった演出を、毎回カスタムスクリプトを書くことなく再利用できます。パララックス係数やスムージングを変えるだけで、同じスクリプトを様々なレイヤーに使い回せるため、ゲーム開発の効率が大きく向上します。
必要に応じて、
- ループ背景(一定距離で位置を巻き戻す)
- Z順(描画順)に応じて自動的に
parallaxFactorを決める
といった拡張も、同じ設計方針で追加していくことができます。まずはこの記事の実装をベースに、あなたのプロジェクトに合ったパララックス表現へとカスタマイズしてみてください。




