【Godot】GodotのVector2入門:マウスを追う砲塔を作り、距離が変わっても弾の速さを一定にする

作成: 2026-09-22最終更新: 2026-10-10

マウスを追う砲塔を作りながら、Vector2の引き算から距離・向き・角度を取り出し、弾の速さを一定にする方法を解説します。

マウスの方を向く砲塔や、敵へまっすぐ飛ぶ弾は、2つの位置の引き算から作ります。引き算でできる矢印(Vector2)から、距離・向き・角度を取り出して使い分けます。

取り出し方を間違えると、遠くの敵を狙うほど弾が速く飛んだり、砲塔が左を通るたびにぐるりと一周したりします。

この記事では、マウスを追う砲塔を作りながら、矢印の作り方・長さ1への縮め方・角度への変換を解説します。

砲塔からマウスへ伸びる1本の矢印から、距離・向き・角度の3つを取り出して使うイメージ

この記事でわかること

  • 引き算でできるのは矢印。向きは 目標 - 自分
  • 長さを1にすると、速さから距離が抜ける
  • angle() も look_at() も、基準は右(+X)
  • 左をまたぐ回転は rotate_toward() に任せる

動作確認環境: Godot 4.7

Sponsored

Vector2(位置・速度・向き)

Vector2 は x と y の2つの数を持つだけの型です。同じ型が、意味の違う3つのものに使われます。

  • 位置: どこにいるか。global_position がこれ
  • 速度: 1秒でどれだけ動くか。velocity がこれ
  • 向き: どっちを指しているか。長さが1のもの

同じ型なので足し算も引き算もできますが、意味は混ざりません。位置と位置を足しても意味のある場所にはなりません。意味が出るのは、位置から位置を引いたときです。

引き算の向きと座標の種類

2点を引くと、片方からもう片方への矢印ができます。

var offset := target.global_position - global_position
目標−自分は自分から目標への矢印で、自分に足すと目標へ届く。逆に引くと反対を向く

順番を逆にすると、矢印も逆を向きます。目標 - 自分 が、自分に足すと目標へ届く矢印です。迷ったら「行き先から出発点を引く」と覚えてください。

引き算の前に、もう1つそろえるものがあります。2つの点が、同じ種類の座標であることです。

positionは親から見た位置、global_positionはワールド、イベントのpositionは画面の画素
  • position: 親から見た位置。親が動くと基準も動く
  • global_position: ワールドの位置。どこにぶら下がっていても同じ基準
  • InputEventMouse の position: 画面(ビューポート)上の画素。カメラの位置やズームは入っていない
  • get_global_mouse_position(): マウスのワールド位置。カメラを含む

狙いを計算するときは global_position と get_global_mouse_position() の組で閉じておくと、あとで砲塔を戦車の子にしてもカメラを動かしてもずれません。

長さ1の向き(normalized)

引き算でできた矢印は、向きと距離の両方を持っています。速度として使う前に、距離のほうを抜きます。

var offset := target.global_position - global_position
var dir := offset.normalized()      # 長さ1。向きだけ残る
var dist := offset.length()         # 距離だけ取り出す
引いたままの矢印は長さがばらばらだが、normalized()で長さ1にすると向きだけが残る

normalized() は矢印の長さを1にします。向きは変わりません。これに速さを掛ければ、距離に関係なく同じ速度になります。

抜き忘れるとどうなるかが、よくある症状です。

velocity = (target.global_position - global_position) * speed    # 距離が速さに乗る
正規化を忘れると、近い的へは遅く遠い的へは速く飛び、速さが距離×speedになる

この式の長さは「距離 × speed」です。100px 先の的へはゆっくり、800px 先の的へは8倍の速さで飛びます。斜めかどうかは関係ありません。距離が長いほうが速くなります。

引き算と正規化をまとめた近道もあります。a.direction_to(b) は (b - a).normalized() と、a.distance_to(b) は (b - a).length() と同じです。

他のエンジンでいうと: Godot の direction_to() と distance_to() は、Unity の (b - a).normalized と Vector2.Distance(a, b) にあたります。

長さ0のときだけ気をつけてください。マウスが砲塔の中心と重なると引き算は (0, 0) になり、normalized() は (0, 0) を返します。エラーは出ませんが、向きも出ません。小数の誤差を許して「ほぼ 0 か」を調べる is_zero_approx() で弾いて、そのフレームは前の向きを使います。

Sponsored

角度(angle)と絵の正面

矢印から角度を取るのが angle() です。ノードを、渡した点(ワールドの座標)のほうへ回すのが look_at() です。

global_rotation = offset.angle()     # 矢印の傾きをラジアンで
look_at(target.global_position)      # ノードを目標へ向ける
angleもlook_atも基準は右。上向きに描いた絵は、子のRotationを90にしてそろえる

どちらも基準は右(+X)です。rotation が 0 のとき、そのノードは右を向いているとみなされます。

だから、上向きに描いたスプライトをそのまま使うと90度ずれます。直し方は、絵のほうを回しておくことです。スプライトを子にして Rotation を 90 にすれば、親から見て右向きにそろいます。親のコードは変えずに済みます。

角度はラジアンです。インスペクタの Rotation 欄は度で表示されますが、コードの rotation はラジアンで、angle() が返すのもラジアンです。一周が約 6.28、1 ラジアンが約57度です。度に直すときは rad_to_deg()、ラジアンに戻すときは deg_to_rad() を使います(3Dの回転は Node3D入門 で扱っています)。

もう1つ、2Dの画面は y が下向きに増えます。角度も、正の値で時計回り(画面の下向き)に回ります。右上を指す矢印の angle() が負の値になるのは、このためです。

角度から矢印へ(rotated)

向きを角度として持ったあと、もう一度矢印に戻したくなります。砲塔の向きへ弾を飛ばすときがそれです。

var dir := Vector2.RIGHT.rotated(global_rotation)

右向きの長さ1の矢印(単位ベクトル)を rotation の分だけ回すと、いまの砲身の方向が取れます。Vector2.from_angle(global_rotation) でも同じものが得られます。

ここが行きと帰りです。マウスへの矢印から角度を作り、その角度から矢印に戻す。戻した矢印を使うと、弾は「マウス」ではなく「いまの砲身」へ飛びます。砲塔が遅れて追っているあいだ、この2つは違う方向を指します。

左をまたぐ回転(rotate_toward)

angle() が返す値は -π から π まで(約 -3.14 から 3.14、度でいうと -180度から180度)です。左をまたぐと、+3.14 と -3.14 のあいだで数値が切り替わります。

左をまたぐと数値が3.14と−3.14で入れ替わる。rotate_towardは2度、素の補間は長いほうを回る

向きとしては2度の変化でも、数値としては遠く離れています。だから数値のあいだを素直に埋めていく(補間する)と、砲塔は長いほうを回ります。

global_rotation = rotate_toward(global_rotation, offset.angle(), TURN_SPEED * delta)

rotate_toward() は Vector2 のメソッドではなく、どこからでも呼べる組み込みの関数です。短いほうを選び、目標を通り越しません。第3引数は時間ではなく、1回に回せる最大の角度(ラジアン)です。だから TURN_SPEED * delta(delta は前のフレームからの経過秒数)の形で渡します。

割合で寄せる lerp_angle() も短いほうを回りますが、毎フレーム同じ割合で寄せると、残りが小さくなるほど遅くなります。「1秒あたり何ラジアン」で決めたい砲塔には rotate_toward() が向きます。

Sponsored

実践:マウスを追う砲塔から、一定の速さで撃つ

シューティングの自機狙い、タワーディフェンスの砲台、見下ろしARPGの遠距離攻撃——「相手の方を向いて撃つ」はジャンルを問わず出てきます。1本の矢印から距離・向き・角度を取り出して、砲塔を組みます。

① 置くもの

まず弾のシーンを作ります。ルートは Node2D で名前は Bullet、子に Sprite2D(Texture に res://icon.svg、Scale (0.1, 0.1))を置き、res://bullet.tscn として保存します。

# bullet.gd(bullet.tscn のルートに付ける)
extends Node2D

var velocity := Vector2.ZERO

var travelled := 0.0

func _process(delta: float) -> void:
	var step := velocity * delta
	global_position += step
	travelled += step.length()
	if travelled > 1200.0:
		queue_free()

次にステージです。新しいシーンを main.tscn として保存してメインシーンに指定します。

Main (Node2D)
├── Turret (Node2D)          turret.gd   位置 (240, 324)
│   ├── Body (Sprite2D)      icon.svg・Scale (0.4, 0.4)・Rotation 0
│   └── Muzzle (Marker2D)    位置 (48, 0)
├── NearTarget (Sprite2D)    位置 (420, 180)   近い斜め
├── MidTarget (Sprite2D)     位置 (560, 324)   近い真横
└── FarTarget (Sprite2D)     位置 (900, 324)   遠い真横

3つの的は、どれも Texture に res://icon.svg を入れて Scale を (0.2, 0.2) にします。入れないと画面に出ません。的は目印なので、コードからは見ていません。

弾が出る砲口の Muzzle を (48, 0) に置いているのは、砲塔から見て右が正面だからです。Body の絵が上向きなら、Body の Rotation を 90 にしてそろえます。

# turret.gd(Turret に付ける)
extends Node2D

const TURN_SPEED := 4.0          # ラジアン/秒
const BULLET_SPEED := 600.0
const BulletScene := preload("res://bullet.tscn")

@onready var muzzle: Marker2D = $Muzzle

func _process(delta: float) -> void:
	var offset := get_global_mouse_position() - global_position
	if offset.is_zero_approx():
		return                   # 重なっている間は向きを変えない
	global_rotation = rotate_toward(global_rotation, offset.angle(), TURN_SPEED * delta)

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.pressed:
		if event.button_index == MOUSE_BUTTON_LEFT:
			_fire()

func _fire() -> void:
	var bullet := BulletScene.instantiate()
	bullet.velocity = Vector2.RIGHT.rotated(global_rotation) * BULLET_SPEED
	get_tree().current_scene.add_child(bullet)  # 砲塔や戦車ではなく、ステージの子にする
	bullet.global_position = muzzle.global_position

左クリックは、UI が使わなかった入力だけが届く _unhandled_input() で受けて、_fire() を呼んでいます。弾をいまのシーンのルート(Main)に足しているのは、砲塔や、あとで乗せる戦車の子にすると、その回転や移動について回ってしまうからです。位置は add_child() のあとに入れます。

② 角度にして、また矢印へ戻す

turret.gd がやっているのは、矢印と角度の行き来です。

マウスへの矢印を角度にし、rotate_towardで遅れて追い、その角度から弾の矢印へ戻す
  1. offset.angle() で、マウスへの矢印を角度にする
  2. rotate_toward() で、砲塔の global_rotation をその角度へ少しずつ寄せる
  3. 撃つときは Vector2.RIGHT.rotated(global_rotation) で、いまの角度から矢印へ戻す

2番目で遅れが入るので、3番目の矢印はマウスへの矢印とは違う向きになります。弾が砲身の方向へ飛ぶのはこのためです。offset.length() はこの記事の実践では使っていませんが、射程の判定に使えます。

global_rotation を使っているのは、offset がワールド座標の引き算だからです。rotation は親から見た角度なので、砲塔を回転する戦車の子にした瞬間にずれます。

③ 動かして確かめる

F5 で実行します。マウスを動かすと、砲塔が少し遅れて追ってきます。見るのは3つです。

真横320px先の的も660px先の的も、弾は0.5秒後にどちらも300px進んでいて、同じ速さで飛ぶ

1つ目は速さです。近い真横の的(320px 先)と、遠い真横の的(660px 先)を順にクリックします。距離は2倍以上違いますが、弾はどちらも1秒に 600px 進み、同じ速さで飛びます。Vector2.RIGHT が最初から長さ1なので、どこを狙っても BULLET_SPEED のままです。斜めの的でも変わりません。

2つ目は砲身です。マウスを画面の反対側へすばやく飛ばし、砲塔が追いつく前にクリックします。弾はマウスではなく、いまの砲身の方向へ飛びます。

3つ目は左側です。マウスを砲塔の真左で上下に横切らせると、砲塔は短いほうを回ります。どこを狙っても同じ速さで、砲身の向きへ、短いほうを回って撃てます。rotate_toward() を lerp(global_rotation, offset.angle(), 0.1)(2つの数のあいだを割合で取る関数)に置き換えると、左をまたいだところで、長いほうへぐるりと大きく回ります。

最後に、_fire() の1行を次に差し替えてみてください。

	bullet.velocity = (get_global_mouse_position() - global_position) * 1.5

この行は狙いも砲身からマウスへ変えているので、砲塔の遅れは消えます。見るのは速さだけです。近い真横の的へ撃った弾は1秒に 320 × 1.5 = 480px、遠い真横の的へ撃った弾は 660 × 1.5 = 990px と、2倍以上の速さで飛びます。正規化は、この差をなくすためにあります。

ポイントは2つです。

  • 速さを掛ける相手を、長さ1の矢印にする: 引き算から作るなら normalized() か direction_to()、角度から作るなら Vector2.RIGHT.rotated() です。この記事の実践は後者で、Vector2.RIGHT が最初から長さ1です
  • 向きは角度で持ち、使うときに矢印へ戻す: 砲塔の global_rotation が正で、弾はそこから Vector2.RIGHT.rotated() で作ります。マウスから直接作ると、追従の遅れが弾に反映されません

うまくいかないときは

症状原因直し方
global_position.angle_to(target) で狙うと、向きがずれるangle_to() は2本の矢印のあいだの角度で、点への向きではないglobal_position.angle_to_point(target) か (target - global_position).angle() を使う
敵が目標に着いたところで、小刻みに震える残りの距離より1フレームの移動量が大きく、通り過ぎては戻っている残りが speed * delta より短くなったら、目標の位置に置いて止める。global_position.move_toward(target, speed * delta) なら通り越さない
look_at(target - global_position) と書くと、砲塔がマウスではなく、ワールドの原点のほうを向くlook_at() が受け取るのは点(ワールドの座標)で、矢印ではないlook_at(target) と点を渡す。矢印から向けるなら global_rotation = offset.angle()
砲塔を look_at() したら、HPバーや名前まで一緒に回る回したノードの子は、一緒に回るHPバーや名前は、回すノード(砲塔)の外に出して、回さない親の子に置く

おまけ:先に知っておくと良いこと

  • 斜めが速くなるのは別の症状: 上下と左右の入力を足して (1, -1) にすると、長さが 1.41 になります。こちらも normalized() で直りますが、原因は距離ではなく軸を2本足したことです
  • 距離の比較だけなら平方根を省ける: distance_squared_to() は距離の2乗を返します。どちらが近いかを比べるだけなら、こちらで足ります
  • CharacterBody2D なら velocity に入れる: この記事の弾は global_position を直接動かしています。体を持つものは velocity に同じ矢印を入れて move_and_slide() に渡します。delta は中で掛けられるので、自分では掛けません。使い分けは move_and_slideとmove_toward にあります
  • 前にいるかどうかの判定: 向きの矢印どうしの dot() を使います。正なら前、負なら後ろです。敵の視界を作る例は 視界システム にあります

まとめ

  • Vector2 は2つの数: 位置にも速度にも向きにも、同じ形を使う
  • 引き算で矢印、長さ1で向き: 向きは 目標 - 自分。normalized() で長さを1にすると、速さから距離が抜ける
  • 近道の関数: direction_to() は引き算と正規化、distance_to() は引き算と長さ
  • 基準は右(+X): angle() も look_at() も同じで、絵のほうを回してそろえる。角度から矢印へは Vector2.RIGHT.rotated() で戻す
  • 左をまたぐ回転は任せる: rotate_toward() を使う

速さを掛ける前に長さを1にしていないと、遠い相手ほど速く動きます。いま書いている「相手へ向かう」コードは、長さを1にしてから速さを掛けていますか。

次に読む学習パスの次の記事
物理・移動

2D物理ボディの使い分け

この記事を読む
Godot このセクションのノート104