Can you write this from memory?
Create a Vector2 called spawn_position with x=100 and y=200 for placing an enemy.
If your GDScript game has movement, aiming, knockback, or chasing, you're doing vector math — even if it doesn't feel like "math." These concepts also power projectiles and enemy AI.
This page is your Vector2 toolbox: the exact operations you'll type constantly in Godot, from basic directions to facing checks, ricochet physics, and performance tricks.
For a printable version, see the GDScript vectors cheat sheet. Vector math underpins collision responses like bounce and knockback, and projectile systems that need aiming and spread patterns.
| Method | Use For |
|---|---|
a.direction_to(b) | Unit vector A→B |
v.normalized() | Make length 1 |
v.length() / length_squared() | Get magnitude (squared is faster) |
a.distance_to(b) / distance_squared_to(b) | Distance between points (squared for perf) |
v.dot(other) | Facing check: >0 same dir, 0 perpendicular, <0 opposite |
v.cross(other) | Left/right check: >0 right, <0 left (Y-down coords) |
v.angle() / angle_to(other) | Get angle in radians |
Vector2.from_angle(rad) | Create unit vector from angle |
v.rotated(rad) | Rotate vector by angle |
v.limit_length(max) | Cap magnitude |
v.move_toward(target, step) | Fixed-step approach |
v.lerp(target, weight) | Fractional approach |
v.bounce(normal) | Physics bounce (ricochet) |
v.reflect(normal) | Mathematical reflection |
v.slide(normal) | Slide along surface |
A (player) • ---------> • B (enemy)
direction_to()
Aim at Mouse
func shoot_at_mouse() -> void:
var mouse_pos := get_global_mouse_position()
var direction := global_position.direction_to(mouse_pos)
spawn_bullet(direction)
Knockback Away From Hit
func take_hit(source_position: Vector2, knockback_force: float) -> void:
var knockback_dir := source_position.direction_to(global_position)
velocity += knockback_dir * knockback_force
Chase with Stop Radius (Optimized)
Use distance_squared_to() instead of distance_to() when comparing against a threshold—avoids a square root calculation.
var stop_radius_sq := stop_radius * stop_radius # Precompute once
func chase_player(delta: float) -> void:
if global_position.distance_squared_to(player.global_position) > stop_radius_sq:
var dir := global_position.direction_to(player.global_position)
velocity = dir * chase_speed
else:
velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
move_and_slide()
The dot product tells you how aligned two vectors are:
- 1.0 = same direction
- 0.0 = perpendicular (90°)
- -1.0 = opposite direction
Is Target In Front?
func is_target_in_front(target_pos: Vector2) -> bool:
var forward := Vector2.RIGHT.rotated(global_rotation)
var to_target := global_position.direction_to(target_pos)
return forward.dot(to_target) > 0.0
FOV Cone Check
func is_in_fov(target_pos: Vector2, fov_dot_threshold: float = 0.5) -> bool:
# Threshold = cos(half_angle): 0.5 = 120° total cone, 0.866 = 60° total cone
var forward := Vector2.RIGHT.rotated(global_rotation)
var to_target := global_position.direction_to(target_pos)
return forward.dot(to_target) > fov_dot_threshold
AI Detection Pattern
func can_attack_target(target: Node2D) -> bool:
var to_target := global_position.direction_to(target.global_position)
var forward := Vector2.RIGHT.rotated(global_rotation)
# Is target roughly in front? (within ~60° cone)
if forward.dot(to_target) < 0.5:
return false
# Is target in range?
var dist_sq := global_position.distance_squared_to(target.global_position)
if dist_sq > attack_range * attack_range:
return false
return true
In 2D, cross() returns a scalar that tells you which side something is on. In Godot's Y-down coordinate system (where clockwise rotation is positive):
- Positive = target is to the RIGHT (clockwise)
- Negative = target is to the LEFT (counter-clockwise)
Turn Toward Target
func turn_toward(target_pos: Vector2, turn_speed: float, delta: float) -> void:
var forward := Vector2.RIGHT.rotated(global_rotation)
var to_target := global_position.direction_to(target_pos)
var side := forward.cross(to_target)
if side > 0.01:
global_rotation += turn_speed * delta # Turn right (clockwise)
elif side < -0.01:
global_rotation -= turn_speed * delta # Turn left (counter-clockwise)
# else: already facing target
These Vector2 methods handle collision responses—pass the collision normal (the surface's outward direction).
| Method | Result |
|---|---|
velocity.bounce(normal) | Physics bounce (ball off wall) |
velocity.reflect(normal) | Mathematical mirror (opposite of bounce) |
velocity.slide(normal) | Slide along surface |
Ricochet Bullet
func _on_body_entered(body: Node) -> void:
if body.is_in_group(&"wall"):
# Get collision normal from the physics system
# For Area2D, you may need to calculate this or use RayCast2D
velocity = velocity.bounce(collision_normal)
Slide Along Wall
# After CharacterBody2D collision
for i in get_slide_collision_count():
var collision := get_slide_collision(i)
var normal := collision.get_normal()
# Get the component of velocity parallel to the wall
var slide_velocity := velocity.slide(normal)
bounce vs reflect: In Godot,
bounce()is what you want for physics (ball bouncing off wall).reflect()is the mathematical mirror which goes the "opposite" direction—rarely what you want for gameplay.
Create Direction from Angle
Vector2.from_angle() creates a unit vector pointing in a direction. Useful for spreads and radial patterns.
# Shoot in 8 directions (radial burst)
func shoot_radial(directions: int = 8) -> void:
var angle_step := TAU / directions # TAU = 2π
for i in directions:
var dir := Vector2.from_angle(angle_step * i)
spawn_bullet(dir)
Random Spread
func shoot_with_spread(base_angle: float, spread: float) -> void:
var random_offset := randf_range(-spread / 2.0, spread / 2.0)
var dir := Vector2.from_angle(base_angle + random_offset)
spawn_bullet(dir)
Zero Vectors Are Safe
In GDScript, Vector2.ZERO.normalized() returns (0, 0)—not NaN. NaNs only happen with manual division:
var safe := Vector2.ZERO.normalized() # Returns (0, 0) ✓
var v := Vector2.ZERO
var bad := v / v.length() # NaN! Division by zero
Near-Zero Vectors Are Tricky
Floating-point precision can cause issues with almost-zero vectors. Use is_zero_approx() for safety:
func safe_direction(v: Vector2) -> Vector2:
if v.is_zero_approx():
return Vector2.ZERO
return v.normalized()
When Direction Matters
Guard zero/near-zero input when you need a valid direction:
func get_move_direction() -> Vector2:
var input := Input.get_vector("left", "right", "up", "down")
if input.is_zero_approx():
return Vector2.ZERO
return input.normalized()
move_toward(target, step) | lerp(current, target, weight) |
|---|---|
| Moves by a fixed amount | Moves by a fraction of remaining distance |
| Reaches target exactly | Approaches but may never reach |
| Good for: constant speed | Good for: smooth easing |
# Constant approach (same speed throughout)
position = position.move_toward(target, speed * delta)
# Eased approach (fast start, slow finish)
position = position.lerp(target, 0.1) # 10% of remaining distance per frame
Safe Lerp Weights
The lerp weight should be 0.0–1.0. Multiplying by delta can exceed 1.0 at low framerates:
# Unsafe: At 20 FPS, 5.0 * 0.05 = 0.25 (fine)
# At 10 FPS, 5.0 * 0.1 = 0.5 (still ok but jumpy)
# At <5 FPS, can exceed 1.0 (snaps instantly)
position = position.lerp(target, 5.0 * delta)
# Safe: Clamp the weight
var weight := clampf(5.0 * delta, 0.0, 1.0)
position = position.lerp(target, weight)
When to Use GDScript Vector Math
- Enemy AI: facing checks (dot), turning toward targets (cross), range detection (distance_squared_to)
- Combat: knockback directions, ricochet bullets (bounce), FOV-based attacks
- Projectiles: aiming, spread patterns (from_angle), rotation
- Performance: replacing distance_to with distance_squared_to in hot paths
Check Your Understanding: GDScript Vector Math
Check Your Understanding: What's the difference between (target - origin).normalized() and origin.direction_to(target)?
They're equivalent for direction vectors: both return a normalized vector pointing from origin to target. direction_to() is the more readable helper; subtract+normalized is more explicit and lets you reuse the raw delta vector when you also need distance (delta.length()).
What You'll Practice: GDScript Vector Math
Common GDScript Vector Math Pitfalls
- Manual v / v.length() on zero vectors—causes NaN. Use v.normalized() or guard with is_zero_approx()
- Using reflect() when you meant bounce()—reflect is mathematical mirror, bounce is physics ricochet
- Lerp weight exceeding 1.0 at low FPS—clamp with clampf(speed * delta, 0.0, 1.0)
- Using distance_to() in hot loops—distance_squared_to() is faster when you only need comparisons
- Mixing global_position and position—parent transforms change meaning; be consistent
GDScript Vector Math FAQ
How do I check if an enemy is facing the player?
Use dot product: var forward = Vector2.RIGHT.rotated(rotation); var to_player = global_position.direction_to(player_pos); if forward.dot(to_player) > 0: # facing player. Threshold is cos(half_angle): 0.5 = 120 degree cone, 0.866 = 60 degree cone.
How do I make an enemy turn toward the player?
Use cross product for side detection: var side = forward.cross(to_target). In Godot Y-down coords: positive = target is right (turn clockwise), negative = left (turn counter-clockwise).
Why use distance_squared_to instead of distance_to?
distance_squared_to() skips the square root calculation, making it faster. When comparing distances (is X closer than Y?), squared values work the same. Just compare against threshold² instead of threshold.
What's the difference between bounce and reflect?
bounce() is for physics—ball bouncing off wall. reflect() is the mathematical mirror (opposite direction). For gameplay ricochet, you almost always want bounce().
How do I create a vector from an angle?
Vector2.from_angle(radians) returns a unit vector. For degrees, use Vector2.from_angle(deg_to_rad(degrees)). Useful for bullet spreads and radial patterns.
Why do I get NaNs when normalizing?
Vector2.ZERO.normalized() is safe (returns zero). NaNs come from manual division: v / v.length() when v is zero. Use v.normalized() or guard with is_zero_approx().
What's is_zero_approx() for?
Checks if a vector is approximately zero, handling floating-point precision. Use it instead of == Vector2.ZERO when you need to guard against near-zero vectors that could cause incorrect results.
direction_to vs (b - a).normalized — any difference?
Functionally identical. direction_to() is more readable. The subtract form is useful when you also need the raw delta for distance calculations.
move_toward vs lerp — which one for movement?
move_toward for constant-speed movement (reaches target exactly). lerp for eased movement (smooth but may never reach target). Use move_toward for gameplay; lerp for cameras/UI.
My lerp is snapping at low FPS — why?
If you do lerp(pos, target, 5.0 * delta), the weight can exceed 1.0 at low framerates. Clamp it: var w = clampf(5.0 * delta, 0.0, 1.0).
How do I clamp speed but keep direction?
velocity = velocity.limit_length(max_speed). Preserves direction while capping magnitude.
GDScript Vector Math Syntax Quick Reference
func is_in_fov(target_pos: Vector2) -> bool:
var forward := Vector2.RIGHT.rotated(global_rotation)
var to_target := global_position.direction_to(target_pos)
return forward.dot(to_target) > 0.5 # 120 degree conevar side := forward.cross(to_target)
if side > 0.01: # Target is right
global_rotation += turn_speed * delta
elif side < -0.01: # Target is left
global_rotation -= turn_speed * deltavar range_sq := attack_range * attack_range
if global_position.distance_squared_to(target_pos) < range_sq:
attack()velocity = velocity.bounce(collision_normal)for i in 8:
var dir := Vector2.from_angle(TAU / 8 * i)
spawn_bullet(dir)func safe_direction(v: Vector2) -> Vector2:
if v.is_zero_approx():
return Vector2.ZERO
return v.normalized()GDScript Vector Math Sample Exercises
Move an enemy each frame by adding direction, speed, and delta to the current position. (Assume direction, speed, and delta exist.)
position += direction * speed * deltaWhat does this code print?
What does this code print?
+ 24 more exercises