Lesson 14 editors

Curve editor

A tuning curve lives in your code as a list of numbers. :set editor curve draws the shape those numbers make beside them, and the keys move the points by rewriting the numbers in place.

The numbers stay the file

A damage falloff, an easing, an XP table: each is a vector of points, and each point is a few numbers a text editor shows but does not draw. Put the cursor anywhere inside the list and :set editor curve opens the tool on it.

std::vector<Point> damage = {
    {0.0f, 0.0f, 1.0f, 1.0f},
    {0.5f, 0.8f, 0.0f, 0.0f},      // the cursor is anywhere in here
    {1.0f, 1.0f, 1.0f, 1.0f},
};

Three windows come up, and they are the arrangement every editor tool uses: the source you started from, the plot split to its right, and a form down the right edge. Focus lands on the plot, because that is where the keys are. Esc closes both and puts the cursor back in the code.

tuning.c In and out of the curve editor 1 / 5
In and out of the curve editor: step 0 In and out of the curve editor: step 1 In and out of the curve editor: step 2 In and out of the curve editor: step 3 In and out of the curve editor: step 4

The picture is a view of the file. Every key that changes something is one edit of the source buffer and one undo step of it — u on the plot and u in the code are the same history, and editing a number by hand redraws the plot. There is nothing to import, nothing to export and no second copy to keep in step: the demo above never wrote a file, and the [+] on the way out is the same [+] a typed character would have left.

tip
Opening the tool starts the playhead, which means the screen is never quite still. :tool curve play off settles it while you work on a tangent; Space pauses it where it stands.

Once you are in, the plot is where the work happens.

tuning.c The curve editor 1 / 4
The curve editor: step 0 The curve editor: step 1 The curve editor: step 2 The curve editor: step 3

What a point is

Four numbers, in Unity’s keyframe order: x is time, y is value, in and out are the tangents as slopes. The struct in your code decides what each number means, and the tool is told the order once:

CommandDoes
:tool curve layout x,y,in,outthe default — _ skips a field
:tool curve layout x,ya plain pair, as Python’s [(0.0, 0.0), …]

A point with fewer numbers than the layout names has zero tangents, and the tool leaves them alone. An empty list is not a dead end: :set editor curve with the cursor in {} seeds the linear preset — (0, 0) to (1, 1) — as one undo step, and from then on it is a list like any other.

The plot is the 0,0 to 1,1 square at a fixed 512 pixels a side, so a curve looks the same every time you open it; a point outside the square stretches the picture to take it in at the same scale. Grid lines run every tenth, with a label every fifth.

Moving points

The plot reads the normal keymap — a rebound j still means down, and the arrows work — plus Tab and Enter, which nothing in normal mode claims for a picture.

KeyDoes
Tab Shift-Tabnext, previous point, wrapping; counts multiply
gg Gfirst, last point
h lx earlier, later by one x step; counts multiply
j ky down, up by one y step
aadd a point halfway to the next one, on the curve, and select it
xdelete the point — the first and last stay, and a curve keeps two
Enterthe point on the ex line: :tool curve y 0.8
u Ctrl-Rundo, redo — the source buffer’s
Escclose the tool and go back where you came from

x is clamped inside 0..1 and between the point’s neighbours, which is the engine’s own rule for time, so the list an engine reads never needs sorting. y is free. A point added with a takes the curve’s value and slope where it lands, so it never bends the curve — it gives you a handle where there was none. From the last point, a adds halfway back to the previous one.

tip
Counts multiply everywhere: 5k is five y steps, 3Tab moves three points along. The steps themselves live in the form — X step, Y step — or :tool curve xstep 0.05.

Tangents

A tangent is a slope, and a slope is an angle, so r turns the plot into a dial: while rotation is on, the keys that moved the point turn its tangent instead, by one angle step — five degrees by default — per press.

KeyDoes
h ← k ↑counter-clockwise: the out handle rises, the in handle drops
l → j ↓clockwise
Tab Shift-Tabon a split point, the other tangent; on a joined one, the next point
r Escrotation off — Esc here leaves rotation, not the tool

A slope is clamped between -89° and 89°, because the engine’s tangent is dy/dx and a vertical one is not a number. The turned value is written with three decimals, one undo step, through the same path every other key takes.

Joined or split is the editor’s, not the file’s. An engine reads four numbers and nothing else, so the file decides it by agreement: a point whose in and out slopes match is joined, one whose slopes differ is split. A joined point turns both tangents together; a split point turns the one Tab picked.

KeyDoes
s on a joined pointsplits it without writing anything — tangents split, and the first turn makes the slopes differ
s on a split pointcopies the out slope into the in — tangents joined

A remembered split lasts only while that point stays selected: pick another point and come back, and equal tangents read as joined again, which is what they look like. With a layout that names no in or out, s says so and turns nothing.

The picture says which state you are in before you press anything: the two handles are one straight stroke through the anchor when joined, and two strokes in a second colour when split. In rotation mode a ring is drawn around the anchor, the turning handle is longer and in the selection’s colour, and the status row says ROTATE instead of CURVE, carrying the slopes — point 2 of 3 x 0.500 y 0.800 out 1.000 in 1.000, with the turning tangent in brackets.

tuning.c Tangents 1 / 4
Tangents: step 0 Tangents: step 1 Tangents: step 2 Tangents: step 3

The playhead

A curve is a motion, so the plot plays it. A lane stands to the left of the square and a filled disc in it rises and falls with the curve’s real eval while a thin red line sweeps the square left to right — two seconds a pass, around again without end. An overshoot bounces and a flat lands the way the engine will land it.

Key or commandDoes
Spacepause the playhead where it is, and let it run again
:tool curve play offthe same switch, typed; on resumes

Opening the tool starts it. A paused plot asks for no frames, so a still editor still blocks on the keyboard.

tuning.c The playhead 1 / 3
The playhead: step 0 The playhead: step 1 The playhead: step 2

Every move as a command

The same moves typed, for exact values and for scripts:

CommandDoes
:tool curve point 3select the third point; reports without a value
:tool curve x 0.5move the selected point in x, clamped to its neighbours
:tool curve y 0.8and in y
:tool curve out 1.5the out tangent’s slope; in the same
:tool curve locked offsplit the tangents; on joins them, copying out into in
:tool curve xstep 0.1what h and l move by; ystep for j and k
:tool curve astep 5degrees per press while rotating
:tool curve layout x,y,in,outread the points a different way

:tool curve alone lists them, and a value that does not parse is refused naming what the field takes.

The form

A readout, plus the three steps:

Point     2                         read-only
X         0.500                     read-only
Y         0.800
In        0.000
Out       0.000
X step    ━●━━━━━━━━   0.010
Y step    ━●━━━━━━━━   0.010
Angle     ━●━━━━━━━━   5

Point, X, Y, In and Out mirror the selected point and nothing in the form turns them: a point has rules — sorted x, clamped to its neighbours, two points at least — that the plot’s keys keep and a freely dragged slider would break. Only the steps turn, with h and l, or Enter to put :tool curve xstep <value> on the ex line. u in the form undoes the source buffer, as on the plot.

Which list the tool found

The literal is found by a bracket scan rather than by tree-sitter, so it works in any language that spells a list with brackets. From the cursor it walks out to the innermost group whose children are groups of at least two numbers, and inside a child it takes the numbers, true and false in the order they appear and ignores everything else. These all read as the same points:

{ {0.0f, 0.0f, 1.0f, 1.0f, false}, {0.5f, 0.8f, 0.0f, 0.0f, true} }
vec![Point { x: 0.0, y: 0.0, in_: 1.0, out: 1.0 }, …]
[(0.0, 0.0), (0.5, 0.8), (1.0, 1.0)]

Writing back rewrites tokens and never the text around them. A number keeps its suffix and at least the decimals it had, so 0.5f moved by 0.01 becomes 0.51f; a bool keeps its spelling. a copies the point’s text — brackets, a glued name like Point { … }, the separator after it — so a one-per-line list stays one per line.

The tool remembers the byte of the outer open bracket and re-reads from there after every change. Delete the list by hand and it says curve lost, leaving the plot blank until the next :set editor curve.

Closing it

:w and :q belong to the code: :w from the plot or the form writes the source buffer, and :q from either closes the plot and the form and puts the cursor back in the code. Esc is :q for the hand already on the keyboard. Closing one of the two by any other route — Ctrl-W q, :close — closes the other, because a plot with no form is half a tool.

A curve field in a .bidata is this same literal spelled in JSON, and Enter on its row in the property view opens this tool over the value, with the view standing where the source window stands.