Godot 4 で 2D アクションや横スクロールゲームを書いていると、プレイヤーや敵キャラの向きを「左右反転」させる処理って、ほぼ全キャラで必要になりますよね。
多くのプロジェクトでは、各キャラのスクリプト内でこんなコードを書いていると思います。

if velocity.x > 0:
    $Sprite2D.flip_h = false
elif velocity.x < 0:
    $Sprite2D.flip_h = true

問題はここからです。

  • プレイヤー、敵、動く足場…すべてのスクリプトに同じような処理がコピペされる
  • スプライトのノード名を変えたら、全部書き換えないといけない
  • 「アニメーションを変えたい」「向きの判定ロジックを変えたい」ときに、全キャラを修正するハメになる

これは完全に「継承 & コピペ地獄」のパターンですね。
そこで、向き制御だけを切り出して「コンポーネント」として再利用できるようにしたのが、今回の SpriteFlipper です。
親の velocity.x を見るだけで、自動的に Sprite2D.flip_h を切り替えてくれる、超シンプルだけど地味に効くコンポーネントにしてあります。

【Godot 4】向き制御はコンポーネントに丸投げ!「SpriteFlipper」コンポーネント

このコンポーネントは:

  • 親ノードの velocity.x を監視
  • 指定した Sprite2D / AnimatedSprite2Dflip_h を自動で切り替え
  • 「右向きがデフォルトか」「左向きがデフォルトか」も設定で変更可能

という役割だけに集中しています。
プレイヤーや敵キャラのスクリプトから「向きの制御ロジック」を完全に排除できるので、メインのロジックに集中できますね。


フルコード:SpriteFlipper.gd

extends Node
class_name SpriteFlipper
## 親ノードの velocity.x を監視して、ターゲットスプライトの flip_h を自動で切り替えるコンポーネント。
##
## 想定する親:
## - CharacterBody2D / CharacterBody3D
## - または `var velocity: Vector2` / `Vector3` を持つ任意のノード
##
## 想定するターゲット:
## - Sprite2D
## - AnimatedSprite2D
## - (flip_h プロパティを持つカスタムノードでもOK)

@export_node_path("Node2D") var target_sprite_path: NodePath
## 左右反転させたいスプライトへのパス。
## 例: `Sprite2D` / `AnimatedSprite2D`
## 空のままにすると、自動で最初に見つかった Sprite2D / AnimatedSprite2D を使います。

@export var default_face_right: bool = true
## true: 「右向き」がデフォルト。右移動で flip_h = false になります。
## false: 「左向き」がデフォルト。右移動で flip_h = true になります。
## 既存アセットの向きに合わせて設定してください。

@export var dead_zone: float = 5.0
## どれくらいの速度を「停止中」とみなすかのしきい値。
## 絶対値が dead_zone 未満のときは向きを変更しません。
## 入力のブレや慣性でチラチラ反転するのを防ぎます。

@export var invert_input: bool = false
## true にすると velocity.x の向きを反転して判定します。
## 物理的な向きと見た目の向きが逆転している特殊なケースで使います。

@export var use_physics_process: bool = true
## true: _physics_process() で更新 (物理ベースのキャラ向け)
## false: _process() で更新 (アニメ的な移動など、delta ベースのキャラ向け)

var _target_sprite: Node2D
var _parent: Node
var _last_facing_right: bool = true


func _ready() -> void:
    _parent = get_parent()
    if _parent == null:
        push_warning("SpriteFlipper: 親ノードが見つかりません。このコンポーネントは何かの子ノードとして使ってください。")

    # ターゲットスプライトの解決
    if target_sprite_path != NodePath(""):
        var node := get_node_or_null(target_sprite_path)
        if node and node is Node2D:
            _target_sprite = node
        else:
            push_warning("SpriteFlipper: target_sprite_path が Node2D ではありません。自動検出を試みます。")
            _auto_find_sprite()
    else:
        _auto_find_sprite()

    if _target_sprite == null:
        push_warning("SpriteFlipper: Sprite2D / AnimatedSprite2D が見つかりません。flip_h は変更されません。")

    # 初期向きの記録
    _last_facing_right = default_face_right


func _auto_find_sprite() -> void:
    ## 親ノード以下から Sprite2D / AnimatedSprite2D を探す簡易ヘルパー。
    if _parent == null:
        return

    # まずは直下の子を優先
    for child in _parent.get_children():
        if child is Sprite2D or child is AnimatedSprite2D:
            _target_sprite = child
            return

    # 見つからなければツリーを深く探索
    var queue: Array = _parent.get_children()
    while not queue.is_empty():
        var node: Node = queue.pop_front()
        if node is Sprite2D or node is AnimatedSprite2D:
            _target_sprite = node
            return
        queue.append_array(node.get_children())


func _physics_process(delta: float) -> void:
    if use_physics_process:
        _update_flip()


func _process(delta: float) -> void:
    if not use_physics_process:
        _update_flip()


func _update_flip() -> void:
    if _parent == null or _target_sprite == null:
        return

    var velocity_x := _get_parent_velocity_x()
    if invert_input:
        velocity_x = -velocity_x

    # dead_zone 内なら「向きは変えない」
    if absf(velocity_x) < dead_zone:
        return

    # 正のとき右向き、負のとき左向き
    var facing_right := velocity_x > 0.0

    # 同じ向きなら何もしない (無駄な代入を避ける)
    if facing_right == _last_facing_right:
        return

    _last_facing_right = facing_right
    _apply_flip(facing_right)


func _get_parent_velocity_x() -> float:
    ## 親から velocity.x 相当を取得するヘルパー。
    ## - CharacterBody2D / 3D: そのまま velocity.x
    ## - それ以外: `var velocity` を持っていればそこから取得
    if _parent == null:
        return 0.0

    # CharacterBody2D / 3D 対応
    if "velocity" in _parent:
        var v = _parent.velocity
        # Vector2 / Vector3 を想定
        if typeof(v) == TYPE_VECTOR2:
            return v.x
        elif typeof(v) == TYPE_VECTOR3:
            return v.x

    # velocity が無い場合は 0 扱い
    return 0.0


func _apply_flip(facing_right: bool) -> void:
    ## 実際に flip_h を適用する処理。
    ## default_face_right と facing_right の組み合わせで flip_h を決定します。
    if _target_sprite == null:
        return

    var flip_h := false

    # 「右向きがデフォルト」の場合:
    # - 右向き: flip_h = false
    # - 左向き: flip_h = true
    # 「左向きがデフォルト」の場合は逆になる
    if default_face_right:
        flip_h = not facing_right
    else:
        flip_h = facing_right

    # Sprite2D / AnimatedSprite2D はどちらも flip_h プロパティを持つ
    if "flip_h" in _target_sprite:
        _target_sprite.flip_h = flip_h
    else:
        push_warning("SpriteFlipper: ターゲットノードに flip_h プロパティがありません。カスタムノードの場合は実装を確認してください。")

使い方の手順

ここからは、具体的なシーン構成と一緒に使い方を見ていきましょう。

例1:プレイヤーキャラに付ける

典型的な 2D プレイヤーのシーン構成はこんな感じだと思います。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpriteFlipper (Node)

手順①:コンポーネントスクリプトを用意

  • 上の SpriteFlipper.gd をプロジェクトの res://components/SpriteFlipper.gd などに保存します。
  • Godot を再読み込みすると、SpriteFlipper がクラスとしてエディタに出てきます。

手順②:Player シーンに SpriteFlipper ノードを追加

  • Player のシーンを開く
  • 子ノードを追加 → Node を追加
  • そのノードに SpriteFlipper.gd をアタッチ
  • ノード名をわかりやすく SpriteFlipper にしておくと管理しやすいです

手順③:インスペクタで設定

  • target_sprite_path
    Player の子にある Sprite2D をドラッグ&ドロップで指定します。
    もし空のままでも、自動で Sprite2D / AnimatedSprite2D を探してくれます。
  • default_face_right
    右向きで描かれているスプライトなら true のままでOK。
    左向きで描かれている場合は false にしましょう。
  • dead_zone
    0~10 くらいで調整。
    入力がニュートラルに戻ったときに、微妙な慣性でカタカタ反転するのが嫌なら、5~20 くらいに上げておくと安定します。
  • use_physics_process
    CharacterBody2D なら物理更新で動いているはずなので、true のままでOKです。

手順④:Player スクリプトから向き処理を削除

もともと Player スクリプトにこんなコードがあったとします:

func _physics_process(delta: float) -> void:
    # 移動ロジック
    velocity.x = move_input * speed
    move_and_slide()

    # ★ もう不要になる部分
    if velocity.x > 0:
        $Sprite2D.flip_h = false
    elif velocity.x < 0:
        $Sprite2D.flip_h = true

この「★ もう不要になる部分」を削除して OK です。
向きの管理はすべて SpriteFlipper コンポーネントに丸投げしましょう。


例2:敵キャラ(AnimatedSprite2D)の向き制御

敵キャラはアニメーション付き、というケースも多いですよね。

Enemy (CharacterBody2D)
 ├── AnimatedSprite2D
 ├── CollisionShape2D
 └── SpriteFlipper (Node)

やることはプレイヤーと同じです。

  • Enemy シーンに SpriteFlipper ノードを追加
  • target_sprite_pathAnimatedSprite2D を指定
  • Enemy スクリプトの flip_h 制御を削除

SpriteFlipper 側は Sprite2DAnimatedSprite2D の両方をサポートしているので、特別なコード変更は不要です。


例3:動く床(Kinematic 的なオブジェクト)にも使える

親に velocity プロパティさえあればいいので、CharacterBody2D でなくても動きます。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpriteFlipper (Node)

たとえば、こんなスクリプトを MovingPlatform に持たせているとします:

extends Node2D

var velocity: Vector2 = Vector2.ZERO
@export var move_speed: float = 60.0

func _physics_process(delta: float) -> void:
    # 左右に往復させる簡単な例
    velocity.x = sin(Time.get_ticks_msec() / 1000.0) * move_speed
    position += velocity * delta

この velocitySpriteFlipper が読むので、特に追加の処理は不要です。
「右へ動いたら右を向く」「左へ動いたら左を向く」床が、コンポーネントを足すだけで完成します。


メリットと応用

このコンポーネント化で得られるメリットをざっと挙げてみます。

  • シーン構造がスッキリする
    各キャラのスクリプトから「向きの制御ロジック」が消えるので、移動や攻撃など、本質的なロジックだけに集中できます。
  • 再利用性が高い
    「親に velocity があって、子に Sprite2D / AnimatedSprite2D がある」なら、どんなノードでもそのまま使えます。
    プレイヤー、敵、弾、動く床…全部同じコンポーネントで済みます。
  • 差し替えが簡単
    「やっぱり右向きじゃなくて左向きをデフォルトにしたい」
    「向きの判定を input.x ベースにしたい」
    などの仕様変更も、SpriteFlipper だけを直せば全キャラに反映されます。
  • 継承ツリーに縛られない
    「全部のキャラを PlayerBase から継承させて、そこで向きを制御する」みたいな継承設計をしなくて済みます。
    どんなノード構成でも、必要なときに SpriteFlipper をポン付けするだけです。

改造案:入力ベースで向きを決めたい場合

「物理的な移動方向ではなく、入力方向で向きを決めたい」
というケースもあります。例えば:

  • 慣性で右に流れているけど、入力は左なので見た目だけ左を向かせたい

そんなときは、SpriteFlipper に「外部から向きを指示する」メソッドを追加するのもアリです。

func set_facing_from_input(input_x: float) -> void:
    ## 入力値 (-1.0 ~ 1.0) から向きを直接設定したいときに呼ぶ。
    if absf(input_x) < 0.1:
        return  # 入力がほぼゼロなら向きは変えない

    var facing_right := input_x > 0.0
    if facing_right == _last_facing_right:
        return

    _last_facing_right = facing_right
    _apply_flip(facing_right)

Player スクリプト側では、こんな感じで呼び出せます。

# プレイヤー側 (例)
func _physics_process(delta: float) -> void:
    var input_x := Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    velocity.x = input_x * speed
    move_and_slide()

    $SpriteFlipper.set_facing_from_input(input_x)

こうやって「向きの決定ロジック」を差し替えられるのも、コンポーネント化しておく大きなメリットですね。
ぜひ自分のプロジェクトに合わせて、SpriteFlipper をベースにカスタマイズしてみてください。