Arrrr! Avast ye screen savers!
By Mark Dalrymple (markd@borkware.com), May 14, 2002
Feedback Farm
Have something to say about this article? Let us know below and your post might be the Post of the Month! Please read our Official Rules and Sponsor List.
Forums
Want to dig even deeper? Post to the new MacEdition Forums!
[Ed. Note: In this article, we begin a practice of including affiliate links for books and other merchandise when appropriate. For example, the Cocoa programming book link in the first paragraph will take you to an affiliate where, if you purchase the book, MacEdition will benefit. In the future, these links may include multiple vendors or even the MacEdition Store. These links are added after the article is edited and are driven by the content, not the other way around.]
Prerequisites: Basic Cocoa knowledge – how to create a Project Builder project, edit .nib files, how to create connections between objects in Project Builder, subclassing.
The first 13 chapters of Aaron Hillegass’ Cocoa Programming for Mac OS X book cover the needed material, and there’s lots of introductory information at Cocoa Dev Central and StepWise.
If you don’t know what a screen saver is, the rest of the article might not make a whole lot of sense, but just in case, go to your System Preferences, choose “Screen Saver,” and play around. You’ll figure it out, assuming you’re on Mac OS X. If you’re still on OS 9, you’re really going to be confused.
Evil black art ... paging evil black art
Historically, writing screen savers has been an evil black art. You’d need to know how to hook into the OS to find out when the user has been idle, how to take over the screen to draw, make sure you clean up after yourself and do your animation effects in a way that doesn’t trash network connections or other background processes. Imagine the user anger when your “Nuke the Barney” screensaver eats a 500-page print job. As is usually the case, though, programmers tired of reinventing the screen-saver wheel and went the framework route. Luckily with Mac OS X, Apple has included such a screen-saver architecture for programmers to use and abuse, leaving plenty of time for unique, cool and totally awesome effects and features.
Why would anyone want to waste their time writing a screen saver? Because they’re fun! The Instant Gratification Factor is high: You can go from idea to crude implementation in under 15 minutes. Screen savers are also great places to play with drawing and animation APIs. You don’t need to build the infrastructure of an application if all you’re wanting to do is plot some random Bezier curves or bounce some icons around the screen.
Enough of the talk; time to get into the code!
Architecture
A screen savers is a bundle (a directory with the executable and other bits and frobs like images and localized strings). When the system registers idle time or hot-corner activation, the program /System/ Library/Frameworks/Screensaver.framework/ Versions/Current/Resources/ScreenSaverEngine.app is started. ScreenSaverEngine loads the screen saver bundle the user has selected from the Screen Saver preference pane, takes over the screen and starts the screen saver running.
As a screen saver author, there are two classes that you need to worry about: one that you subclass to perform your drawing and one you use for storing data that you can access later (which is very handy for storing user preferences or state between screen saver invocations).
ScreenSaverView is what you’ll subclass every time you write a new screen saver. It’s basically just an NSView with some added frobs for drawing your graphics.
ScreenSaverDefaults is what you’ll use to store data. I’ll talk about this later – really!
Resources
- Official Apple documentation: Screen Saver API reference
- Cocoa Dev Central’s Writing a Screen Saver Module
- Mac OS X screen saver programming common errors and how to avoid them
- Borkware Drip, a cheesy little screen saver that includes a preference panel (source code available).
- Borkware Quickies page for Screen Savers
Also check VersionTracker and your favorite search engine for “Mac OS X screen savers” and “Mac OS X screensavers.” There are a lot of nifty screen savers out there, many of them including source.
Getting up and running
Of course, the first step is getting an idea. What is that you want to do?
Toy animations are fun and popular. A random number generator plus some
drawing primitives can lead to groovy and hypnotic effects. Even though
you can also do Real Work during your screen-saving animation time like
some popular E.T. signal filters or crypto key crackers do while wading
through piles of data, you’re actually better off doing that sort of
work as a background (daemon) process, since you can tweak the
daemon’s priority (via the nice
command in
the terminal or the setpriority function in C code)
so that it doesn’t interfere with the user’s Real Work.
The next step is to fire up Project Builder. Create a new project of type “Standard Apple Plug-ins / ScreenSaver.” Important safety tip: Do not call your project “ScreenSaver” (as I once did, and was really confused as to why things were not working). Project Builder automatically creates a ScreenSaverView subclass for you using the name of your project. If you name the project “ScreenSaver,” it happily creates ScreenSaverView, which then conflicts with the system ScreenSaverView. Hilarity ensues.
Step three ... well, yes, Mr. Goldblum, there is a step three. Graphics code. The screen saver class created by Project Builder has stub implementations for many methods: In particular, you’ll want to put some drawing code into animateOneFrame. Drawing focus is locked when this method is called so you can use graphic calls such as NSBezierPath, NSImage, or code directly to the Quartz APIs. Note that the background isn’t erased every time animateOneFrame is called, leading to cumulative graphic results on the screen. Depending on your idea, this could be a major win (Look! The graphical slug is leaving a trail behind! Cool!) or a real pain in the neck. If you need to start over with a blank canvas, you can always erase things manually.
By default, animateOneFrame is called 30 times a second, which is a good speed for smooth animation. This also means you can’t spend too much time in the function figuring out your next bit of drawing. You can control the animation speed by calling setAnimationTimeInterval: at any time.
Here’s a pretty minimal animateOneFrame that generates a random color (with random transparency), a random rectangle inside of our bounds, and the rectangle with the random color.
- (void) animateOneFrame { // how much room do we have to play in? NSRect bounds = [self bounds]; // make a random color, because random colors are cool float red = SSRandomFloatBetween (0.0, 1.0); float green = SSRandomFloatBetween (0.0, 1.0); float blue = SSRandomFloatBetween (0.0, 1.0); float alpha = SSRandomFloatBetween (0.0, 1.0); NSColor *randomColor = [NSColor colorWithDeviceRed: red green: green blue: blue alpha: alpha]; // make a random rectangle, because random rectangles are really cool // make a random width and height from 5 to 100 NSRect blobRect; blobRect.size = NSMakeSize (SSRandomFloatBetween(5.0, 100.0), SSRandomFloatBetween(5.0, 100.0)); // set a random origin point blobRect.origin = SSRandomPointForSizeWithinRect (blobRect.size,bounds); // set our color [randomColor set]; // and fill our rect [NSBezierPath fillRect: blobRect]; } // animateOneFrame
Step IV is to build it, fix any errors and warnings, and copy the screen saver bundle (it lives in the build directory where your project file lives) to ~/Library/Screen Savers (translation: the Screen Savers folder of the Library folder of your home folder). You might be able to create an alias in the Finder so you don’t have to do this copy operation every time you rebuild the project. I’m not smart enough to make an alias work, so I just fire up a Terminal and make a Unix symbolic link (symlink), which won’t break unless you rename the screen saver or move the project to a different directory. Assuming your screen saver project (let’s call it PantsSaver) lives in /Users/bork/development/cocoa/PantsSaver, here’s how to make the proper symlink:
% cd /Users/bork/Library/Screen\ Savers
(The backslash is important since space characters are separators. You
can use tab-completion to reduce the amount of typing you have to do.)
% ln -s
/Users/bork/development/cocoa/PantsSaver/build/PantsSaver.saver .
(This is actually just one long command, in case your browser
wraps it. Note the dot at the end of the command. It means “the current
directory,” which is the Screen Savers
directory we went to in the first command. You can combine both of them
into one command, but that’s a little frightening to unleash on folks first
thing in the morning. You can run the ls -l
command to see that the PantsSaver.saver symbolic
link actually is there.)
Step the fifth: Your project is now built and exists in the folder for the screen saver engine to find. Now open the System Preferences, choose Screen Savers, and select your new screen saver. You might want to go ahead and set up a hot corner to engage the screen saver. This will turn your work cycle into Code, Build, Swing the mouse to a corner to check your work. Very fast.
More goodies
Since ScreenSaverView (and hence your screen saver) inherits from NSView, you have all the creamy goodness that NSView brings to the table, such as the drawRect: method.
drawRect: is called the first time your screen saver is displayed. The default implementation fill the bounds rectangle with black. If you override this but don’t do anything you’ll get that weird Aqua pinstripe effect that’s everywhere. When drawRect: is called, the drawing focus is locked, so you can do whatever drawing you want. I use this method for clearing the screen to a specific color or for drawing some kind of background for the animation to play over. You can grab the user’s desktop and draw over that if you wish (which is actually pretty cool). How to do this is non-obvious, and not really documented with what I could find on the Net or from the folks on IRC. (Check out this hint for the code.)
You can consolidate all of your drawing code into drawRect: if you wish by having your animateOneFrame call [self setNeedsDisplay: YES] to force a redraw. Don’t forget there’s also setNeedsDisplayInRect: if you only need to update a small portion of your display for each animation frame.
If having your coordinate system origin at the bottom left isn’t convenient (like if you’re porting drawing code from just about any other platform), you can override isFlipped (and return YES) which will put your (0, 0) point in the upper left corner of the screen. Also, you can use NSAffineTransform to generate a transformation matrix to shift and rotate your coordinate system as you see fit.
Lastly, you can override the key handling methods. If you override keyDown: and don’t invoke the superclass’s method your screen saver won’t go away when the user presses keys on the keyboard. This opens up all sorts of possibilities for interactive screen savers (games). Regarding preventing mouse movement from dismissing the screen saver: In the 5 minutes I devoted to the task I couldn’t figure out how to do it.
If you looked at the code sample above, you might have noticed a bunch of SSRandom* calls. They’re conveniences for generating random numbers (remember how important random number are when generating variety for screen savers). They’re documented under “Functions” on the ScreenSaver API reference page. You can also look at their implementation by browsing ScreenSaverView.h.
Lastly, you can put resources (images, sounds, smells, etc.) into your project and they’ll end up in your screen saver bundle. You can use NSBundle to load these resources:
NSBundle *bundle = [NSBundle bundleForClass: [self class]];
NSString *path = [bundle pathForResource: @"juan-valdez"ofType:
@"jpg"];
NSImage *image = [[NSImage alloc] initWithContentsOfFile: path];
... and then use them as appropriate.
Tune in next time ...
This is enough to get going on your own screen saver. This isn’t the end of the article, though. Tune in for the next installment in two days to learn about Preview Mode, preferences and all sorts of other wacky things you can do.