Godot Vector2 Basics: A Mouse-Tracking Turret with Constant Bullet Speed

Created: 2026-09-22Last updated: 2026-10-10

Build a turret that tracks the mouse while getting distance, direction, and angle out of Vector2 subtraction, and keep bullet speed constant.

A turret that faces the mouse, or a bullet that flies straight at an enemy, is built from subtracting two positions. From the arrow (Vector2) that subtraction gives you, you pull out distance, direction, and angle, and use each where it fits.

Pull out the wrong one, and the farther away the enemy you aim at, the faster the bullets fly, or the turret swings all the way around every time it passes the left.

This article builds a turret that tracks the mouse while covering how to make the arrow, shrink it to length 1, and turn it into an angle.

Pulling three things, distance, direction, and angle, out of one arrow stretching from the turret to the mouse

What You'll Learn

  • A subtraction gives you an arrow. The direction is target - self
  • Shrink it to length 1 and distance drops out of the speed
  • Both angle() and look_at() use right (+X) as the reference
  • Leave rotation across the left to rotate_toward()

Tested with: Godot 4.7

Sponsored

Vector2 (position, velocity, direction)

Vector2 is a type that just holds two numbers, x and y. The same type is used for three things with different meanings.

  • Position: where it is. global_position is this
  • Velocity: how far it moves in one second. velocity is this
  • Direction: which way it points. One with a length of 1

Because they're the same type, you can add and subtract them, but the meanings don't mix. Adding a position to a position doesn't give a meaningful place. Meaning appears when you subtract a position from a position.

Which way to subtract, and kinds of coordinates

Subtract two points and you get an arrow from one to the other.

var offset := target.global_position - global_position
target − self is the arrow from self to the target, and adding it to self reaches the target. Subtracting the other way points in the opposite direction

Reverse the order and the arrow points the opposite way. target - self is the arrow that, added to yourself, reaches the target. When in doubt, remember "subtract the starting point from the destination."

There's one more thing to line up before subtracting: the two points need to be the same kind of coordinate.

position is the position relative to the parent, global_position is the world, and an event's position is screen pixels
  • position: the position as seen from the parent. When the parent moves, the reference moves too
  • global_position: the position in the world. The same reference wherever the node hangs
  • InputEventMouse's position: pixels on the screen (viewport). It doesn't include the camera's position or zoom
  • get_global_mouse_position(): the mouse's position in the world. Includes the camera

When calculating aim, keep to the pair of global_position and get_global_mouse_position(), and nothing drifts later even if you make the turret a child of a tank or move the camera.

A direction of length 1 (normalized)

The arrow from a subtraction carries both direction and distance. Before using it as a velocity, take the distance out.

var offset := target.global_position - global_position
var dir := offset.normalized()      # length 1. Only the direction remains
var dist := offset.length()         # pull out just the distance
Arrows straight from subtraction have all different lengths, but normalized() shrinks them to length 1 so only the direction remains

normalized() makes the arrow's length 1. The direction doesn't change. Multiply this by the speed and you get the same velocity regardless of distance.

What happens when you forget to take it out is a common symptom.

velocity = (target.global_position - global_position) * speed    # distance goes into the speed
Forgetting to normalize makes bullets fly slowly at near targets and fast at far ones, with speed equal to distance × speed

This expression's length is "distance × speed." At a target 100px away it flies slowly, and at a target 800px away it flies eight times as fast. Whether it's diagonal doesn't matter. The farther the distance, the faster it goes.

There are also shortcuts that combine subtraction and normalizing. a.direction_to(b) is the same as (b - a).normalized(), and a.distance_to(b) is the same as (b - a).length().

In other engines: Godot's direction_to() and distance_to() correspond to Unity's (b - a).normalized and Vector2.Distance(a, b).

Only be careful when the length is 0. When the mouse overlaps the turret's center, the subtraction is (0, 0), and normalized() returns (0, 0). There's no error, but there's no direction either. Filter it out with is_zero_approx(), which checks whether something is "nearly 0" while tolerating floating-point error, and use the previous direction for that frame.

Sponsored

Angles (angle) and the art's front

Getting an angle from an arrow is angle(). Turning the node toward the point you pass in (world coordinates) is look_at().

global_rotation = offset.angle()     # the arrow's tilt in radians
look_at(target.global_position)      # turn the node toward the target
Both angle and look_at use right as the reference. Art drawn facing up is lined up by setting the child's Rotation to 90

Both use right (+X) as the reference. When rotation is 0, the node is considered to be facing right.

So if you use a sprite drawn facing up as-is, it's 90 degrees off. The fix is to turn the art instead. Make the sprite a child and set its Rotation to 90, and it lines up facing right from the parent's point of view. The parent's code doesn't need to change.

Angles are in radians. The Inspector's Rotation field shows degrees, but rotation in code is radians, and so is what angle() returns. One full turn is about 6.28, and 1 radian is about 57 degrees. Convert to degrees with rad_to_deg() and back to radians with deg_to_rad() (3D rotation is covered in Node3D Basics).

One more thing: on a 2D screen, y increases downward. Angles also turn clockwise (toward the bottom of the screen) for positive values. That's why the angle() of an arrow pointing up and to the right is negative.

From an angle to an arrow (rotated)

After holding a direction as an angle, you'll want to turn it back into an arrow. Firing bullets in the turret's direction is exactly that.

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

Rotate a rightward arrow of length 1 (a unit vector) by rotation, and you get the barrel's current direction. Vector2.from_angle(global_rotation) gives the same result.

This is the round trip. Make an angle from the arrow to the mouse, then go from that angle back to an arrow. Use the arrow you got back, and the bullet flies toward "the current barrel," not "the mouse." While the turret is lagging behind, these two point in different directions.

Turning across the left (rotate_toward)

angle() returns values from -π to π (about -3.14 to 3.14, or -180 to 180 degrees). Crossing the left switches the number between +3.14 and -3.14.

Crossing the left swaps the number between 3.14 and −3.14. rotate_toward turns 2 degrees, while plain interpolation goes the long way

As a direction it's a 2-degree change, but as numbers they're far apart. So if you simply fill in between the numbers (interpolate), the turret goes around the long way.

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

rotate_toward() isn't a Vector2 method. It's a built-in function you can call from anywhere. It picks the short way and doesn't overshoot the target. The third argument isn't time. It's the largest angle (in radians) it can turn in one call. That's why you pass it in the form TURN_SPEED * delta (delta is the seconds since the previous frame).

lerp_angle(), which moves closer by a fraction, also takes the short way, but moving by the same fraction every frame gets slower as the remaining gap shrinks. For a turret you want to set as "so many radians per second," rotate_toward() is the better fit.

Sponsored

Hands-On: Fire at a Constant Speed From a Turret That Tracks the Mouse

Aimed shots in a shooter, a tower in tower defense, ranged attacks in a top-down ARPG—"face the target and fire" shows up in every genre. You'll build a turret by pulling distance, direction, and angle out of one arrow.

① What to place

First, make the bullet scene. The root is a Node2D named Bullet. Put a Sprite2D under it (res://icon.svg as its Texture, Scale (0.1, 0.1)), and save it as res://bullet.tscn.

# bullet.gd (attach to the root of 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()

Next, the stage. Save a new scene as main.tscn and set it as the main scene.

Main (Node2D)
├── Turret (Node2D)          turret.gd   position (240, 324)
│   ├── Body (Sprite2D)      icon.svg, Scale (0.4, 0.4), Rotation 0
│   └── Muzzle (Marker2D)    position (48, 0)
├── NearTarget (Sprite2D)    position (420, 180)   near, diagonal
├── MidTarget (Sprite2D)     position (560, 324)   near, straight across
└── FarTarget (Sprite2D)     position (900, 324)   far, straight across

For all three targets, put res://icon.svg in Texture and set Scale to (0.2, 0.2). Without it, they don't appear on screen. The targets are just markers, so the code doesn't look at them.

Muzzle, where bullets come out, is placed at (48, 0) because right is the front as seen from the turret. If Body's art faces up, set Body's Rotation to 90 to line it up.

# turret.gd (attach to Turret)
extends Node2D

const TURN_SPEED := 4.0          # radians/second
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                   # don't change direction while overlapping
	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)  # make it a child of the stage, not the turret or a tank
	bullet.global_position = muzzle.global_position

The left click is received in _unhandled_input(), where only input the UI didn't use arrives, and it calls _fire(). Bullets are added to the root of the current scene (Main) because as children of the turret, or of a tank you mount it on later, they'd follow along with its rotation and movement. The position is set after add_child().

② Turn it into an angle, then back into an arrow

What turret.gd does is go back and forth between arrows and angles.

Turn the arrow to the mouse into an angle, follow it with a lag using rotate_toward, and go from that angle back to the bullet's arrow
  1. offset.angle() turns the arrow to the mouse into an angle
  2. rotate_toward() moves the turret's global_rotation toward that angle little by little
  3. When firing, Vector2.RIGHT.rotated(global_rotation) goes from the current angle back to an arrow

Step 2 introduces a lag, so the arrow in step 3 points differently from the arrow to the mouse. That's why bullets fly in the barrel's direction. offset.length() isn't used in this article's hands-on, but you could use it to check range.

global_rotation is used because offset is a subtraction of world coordinates. rotation is the angle as seen from the parent, so it drifts the moment you make the turret a child of a rotating tank.

③ Run it and check

Run with F5. Move the mouse and the turret follows with a slight lag. Check three things.

Whether the target is 320px or 660px away straight across, after 0.5 seconds both bullets have traveled 300px, flying at the same speed

The first is speed. Click the near straight-across target (320px away), then the far straight-across target (660px away). The distances differ by more than double, but both bullets travel 600px per second, flying at the same speed. Vector2.RIGHT has length 1 from the start, so it stays BULLET_SPEED wherever you aim. Diagonal targets are the same.

The second is the barrel. Flick the mouse quickly to the opposite side of the screen and click before the turret catches up. The bullet flies in the barrel's current direction, not toward the mouse.

The third is the left side. Sweep the mouse up and down across the area directly left of the turret, and the turret turns the short way. Wherever you aim, it fires at the same speed, in the barrel's direction, turning the short way. Replace rotate_toward() with lerp(global_rotation, offset.angle(), 0.1) (a function that takes a point between two numbers by a fraction), and where it crosses the left, it swings all the way around the long way.

Finally, swap one line in _fire() for this:

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

This line also changes the aim from the barrel to the mouse, so the turret's lag disappears. Look only at the speed. A bullet fired at the near straight-across target flies at 320 × 1.5 = 480px per second, and one fired at the far straight-across target at 660 × 1.5 = 990px, more than twice as fast. Normalizing exists to remove this difference.

Two points.

  • Multiply the speed by an arrow of length 1: built from a subtraction, that's normalized() or direction_to(), and built from an angle, it's Vector2.RIGHT.rotated(). This article's hands-on uses the latter, where Vector2.RIGHT has length 1 from the start
  • Hold the direction as an angle, and turn it back into an arrow when you use it: the turret's global_rotation is the source of truth, and bullets are built from it with Vector2.RIGHT.rotated(). Build them straight from the mouse, and the tracking lag isn't reflected in the bullets

Troubleshooting

SymptomCauseFix
Aiming with global_position.angle_to(target) gives the wrong directionangle_to() is the angle between two arrows, not the direction to a pointUse global_position.angle_to_point(target) or (target - global_position).angle()
An enemy jitters in place when it reaches its targetOne frame's movement is larger than the remaining distance, so it overshoots and comes backWhen the remaining distance is shorter than speed * delta, place it at the target and stop. global_position.move_toward(target, speed * delta) doesn't overshoot
Writing look_at(target - global_position) makes the turret face the world origin instead of the mouselook_at() takes a point (world coordinates), not an arrowPass the point, as in look_at(target). To turn from an arrow, use global_rotation = offset.angle()
After using look_at() on the turret, the HP bar and name turn along with itChildren of the node you turn turn along with itMove the HP bar and name out of the node you turn (the turret), and place them under a parent that doesn't turn

Bonus: Good to Know First

  • Diagonals getting faster is a separate symptom: add up-down and left-right input to get (1, -1), and the length is 1.41. This is also fixed with normalized(), but the cause is adding two axes, not distance
  • For comparing distances only, you can skip the square root: distance_squared_to() returns the distance squared. If you're only comparing which is closer, this is enough
  • For a CharacterBody2D, put it in velocity: this article's bullets move global_position directly. For things with a body, put the same arrow in velocity and hand it to move_and_slide(). delta is multiplied inside, so don't multiply it yourself. When to use which is in move_and_slide and move_toward
  • Checking whether something is in front: use the dot() of two direction arrows. Positive means in front, negative means behind. An example of building an enemy's field of view is in Vision System

Summary

  • Vector2 is two numbers: the same shape is used for position, velocity, and direction
  • Subtraction gives an arrow, length 1 gives a direction: the direction is target - self. Shrink it to length 1 with normalized() and distance drops out of the speed
  • Shortcut functions: direction_to() is subtraction and normalizing, distance_to() is subtraction and length
  • The reference is right (+X): the same for angle() and look_at(), so turn the art to line it up. Go from an angle back to an arrow with Vector2.RIGHT.rotated()
  • Leave rotation across the left to it: use rotate_toward()

If you don't shrink the length to 1 before multiplying by speed, the farther the target, the faster things move. Does the "move toward the target" code you're writing now shrink the length to 1 before multiplying by speed?

Read nextNext in the learning path
Physics

Choosing the Right 2D Physics Body

Read this note
Godot Notes in this section104