Godotで戦車やタレットを作るとき、つい「戦車シーンを継承して、砲塔付き戦車」「固定砲台戦車」みたいにシーンを増やしてしまいがちですよね。さらに、_process()の中で「親ノードの回転」「子ノードの見た目」「ターゲット座標」を全部ごちゃ混ぜにすると、あとから仕様変更したときに地獄を見ます。
典型的なのが、こんな感じの実装です:
- プレイヤー戦車のスクリプトの中に「移動」「砲塔回転」「射撃」のロジックが全部入り
- 敵戦車はそのスクリプトを継承して一部だけ上書き
- 固定砲台はまた別のシーンでコピペ改造
これを続けると、砲塔の仕様を変えたいだけなのに、3~4個のシーンを開いて同じ修正をしないといけなくなります。まさに「継承地獄」ですね。
そこで今回は、「砲塔だけをターゲットに向ける」処理を丸ごとコンポーネント化した TurretRotation コンポーネント を用意しました。戦車本体のスクリプトから砲塔ロジックを切り離し、どんなノードにもポン付けできるようにしておきましょう。
【Godot 4】どの戦車にもポン付けOK!「TurretRotation」コンポーネント
このコンポーネントは、「親(戦車など)の本体とは別に、砲塔部分だけをターゲットに向ける」ための汎用スクリプトです。
- ターゲットは「ノード参照」でも「ワールド座標」でも指定可能
- 回転速度の上限付き(急にガクッと向きが変わらない)
- 左右どちら回りで回転するかを自動で最短経路で選択
- 最小・最大角度を設定して、砲塔の可動範囲を制限可能(例:左右90度まで)
フルコード:TurretRotation.gd
extends Node2D
class_name TurretRotation
## TurretRotation (砲台旋回) コンポーネント
##
## 親(戦車など)の本体とは別に、砲塔部分だけをターゲットに向けるコンポーネント。
## - このノード自身の rotation を操作します。
## - 親ノードの移動・回転とは独立して動作します。
## - ターゲットは Node2D 参照 or ワールド座標(Vector2)の両方に対応。
@export_group("Target Settings")
## 砲塔が向くターゲットノード(例:プレイヤー)
## - Node2D であれば何でもOK(CharacterBody2D, Sprite2D など)
## - シーン内で直接ドラッグ&ドロップして参照を設定できます。
@export var target_node: Node2D
## スクリプトから直接ターゲット座標を指定したい場合に使います。
## 例: turret_rotation.target_global_position = some_position
var target_global_position: Vector2:
get:
# target_node があればそれを優先
if is_instance_valid(target_node):
return target_node.global_position
return _target_position_internal
set(value):
_target_position_internal = value
## 内部用のターゲット座標(target_node が無い場合に使用)
var _target_position_internal: Vector2 = Vector2.ZERO
@export_group("Rotation Settings")
## 1秒あたりの最大回転速度(度数法)
## 例: 90 とすると、1秒で最大90度までしか回転しません。
@export_range(0.0, 720.0, 1.0, "suffix:deg/sec")
var max_rotation_speed_deg: float = 180.0
## 砲塔の初期向き(デフォルトの前方向)をどこにするか。
## 通常は 0 度(右向き)でOKですが、スプライトの向きに合わせて調整します。
@export_range(-180.0, 180.0, 1.0, "suffix:deg")
var base_angle_deg: float = 0.0
## 砲塔の可動範囲(最小角度)
## base_angle_deg を中心として、どこまで回せるかを制限します。
## 例: base_angle_deg = 0, min_angle_deg = -90, max_angle_deg = 90 なら左右90度まで。
@export_range(-180.0, 0.0, 1.0, "suffix:deg")
var min_angle_deg: float = -180.0
## 砲塔の可動範囲(最大角度)
@export_range(0.0, 180.0, 1.0, "suffix:deg")
var max_angle_deg: float = 180.0
@export_group("Behavior")
## ターゲットが存在しないときに、base_angle_deg に戻すかどうか。
@export var return_to_base_when_no_target: bool = true
## ターゲットが存在しないときに、base_angle_deg に戻る速度(度/秒)
@export_range(0.0, 720.0, 1.0, "suffix:deg/sec")
var return_speed_deg: float = 90.0
## ターゲットとの距離がこの値より大きいときは無視する(0 なら無制限)
@export_range(0.0, 10000.0, 1.0, "suffix:px")
var max_target_distance: float = 0.0
## ターゲットとの距離がこの値より小さいときは無視する(0 なら無制限)
@export_range(0.0, 10000.0, 1.0, "suffix:px")
var min_target_distance: float = 0.0
## デバッグ用: ターゲット方向のラインを描画する
@export var debug_draw: bool = false
@export var debug_color: Color = Color.YELLOW
func _ready() -> void:
# base_angle_deg をラジアンに変換して初期回転に反映
rotation = deg_to_rad(base_angle_deg)
func _physics_process(delta: float) -> void:
var has_valid_target := is_instance_valid(target_node) or _target_position_internal != Vector2.ZERO
if has_valid_target:
var target_pos: Vector2 = target_global_position
var my_pos: Vector2 = global_position
var to_target: Vector2 = target_pos - my_pos
# ターゲットとの距離チェック
var dist := to_target.length()
if (max_target_distance > 0.0 and dist > max_target_distance) \
or (min_target_distance > 0.0 and dist < min_target_distance):
# 範囲外の場合、ターゲット無しと同じ扱いにする
_handle_no_target(delta)
return
# ターゲット方向の角度(ラジアン)
var target_angle := to_target.angle()
# base_angle_deg を考慮した「ローカル目標角度」
var desired_angle := _clamp_to_limits(target_angle)
# 現在の向きから desired_angle へ、最大回転速度以内で補間
var max_step := deg_to_rad(max_rotation_speed_deg) * delta
rotation = _move_towards_angle(rotation, desired_angle, max_step)
else:
_handle_no_target(delta)
update() # debug_draw 用
func _handle_no_target(delta: float) -> void:
if not return_to_base_when_no_target:
return
var base_angle := deg_to_rad(base_angle_deg)
var max_step := deg_to_rad(return_speed_deg) * delta
rotation = _move_towards_angle(rotation, base_angle, max_step)
func _clamp_to_limits(world_target_angle: float) -> float:
# base_angle_deg を基準にしたローカル角度へ変換
var base_angle := deg_to_rad(base_angle_deg)
var local_angle := _normalize_angle(world_target_angle - base_angle)
# 可動範囲でクランプ
var min_local := deg_to_rad(min_angle_deg)
var max_local := deg_to_rad(max_angle_deg)
local_angle = clamp(local_angle, min_local, max_local)
# 再びワールド角度に戻す
return base_angle + local_angle
func _move_towards_angle(current: float, target: float, max_step: float) -> float:
# 角度差を -PI..PI の範囲に正規化
var diff := _normalize_angle(target - current)
# すでに十分近いならターゲット角度にスナップ
if abs(diff) <= max_step:
return target
# diff の符号に応じて回転方向を決定
return current + sign(diff) * max_step
func _normalize_angle(angle: float) -> float:
# 角度を -PI..PI の範囲に正規化
angle = fmod(angle + PI, TAU)
if angle < 0.0:
angle += TAU
return angle - PI
func _draw() -> void:
if not debug_draw:
return
# 現在の向きのライン
var line_length := 40.0
var end_point := Vector2.RIGHT.rotated(rotation) * line_length
draw_line(Vector2.ZERO, end_point, debug_color, 2.0)
# ターゲット方向ライン(あれば)
if is_instance_valid(target_node) or _target_position_internal != Vector2.ZERO:
var target_pos := target_global_position
var to_target := target_pos - global_position
draw_line(Vector2.ZERO, to_target, Color.RED, 1.0)
使い方の手順
ここでは、代表的な3パターンで使い方を説明します。
① プレイヤー戦車の砲塔をマウスカーソルに向ける
まずは一番わかりやすい例として、「プレイヤー戦車の砲塔が常にマウスカーソルを向く」パターンです。
シーン構成例:
PlayerTank (CharacterBody2D) ├── BodySprite (Sprite2D) ├── Turret (Node2D) │ └── TurretSprite (Sprite2D) ├── CollisionShape2D └── TurretRotation (TurretRotation) ← このコンポーネント
- TurretRotation.gd をプロジェクトに保存(例:
res://components/TurretRotation.gd)。 - PlayerTank シーンを開き、TurretRotation という Node2D を追加し、上記スクリプトをアタッチ。
- 砲塔本体となる
Turret (Node2D)に、次のような簡単なスクリプトを付けます:
extends Node2D
@onready var turret_rotation: TurretRotation = $"../TurretRotation"
func _process(delta: float) -> void:
# マウスカーソルのワールド座標をターゲットにする
var viewport := get_viewport()
var mouse_pos := viewport.get_mouse_position()
var world_mouse_pos := get_viewport().get_camera_2d().get_screen_to_world(mouse_pos)
turret_rotation.target_global_position = world_mouse_pos
# TurretRotation 自身の rotation を、この Turret ノードにコピー
rotation = turret_rotation.rotation
ポイント:
- TurretRotation は「どこを向くか」だけを決めるコンポーネントとして使っています。
- 実際の砲塔スプライトは
Turret側にあり、rotation = turret_rotation.rotationで向きを同期しています。 - こうしておくと、
TurretRotationを別の敵タレットにも簡単に再利用できます。
② 敵戦車がプレイヤーを自動追尾する砲塔
次は、敵戦車の砲塔がプレイヤーを常に狙う例です。
シーン構成例:
EnemyTank (CharacterBody2D) ├── BodySprite (Sprite2D) ├── Turret (Node2D) │ └── TurretSprite (Sprite2D) ├── CollisionShape2D └── TurretRotation (TurretRotation)
- EnemyTank シーンに TurretRotation ノードを追加し、スクリプトをアタッチ。
- インスペクタで
target_nodeにPlayerノードをドラッグ&ドロップ。 - 砲塔の可動範囲を制限したい場合は、
base_angle_deg = 0,min_angle_deg = -90,max_angle_deg = 90のように設定。 - Turret ノードに次のスクリプトを付けて、向きを同期します:
extends Node2D
@onready var turret_rotation: TurretRotation = $"../TurretRotation"
func _physics_process(delta: float) -> void:
rotation = turret_rotation.rotation
これだけで、敵戦車の砲塔は常にプレイヤーの方向を向いてくれます。EnemyTank の移動ロジックとは完全に分離されているので、「砲塔だけを持つ固定砲台」にもそのまま流用できます。
③ 固定砲台が一定距離内のターゲットだけを狙う
最後に、固定砲台が「一定距離内にいるターゲットだけを狙う」例です。
シーン構成例:
TurretBase (StaticBody2D) ├── BaseSprite (Sprite2D) ├── Turret (Node2D) │ └── TurretSprite (Sprite2D) └── TurretRotation (TurretRotation)
設定例:
target_nodeに Player を指定max_target_distance = 400(400px 以上離れたら追尾しない)min_target_distance = 64(近すぎると追尾しない)return_to_base_when_no_target = true(ターゲットが範囲外になったら正面に戻る)
Turret ノードのスクリプト:
extends Node2D
@onready var turret_rotation: TurretRotation = $"../TurretRotation"
func _physics_process(delta: float) -> void:
rotation = turret_rotation.rotation
これで、プレイヤーが一定距離内に入ると砲台が追尾し、離れるとスッと正面に戻る挙動になります。
メリットと応用
TurretRotation をコンポーネントとして切り出すことで、次のようなメリットがあります。
- 戦車本体のスクリプトがスリムになる
移動・HP・射撃などのロジックと、砲塔の回転ロジックを完全に分離できます。
「戦車の動きは変えずに、砲塔だけ高度な挙動にしたい」といった要望にも対応しやすくなります。 - シーン構造がシンプルに保てる
「砲塔付きプレイヤー」「砲塔付き敵」「固定砲台」などを、全部同じコンポーネントで賄えるので、シーンのバリエーションが増えても管理が楽です。 - レベルデザインが直感的になる
レベルデザイナーは「この敵の砲塔は180度まで回せる」「この固定砲台はプレイヤーが近づいたときだけ追尾する」などを、インスペクタのパラメータ調整だけで実現できます。 - テストとデバッグがしやすい
debug_drawをオンにすれば、今どこを向こうとしているのかが一目でわかるので、挙動のチューニングが楽になります。
さらに応用として、「ターゲットが一定角度以内に入ったら発射許可を出す」といったロジックも、このコンポーネントに追加できます。
例えば、TurretRotation に次の関数を足すと、「今、砲塔がターゲットをちゃんと狙えているか?」を判定できます:
## 砲塔がターゲットをどれくらい正確に狙えているかを判定するヘルパー関数。
## 許容角度 threshold_deg 以内なら true を返します。
func is_aiming_at_target(threshold_deg: float = 5.0) -> bool:
if not (is_instance_valid(target_node) or _target_position_internal != Vector2.ZERO):
return false
var target_pos := target_global_position
var to_target := target_pos - global_position
var target_angle := to_target.angle()
# 現在の向きとターゲット方向の角度差
var diff := abs(_normalize_angle(target_angle - rotation))
return rad_to_deg(diff) <= threshold_deg
これを使えば、射撃コンポーネント側で
if turret_rotation.is_aiming_at_target():
shoot()
のように書けるので、「砲塔の向き」と「射撃タイミング」もきれいに分離できます。継承ベースでごちゃごちゃ書きがちな部分こそ、コンポーネントに分解していきましょう。




