Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. GDScript
  3. GDScript Vector Math Practice
GDScript27 exercises

GDScript Vector Math Practice

Vector2 recipes for Godot 4: dot() for FOV, distance_squared_to() for performance, bounce()/reflect()/slide() for collisions, from_angle() for spreads. The complete toolbox.

Cheat SheetCommon ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Create a Vector2 called spawn_position with x=100 and y=200 for placing an enemy.

On this page
  1. 1Quick Cheat Sheet
  2. 2Directions & Distance
  3. Aim at Mouse
  4. Knockback Away From Hit
  5. Chase with Stop Radius (Optimized)
  6. 3Dot Product: Facing & FOV Checks
  7. Is Target In Front?
  8. FOV Cone Check
  9. AI Detection Pattern
  10. 4Cross Product: Left or Right?
  11. Turn Toward Target
  12. 5Bounce, Reflect, Slide
  13. Ricochet Bullet
  14. Slide Along Wall
  15. 6Angles & from_angle()
  16. Create Direction from Angle
  17. Random Spread
  18. 7Normalization Safety
  19. Zero Vectors Are Safe
  20. Near-Zero Vectors Are Tricky
  21. When Direction Matters
  22. 8move_toward vs lerp
  23. Safe Lerp Weights
  24. 9References
Quick Cheat SheetDirections & DistanceDot Product: Facing & FOV ChecksCross Product: Left or Right?Bounce, Reflect, SlideAngles & from_angle()Normalization Safetymove_toward vs lerpReferences

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.

Related GDScript Topics
GDScript MovementGDScript ProjectilesGDScript CollisionsGDScript Randomness

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.

MethodUse 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

Ready to practice?

Start practicing GDScript Vector Math with spaced repetition

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()

dot() returns 1.0 for same direction, 0.0 for perpendicular, -1.0 for opposite. The threshold is cos(half_angle): use 0.5 for a 120° cone, 0.866 for 60°.

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).

MethodResult
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)

Vector2.ZERO.normalized() is safe and returns (0, 0). NaNs only come from manual v / v.length() division—always use normalized() or guard with is_zero_approx().

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 for constant-speed movement that reaches the target exactly. lerp for smooth easing that approaches but may never arrive. Clamp lerp weights to prevent snapping at low FPS.

move_toward(target, step)lerp(current, target, weight)
Moves by a fixed amountMoves by a fraction of remaining distance
Reaches target exactlyApproaches but may never reach
Good for: constant speedGood 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)

  • Vector math tutorial (Godot docs)
  • Vector2 class reference
  • Interpolation tutorial

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

Prompt

Check Your Understanding: What's the difference between (target - origin).normalized() and origin.direction_to(target)?

What a strong answer looks like

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

Compute directions with direction_to() and normalize safely with is_zero_approx() guardUse dot() to check facing direction and implement FOV conesUse cross() to determine left/right for steering and turningUse distance_squared_to() for fast range checks (avoid square root)Use bounce()/reflect()/slide() for collision responsesUse Vector2.from_angle() for radial patterns and bullet spreadsClamp velocity with limit_length() and lerp weights with clampf()

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

FOV check with dot()
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 cone
Turn toward with cross()
var 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 * delta
Fast range check
var range_sq := attack_range * attack_range
if global_position.distance_squared_to(target_pos) < range_sq:
	attack()
Ricochet with bounce()
velocity = velocity.bounce(collision_normal)
Radial burst with from_angle()
for i in 8:
	var dir := Vector2.from_angle(TAU / 8 * i)
	spawn_bullet(dir)
Safe direction
func safe_direction(v: Vector2) -> Vector2:
	if v.is_zero_approx():
		return Vector2.ZERO
	return v.normalized()

GDScript Vector Math Sample Exercises

Example 1Difficulty: 2/5

Move an enemy each frame by adding direction, speed, and delta to the current position. (Assume direction, speed, and delta exist.)

position += direction * speed * delta
Example 2Difficulty: 2/5

What does this code print?

Example 3Difficulty: 2/5

What does this code print?

+ 24 more exercises

Quick Reference
GDScript Vector Math Cheat Sheet →

Copy-ready syntax examples for quick lookup

Practice in Build a Game

Roguelike: Part 2: Every Turn Has TeethBuild a Game

Further Reading

  • GDScript Dictionary map() and map_in_place12 min read
  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Start practicing GDScript Vector Math

Free daily exercises with spaced repetition. No credit card required.

← Back to GDScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.