L06 — Space, points, vectors
Goal
Tell apart points and vectors, measure how long a vector is, turn it into a pure direction, and add two vectors. Your controller stick becomes a vector on screen.
Who this is for
If words like “normalize” sound scary: good, you’re in the right place. We’ll define every term with a story first.
Before you touch math
Imagine a paper map.
- A point is a pin in the map: “the treasure is here.”
- A vector is an arrow: “walk 3 steps east and 1 step north” — it has direction and length, but it is not a place by itself.
You can put the same arrow anywhere: “3 east, 1 north” from home or from the dock is the same instruction, different endpoints.
Games are full of both:
| Situation | Point or vector? |
|---|---|
| Player position | Point |
| “Move left this frame” | Vector |
| Velocity / speed + direction | Vector |
| Collectible location | Point |
| “From me to the collectible” | Vector (point − point) |
What you will see (ROM)
source scripts/env.sh
make -C lessons/l06-vectors| On screen | Meaning |
|---|---|
| Cross axes | Origin at screen center — our “zero” |
| Yellow-ish arrow | Your vector v from center |
Numbers for v and |v| | Components and length |
dir = (...) | Same direction, length forced to 1 |
| C-right | Toggle adding a fixed wind vector |
Controls
| Input | Effect |
|---|---|
| Stick | Change the free vector |
| C-right | Toggle v = stick + wind |
Play first: push the stick, watch the tip move, toggle wind and watch the tip jump. That jump is vector addition.
Core ideas (slow)
1. Coordinates are just labeled rulers
In 2D we store two numbers: (x, y).
(0, 0) origin
(10, 0) ten units along +X
(0, 5) five units along +YA point uses those numbers as “where.”
A vector uses the same kind of numbers as “how much offset.”
In code they often look identical (float x, y) — the meaning is what differs. Naming helps: position vs velocity.
2. Point vs vector (table you can trust)
| Point | Vector | |
|---|---|---|
| Means | A location | A displacement / direction + amount |
| Example | “Player is at (3, 0, 5)” | “Move by (1, 0, 0)” this frame |
| From two points | — | B - A = arrow from A to B |
| Point + vector | New point | — |
Recipe you’ll use forever:
new_position = old_position + velocity * dtThat’s “start at a point, add a vector.”
3. Length (magnitude)
How long is the arrow?
|v| = sqrt(x*x + y*y)- Stick centered → length ≈ 0
- Stick pushed hard → length bigger
On screen the ROM prints |v|. Deflect the stick fully and watch it grow.
Why care?
Speed is often “length of the velocity vector.” Distance to an enemy is length of (enemy - me).
4. Normalize = “keep direction, forget length”
Sometimes you only want which way, not how hard:
dir = v / |v| (when |v| is not zero)Now |dir| is about 1. You can say:
velocity = dir * move_speedSo walking is always the same speed no matter how hard you smashed the stick (after you decide to ignore magnitude) — or you use length for analog speed. Either way, normalize is the tool for “unit direction.”
In the ROM, look at dir = (...) while you move the stick. The direction numbers change; their combined length stays ~1 when you’re not at zero.
Division by zero
If the stick is centered, length is 0 — you must not divide. Helpers return a zero vector instead. Same in real games: “if almost stopped, play idle; don’t normalize noise.”
5. Adding vectors (the wind trick)
result = stick + windPicture tip-to-tail: draw stick, then from its tip draw wind; the big arrow from start to final tip is the sum.
C-right toggles wind so you feel addition. Order doesn’t matter for plain addition (a+b = b+a).
Deadzone (from L03, still matters)
Raw sticks jitter around zero. We ignore tiny values so “idle” is truly zero length — otherwise you’d normalize noise into a random direction. The ROM still uses a deadzone on the stick.
Screen +Y vs world +Y (don’t get ambushed later)
| Space | Usual +Y |
|---|---|
| N64 framebuffer / many 2D UIs | Down the screen |
| 3D world (this course) | Up |
The L06 demo negates stick Y so “stick up” feels like up on screen. In true 3D levels, +Y is up in world space. When UI and world math mix, be explicit about which space you’re in.
Course helpers (you don’t write the sqrt yourself)
#include "ng_math.h"
ng_vec2 v = ng_v2(x, y);
float len = ng_v2_len(v);
ng_vec2 dir = ng_v2_normalize(v);
ng_vec2 sum = ng_v2_add(a, b);Same ideas exist as ng_vec3 for 3D (x, y, z).
Walk the source (optional)
Open lessons/l06-vectors/src/main.c:
- Update — read stick → build
ng_vec2→ optional wind → length / normalize. - Render — draw axes, draw line from center to tip, print numbers.
If the file feels long, ignore drawing helpers (draw_line) and only read the block that builds v and dir.
Common noob confusions
| Feeling | Reality |
|---|---|
| “Points and vectors are the same in C” | Same storage, different meaning — name them clearly |
| “Normalize broke my speed” | You removed length on purpose; multiply by a speed after |
| “Wind made it jump” | That’s addition working — good! |
| “Why is dir (0,0) at rest?” | Length was ~0; we refuse to normalize |
Exercises
- Push the stick and confirm
|dir|stays near 1 when not centered. - Change wind’s numbers in source; rebuild; feel the new pull.
- (Stretch) On paper: point A=(1,1), B=(4,5). What vector goes from A to B? What’s its length?
Full lesson source
The blocks below are imported from the real repository files at build time (VitePress <<< snippets). They are not hand-copied into this markdown.
lessons/l06-vectors/Makefile · lessons/l06-vectors/src/main.c
lessons/l06-vectors/Makefile
ROMNAME := l06_vectors
ROM_TITLE := "L06 Vectors"
USE_NG_MATH := 1
include ../../common/lesson.mklessons/l06-vectors/src/main.c
/**
* L06 — Space, points, vectors
* ============================================================================
* Point = location. Vector = direction + length (not a place by itself).
* Stick builds a free vector from screen center.
* C-right toggles adding a fixed "wind" vector (addition).
* We print |v| (length) and normalized dir (length ~1).
* DOCS: docs/guide/m1/l06-vectors.md
*/
#include <libdragon.h>
#include <stdio.h>
#include "ng_math.h"
#define DEADZONE 8
#define SCALE 1.2f /* stick units → pixels */
static int dz(int v)
{
return (v > -DEADZONE && v < DEADZONE) ? 0 : v;
}
static void draw_dot(float x, float y, color_t c)
{
rdpq_set_mode_fill(c);
rdpq_fill_rectangle((int)x - 2, (int)y - 2, (int)x + 3, (int)y + 3);
}
static void draw_line(float x0, float y0, float x1, float y1, color_t c)
{
/* Cheap dotted line for teaching demos */
float dx = x1 - x0;
float dy = y1 - y0;
float len = sqrtf(dx * dx + dy * dy);
int steps = (int)(len / 3.f);
if (steps < 1) {
steps = 1;
}
rdpq_set_mode_fill(c);
for (int i = 0; i <= steps; i++) {
float t = (float)i / (float)steps;
int x = (int)(x0 + dx * t);
int y = (int)(y0 + dy * t);
rdpq_fill_rectangle(x, y, x + 2, y + 2);
}
}
int main(void)
{
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE,
FILTERS_RESAMPLE);
rdpq_init();
rdpq_text_register_font(1, rdpq_font_load_builtin(FONT_BUILTIN_DEBUG_VAR));
joypad_init();
/* Fixed "wind" vector in screen space (pixels) — toggled contribution */
const ng_vec2 wind = ng_v2(40.f, -15.f);
bool add_wind = false;
char line[72];
while (1) {
joypad_poll();
joypad_inputs_t in = joypad_get_inputs(JOYPAD_PORT_1);
joypad_buttons_t pressed = joypad_get_buttons_pressed(JOYPAD_PORT_1);
if (pressed.c_right) {
add_wind = !add_wind;
}
ng_vec2 stick = ng_v2((float)dz(in.stick_x) * SCALE,
(float)-dz(in.stick_y) * SCALE); /* screen +Y down */
ng_vec2 v = stick;
if (add_wind) {
v = ng_v2_add(v, wind);
}
float len = ng_v2_len(v);
ng_vec2 dir = ng_v2_normalize(v);
float cx = 160.f;
float cy = 120.f;
surface_t *disp = display_get();
rdpq_attach(disp, NULL);
rdpq_clear((color_t){ .r = 14, .g = 16, .b = 28, .a = 255 });
/* Axes */
draw_line(40, cy, 280, cy, (color_t){ 40, 40, 60, 255 });
draw_line(cx, 40, cx, 200, (color_t){ 40, 40, 60, 255 });
if (add_wind) {
draw_line(cx, cy, cx + wind.x, cy + wind.y,
(color_t){ 80, 160, 255, 255 });
}
draw_line(cx, cy, cx + v.x, cy + v.y, (color_t){ 255, 200, 80, 255 });
draw_dot(cx, cy, (color_t){ 220, 220, 220, 255 });
draw_dot(cx + v.x, cy + v.y, (color_t){ 255, 100, 100, 255 });
rdpq_text_print(NULL, 1, 12, 12, "L06 — Vectors");
snprintf(line, sizeof(line), "v = (%+.0f, %+.0f) |v|=%.1f", v.x, v.y, len);
rdpq_text_print(NULL, 1, 12, 28, line);
snprintf(line, sizeof(line), "dir = (%+.2f, %+.2f)", dir.x, dir.y);
rdpq_text_print(NULL, 1, 12, 44, line);
rdpq_text_print(NULL, 1, 12, 210,
add_wind ? "C-right: wind ON (v = stick + wind)"
: "C-right: toggle wind addition");
rdpq_text_print(NULL, 1, 12, 224, "Stick moves the free vector");
rdpq_detach_show();
}
}What you learned
- Point = where; vector = how much / which way
- Length and normalize
- Adding vectors
- Deadzone + “don’t normalize zero”
Next
L07 — Matrices — how we move, spin, and resize whole objects without editing every point by hand.