Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing game math — vectors, transforms, interpolation, curves, random number generation, and common geometric recipes
.claude/skills/jame581-math-essentials/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 164% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 142% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 158% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 264% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 292% | 0% |
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.
| 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 |
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();
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");
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();
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;
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);
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); }
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, use move_toward() instead.
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);
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);
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)
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
| 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 |
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 Curve setup, Path nodes, PathFollow properties.
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 FastNoiseLite for procedural generation noise.
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.
| 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) |
distance_squared_to() for performancelerp_angle(), not lerp()-transform.basis.z, not +zmove_toward() is used when exact arrival at target is neededlerp(a, b, speed * delta) is understood as frame-rate dependent smooth following, not exact movementRandomNumberGenerator is used for deterministic/seeded randomness (procedural generation, replays)FastNoiseLite with appropriate frequency and seedPathFollow2D/3D with progress or progress_ratio| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 12,744 | 11,305 | -11% | 1 | 1 | 0% | 2,385 | 5,774 | +142% | 0 | 0 | — |
case-02 | pass→pass | 12,559 | 8,943 | -29% | 1 | 1 | 0% | 1,981 | 5,105 | +158% | 0 | 0 | — |
case-03 | pass→pass | 8,117 | 5,447 | -33% | 1 | 1 | 0% | 1,265 | 4,608 | +264% | 0 | 0 | — |
case-04 | pass→pass | 7,407 | 7,091 | -4% | 1 | 1 | 0% | 1,233 | 4,830 | +292% | 0 | 0 | — |
case-05 | pass→pass | 6,563 | 4,077 | -38% | 1 | 1 | 0% | 1,156 | 4,333 | +275% | 0 | 0 | — |
case-06 | pass→pass | 7,271 | 7,212 | -1% | 1 | 1 | 0% | 1,351 | 4,965 | +268% | 0 | 0 | — |
case-07 | fail→pass | 11,439 | 9,439 | -17% | 1 | 1 | 0% | 2,044 | 5,394 | +164% | 0 | 0 | — |
case-08 | pass→pass | 13,576 | 9,625 | -29% | 1 | 1 | 0% | 2,429 | 5,348 | +120% | 0 | 0 | — |
case-09 | pass→pass | 9,059 | 6,385 | -30% | 1 | 1 | 0% | 1,568 | 4,822 | +208% | 0 | 0 | — |
case-10 | pass→pass | 10,413 | 10,043 | -4% | 1 | 1 | 0% | 1,644 | 5,313 | +223% | 0 | 0 | — |
case-11 | pass→pass | 5,216 | 4,645 | -11% | 1 | 1 | 0% | 930 | 4,487 | +382% | 0 | 0 | — |
case-12 | pass→pass | 6,477 | 5,518 | -15% | 1 | 1 | 0% | 1,035 | 4,587 | +343% | 0 | 0 | — |
case-13 | pass→pass | 7,826 | 6,169 | -21% | 1 | 1 | 0% | 1,381 | 4,760 | +245% | 0 | 0 | — |
case-14 | pass→pass | 6,812 | 5,289 | -22% | 1 | 1 | 0% | 1,259 | 4,724 | +275% | 0 | 0 | — |
case-15 | pass→pass | 12,004 | 10,599 | -12% | 1 | 1 | 0% | 2,100 | 5,462 | +160% | 0 | 0 | — |
case-16 | pass→pass | 5,958 | 5,394 | -9% | 1 | 1 | 0% | 1,078 | 4,556 | +323% | 0 | 0 | — |
case-17 | pass→pass | 6,675 | 5,849 | -12% | 1 | 1 | 0% | 1,179 | 4,726 | +301% | 0 | 0 | — |
case-18 | pass→pass | 8,976 | 4,959 | -45% | 1 | 1 | 0% | 1,600 | 4,540 | +184% | 0 | 0 | — |
case-19 | pass→pass | 13,270 | 11,479 | -13% | 1 | 1 | 0% | 2,358 | 5,820 | +147% | 0 | 0 | — |
case-20 | pass→pass | 11,052 | 12,575 | +14% | 1 | 1 | 0% | 2,096 | 6,001 | +186% | 0 | 0 | — |
case-21 | pass→pass | 12,025 | 11,425 | -5% | 1 | 1 | 0% | 2,123 | 5,776 | +172% | 0 | 0 | — |
case-22 | pass→pass | 13,470 | 13,686 | +2% | 1 | 1 | 0% | 2,514 | 6,395 | +154% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.