【Cocos Creator 3.8】SpriteFlipper の実装:アタッチするだけで「移動方向に合わせて自動で左右反転」する汎用スクリプト
2Dアクションや横スクロールゲームでは、「キャラが右に動いたら右向き、左に動いたら左向きに自動でスプライトが反転してほしい」というケースが非常に多くあります。
本記事では、ノードにアタッチするだけで、親ノードの移動方向(velocity.x)に合わせてスプライトを自動で左右反転する汎用コンポーネント「SpriteFlipper」を実装します。
Rigidbody2D で物理移動しているキャラにも、スクリプトで position を更新しているキャラにも対応できるよう、速度の取得方法をインスペクタから選べる設計にします。
目次
コンポーネントの設計方針
要件整理
- このコンポーネントをアタッチしたノード、またはその親ノードの「移動方向(x 方向)」を監視する。
- 右向きに動いているときはスプライトを正方向(flipX = false)、左向きに動いているときは反転(flipX = true)にする。
- 速度の取得方法を複数サポートする:
- Rigidbody2D の velocity.x を読むモード
- 前フレームからの position の差分 から速度を推定するモード
- 他のカスタムスクリプトへの依存は禁止。必要なものはすべて @property 経由で設定できるようにする。
- Sprite コンポーネントが見つからない場合などは、防御的にエラーログを出す。
対象ノードと依存コンポーネント
- この SpriteFlipper をアタッチするノード:
- 基本想定: Sprite を持つ見た目用ノード(キャラ本体のノード、あるいはキャラ子ノード)
- Sprite は
Spriteコンポーネントとしてアタッチされている必要がある
- 速度の参照元ノード:
- デフォルトでは 親ノード の Rigidbody2D または Transform から速度を算出
- インスペクタから任意のノードを指定することも可能
インスペクタで設定可能なプロパティ
SpriteFlipper に用意する @property とその役割は以下の通りです。
- velocitySourceMode(列挙型)
- 速度の取得方法を選択する。
RIGIDBODY2D: Rigidbody2D のlinearVelocity.xを使用。POSITION_DELTA: 前フレームとの位置差分から速度を推定。
- targetNode(Node | null)
- 速度を取得する対象ノード。
- null の場合は 親ノード を自動で使用。
- Rigidbody2D モードでは、このノードから Rigidbody2D を探す。
- spriteNode(Node | null)
- 左右反転を行う Sprite を持つノード。
- null の場合は このコンポーネントが付いているノード自身 を使用。
- flipWhenMovingThreshold(number, 既定値 0.01)
- 速度の絶対値がこの値を超えたときに「動いている」とみなす。
- 小さすぎると微妙な揺れで頻繁に反転してしまうため、0.01〜0.1 程度を推奨。
- invertFlip(boolean, 既定値 false)
- true の場合、左右の判定を逆転させる。
- アートの向きが「デフォルトで左向き」などの場合に利用。
- lockWhenIdle(boolean, 既定値 true)
- 速度がしきい値未満(ほぼ停止)のときの挙動。
- true: 最後に動いていた向きを維持する。
- false: 停止中は常に flipX = false(右向き)に戻す。
- debugLog(boolean, 既定値 false)
- true のとき、取得した速度や反転状態をログ出力する。
- 動作確認やチューニングに利用。
TypeScriptコードの実装
以下が SpriteFlipper コンポーネントの完全な実装コードです。
import { _decorator, Component, Node, Sprite, RigidBody2D, Vec2, Vec3, Enum } from 'cc';
const { ccclass, property } = _decorator;
/**
* 速度の取得方法
*/
enum VelocitySourceMode {
RIGIDBODY2D = 0,
POSITION_DELTA = 1,
}
Enum(VelocitySourceMode); // インスペクタに表示するため
@ccclass('SpriteFlipper')
export class SpriteFlipper extends Component {
@property({
type: VelocitySourceMode,
tooltip: '速度の取得方法を選択します。\n' +
'RIGIDBODY2D: targetNode の RigidBody2D.linearVelocity.x を使用\n' +
'POSITION_DELTA: 前フレームとの位置差分から速度を推定'
})
public velocitySourceMode: VelocitySourceMode = VelocitySourceMode.RIGIDBODY2D;
@property({
type: Node,
tooltip: '速度を取得する対象ノード。\n' +
'未指定(null)の場合は、このコンポーネントが付いているノードの親ノードを使用します。'
})
public targetNode: Node | null = null;
@property({
type: Node,
tooltip: '左右反転を行う Sprite を持つノード。\n' +
'未指定(null)の場合は、このコンポーネントが付いているノード自身を使用します。'
})
public spriteNode: Node | null = null;
@property({
tooltip: 'この速度(絶対値)を超えたときに「動いている」と判定します。\n' +
'小さすぎると微小な揺れで反転が頻発するため、0.01〜0.1 程度を推奨します。'
})
public flipWhenMovingThreshold: number = 0.05;
@property({
tooltip: 'true の場合、左右の判定を反転させます。\n' +
'アートのデフォルト向きが左向きの場合などに使用します。'
})
public invertFlip: boolean = false;
@property({
tooltip: '停止中(速度がしきい値未満)のときの挙動。\n' +
'true: 最後に動いていた向きを維持\n' +
'false: 常に右向き(flipX = false)に戻す'
})
public lockWhenIdle: boolean = true;
@property({
tooltip: 'true のとき、取得した速度や反転状態をログに出力します。\n' +
'デバッグ用途に使用してください。'
})
public debugLog: boolean = false;
private _sprite: Sprite | null = null;
private _rigidbody2D: RigidBody2D | null = null;
private _lastPosition: Vec3 | null = null;
private _lastFacingRight: boolean = true; // 最後に向いていた方向(true: 右, false: 左)
onLoad() {
// spriteNode が未設定なら自ノードを使う
const spriteNode = this.spriteNode ?? this.node;
// Sprite コンポーネント取得
this._sprite = spriteNode.getComponent(Sprite);
if (!this._sprite) {
console.error('[SpriteFlipper] Sprite コンポーネントが見つかりません。ノードに Sprite を追加してください。', spriteNode);
}
// targetNode が未設定なら親ノードを使う
if (!this.targetNode) {
this.targetNode = this.node.parent;
if (!this.targetNode) {
console.warn('[SpriteFlipper] targetNode が未設定で親ノードも存在しません。速度を取得できません。', this.node);
}
}
// Rigidbody2D モードの場合は RigidBody2D を取得
if (this.velocitySourceMode === VelocitySourceMode.RIGIDBODY2D && this.targetNode) {
this._rigidbody2D = this.targetNode.getComponent(RigidBody2D);
if (!this._rigidbody2D) {
console.warn('[SpriteFlipper] RIGIDBODY2D モードですが、targetNode に RigidBody2D が見つかりません。POSITION_DELTA モードへの変更を検討してください。', this.targetNode);
}
}
// 位置差分モード用の初期位置
if (this.velocitySourceMode === VelocitySourceMode.POSITION_DELTA && this.targetNode) {
this._lastPosition = this.targetNode.worldPosition.clone();
}
}
start() {
// 初期向きは現在の flipX から決定
if (this._sprite) {
this._lastFacingRight = !this._sprite.flipX;
}
}
update(dt: number) {
if (!this._sprite) {
return; // 必須コンポーネントがない場合は何もしない
}
if (!this.targetNode) {
return; // 参照ノードがない場合も何もしない
}
let vx = 0;
if (this.velocitySourceMode === VelocitySourceMode.RIGIDBODY2D) {
// Rigidbody2D の velocity.x を使用
if (this._rigidbody2D) {
const v: Vec2 = this._rigidbody2D.linearVelocity;
vx = v.x;
}
} else {
// POSITION_DELTA: 前フレームとの位置差分から速度を推定
const currentPos = this.targetNode.worldPosition;
if (this._lastPosition) {
const dx = currentPos.x - this._lastPosition.x;
// dt が 0 の場合はゼロ除算を避ける
if (dt > 0) {
vx = dx / dt;
}
}
// 現在位置を保存
if (!this._lastPosition) {
this._lastPosition = currentPos.clone();
} else {
this._lastPosition.set(currentPos);
}
}
// デバッグログ
if (this.debugLog) {
console.log(`[SpriteFlipper] vx=${vx.toFixed(3)}`);
}
const absVx = Math.abs(vx);
if (absVx >= this.flipWhenMovingThreshold) {
// 動いていると判定
const movingRight = vx > 0;
this._lastFacingRight = movingRight;
this.applyFlip(movingRight);
} else {
// 停止中
if (this.lockWhenIdle) {
// 最後に動いていた向きを維持
this.applyFlip(this._lastFacingRight);
} else {
// 常に右向きに戻す
this.applyFlip(true);
}
}
}
/**
* 向きに応じて Sprite.flipX を更新
* @param facingRight true: 右向き, false: 左向き
*/
private applyFlip(facingRight: boolean) {
if (!this._sprite) return;
// invertFlip が true の場合は左右を逆転
const effectiveFacingRight = this.invertFlip ? !facingRight : facingRight;
// Cocos の Sprite.flipX は「左右反転しているかどうか」
// 右向き => flipX = false, 左向き => flipX = true
const shouldFlipX = !effectiveFacingRight;
if (this._sprite.flipX !== shouldFlipX) {
this._sprite.flipX = shouldFlipX;
if (this.debugLog) {
console.log(`[SpriteFlipper] flipX=${shouldFlipX} (facingRight=${facingRight}, invert=${this.invertFlip})`);
}
}
}
}
主要メソッドの解説
- onLoad()
spriteNode未指定時に自ノードを使用。- Sprite コンポーネントを取得し、存在しない場合は
console.errorで警告。 targetNode未指定時に親ノードを自動設定。- RIGIDBODY2D モードの場合は RigidBody2D を取得し、なければ
console.warn。 - POSITION_DELTA モードでは初期位置を保存。
- start()
- 初期向きを
this._sprite.flipXから判定し、_lastFacingRightに保存。 - これにより、停止中に「初期向き」を維持できる。
- 初期向きを
- update(dt)
- 設定されたモードに応じて
vx(x 方向速度)を取得。 - RIGIDBODY2D モード:
RigidBody2D.linearVelocity.x - POSITION_DELTA モード:
(currentPos.x - lastPos.x) / dt - 速度の絶対値が
flipWhenMovingThreshold以上なら「動いている」と判定し、向きを更新。 - 停止中の挙動は
lockWhenIdleに従って決定。 - 最終的に
applyFlip()でSprite.flipXを更新。
- 設定されたモードに応じて
- applyFlip(facingRight)
invertFlipが true の場合は左右を反転して解釈。- 右向き =>
flipX = false、左向き =>flipX = trueを適用。 - 状態が変わったときだけ flipX を更新し、不要な更新を避ける。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create > TypeScriptを選択します。- ファイル名を
SpriteFlipper.tsにします。 - 作成された
SpriteFlipper.tsをダブルクリックしてエディタで開き、先ほどのコード全文を貼り付けて保存します。
2. テスト用ノードの作成(Sprite + Rigidbody2D の例)
- Hierarchy パネルで右クリックし、
Create > 2D Object > Spriteなどでキャラクター用ノードを作成します。(例: ノード名をPlayerとする) Playerノードを選択し、Inspector で以下を追加します:Add Component > 2D > Physics > RigidBody2D- 必要に応じて
BoxCollider2Dなどのコライダーも追加します。
RigidBody2DのTypeを Dynamic に設定し、物理挙動が有効になるようにします。- Sprite の画像(右向きのキャラなど)を設定しておきます。
3. SpriteFlipper コンポーネントのアタッチ
- 左右反転させたい Sprite を持つノードを選択します。
- 最もシンプルな例:
Playerノード自身に Sprite が付いている場合はPlayerを選択。 - もし
Playerの子ノードに Sprite がある場合、その子ノードにアタッチしても構いません。
- 最もシンプルな例:
- Inspector で
Add Component > Custom > SpriteFlipperを選択し、コンポーネントを追加します。 - プロパティを設定します:
- velocitySourceMode:
- Rigidbody2D を使っている場合:
RIGIDBODY2Dを選択。
- Rigidbody2D を使っている場合:
- targetNode:
- デフォルトで親ノードを参照したい場合: 空欄のままで OK(Sprite を子ノードにしている場合に便利)。
- 今回の例のように
Player自身に SpriteFlipper を付けている場合:targetNodeにPlayer自身をドラッグ&ドロップしておくと確実です。
- spriteNode:
- SpriteFlipper を付けたノード自身に Sprite があるなら空欄で OK。
- 別ノードの Sprite を反転させたい場合は、そのノードをドラッグ&ドロップ。
- flipWhenMovingThreshold:
- まずは
0.05のままで試し、反転がカクつくようなら0.1などに上げてみてください。
- まずは
- invertFlip:
- キャラ画像のデフォルト向きが「右向き」の場合:
falseのままで OK。 - デフォルトが「左向き」の場合:
trueにすると自然な向きになります。
- キャラ画像のデフォルト向きが「右向き」の場合:
- lockWhenIdle:
- 停止中も最後の向きを維持したい場合:
true(推奨)。 - 停止中は必ず右向きに戻したい場合:
false。
- 停止中も最後の向きを維持したい場合:
- debugLog:
- 動きの確認をしたいときだけ
trueにし、コンソールでログを確認します。
- 動きの確認をしたいときだけ
- velocitySourceMode:
4. 動作確認(Rigidbody2D 版)
- 簡単な移動用スクリプトを別途用意し、左右キーで
Playerに力を加える、またはlinearVelocity.xを更新するようにします。- この移動スクリプトは SpriteFlipper とは完全に独立していて構いません。
- ゲームを再生し、右に動かしたときに Sprite が右向き(flipX = false)、左に動かしたときに左向き(flipX = true) になることを確認します。
- もし反転方向が逆に感じる場合は、Inspector で invertFlip を切り替えて再生し直してください。
5. POSITION_DELTA モードでの確認(スクリプト移動キャラなど)
Rigidbody2D を使わず、update() で node.position を直接更新しているキャラにも対応できます。
- 移動スクリプト側では、通常通り
node.setPosition()などでキャラを移動させます。 - SpriteFlipper の設定:
- velocitySourceMode:
POSITION_DELTAを選択。 - targetNode: 位置を動かしているノードを指定(多くの場合はキャラ本体のノード)。
- velocitySourceMode:
- 再生して、左右移動に合わせてスプライトが反転することを確認します。
まとめ
SpriteFlipper コンポーネントは、ノードにアタッチするだけで「移動方向に合わせた自動左右反転」を実現できる、汎用的なユーティリティです。
- Rigidbody2D ベースのキャラでも、スクリプトで position を更新するキャラでも同じコンポーネントを使い回せる。
- 反転対象ノード(spriteNode)と速度参照ノード(targetNode)をプロパティで自由に指定できるので、アニメーション用子ノード構成にも柔軟に対応。
invertFlipやflipWhenMovingThresholdによって、アートの向きや挙動に合わせた細かい調整が可能。- 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結しているため、他プロジェクトへの持ち運びも容易。
今後は、アニメーション再生や足音など、「向きの変化」をトリガーにした別の汎用コンポーネントと組み合わせることで、よりリッチなキャラクター挙動を簡潔な構成で実現できます。
まずはこの SpriteFlipper をベースに、あなたのプロジェクトのキャラクターへ手軽に「向きの自動制御」を導入してみてください。
