L1.6 — Built-in Value Types & Math¶
What you'll learn
- Use
Vector2for positions and directions: components, arithmetic,length,normalized,distance_to,lerp. - Recognize
Color(channels 0–1) andRect2, and that all three are value types. - Reach for the global math helpers:
clamp,lerp,min/max,round/floor, therand*family. - Normalize a direction before scaling it, to avoid faster-on-the-diagonal movement.
How it applies
- Un-normalized input makes diagonals faster. Pressing right-plus-down yields a vector of length
≈1.41; pressing right alone yields length 1. Scale both by the same speed and the player moves
~41% faster diagonally — a real, shippable movement bug that
normalized()fixes in one call. - Editing a returned vector silently does nothing. A getter that returns a
Vector2returns a copy; writing to.xon that copy changes a throwaway and is lost. Knowing vectors are value types tells you why the change "didn't take" and what to do instead. - Color channels are 0–1, not 0–255. Passing
255where the engine expects0.0–1.0produces wrong, over-bright colors. The convention is a frequent first-day surprise; knowing it up front saves a confused debugging session. - Unclamped values escape their valid range. Health that can read negative, a volume above 1.0,
an index past the end —
clampis the boundary-value guard that keeps a quantity inside the range the rest of the code assumes.
Concepts¶
Vector2 — the workhorse of 2D¶
A Vector2 packs two floats, .x and .y. It is used for both positions (a point in space) and
directions/velocities (an arrow):
var pos := Vector2(100, 50)
print(pos.x, ", ", pos.y) # 100, 50
var a := Vector2(1, 0)
var b := Vector2(0, 1)
print(a + b) # (1, 1) — component-wise
print(a * 3) # (3, 0) — scalar scaling
The methods you reach for constantly:
var v := Vector2(3, 4)
print(v.length()) # 5.0 — magnitude
print(v.normalized()) # (0.6, 0.8) — same direction, length 1
print(Vector2(0,0).distance_to(v)) # 5.0
print(Vector2(0,0).lerp(v, 0.5)) # (1.5, 2) — halfway toward v
Handy constants exist: Vector2.ZERO, Vector2.ONE, Vector2.RIGHT, Vector2.LEFT, Vector2.UP,
Vector2.DOWN. Note that Vector2.UP is (0, -1): in Godot's 2D space the y-axis points down, so
"up" is negative y. That coordinate convention gets its own treatment in G2.2; for now just expect
UP to be negative y. Vector2i is the integer-component sibling, for grid coordinates and sizes.
Normalizing a direction before you scale it¶
A direction assembled from axis input does not have length 1. To move at a fixed speed regardless of direction, normalize first, then scale:
var direction := Vector2(x_input, y_input) # could be length 1.41 on a diagonal
var step := direction.normalized() * speed # now exactly `speed` in any direction
Example
The bug this prevents, made concrete:
# WRONG: diagonal is faster
var dir := Vector2(1, 1) # length ≈ 1.41
var step := dir * 200 # length ≈ 283 — too fast
# RIGHT: every direction moves at 200
var step2 := dir.normalized() * 200 # length exactly 200
A player cannot articulate "my diagonal speed is 1.41× my cardinal speed," but they feel that the
character lurches when moving diagonally. The engine's input helper Input.get_vector (G2.5)
normalizes for you; when you build a direction by hand, you must.
Color and Rect2¶
A Color has four channels — red, green, blue, alpha — each a float from 0.0 to 1.0 (not
0–255):
var red := Color(1, 0, 0) # opaque red
var faded := Color(1, 1, 1, 0.5) # half-transparent white
var hex := Color("ff8800") # also accepts hex strings
A Rect2 is a rectangle — a position (Vector2) and a size (Vector2) — with helpers like
has_point(p) and intersects(other). You meet it in UI layout and hit-testing.
These are value types¶
Vector2, Color, and Rect2 are value types (L1.5): copied on assignment and passed by value.
That has one practical consequence worth stating now:
The flip side is the "why didn't my change take" trap: if a function or property returns a
Vector2, it returns a copy, so writing to that copy's .x changes a temporary and is discarded. To
change a node's position you assign the whole vector back (node.position = new_vec) or assign the
component on the property directly (node.position.x = 5, which the property supports); you do not
mutate the result of an arbitrary vector-returning call and expect it to persist.
Global math helpers¶
Godot exposes a large set of math functions globally (no prefix needed):
clamp(value, 0.0, 1.0) # keep value within [0, 1] (clampi / clampf for typed variants)
lerp(a, b, t) # linear interpolation, t in [0, 1]
min(a, b) max(a, b) # also mini/maxi/minf/maxf for typed variants
abs(x) round(x) floor(x) ceil(x) # roundi/floori/ceili return ints
sqrt(x) pow(base, exp)
deg_to_rad(d) rad_to_deg(r)
PI # the constant
For randomness:
randf() # float in [0, 1)
randf_range(1.0, 5.0) # float in [1, 5)
randi_range(1, 6) # int in [1, 6] inclusive — a die roll
clamp is the boundary guard you reach for most: clamp health to [0, max_health], clamp a volume to
[0, 1], clamp an index to a valid range. The typed variants (clampi, clampf, maxf) avoid
accidental int/float surprises (L1.3) and appear throughout the game books.
Walkthrough¶
Use your L1.1 scene and script.
- Make
var dir := Vector2(1, 1). Printdir.length()(≈1.41), then(dir * 200).length()(≈283), then(dir.normalized() * 200).length()(200). You have just measured the diagonal-speed bug and its fix. - Demonstrate value-copy: copy a
Vector2to a second name, change the copy's.x, print the original, confirm it is unchanged. - Clamp experiments:
print(clamp(150, 0, 100))andprint(clamp(-20, 0, 100)); confirm100and0. Then roll a die ten times withrandi_range(1, 6)in a loop. - Make a
Color(255, 0, 0)and aColor(1, 0, 0), set them on something visible later if you like — for now just note that the first is out of the 0–1 range and will not mean "bright red ×255."
Optional sanity check
Write a function func get_offset() -> Vector2: return Vector2(0, 0) and then, in _ready, try
get_offset().x = 5 followed by print(get_offset()). It still prints (0, 0) — you mutated the
returned copy and threw it away. That is the value-type "returned copy" trap in two lines.
Self-check quiz¶
Q1 — Pressing right+down gives Vector2(1, 1). You move with velocity = dir * speed. What is wrong, and the fix?
A. Nothing is wrong; diagonal movement should be faster.
B. The diagonal vector has length ≈1.41, so diagonal speed is ~41% too high; fix with dir.normalized() * speed.
C. Vector2(1, 1) is invalid; you must use Vector2(1.0, 1.0).
D. You must divide by 2 for diagonals specifically.
Reveal answer
B. Vector2(1, 1).length() is ≈1.41, so scaling it by speed produces a velocity ~41%
longer than a cardinal direction. normalized() rescales the direction to length 1 so every
direction moves at speed. A accepts a real defect. C is false (1 is fine; it becomes a float
in a Vector2). D is a hack that only fixes the exact 45° case, not arbitrary directions.
Q2 — Color(1, 1, 1) represents what, and what does Color(255, 255, 255) do?
A. Both are white; channels accept 0–255.
B. Color(1,1,1) is white (channels are 0–1); Color(255,255,255) is out-of-range and not the
intended white.
C. Color(1,1,1) is black; you need 255 for white.
D. Both error; you must use hex strings.
Reveal answer
B. Color channels run 0.0–1.0, so Color(1,1,1) is full-intensity white. Passing 255
is far outside the expected range and does not mean "white ×255" — it is the classic mistake
for people arriving from 0–255 color APIs. C inverts the scale. D is false — numeric
constructors are valid; hex strings are merely an alternative.
Q3 — var p := Vector2(2, 3), var q := p, q.x = 0. What is p.x?
A. 0, because q references p.
B. 2, because Vector2 is a value type and q is an independent copy.
C. Error — you cannot assign to .x.
D. null.
Reveal answer
B. Vector2 is a value type, so q := p copies it; mutating q.x leaves p untouched.
A would be true for a reference type like Array, not for a vector. C is wrong — .x is
assignable on a vector you own. D invents a null that never appears.
Integration question¶
Q4 — open
A movement routine reads input into var dir := Vector2(ix, iy), then tries to cap speed with
dir.normalized().x = clamp(dir.x, -1, 1) before doing position += dir * speed * delta. The
character still moves faster on diagonals. Identify the two distinct mistakes — one about value
types, one about normalization — and write the corrected two lines.
Reveal expected answer
Mistake 1 — writing to a returned copy. dir.normalized() returns a new Vector2
(a value type); assigning to its .x mutates that throwaway copy and discards it. dir is never
changed, so nothing is normalized. Mistake 2 — clamping components instead of normalizing the
vector. Clamping .x and .y each to [-1, 1] still leaves (1, 1) with length 1.41;
per-component clamping is not the same as scaling the whole vector to length 1. The correct
approach normalizes the direction and then scales it:
normalized() returns the unit-length direction; multiplying by speed * delta gives a
frame-rate-independent step of equal magnitude in every direction. The chapter's two threads —
vectors are value types (so a returned vector is a copy) and a direction must be normalized
before scaling — are exactly the two things this code got wrong.