In the previous post, we discussed how to draw graphics to a window on the screen. This is great, but a static image doesn’t really make for an interesting game. If only we had a way to move our graphical objects around!

As it turns out, we do! Hopefully to no one’s surprise. Animation is an essential aspect of most computer games, and in this post we will start delving into some very basic techniques for that.

Check out the commit named “Introduction: Animation” in the GitHub repository to see the complete code for this post.

The concept of animation simply means that the image we see on the screen is changing over time. Although in a strictly technical sense, an image on the screen is static and cannot change. What we can do, however, is create an illusion of change by rapidly drawing new images, each one slightly different from the previous.

For our first animation, we will simply take the static objects we created in the previous post, and move them around. There are two ways to do this. Back in the days of the Commodore 64 and other similar computers with a fixed clock frequency, it was quite common to do frame-based animation. This means that we adjust the animation state by a fixed amount each time we render a new frame. On a modern computer where the clock frequency can vary greatly, this is usually not the best approach. In the vast majority of use cases, time-based animation is a far superior choice. This means that rather than adjusting by a fixed rate, we instead check how much time has actually elapsed since the previous frame, and adjust the animation state based on that.

That being said, the most robust way to control animations is actually a hybrid solution where we combine the two approaches to get the best of both worlds. We will probably try to implement this at some point, but for now, a simpler solution will be sufficient.

In our code, we introduce a global variable to keep track of elapsed time:

static float g_time = .0f;

In WinMain(), we will create a timer that sends a WM_TIMER event to our main window every 20ms:

SetTimer(mainWindow, 0, 20, nullptr);

The next step is to catch the event in WindowProc(). This is done by adding a WM_TIMER case to the message switch:

      case WM_TIMER:
         g_time += .02f;
         InvalidateRect(windowHandle, nullptr, TRUE);
         return 0;

Here we add our 0.02 seconds (20ms) to the g_time variable, and then call InvalidateRect() to inform Windows that the window needs to be refreshed. In practice, the call to InvalidateRect() simply causes a new WM_PAINT event to be raised.

That’s it – our very simple animation system is complete. But if we run our application at this point, we still won’t see any movement. This is because we are still drawing our object in the same place, and in the same way, in every frame. So let’s modify our DrawToWindow() function to make it dependent on the g_time variable, where we store our elapsed time. We’ll begin by calculating a couple of intermediate values, which we will be using as displacements for our objects:

   constexpr float AMPLITUDE = 100.f;
   constexpr float TIMESCALE = 3.f;
   const int displacement1 = static_cast<int>(AMPLITUDE + sin(g_time * TIMESCALE) * AMPLITUDE);
   const int displacement2 = static_cast<int>(AMPLITUDE + cos(g_time * TIMESCALE) * AMPLITUDE);

To produce smooth movement within a limited range, we are using the sin() and cos() functions. If you are unfamiliar with the mathematical concepts of sine and cosine, now would be a good time to read up on them, because they will be very important later on in this series.

The AMPLITUDE constant specifies the distance the objects will be moving over, while the TIMESCALE constant determines how fast they move.

Anyway, simply calculating these displacements will not change anything. We need to actually use them in our calculations. Let’s change our calls to Rectangle() and Ellipse() into the following:

  Rectangle(deviceContext, 20 + displacement1, 20, 150 + displacement1, 150);
  Ellipse(deviceContext, 180 + displacement1, 20, 400 + displacement1, 150);

This makes the positioning of the objects dependent on the displacements we have calculated, meaning our objects should be moving as time passes.

In the code on GitHub, I have also adjusted the placement of the other graphical objects in a similar way. Now the rectangles and the ellipses will move horizontally, while the text will move in a circle, like this:

As we can see, this works pretty nicely. However there is a slightly annoying flicker going on, especially for the text. This is expected, and it can be solved using a technique called double buffering.

As a final remark, I want to comment briefly on some changes I made to the window creation code in WinMain(). Instead of just creating the window with default values, I am now setting a specific width and height. The purpose of this has nothing to do with the animation itself. It is only there to simplify the process of capturing the window contents using third-party video capture software. The width and height that we pass to CreateWindow() will be interpreted as the size of the complete window including borders and the caption bar, which is not what we want. Ideally, we want to specify the dimensions of the client area of the window. In order to adjust for this, we use the Windows API function AdjustWindowRectEx(). Also, in order for the application to adjust for DPI scaling, we issue a call to SetProcessDpiAwarenessContext() before creating the window.

In the next post, we will implement double buffering to get rid of the flickering in the animation.

By alfred

Leave a Reply

Your email address will not be published. Required fields are marked *