Game Math in Godot 4.3+
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Related skills: player-controller for movement physics, ai-navigation for pathfinding math, camera-system for camera interpolation, tween-animation for easing curves, physics-system for collision math.
1. Vector Operations
Essential Vector Methods
| Method | Returns | Description |
|---|---|---|
length() | float | Magnitude of the vector |
length_squared() | float | Squared magnitude (faster, skip sqrt) |
normalized() | Vector | Unit vector (length 1) in the same direction |
distance_to(b) | float | Distance between two points |
distance_squared_to(b) | float | Squared distance (faster for comparisons) |
direction_to(b) | Vector | Normalized direction from this to b |
angle_to(b) | float | Angle in radians between two vectors |
angle_to_point(b) | float | Angle from this point to b (2D) |
dot(b) | float | Dot product |
cross(b) | float/Vector3 | Cross product (2D returns float, 3D returns vector) |
rotated(angle) | Vector2 | Rotated by radians (2D) |
move_toward(to, delta) | Vector | Move toward target by at most delta |
clamp(min, max) | Vector | Clamp each component |
snapped(step) | Vector | Snap to grid |
reflect(normal) | Vector | Reflect off a surface |
bounce(normal) | Vector | Bounce off a surface (inverted reflect) |
slide(normal) | Vector | Slide along a surface |
Direction and Distance
gdscript# Get direction from A to B (normalized) var dir: Vector2 = global_position.direction_to(target.global_position) # Get distance var dist: float = global_position.distance_to(target.global_position) # Use squared distance for comparisons (faster — avoids sqrt) if global_position.distance_squared_to(target.global_position) < detection_range * detection_range: chase_target()
csharpVector2 dir = GlobalPosition.DirectionTo(target.GlobalPosition); float dist = GlobalPosition.DistanceTo(target.GlobalPosition); if (GlobalPosition.DistanceSquaredTo(target.GlobalPosition) < detectionRange * detectionRange) ChaseTarget();
Dot Product
The dot product tells you how aligned two vectors are.
gdscript# Is the target in front of us? (dot > 0 = in front, < 0 = behind) var forward: Vector2 = Vector2.RIGHT.rotated(rotation) var to_target: Vector2 = global_position.direction_to(target.global_position) var dot: float = forward.dot(to_target) if dot > 0.7: # roughly within ~45° cone print("Target is ahead") elif dot < -0.7: print("Target is behind")
csharpVector2 forward = Vector2.Right.Rotated(Rotation); Vector2 toTarget = GlobalPosition.DirectionTo(target.GlobalPosition); float dot = forward.Dot(toTarget); if (dot > 0.7f) GD.Print("Target is ahead");
Cross Product (3D)
The cross product gives a vector perpendicular to two input vectors.
gdscript# Get the surface normal from two edge vectors var edge1: Vector3 = vertex_b - vertex_a var edge2: Vector3 = vertex_c - vertex_a var normal: Vector3 = edge1.cross(edge2).normalized()
csharpVector3 edge1 = vertexB - vertexA; Vector3 edge2 = vertexC - vertexA; Vector3 normal = edge1.Cross(edge2).Normalized();
2. Transforms
Transform2D
A 2D transform holds position, rotation, and scale.
gdscript# Get the global transform var xform: Transform2D = global_transform # Convert between local and global space var local_point: Vector2 = to_local(global_point) var world_point: Vector2 = to_global(local_point) # Apply transform to a point var transformed: Vector2 = xform * Vector2(10, 0) # point in local space → global # Inverse transform var local: Vector2 = xform.affine_inverse() * global_point
csharpTransform2D xform = GlobalTransform; Vector2 localPoint = ToLocal(globalPoint); Vector2 worldPoint = ToGlobal(localPoint); Vector2 transformed = xform * new Vector2(10, 0); Vector2 local = xform.AffineInverse() * globalPoint;
Transform3D & Basis
gdscript# Basis holds rotation and scale as 3 column vectors var basis: Basis = global_transform.basis # Forward direction (looking along -Z in Godot) var forward: Vector3 = -basis.z var right: Vector3 = basis.x var up: Vector3 = basis.y # Look at a target look_at(target.global_position, Vector3.UP) # Rotate around an axis rotate_y(deg_to_rad(90.0)) rotate_object_local(Vector3.UP, deg_to_rad(45.0)) # Interpolate between two transforms (smooth transition) var a: Transform3D = $Start.global_transform var b: Transform3D = $End.global_transform global_transform = a.interpolate_with(b, 0.5) # halfway
csharpBasis basis = GlobalTransform.Basis; Vector3 forward = -basis.Z; Vector3 right = basis.X; Vector3 up = basis.Y; LookAt(target.GlobalPosition, Vector3.Up); RotateY(Mathf.DegToRad(90.0f)); Transform3D a = GetNode<Node3D>("Start").GlobalTransform; Transform3D b = GetNode<Node3D>("End").GlobalTransform; GlobalTransform = a.InterpolateWith(b, 0.5f);
is_orthonormal() (Godot 4.7+)
Basis.is_orthonormal() (const) returns true if the basis is orthogonal (axes perpendicular to each other) and normalized (every axis has length 1.0) — especially useful during physics calculations. It complements orthonormalized(): check first, and only re-orthonormalize when accumulated floating-point drift has denormalized the basis.
gdscriptif not global_transform.basis.is_orthonormal(): global_transform.basis = global_transform.basis.orthonormalized()
csharpif (!GlobalTransform.Basis.IsOrthonormal()) { GlobalTransform = new Transform3D( GlobalTransform.Basis.Orthonormalized(), GlobalPosition); }
3. Interpolation
lerp — Linear Interpolation
gdscript# Interpolate between two values (t = 0.0 to 1.0) var mid: float = lerp(0.0, 100.0, 0.5) # 50.0 var pos: Vector2 = lerp(start_pos, end_pos, 0.75) # 75% of the way # Smooth following — lerp with delta for frame-rate independence func _process(delta: float) -> void: position = position.lerp(target_position, 5.0 * delta)
csharpfloat mid = Mathf.Lerp(0.0f, 100.0f, 0.5f); Vector2 pos = startPos.Lerp(endPos, 0.75f); public override void _Process(double delta) { Position = Position.Lerp(targetPosition, 5.0f * (float)delta); }
Warning:
lerp(a, b, speed * delta)is frame-rate dependent and never fully reaches the target. For precise movement, usemove_toward()instead.
move_toward — Fixed-Speed Approach
gdscript# Move exactly `speed * delta` units toward target each frame position.x = move_toward(position.x, target_x, speed * delta) # Vector version position = position.move_toward(target_position, speed * delta)
csharpfloat newX = Mathf.MoveToward(Position.X, targetX, speed * (float)delta); Position = Position.MoveToward(targetPosition, speed * (float)delta);
slerp — Spherical Interpolation
For smooth rotation interpolation (preserves arc, not straight line).
gdscript# Quaternion slerp for smooth 3D rotation var current_quat: Quaternion = global_transform.basis.get_rotation_quaternion() var target_quat: Quaternion = target_transform.basis.get_rotation_quaternion() var result: Quaternion = current_quat.slerp(target_quat, 5.0 * delta) global_transform.basis = Basis(result)
csharpQuaternion currentQuat = GlobalTransform.Basis.GetRotationQuaternion(); Quaternion targetQuat = targetTransform.Basis.GetRotationQuaternion(); Quaternion result = currentQuat.Slerp(targetQuat, 5.0f * (float)delta); GlobalTransform = new Transform3D(new Basis(result), GlobalPosition);
smoothstep — S-Curve Easing
gdscript# Returns 0.0 when x <= from, 1.0 when x >= to, smooth curve between var t: float = smoothstep(0.0, 10.0, distance) # 0→1 as distance goes 0→10 # Useful for soft thresholds (fog density, volume falloff) var fog_intensity: float = smoothstep(50.0, 100.0, camera_distance)
cubic_interpolate — Smooth Path Following
gdscript# Smooth interpolation using 4 control points (catmull-rom style) var point: Vector2 = p1.cubic_interpolate(p2, p0, p3, t) # p0 = before start, p1 = start, p2 = end, p3 = after end
Interpolation Comparison
| Function | Speed | Reaches Target | Smooth | Use For |
|---|---|---|---|---|
lerp(a, b, t) | Variable | Only at t=1 | Yes | UI transitions, blending |
move_toward() | Constant | Yes | No | Movement, timers |
slerp() | Variable | Only at t=1 | Yes | Rotation blending |
smoothstep() | S-curve | Soft threshold | Yes | Fog, volume, thresholds |
cubic_interpolate() | Variable | Only at t=1 | Very | Paths, camera rails |
4. Curves and Paths
Curve resource for value-over-time (e.g., damage falloff curves). Path2D / Path3D for spatial paths sampled by PathFollow2D / PathFollow3D — use for moving platforms, missile guidance, camera rails.
See references/curves-and-paths.md for
Curvesetup, Path nodes, PathFollow properties.
5. Random Number Generation
Global functions (randf(), randi() % N, randf_range(a, b)) for one-shot randomness. RandomNumberGenerator for seeded, reproducible randomness (procgen, replay, save-state). Weighted selection via cumulative-sum or alias method.
See references/random-numbers.md for full GDScript on each pattern, plus
FastNoiseLitefor procedural generation noise.
6. Common Game Math Recipes
Five recipes: look at target (2D Vector2.angle_to_point), orbit around a point (polar coordinates), sine-wave bob (floating UI elements, treasure), angle wrapping (-PI..PI canonicalization), clamped approach with deadzone (analog input + small-input ignore).
See references/game-math-recipes.md for ready-to-use code on each recipe.
7. Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
lerp never reaches target | Using lerp(a, b, speed * delta) each frame | Use move_toward() for exact arrival |
| Rotation jumps at 180° | Using lerp instead of lerp_angle | Always use lerp_angle() for angle interpolation |
| Object faces wrong direction (3D) | Forgot Godot uses -Z as forward | Forward direction is -global_transform.basis.z |
| Distance check too slow | Calling distance_to on many objects | Use distance_squared_to and compare against range * range |
| Normalized zero vector crashes | Calling normalized() on Vector2.ZERO | Check length() > 0 first, or use direction_to() |
| Transform interpolation looks wrong | Lerping euler angles instead of quaternions | Use Quaternion.slerp() or Transform3D.interpolate_with() |
| Random results repeat after restart | Using RandomNumberGenerator with fixed seed | Godot 4.x auto-seeds global RNG; for RandomNumberGenerator use randomize() or set seed |
| Noise values are all ~0 | frequency too low | Increase FastNoiseLite.frequency (try 0.01–0.1) |
8. Implementation Checklist
- Distance comparisons use
distance_squared_to()for performance - Angle interpolation uses
lerp_angle(), notlerp() - 3D forward direction is
-transform.basis.z, not+z -
move_toward()is used when exact arrival at target is needed -
lerp(a, b, speed * delta)is understood as frame-rate dependent smooth following, not exact movement -
RandomNumberGeneratoris used for deterministic/seeded randomness (procedural generation, replays) - Noise-based generation uses
FastNoiseLitewith appropriate frequency and seed - Weighted random selection is used for loot tables and probability-based systems
- Path following uses
PathFollow2D/3Dwithprogressorprogress_ratio - Quaternion slerp is used for 3D rotation interpolation instead of euler angles


