Horizontal Positioning#
This is the technique that defines VCS sprite work, and the first place every beginner hits the cycle wall. A player has no X-coordinate register. You cannot write “put P0 at column 40.” Instead you position it by timing — and then nudge it into place.
Step one: strobe to a coarse position#
Writing to RESP0 is a strobe: the value doesn’t matter, the act of writing does. It snaps player 0’s horizontal position to wherever the beam happens to be at the moment the store executes. So you move a sprite by choosing when in the scanline to run STA RESP0.
But the resolution is coarse. The CPU ticks once for every three color clocks, and the tightest positioning loop costs five cycles per iteration — so a timed strobe can only land the sprite on roughly every 15th pixel. That gets you close, never exact.
The whole “loop burns time, then strobe” idea fits in a few lines. With the desired column in A, divide it by 15 by repeated subtraction — and the loop’s own running time is the positioning:
sta WSYNC ; align to the start of a scanline (beam at far left)
sec
.Div15 sbc #15 ; 2 cycles ─┐ 5 cycles = 15 color clocks per pass,
bcs .Div15 ; 3 cycles ─┘ so each pass walks one coarse slot
sta RESP0 ; strobe: P0 snaps to wherever the beam reachedThat lands P0 within ~15 pixels of the target — the right slot, not yet the right pixel. Closing that last gap is Step two.
Step two: fine-tune with HMOVE#
The gap between those coarse landing spots is closed by the fine-motion registers. HMP0 holds a signed 4-bit value in its high nibble — a nudge of +7 to −8 color clocks — and writing to HMOVE applies the fine offsets of all the movable objects at once, shifting each by its HMxx amount.
So the full move is two steps: strobe RESP0 to the nearest coarse slot, set HMP0 to the leftover distance, then STA HMOVE to slide the sprite the final few pixels. The classic routine computes both at once — a divide-by-15 of the target column gives the coarse strobe timing, and the remainder becomes the HMP0 fine value.
It’s the same divide loop as above, with the leftover remainder turned into a fine offset. Indexing every store by X (the object number, 0 = P0) makes one routine serve all five objects — HMP0/RESP0 sit at the head of their register runs:
SetHorizPos: ; A = X position (0..159), X = object # (0 = P0)
sta WSYNC ; line up to the start of a scanline
sec
.Div15 sbc #15 ; divide by 15 — the loop time sets the coarse slot
bcs .Div15
eor #7 ; turn the remainder into a signed 4-bit fine offset
asl
asl
asl
asl ; shift it up into HMxx's high nibble
sta HMP0,x ; stash the fine nudge for this object
sta RESP0,x ; coarse strobe: snap to the nearest 15-clock slot
sta WSYNC
sta HMOVE ; apply the fine nudge — exact pixel
rtsThis is the routine almost every game carries in some form (it traces back to the Battlezone cartridge). One call, run during VBLANK, and the object is pixel-exact.
The sign is the single most-flipped detail in VCS programming. By the usual convention a positive
HMxxvalue moves the object left and a negative value moves it right ($70= +7 left,$80= −8 right). It’s worth confirming the direction against yourvcs.hconstants and watching it in Stella rather than trusting memory.
The HMOVE comb, and clearing#
HMOVE has two things to know about:
- It must be strobed at the very start of the line (right after
WSYNC, during horizontal blank), because it works by briefly extending the blank to retime the objects. - That extension blanks the leftmost 8 pixels of any line where you use it — the “HMOVE comb,” a row of black notches down the left edge if you move sprites every scanline. Games hide it under a border or simply accept it.
HMCLR zeroes all five fine-motion registers in one write — handy at the top of a frame so last frame’s offsets don’t reapply. (The HMxx values persist and reapply on every HMOVE, so motion you don’t want must be cleared.)
In Practice#
- Position during blank, draw during the visible lines. Coarse-positioning loops burn a whole scanline, so they’re done in VBLANK before the kernel, not in the middle of it.
- The positioning routine itself must be cycle-counted. Where the sprite lands depends on the exact cycle
STA RESP0runs, so this is the textbook example of code whose timing is its function — the deep version lives in Kernel Techniques. - Smooth motion = HMOVE every frame. To move a sprite by one pixel, you don’t re-strobe
RESP0; you setHMP0to ±1 andHMOVE. Re-strobing is for jumps; fine motion is for gliding.