"Space, the final frontier...

During my tenure as an X-Windows instructor, I tried to come up with fun and interesting toy programs that demonstrated key Xlib concepts. One of my students' favorites was a small program that handles (among other things) the topics of: I challenged my students to write an Xlib application that displayed an overhead shot of a certain famous starship flying through a moving starfield (see snapshot below). For variety and added realism, there should be two different sizes of stars, and they should all be moving at slightly different velocities to give the illusion of depth (i.e. stars "farther away" should move more slowly than "closer" ones).

To achieve this effect, the first thing you must realize is that you cannot use the XNextEvent() routine as usual in your main event loop. This application requires redrawing the starfield every couple of milliseconds, which makes the XNextEvent() function (by itself) unacceptable, since it is a blocking routine. Putting it another way, the "typical" Xlib event loop alrorithm is:

1   while forever
2      wait for an event
3      process that event
4   endwhile
Placing the update-screen code immediately after line 3 will not work, since eventually the progarm will hang on line 2, and won't execute your update-screen code until a new event kicks it out of its waiting state. We need to perform screen updates on a regular basis, even if there are no events to process.

We change the algorithm slightly so that if there are no events to process on a given iteration, we go ahead and update our screen anyway. Only when there are events to process do we handle them as before.

1   while forever
2      while there are any events to process
3         get an event
4         process that event
5      endwhile
6      update screen
7   endwhile
Let's skip coding this new algorithm for now, and concentrate on the graphics portion of the code.

Drawing the Starship

Our first step is to create our Enterprise cursor. Fortunately for us, the standard Xlib distribution includes a variety of cursor shapes, one of which is the Enterprise. Using these predefined cursors is extremely easy. You simply include the cursorfont.h file, and make the appropriate Xlib calls:
 4  #include <X11/cursorfont.h>
    ...
15    Cursor  trek;
      ...
38    trek = XCreateFontCursor( dsp, XC_trek );
39    XDefineCursor( dsp, win, trek );
Line 38 creates the cursor and Line 39 tells our window to use it. There is a small problem, however. When viewing the cursor, it appears as a black Enterprise surrounded by a white border. This is the standard coloring scheme of all default cursors, but it is not what we prefer for our application. Since we will be "flying" our Enterprise cursor over a black starfield, we would much rather have a white Enterprise surrounded by a black (and thereby "invisible") border. To do this, we must use the XRecolorCursor() command.
   XColor  fg, bg;
   ...
   XParseColor( dsp, cmap, "white", &fg );
   XParseColor( dsp, cmap, "black", &bg );
   XRecolorCursor( dsp, trek, &fg, &bg );
These commands effectively reverse the standard coloring scheme, and give us an Enterprise cursor much better suited to our needs.

"My God, it's full of stars!"

Next, we're ready to tackle the problem of our moving starfield. After choosing a window size of 300x300, I decided that there shall always be 100 stars visible at any given time. This number was chosen after some trial-and-error testing; others might find different values more to their liking. Anyway, each of these 100 stars needs to know its position, velocity, and size. We create a simple structure to suit our needs:
#define  NUMSTARS  100
struct STAR {
  int  x, y;   // the star's (x,y) position
  int  vel;    // velocity can be 1 through 5
  int  size;   // size can be SMALL=1 or LARGE=2
} stars[NUMSTARS];
A star's coordinates are stored in the (x,y) members. Velocity is measured in pixels per iteration; that is, the number of pixels a star will move between successive iterations within the event loop. I decided that stars can move anywhere between 1 to 5 pixels per iteration (where slow stars move 1 pixel at a time, and fast stars move 5 pixels at a time). Once again, you may decide to use different velocity values that suit your tastes. Lastly, I chose to allow only two star sizes. Stars of size 1 are single pixels. Stars of size 2 are filled circles with a diameter of 3.

Stars are initialized by being randomly placed throughout the window. They move down the screen at their respective velocities until they reach the bottom of the window. At that point, they reappear at the top of the window at a new random x-position, and continue their journey downwards again.


Virtual Flip Books

Whenever one is updating a window in real-time, steps must be taken to avoid "flicker". Inevitably, most students code the update-screen section by simply clearing the window and then redrawing all the stars at their new locations. Although this will work, the constant clearing and redrawing of stars shows up as an annoying flicker to the human eye.

A better solution is to draw our new star pattern on an "offscreen" Pixmap, and once our new starfield is ready, we "blit" it onto our window all at once. This is analagous to those old style paper flip books, where each pre-drawn "frame" is shown briefly, one after the other, giving the illusion of motion.

We create a Pixmap in which to draw our starfield. Since our stars will all be drawn in the same color (white) on top of a black background, we can conserve server memory by creating our Pixmap with a depth of 1 (i.e. a Bitmap). We can then use the XCopyPlane() routine to copy our starfield Bitmap (of depth 1) into our viewing Window (usually of depth 8 or 24, in most cases).

   Pixmap  starPix;
   GC      pixGC;
   ...
   starPix = XCreatePixmap( dsp, win, 300, 300, 1 );
   pixGC = XCreateGC( dsp, starPix, 0, NULL );
   XSetForeground( dsp, pixGC, 0 );
   XFillRectangle( dsp, starPix, pixGC, 0, 0, 300, 300 );
We create our Pixmap and a corresponding GC. Note that the GC we create based on this Pixmap cannot be used for our Window as well, since the depths do not match. This is a common error that Xlib novices make, thinking that they can use the same GC for both the Bitmap and the Window. We will need to create another GC to be used with our Window. Lastly, newly created Pixmaps need to be cleared, since they will typically have garbage bits in them. Since we're dealing with a bitmap, we set our GC foreground to zero and do a big XFillRectangle() to clear our Pixmap.

Chaos in the Universe

Now we're ready to place our stars. You need to have some sort of random number generator (RNG) to place the stars effectively. On my system, I use srand48() to seed the generator, and drand48() to give me a random number in the range of 0 < x < 1. If you are using a different RNG, it should be simple for you to modify the code to use your own RNG routines.
   srand48( getpid() );
   for ( i = 0; i < NUMSTARS; i++ )
   {
      stars[i].x = (int)(300.0 * drand48());
      stars[i].y = (int)(300.0 * drand48());
      stars[i].vel = 1 + (int)(5.0 * drand48());
      stars[i].size = (drand48() < 0.9)?  SMALL : LARGE;
   }
I seed my RNG with the application's PID, so that each run will generate different star patterns. Then, each star gets placed at a random location within the window, with a random velocity and a random size. I skew my star size by making only one tenth of them of the "large" variety.

Now, for each iteration through my event loop, I need to move all the stars by their proper velocity and redisplay the new starfield. First, I clear the "scratch" Pixmap again. Then, for each star, I update its position and draw it. When all the stars have been redrawn, I'm ready to update the Window.

   XSetForeground( dsp, pixGC, 0 );
   XFillRectangle( dsp, starPix, pixGC, 0, 0, 300, 300 );
   XSetForeground( dsp, pixGC, 1 );
   for ( i = 0; i < NUMSTARS; i++ )
   {
      stars[i].y += stars[i].vel;
      if ( stars[i].y >= 300 )
      {
        stars[i].y -= 300;
        stars[i].x = (int)(300.0 * drand48());
      }
      if ( stars[i].size == SMALL )
        XDrawPoint( dsp, starPix, pixGC, stars[i].x, stars[i].y );
      else
        XFillArc( dsp, starPix, pixGC, stars[i].x, stars[i].y,
                    3, 3, 0, 360*64 );
   }
After clearing the Pixmap, I reset the GC foreground to 1, so I can draw my stars. Note that when drawing into Bitmaps, you should not use the BlackPixel() or WhitePixel() macros to set your foreground. On some systems, BlackPixel() returns a zero, and on others, a one. Therefore you should always draw using raw 0's and 1's when using Bitmaps.

For each star, I add that star's velocity to its current y-coordinate. If this takes the star off the bottom of the Window, I pop it back up at the top again, at a new random x-location. The size of the star determines whether I use XDrawPoint() or XFillArc() to draw it.

To copy the Pixmap into our Window, we use the intimidating, eleven-parameter XCopyPlane() function. In our case, the call is fairly easy, since we're copying our entire Pixmap into a Window of the exact same size. We set our Window GC foreground to be white and the GC background to be black, so that our Bitmap is copied into the Window with our desired colors.

   XSetForeground( dsp, winGC, WhitePixel(dsp,scr) );
   XSetBackground( dsp, winGC, BlackPixel(dsp,scr) );
   XCopyPlane( dsp, starPix, win, winGC, 0, 0, 300, 300, 0, 0, 1 );

To Block, or Not to Block

The final step is to code our event loop to work in "real-time"; that is, to handle events without blocking. There are many ways to do this, but I chose the simple method of using the XPending() routine. This command returns the number of events currently on the event queue which have not yet been processed; therefore, as long as this return value is non-zero, we know that there are events to process. And if you know there are events to process, then you can safely call the XNextEvent() routine, since it is guaranteed not to block under those conditions.
   while ( 1 )
   {
      while ( XPending(dsp) > 0 )
      {
         XEvent ev;
         XNextEvent( dsp, &ev );    // guaranteed NOT to block
         ...
      }

      // if we get here, there are no events to process;
      // so perform the real-time update-screen stuff now
      ...
   }
Looking at the code, some people might not like the idea of processing all events on the queue before proceeding with the real-time stuff. They argue that if there are "a bunch" of events on the queue, then the real-time section of code won't get executed in a timely fashion. They would rather only process one event per iteration, rather than all of them. I can appreciate the logic behind this mentality, and in our program either method would probably work equally well. However, if one were processing MotionEvents (where the event queues can build up rather quickly), you would not want to use this "one event per iteration" method, as the user might experience motion-lag as the code attempts to juggle between processing the motion events singularly and updating the screen.

"Fire photon torpedo number one!!"

Once the students completed this part of the program, I let those desiring brownie points to add some additional functionality. When the left mouse button is pressed, a yellow "sparkling photon torpedo" is fired from the Enterprise cursor, moving "up" the screen, until it reaches the top and vanishes. Up to five torpedos may be present on the screen at any given time. To achieve the "sparkle" effect, I alternate between displaying two different 8x8 torpedo bitmaps:

    

When shown in rapid succession, the bitmaps give the illusion of a pulsating sparkling ball of energy. (Okay, I'm reaching a bit. Work with me here ). Each torpedo has an (x,y) coordinate, and a sparkle-toggle flag to determine which bitmap state we are in.

#define  MAXTORPS  5
struct  TORP  {
   int  x, y, flag;
} torp[MAXTORPS];
When the left mouse button is pressed, I check to see if there are less than five torpedos present. If so, a new torpedo is added to the "active" list. I use a simple test to determine which torpedos in the list are active. If the y-coordinate is greater than zero, it's active and visible; otherwise, it's free to use. My torpedoes move at five pixels per iteration, but move up the screen; when their y-coordinate becomes zero or less, it is automatically taken off the active list and no longer shown.

Ahead, Warp Factor Five!

Finally, for those who truly wish to "boldly go", you can try to simulate the Enterprise entering (and exiting) warp speed. To achieve this effect, we need to "streak" the stars as they move down the screen (as in the "hyperspace jump" sequences in the Star Wars movies, only from an "above" point of view). Pressing the right mouse button kicks us into warp drive, and releasing it drops us back to impulse engines only. As an extra touch, I draw all star streaks in red instead of white, for an impressive "doppler shift" effect

I decided that when engaging the warp drive, all the stars should start their streaking for ten frames of animation. The "small" stars streak as thin lines, but "large" stars streak as filled rectangles. Once ten frames of animated streaking has occured, I start moving the actual streaks in the same way as I moved the stars during non-warp. When coming out of warp drive, the stars reverse their streaking until they return to their original form. This whole "streaking" effect is a lot easier to grasp by seeing it rather than my poor attempt at a written explanation. The five images below are snapshots of the window just as the ship is entering warp drive, to give you an idea of what I'm talking about.

               

A lot of code had to be added to implement this warp effect, but the final result was worth the effort.

If you like, you can download the code for the entire program, which includes the photon torpedos and "warp speed" functionality.


written by Kevin L. Suffecool, copyright 1996