Friday, July 13, 2007

Smooth Scrolling

Here's another thing that keeps developers employed: rising expectations. People are surrounded by XBoxes and Pixar movies and iPhones, so they've come to expect lots of dazzle for low prices. This is tough on the lone guy trying to compete with a computer in a garage. Getting rid of every little flash or unpainted pixel on the screen is a big chore, and game-level animation is tricky.

My idol Joel Spolsky recently wrote that software development is a "game of inches." You just keep inching along, ironing out the next little wrinkle, and eventually your program will be as cool and popular as Joel's. If it does animation, you have to keep tweaking to make it smoother and more attractive.

I recently inched. My program has always had this annoying glitch in one aspect of the animation. One afternoon this week I came up with a nice solution, so I will now go into Technical Blog Mode and tell you all about it.

The Problem

An object is moving across a background on the screen. When it gets near the edge, you don't want it to move off and become invisible, you want the background to automatically scroll so the object stays in view. Call it "auto-follow."

It works like this. If the object is about to move out of view -- about to cross an invisible boundary about an inch from the edge -- then you need to shift the background enough to keep it in bounds. Say it's moving east, and the next move will carry it 10 pixels beyond the right boundary: what you do is scroll the background 10 pixels to the left, then redraw the object.

The first time I tried this, it looked ok, but kind of jittery and nervous. The object (ok, it's a train) is moving with arbitrary speed and direction, so maybe on one tick it needs to scroll by 10, then by 4, then 15, etc. Shifting the whole screen by a different amount each time looks unsmooth.

I solved this with a little governor mechanism. It said: if you need to scroll by any amount up to 20 pixels, scroll by 1 instead. The train moves beyond the boundary, but the scrolling is smooth and usually manages to catch up after a while. I fiddled with the parameters of this until it looked pretty good at most speeds, and it's been in effect since 1.0.

But there is still jittering, due to a more fundamental problem. Consult the pictures. In the top frame, the train is at the edge, about to cross the boundary (red line). Call ScrollWindow, and you get what's shown in the next frame: the window contents have shifted to the left, leaving an unpainted stripe along the right edge. This stripe gets repainted, and you end up with what's in the bottom frame.

The problem is that when you scroll the window, it moves the train image along with it. So the train appears to hit the boundary, then jump backwards a little bit as the window is scrolling, then jump forward again as it's painted in the new position. The background is scrolling smoothly along, but the train is doing the rhumba against the edge of the screen.

The Better Way
This is one of those problems that took a long time because I kept looking for a built-in solution. Figured I was calling the scroll routine wrong or something. I finally figured out that the built-in solutions are made for scrolling text, and for doing animation I was on my own.

What has to happen is obvious, right? We need to go straight from the top frame to the bottom, without letting the user see the one in between. Make it look like both the background and the train move in a single update.

To do this I had to duplicate some code from various layers of MFC, so it would do all the same positioning of scrollbars, moving of view origin, etc., down to the point where it called the system ScrollWindow routine. This is the routine that actually moves the pixels on the screen and gives the middle frame. So I just skipped it. Then moved the train, then repainted the entire window, and voila, it looked like everything moved at once.

Should've done this years ago.

No comments: