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/AnimatedSprite2Dのflip_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_pathにAnimatedSprite2Dを指定- Enemy スクリプトの
flip_h制御を削除
SpriteFlipper 側は Sprite2D と AnimatedSprite2D の両方をサポートしているので、特別なコード変更は不要です。
例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
この velocity を SpriteFlipper が読むので、特に追加の処理は不要です。
「右へ動いたら右を向く」「左へ動いたら左を向く」床が、コンポーネントを足すだけで完成します。
メリットと応用
このコンポーネント化で得られるメリットをざっと挙げてみます。
- シーン構造がスッキリする
各キャラのスクリプトから「向きの制御ロジック」が消えるので、移動や攻撃など、本質的なロジックだけに集中できます。 - 再利用性が高い
「親に 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 をベースにカスタマイズしてみてください。
