Sprites Make the World Go Round

Dale Rogerson
Microsoft Developer Network Technology Group

Created: April 29, 1994

Click to open or copy the files in the Gravity sample application for this technical article.

Abstract

In his article titled "Animation in Win32" in the Microsoft® Development Library, Mr. Herman Rodent shows how to implement sprite-based animation in Win32® environments such as Windows NT, Windows 95 (the next version of the Windows® operating system, formerly called "Chicago"), and Windows 3.1 (under Win32s™) using the Microsoft Foundation classes (MFC). While Mr. Rodent's article discusses the technical issues involved in implementing sprite-based animation, this article describes how to use Mr. Rodent's code for something worthwhile: to simulate the gravitational interactions of heavenly bodies (that is, planets). The Gravity sample application implements the concepts described in this article.

Introduction

Let's go way back with Mr. Peabody to my college days. A friend of one of my roommates was getting a doctorate degree in mathematics. He claimed to have this wonderful algorithm for simulating Newton's Law of Gravitation. For some strange reason, he thought that this simulation would make a fun shareware program that he wanted me to help him write. I thought that this would be very boring, so I kept putting him off until he finally went back to Europe.

About a year later, the school put together a Macintosh® lab. One day, I went to the lab to play with the Mac®. One of the programs on the Mac was a two-dimensional Newton's Law of Gravitation simulation. This had to be one of the most enjoyable computer programs I had ever encountered!

I ran home and coded up a quick version of the simulation program for my Radio Shack TRS-80® Model III. Boy, was that a slow simulation. . . . It was so slow, it practically ran in real time. It represented the earth with the letter "E" and the moon with the letter "m", and it was still slow.

Because my TRS-80 was so slow, I put aside my gravitational simulator project until the September 1989 issue of Dr. Dobb's Journal published an article titled "Force-based Simulations" by Todd King. I converted the C++ code published in the article to Turbo Pascal® (yes, a Borland product) and ran it on a Tandy® 1000. It was still slow, and the earth was still represented by an "E", so I shelved the project once again and turned to more pressing tasks.

One day, all the pieces fell together. I got a new Pentium™ system at work, installed Windows NT™ 3.5, and Herman Rodent finished his article on sprite animation in Win32®. Until Herman evangelized the implementation of sprites for 32-bit Windows™ environments, book and magazine writers had been avoiding the subject or providing poor solutions.

Thus, with an arsenal of really cool tools and code, I once again went forth to move the world.

Note   After I finished developing the Gravity application, someone pointed me to Appendix A ("A Personal View of the C++ Language") in David J. Kruglinski's book Inside Visual C++ (Kruglinski 1993, 529). Mr. Kruglinski uses space simulation to demonstrate many of the features of the C++ language. This appendix is great for beginning and intermediate C++ users; it explains the intricacies of the C++ language very clearly. Mr. Kruglinski does, however, leave the sprite animation as an exercise for the reader.

Gravity

The Gravity sample application is fairly easy to use. Choose the Load command from the File menu to load the EM.GVY (earth–moon) simulation. After loading, the screen should display a bitmap of space (sorry, I drew this myself—I couldn't find a cool space picture), the planet earth (close enough), and the moon (a gray object that looks like a bowling ball). Clicking the green button on the toolbar starts a simulation of the moon rotating around the earth. Clicking the red button on the toolbar stops the simulation. Clicking the button labeled Xi sets the planets back to their initial position and velocity.

Double-clicking a planet brings up a dialog box from which you can set the initial position, velocity, and the mass of the planets. You can use this dialog box to see how different masses and velocities affect the simulation. Take a look at emc.bmp—it's fun to see the interactions of three planets.

If you want to create your own simulation, select the New command from the File menu. Load a background (such as space.bmp) with the Load Background command from the File menu. Use the Load Sprite command from the File menu to load the planets that you want in your simulation. Double-click the sprites to set their parameters.

In the Beginning

Like most programmers, I want to avoid writing (or rewriting) as much code as possible, so I like to start a program either by using another program as the base or by asking AppWizard to generate an application for me. To build Gravity, I used a combination of AppWizard and code from Herman Rodent's anim32 sample application. I believe Newton built real gravity from apples.

  1. Ask AppWizard to create a single-document interface (SDI) application called Gravity with toolbars but no print preview.

  2. Copy the following files from ANIM32 to Gravity:

    ANIMVW.H
    ANIMVW.CPP
    ANIMDOC.H
    ANIMVW.CPP
    DIB.H
    DIB.CPP
    DIBPAL.H
    DIBPAL.CPP
    OSDIBVW.H
    OSDIBVW.CPP
    PHSPRITE.H
    PHSPRITE.CPP
    RECTLS.H
    RECTLS.CPP
    SPLSNO.H
    SPLSNO.CPP
    SPRITE.H
    SPRITE.CPP
    SPRITEDL.H
    SPRITEDL.CPP
    SPRITELS.H
    SPRITELS.CPP
    SPRITENO.H

  3. Edit the project file. Remove the gravidoc.h document class and the gravivw.h view class, and add the .CPP files listed above.

  4. Edit CGravityApp::InitInstance in the GRAVITY.CPP file. Change:
    AddDocTemplate(new CSingleDocTemplate(IDR_MAINFRAME,
    RUNTIME_CLASS(CGravityDoc),
    RUNTIME_CLASS(CMainFrame),
    RUNTIME_CLASS(CGravityView)));
    

    to:

    AddDocTemplate(new CSingleDocTemplate(IDR_MAINFRAME,
    RUNTIME_CLASS(CAnimDoc),
    RUNTIME_CLASS(CMainFrame),
    RUNTIME_CLASS(CAnimView)));
    

    You have just modified Gravity so that it uses the CAnimView and CAnimDoc classes (which are part of ANIM32) instead of the CGravityView and CGravityDoc classes.

  5. Use AppStudio to copy the resources from Anim32 to your new application. Make sure to copy the toolbar buttons, dialogs, icons, and menus that you want.

  6. Modify mainfrm.cpp to add the toolbar button ids.

  7. Change the references to the following include files:
  8. If you do not want to use the ANIM32 debug window, comment out all references to dprintf*, dgbGetTime, and dgbShowElapsedTime. Use the Find In Files command from the Edit menu to find the occurrences of these functions. If you do want to include the debug window, look at ANIM32.H and ANIM32.CPP for the required statements.

  9. Change the CDIBPal::SetSysPalColors function. Modify the line:
    HWND hwndActive = ::GetActiveWindow() ;
    

    to:

    HWND hwndActive = NULL ;
    

    Remove the following ASSERT statement:

    ASSERT(hwndActive) ;
    

Now that we've made the necessary changes to the ANIM32 code, we are ready to code Gravity's own features.

Replacing CAnimSprite

I wanted to see planets rotating as soon as possible, so I started by replacing the CAnimSprite class with my own class, CBody, which would control the planetary bodies. The use of the CSprite class is similar to CView in the Microsoft® Foundation classes. Instead of using a CView object directly, you usually create your own view object, which inherits from CView but has its own special routines. Similarly, you don't use a CSprite object directly, but instead create your own sprite class, which inherits from CSprite. The figure below compares the view hierarchy with the sprite hierarchies in ANIM32 and Gravity.

Class hierarchies

I removed CAnimSprite from the project and created the body.h and body.cpp files to hold the code I needed for my CBody objects. CAnimDoc and CAnimView refer to the CAnimSprite class explicitly, so I had to change all the CAnimSprite references to CBody. The browser in Visual C++™ allowed me to make these changes easily. I did have to choose the Include Local Variables check box in the C/C++ Compiler Options dialog box to find all occurrences of CAnimSprite. (To get to this dialog box, select the Project command from the Options menu, click the Compiler button, and select Listing Files from the Category list on the left.)

I modeled CBody after CAnimSprite, so the two classes share a number of features. They both:

Of course, there are some differences between CBody and CAnimSprite. (Why else would I bother writing CBody?) CBody positions the planets in a 108-by-108 universe, while CAnimSprite places the sprites on the screen. Therefore, CAnimSprite stores position and velocity with integers while CBody uses double-precision floating-points. As a result, I had to modify CAnimSprite::DoDialog to create CBody::DoDialog. I also had to munge the CSpriteDlg class to work with CBody. Munging was faster than recreating all of the code.

Apply Some Force

The real difference between CBody and CAnimSprite is what happens each time we update the sprites. A CAnimSprite object has a velocity, and moves each time it is updated. A CBody object also has a velocity, but its velocity is affected by other CBody objects in the universe. To determine what we need to change, let's start at the beginning.

In Gravity, the Big Bang is in CAnimApp::OnIdle. Digging through the volumes of code required to find the document object, we find that OnIdle calls CAnimDoc::UpdateSpritePositions, which is listed below. (I've removed all ASSERT statements for clarity.)

// Update the positions of all the sprites.
BOOL CAnimDoc::UpdateSpritePositions()
{
    int i = 0;
    POSITION pos;
    CAnimSprite *pSprite;

    if (m_spList.IsEmpty()) return FALSE; // no sprites
    for (pos = m_spList.GetHeadPosition(); pos != NULL;) {
        pSprite = (CAnimSprite *)m_spList.GetNext(pos);
        i += pSprite->UpdatePosition(this);
    }
    if (i) {
        // Draw the changes.
        UpdateAllViews(NULL, HINT_DIRTYLIST, 0);
    }
    return TRUE;
}

The code above loops through the sprites and updates each sprite position by calling CAnimSprite::UpdatePosition. We need to modify UpdateSpritePosition to apply Newton's Law of Universal Gravitation to each CBody object before the function updates the sprite positions. The UpdateSpritePosition function used in Gravity is listed below. (I've removed all ASSERT statements for clarity.)

BOOL CAnimDoc::UpdateSpritePositions()
{
   CBody* pBodyI ;
   CBody* pBodyJ ;
   POSITION i,j ;
   if (m_spList.IsEmpty()) return FALSE; // no sprites

   i = m_spList.GetHeadPosition();
   while(i != NULL )
   {
      pBodyI = (CBody *)m_spList.GetNext(i); 
      j = m_spList.GetHeadPosition() ; 
      while(j != NULL)
      {
         pBodyJ = (CBody *)m_spList.GetNext(j); 
         if ( j != i)
         {
             pBodyI->ApplyForce(pBodyJ);
         }
      }   
   }

   i = m_spList.GetHeadPosition();
   while (i != NULL)
   {
      pBodyI = (CBody *)m_spList.GetNext(i);
      pBodyI->Update() ;
   }

   UpdateAllViews(NULL, HINT_DIRTYLIST, 0);
   return TRUE;
}

The code above shows that CBody needs two new member functions: CBody::ApplyForce and CBody::Update. CBody::ApplyForce applies Newton's Law of Universal Gravitation:

void CBody::ApplyForce(CBody* pbody)
{
   CVector d ;
   double rs, r, v, vr ;

   d.x = m_position.x - pbody->m_position.x ;
   d.y = m_position.y - pbody->m_position.y ;

   rs = (d.x * d.x) + (d.y * d.y) ;
   if (rs != 0.0)
   {
      r = sqrt(rs) ;
      v = (pbody->m_gmass / rs) * CUniverse::SECS_PER_TICK ; 
      vr = v / r ;
      m_velocity.x += vr * d.x ;
      m_velocity.y += vr * d.y ;
   }
}

CBody::Update updates the position and velocity of each CBody object. It is equivalent to CAnimSprite::UpdatePosition. The code for CBody::Update is listed below:

void CBody::Update()
{
   m_position.x += m_velocity.x * CUniverse::SECS_PER_TICK ; 
   m_position.y += m_velocity.y * CUniverse::SECS_PER_TICK ;

   // Cycle through the phases.
   if (GetNumPhases() > 1)
       SetPhase((GetPhase()+1)%GetNumPhases());

   // Set the sprite screen position...
   SetSPosition() ;
}

void CBody::SetSPosition()
{
   int x = (int)(m_position.x * s_scale.x) ;
   int y = (int)(m_position.y * s_scale.y) ;
   CPhasedSprite::SetPosition(x,y) ;
}

You may remember that CBody inherits from CPhasedSprite. If we want the user to see phases, we must write some code to cycle through the phases. Luckily, Herman Rodent has already figured out some clever code to do this for us, so we simply copy:

if (GetNumPhases() > 1)
   SetPhase((GetPhase()+1)%GetNumPhases());

from CAnimSprite and put it in our own code. (In your code, you may want to encapsulate these lines in a NextPhase function.)

Bitmaps

I used bitedit, which is available in the Microsoft Development Library as an unsupported tool (under Unsupported Tools and Utilities, Windows Tools), to build the images that I used in Gravity. If you remember from reading "Animation in Win32", we need to make sure that the palettes for our planets are the same as the background palette. To ensure that the palettes were the same for all of my images, I used bitedit to make the palette an "identity" palette. (This is really a misnomer because you can generate a true identity palette only at run time; it's not something that you can pass from one system to another.) All of my bitmaps use only the 20 system colors. Third-party tools such as PhotoStyler might make this process easier.

I also used bitedit to build the phased earth bitmap (earth-p.bmp). I first rotated the earth bitmap (EARTH.BMP), and then combined the rotated earth images into one bitmap. This was a real pain. If you plan on using this approach for creating phased sprites, you should write a tool to build a phased sprite bitmap from separate bitmaps. Don't plan on using the Toolbar Editor in AppStudio to build the phased bitmap—AppStudio works only with 16-color bitmaps.

Ripping Out Code

anim32 had many features that I didn't want to use, so I ripped them out. One of these features was the ability to drag the sprites with the mouse. I removed all of the mouse handlers except OnLButtonDblClk, so CBody does not need the IsSelectable member function that CAnimSprite contained.

CBody also does not override the CPhasedSprite::InitFromDib function because it does not need to initialize anything after a file load. CAnimSprite implemented CPhasedSprite::InitFromDib, but the function didn't do anything that the CPhasedSprite implementation didn't already handle.

Using the Timer

You don't need a timer to do simulations; you can use CMainFrame::OnIdle as ANIM32 does. However, I wanted to use a timer so that I could have a little more control over my simulation. I added two functions to start and stop the timer: CAnimView::OnSimulateGo and CAnimView::OnSimulateStop.

void CAnimView::OnSimulateGo()
{
   CAnimDoc*pDoc = GetDocument() ;
   if (pDoc->AnySprites())
   {
      if (pDoc->m_bSimulate == FALSE)
      {
         m_timer = SetTimer(G_TIMER,CUniverse::TIME,NULL) ;
         if (m_timer != 0)
         {
            pDoc->m_bSimulate = TRUE ;
            return ;
         }
      }
   }
   MessageBeep(0) ;
}

void CAnimView::OnSimulateStop()
{
   CAnimDoc*pDoc = GetDocument() ;
   if (pDoc->m_bSimulate == TRUE)
   {
      KillTimer(m_timer) ;
      m_timer = NULL ;
      pDoc->m_bSimulate = FALSE ;
   }
   else
   {
      MessageBeep(0) ;
   }

I nuked the CAnimApp::OnIdle function and replaced it with CAnimView::OnTimer:

void CAnimView::OnTimer(UINT nIDEvent)
{
   COffScreenDIBView::OnTimer(nIDEvent);
   CAnimDoc *pDoc = GetDocument();
   ASSERT(pDoc->m_bSimulate == TRUE) ;
   pDoc->UpdateSpritePositions();
}

The Boolean variable, CAnimDoc::m_bSimulate, tracks the simulation and enables or disables menu commands such as the Simulate menu Reset command, which is handled by CAnimDoc::OnUpdateSimulateReset:

void CAnimDoc::OnUpdateSimulateReset(CCmdUI* pCmdUI)
{
   // Allow a reset only if we are not simulating.
   pCmdUI->Enable(!m_bSimulate) ;
}

Note that CAnimDoc has handlers to disable the File Save and File Save As commands although the MFC framework still handles the functions themselves.

SDI Applications Don't Delete Views

Single-document interface (SDI) applications are different from multiple-document interface (MDI) applications in many ways. The difference that concerns us here is that the view object in an SDI application is not destroyed until the SDI application is closed. If the user chooses the File New command, we cannot depend on the code in the view class destructor or in the OnDestroy message handler to clean up for us.

Instead, we need to make sure that we clean things up in OnInitialUpdate. What I am particularly worried about is the timer. I don't want the timer to be running after the user has selected the File New command, so I kill the timer in CAnimView::OnInitialUpdate:

void CAnimView::OnInitialUpdate()
{
   if (m_timer)
   {
      VERIFY(KillTimer(m_timer)) ;
      m_timer = NULL ;
   }
}

The File New command does not destroy and reconstruct the document class either, so we cannot rely on the destructor to reset the m_bSimulate variable. Instead, I used DeleteContents to reset this variable:

void CAnimDoc::DeleteContents()
{
...
   m_bSimulate = FALSE ;
...
}

Conclusion

So there you have an (unbiased) testimonial proving how easy it is to build sprite-based applications with MFC and Herman Rodent's sprite class. Herman Rodent's anim32 sample application is a good starting point for building your own sprite-based application.

This is a long way from the old "E" and "m" simulations I used to play with—we now have full 256 color images, rotating planets, and file loading. Great stuff. If anyone comes up with some cool simulations or better bitmaps for the planets, send them my way. Maybe that old friend of mine will be happy that I finally wrote the simulation program.

Bibliography

King, Todd. "Forced-based Simulations," Dr. Dobb's Journal (September 1989): 40-42.

Kruglinski, David J. Inside Visual C++. Redmond, WA: Microsoft Press, 1993.

Rodent, Herman. "Animation in Win32." February 1994.

Stanford, A.L., and J.M. Tanner. Physics for Students of Science and Engineering. Orlando, FL: Academic Press, 1985.