Godot 4で「プレイヤーに銃を持たせてマウス方向に向けたい」「剣を肩の位置から振り回したい」といったとき、つい親ノードの中心を回転の基準にしてしまって、こんな問題が起きがちです。

  • キャラの「体の中心」で武器が回転してしまい、肩や手首からズレる
  • プレイヤーのスプライトを描き直すたびに、Sprite2D.offsetposition を微調整し直す羽目になる
  • 「プレイヤー専用の武器ノード」「敵専用の武器ノード」など、継承ツリーが増えて管理がカオスになる

Godot標準のやり方だと、Node2D を継承した「プレイヤーシーン」に直接武器用の子ノードを生やして、そこでグリグリ回転させることが多いですよね。でもこのやり方だと、

  • プレイヤー、敵、動く砲台など、似たような「武器の持ち方ロジック」がコピペで増殖
  • 「肩の位置」を変えたいだけなのに、シーン構造を変えたりスクリプトをいじったりと手間が多い

そこで今回は、「継承より合成」の考え方で、どんなキャラにもポン付けできる WeaponPivot コンポーネント を用意してみましょう。
親ノードの中心ではなく、「肩の位置」など任意のポイントを回転の基準にして武器スプライトを回せるようになります。

【Godot 4】肩からきれいに武器を振り回す!「WeaponPivot」コンポーネント

このコンポーネントはざっくりいうと、

  • 親(プレイヤーや敵)のローカル座標で「肩の位置」を指定
  • そこを中心にして武器スプライトを回転させる
  • マウス方向 or 任意のターゲット方向を向く

という「武器の持ち手」を担当するノードです。
プレイヤーだろうが敵だろうが、単にシーンにこのコンポーネントを生やすだけで同じ挙動を共有できるのがポイントですね。


GDScript フルコード


extends Node2D
class_name WeaponPivot
"""
WeaponPivot (武器持ち手) コンポーネント

親ノードの「中心」ではなく、「肩の位置」など任意のポイントを
回転の基準にして武器スプライトを回すためのコンポーネントです。

想定ノード構成:
  CharacterBody2D / Node2D / etc...
    └── WeaponPivot (このコンポーネント)
         └── Sprite2D / AnimatedSprite2D / WeaponScene など
"""

# --- 設定パラメータ (@export) -----------------------------

@export_category("WeaponPivot 基本設定")

@export var pivot_position: Vector2 = Vector2(8, -4)
## 親ノードのローカル座標での「肩の位置」。
## 親の原点 (0,0) からのオフセットとして指定します。
## 例: 右肩が右方向に8px / 上方向に4px なら (8, -4)

@export var default_angle_deg: float = 0.0
## 何もターゲットがないときのデフォルト角度(度数法)。
## 0度は右向き、90度は下向き、-90度は上向きという Godot 標準の角度系です。

@export var flip_with_parent: bool = true
## 親スプライトの左右反転に追従するかどうか。
## 親が Sprite2D で flip_h している場合などに、武器の向きも合わせたいときに使います。

@export var auto_aim_mouse: bool = false
## true にすると、毎フレーム マウスカーソル方向を向く「エイム武器」になります。
## プレイヤーの銃などに便利です。

@export var smooth_rotate: bool = false
## true にすると、ターゲット角度に対して徐々に補間して回転します。
## いきなりカクッと向きを変えたくないときに。

@export_range(0.0, 20.0, 0.1)
var rotate_speed_deg_per_sec: float = 720.0
## smooth_rotate = true のときの回転速度(度/秒)。
## 360 なら 1秒で1回転、720 なら 0.5秒で1回転するイメージです。

@export_category("ターゲット設定")

@export var target_node: Node2D
## 武器が向くべきターゲットノード。
## 例: プレイヤーの銃なら敵ノード、敵の砲台ならプレイヤーノードなど。
## auto_aim_mouse が true の場合は無視されます。

@export var use_global_target: bool = true
## true: target_node のグローバル位置を使って方向を計算します。
## false: 親ノードのローカル座標系での位置を使います。
## 通常は true で OK。ローカルで完結したい特殊なケースのみ false に。

# --- 内部用変数 -------------------------------------------

var _current_angle_rad: float = 0.0
var _target_angle_rad: float = 0.0

func _ready() -> void:
    # 初期位置を「肩の位置」に合わせる
    position = pivot_position

    # 初期角度を設定
    _current_angle_rad = deg_to_rad(default_angle_deg)
    _target_angle_rad = _current_angle_rad
    rotation = _current_angle_rad


func _process(delta: float) -> void:
    # 1. 肩の位置を親のローカル座標から更新
    #    (アニメーションなどで親の見た目が変わっても、pivot_position を変えれば追従可能)
    position = pivot_position

    # 2. ターゲット角度を算出
    if auto_aim_mouse:
        _update_target_angle_to_mouse()
    elif is_instance_valid(target_node):
        _update_target_angle_to_node(target_node)
    else:
        # ターゲットがいなければデフォルト角度に戻す
        _target_angle_rad = deg_to_rad(default_angle_deg)

    # 3. 親の左右反転に追従する場合の調整
    if flip_with_parent:
        _apply_parent_flip_adjustment()

    # 4. スムーズ回転 or 即時回転
    if smooth_rotate:
        _current_angle_rad = _rotate_towards(
            _current_angle_rad,
            _target_angle_rad,
            deg_to_rad(rotate_speed_deg_per_sec) * delta
        )
    else:
        _current_angle_rad = _target_angle_rad

    rotation = _current_angle_rad


# --- ターゲット角度の計算 ---------------------------------

func _update_target_angle_to_mouse() -> void:
    var viewport := get_viewport()
    if viewport == null:
        return

    # マウス位置(ビューポート座標)をワールド座標に変換
    var mouse_pos_global: Vector2 = viewport.get_mouse_position()
    mouse_pos_global = viewport.get_camera_2d().get_screen_to_world(mouse_pos_global) \
        if viewport.get_camera_2d() != null \
        else mouse_pos_global

    # WeaponPivot のグローバル位置から見たマウス方向
    var dir: Vector2 = (mouse_pos_global - global_position)
    if dir.length_squared() <= 0.0001:
        return

    _target_angle_rad = dir.angle()


func _update_target_angle_to_node(target: Node2D) -> void:
    if not is_instance_valid(target):
        return

    var target_pos: Vector2
    if use_global_target:
        target_pos = target.global_position
    else:
        # 親のローカル座標系で扱いたい特殊ケース
        if get_parent() is Node2D:
            var parent_2d := get_parent() as Node2D
            target_pos = parent_2d.to_global(target.position)
        else:
            target_pos = target.global_position

    var dir: Vector2 = (target_pos - global_position)
    if dir.length_squared() <= 0.0001:
        return

    _target_angle_rad = dir.angle()


# --- 親の左右反転に追従するロジック ------------------------

func _apply_parent_flip_adjustment() -> void:
    var parent := get_parent()
    if parent is Sprite2D:
        var sprite := parent as Sprite2D
        if sprite.flip_h:
            # 左右反転しているときは、X軸を反転したのと同じ扱いにする
            # 角度θをπ - θ に変換すると、左右反転後の見た目に合う
            _target_angle_rad = PI - _target_angle_rad
    elif parent is Node2D:
        # 親がスケールで左右反転している場合(scale.x < 0)
        var p2d := parent as Node2D
        if p2d.scale.x < 0.0:
            _target_angle_rad = PI - _target_angle_rad
    # それ以外の親は特に考慮しない(必要になったら拡張)


# --- 角度補間ユーティリティ --------------------------------

func _rotate_towards(current: float, target: float, max_step: float) -> float:
    """
    current 角度を target 角度に向かって max_step だけ近づける。
    ラジアンで扱い、±π の範囲を考慮して最短方向に回転します。
    """
    var delta: float = wrapf(target - current, -PI, PI)
    if absf(delta) <= max_step:
        return target
    return current + signf(delta) * max_step


# --- 便利メソッド(任意で呼び出し可能) ---------------------

func aim_at_global_position(pos: Vector2) -> void:
    """
    任意のグローバル座標を向かせたいときに手動で呼び出す補助メソッド。
    auto_aim_mouse / target_node を使わないカスタム用途向け。
    """
    var dir := pos - global_position
    if dir.length_squared() > 0.0001:
        _target_angle_rad = dir.angle()


func set_pivot_from_node(node: Node2D) -> void:
    """
    親ノードの子など、特定ノードの位置を「肩の位置」として使いたいときに便利。
    例: Skeleton2D のボーン先端など。
    """
    if get_parent() is Node2D:
        var parent_2d := get_parent() as Node2D
        pivot_position = parent_2d.to_local(node.global_position)

使い方の手順

① コンポーネントスクリプトをプロジェクトに追加

  1. 上記の WeaponPivot.gd をプロジェクト内(例: res://components/WeaponPivot.gd)に保存します。
  2. Godot エディタで開くと、class_name WeaponPivot のおかげで「WeaponPivot」がノード追加ダイアログに出てくるようになります。

② プレイヤーに「武器持ち手」をアタッチする

例として、マウス方向に銃を向ける 2D プレイヤーを考えます。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WeaponPivot (Node2D, <WeaponPivot.gd> をアタッチ)
      └── GunSprite (Sprite2D)
  • Player: いつもの移動ロジックを持ったプレイヤー
  • WeaponPivot: 今回のコンポーネント(class_name WeaponPivot
  • GunSprite: 実際の武器画像(銃の Sprite2D など)

設定例:

  • pivot_position: プレイヤーの右肩の位置(例: (8, -4)
  • auto_aim_mouse: true(マウス方向を向く)
  • flip_with_parent: true(プレイヤーの左右反転に合わせる)
  • smooth_rotate: 好みで(true ならヌルッと回転)

これで、プレイヤーの移動ロジックに一切手を入れずに、肩の位置から銃がマウスを追従して回転するようになります。

③ 敵や砲台にもそのまま再利用

次に、プレイヤーを狙ってくる敵砲台を作る例です。

Turret (Node2D)
 ├── BaseSprite (Sprite2D)
 ├── BarrelSprite (Sprite2D) ← ここは回転させない土台でもOK
 └── WeaponPivot (Node2D, <WeaponPivot.gd>)
      └── CannonSprite (Sprite2D)

Turret.gd 側で、プレイヤーノードをターゲットとして渡します。


# Turret.gd (例)
extends Node2D

@export var player: Node2D
@onready var weapon_pivot: WeaponPivot = $WeaponPivot

func _ready() -> void:
    if player:
        weapon_pivot.target_node = player
        weapon_pivot.auto_aim_mouse = false
        weapon_pivot.use_global_target = true
        weapon_pivot.pivot_position = Vector2(0, -4) # 砲台の付け根位置など

プレイヤーはプレイヤーでマウスエイム、敵砲台はプレイヤーを追尾、といったロジックを、どちらも同じ WeaponPivot コンポーネントで共有できます。

④ 動く床や乗り物にも応用できる

例えば、動く船の上に大砲を載せたい場合:

Ship (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WeaponPivot (Node2D, <WeaponPivot.gd>)
      └── CannonSprite (Sprite2D)
  • pivot_position: 船の甲板上の大砲の設置位置
  • target_node: 敵船のノード

船がどんなに動き回っても、WeaponPivot は「船のローカル座標での肩(砲台の基部)位置」を基準にして、敵船を狙い続けてくれます。


メリットと応用

この WeaponPivot コンポーネントを使うと、次のようなメリットがあります。

  • シーン構造がシンプル
    プレイヤー、敵、砲台など、どれも「親 + WeaponPivot + Sprite2D」という同じ構成で済みます。
  • ロジックの再利用性が高い
    「マウスエイム」「ターゲットノードを向く」「スムーズ回転」などのロジックがコンポーネントに集約されるので、
    新しい敵やギミックを作るときは、ただ WeaponPivot を貼るだけで OK です。
  • アート変更に強い
    スプライトを描き直して肩の位置が変わっても、pivot_position をちょっといじるだけで調整完了。
    プレイヤーのスクリプトを触る必要はありません。
  • 「継承ツリー地獄」からの解放
    PlayerWithGunPlayerWithSwordEnemyWithGun…といったサブクラスを増やさず、
    「武器持ち手」という 1 コンポーネントで共通化できます。

コンポーネント指向で「武器を持つ」という行為を WeaponPivot に切り出しておけば、

  • あとから「2丁拳銃にしたい」→ WeaponPivot を 2 つ付けるだけ
  • 「肩じゃなくて腰から振り回したい」→ pivot_position を変えるだけ

といった変更にも柔軟に対応できます。
ノード階層を深くせずに、「武器をどう持つか」という責務を 1 ノードに閉じ込められるのが気持ちいいですね。

改造案:攻撃時に一瞬だけ「振りかぶり」アニメーションを付ける

例えば、近接攻撃のときに「肩から一瞬だけ大きく振りかぶる」演出を入れたい場合、WeaponPivot にこんなメソッドを追加できます。


func play_swing(offset_deg: float = -30.0, duration: float = 0.1) -> void:
    """
    現在のターゲット角度から、offset_deg だけ一瞬ズラしてから
    duration 秒かけて元の角度に戻す「振りかぶり」アニメーション。
    """
    var start_angle := _target_angle_rad + deg_to_rad(offset_deg)
    var end_angle := _target_angle_rad
    var tween := create_tween()
    tween.tween_property(self, "rotation", start_angle, 0.0)
    tween.tween_property(self, "rotation", end_angle, duration).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)

攻撃ボタンを押したタイミングで weapon_pivot.play_swing() を呼べば、どのキャラでも同じ「振りかぶり演出」を再利用できます。
こうやって「演出」までコンポーネント側に寄せていくと、親のスクリプトはますますスリムになりますね。

ぜひ、自分のプロジェクトでも「武器の持ち方ロジック」をコンポーネント化して、継承ツリーから解放されてみてください。