【Godot 4】TurretRotation (砲台旋回) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

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) ← このコンポーネント
  1. TurretRotation.gd をプロジェクトに保存(例: res://components/TurretRotation.gd)。
  2. PlayerTank シーンを開き、TurretRotation という Node2D を追加し、上記スクリプトをアタッチ。
  3. 砲塔本体となる 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)
  1. EnemyTank シーンに TurretRotation ノードを追加し、スクリプトをアタッチ。
  2. インスペクタで target_nodePlayer ノードをドラッグ&ドロップ。
  3. 砲塔の可動範囲を制限したい場合は、base_angle_deg = 0, min_angle_deg = -90, max_angle_deg = 90 のように設定。
  4. 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()

のように書けるので、「砲塔の向き」と「射撃タイミング」もきれいに分離できます。継承ベースでごちゃごちゃ書きがちな部分こそ、コンポーネントに分解していきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!