Implementing Steady Motion

Home | Up | Search | X-Zone News | Services | Book Support | Links | Feedback | Smalltalk MT | The Scrapyard | FAQ | Technical Articles

 

Implementing Steady Motion Control

Written by Robert Dunlop
Microsoft DirectX MVP


Related Articles of Interest:

Introduction to Catmull-Rom Splines
Writing the Game Loop

The Problem at Hand

Even if our application is throttled to a specific frame rate through a timer, the actual time between frames will vary.  Often this leads to a jerky appearance in graphics, as the apparent motion of the game speeds up and slows down erratically.  This can also lead to difficulties in matching the playing characteristics between users in a multi-player environment.

To compound this issue, the screen is divided into discrete pixels, making compensation for slight variations difficult.  In this article we will look at ways to compensate for timing variations, as well as exploring the use of floating point coordinates in 2D applications to allow for continuous motion control.

Three Types of Granularity

While we are usually quick to blame system overhead and machine performance, much of our problems arise from a characteristic known as "granularity" - that is, they cannot be measured or controlled in a linear fashion, but rather suffer from limits in resolution or precision.  Let's start off by taking a look at three factors that exhibit granularity, and then we will explore some solutions.

Video Refresh Rate

To avoid screen tearing, which occurs when changes are made to the portion of the screen currently being re-generated by the monitor, DirectDraw delays page flipping until the next vertical refresh.  This makes use of the "blanking interval", which is the time during which the electron gun of the monitor is turned off, so that it can return from the lower right corner of the monitor to the upper left, so that it can begin drawing the next page.

This can cause some real issues for us, because if our rendering function completes immediately after a refresh, it will mean that our surface will not flip until one full refresh period later.  At a rate of 60 Hz, this means that a delay that made us miss the refresh by 100 us (microseconds) will actually cost us almost 17 ms (milliseconds), multiplying the delay by almost 200 fold!

Screen Resolution

Because the screen is divided into discrete pixels, we are limited in our ability to move an object in a continuous fashion.  In particular, it limits us to moving objects in  integer multiples of a pixel.

We do not see this issue in 3D applications because positions are represented as  floating point values, and are not converted to integer pixel values until rasterization.  By the same token, we can represent our 2D positions as floating point values, to give use the ability for continuous motion.

Timer Resolution

The use of a timer with sufficient resolution is very important to achieve smooth motion in a game title.  Selecting the wrong timer function may result in resolution as low as 55 ms.  At a refresh rate of 60 Hz, that works out to just over 3 refresh cycles, which is far from sufficient for driving today's game titles.

To find out more about the resolution and accuracy of various timer functions, check out the article Selecting Timer Functions.

Now that we have an idea of the nature of the problem we face, let's move on to look at some solutions.

Providing a Time Base

The first step we must take is to provide our object motion and rendering routines with a measure of the time that has elapsed for each frame, so that we can compensate for variations in frame time.  We will use the following code to provide a time base:

    Global Variables

BOOL perf_flag;        // Timer Selection Flag
double time_factor;    // Time Scaling Factor
LONGLONG last_time;    // Previous timer value

    Initialization - Perform once at at start of program

// is there a performance counter available?

LONGLONG perf_cnt;
if (QueryPerformanceFrequency((LARGE_INTEGER *) &perf_cnt)) {

    // yes, timer choice flag

perf_flag=TRUE;

// set scaling factor

time_factor=1.0/perf_cnt;

// read initial time

QueryPerformanceCounter((LARGE_INTEGER *) &last_time);

} else {

// no performance counter, read in using timeGetTime

last_time=timeGetTime();

// clear timer selection flag

perf_flag=FALSE;

// set timer scaling factor

time_factor=0.001;

}

    Execute Each Frame to Establish Time Base

LONGLONG cur_time;        // current timer value
double time_span;         // time elapsed since last frame

// read appropriate counter

if (perf_flag)
   QueryPerformanceCounter((LARGE_INTEGER *) &cur_time);
else
   cur_time=timeGetTime();

// scale time value and save

time_span=(cur_time-last_time)*time_factor;

// save new time reading for next pass through the loop

last_time=cur_time;

We now have a measure of the elapsed time for the previous frame, which we can in turn pass on to our functions for object motion, for example:

move_objects(time_span);

So now let's take a look at how we can apply this time base.... continue on to the next page of this article for more.

Calculating Motion at a Constant Velocity

We are probably all fairly familiar with the calculation of distance traveled at a constant velocity:

Distance = Velocity x Time

Given that we are providing our motion control routine with a time base that reflects the amount of time that has elapsed since the last frame, we can easily apply this information to compute the distance that an object will travel during that frame.

The first thing that we must do, if we are not already, is use floating point values to represent the positions of our objects.  If you are using Direct3D, you are already using a floating point representation for vectors.  However, many 2D applications start their lives based on integer based routines.

Once we are working with a floating point position, calculating movement is simply a matter of multiplying speed of travel (in units/pixels per second) by the time elapsed since the last frame.  For example:

For 2D motion, given:

    x_pos, y_pos : Current position
    x_vel, y_vel : Speed in pixels / second

x_pos += x_vel * time_span;
y_pos += y_vel * time_span;

For 3D motion, given:

   loc : D3DVECTOR containing current location
    vel : D3DVECTOR containing velocity in units / second

loc += vel * time_span;

Implementing the Acceleration Curve

Calculating motion at a constant velocity helps, but how did we get to that speed?  If we change velocity suddenly, in fixed steps, this will appear jerky.  If you need a visual, think back to "Space Invaders" with it's single velocity in either direction.

To avoid this, we need to implement an acceleration curve.  The calculation for the effect of acceleration on velocity is very similar to our previous equation for velocity to distance :

Velocity = Initial Velocity + Acceleration x Time

By nature, acceleration is a constant change, defined by this linear function.  However, in our program loop, we deal with this in finite increments of time.

If we are re-calculating the velocity several times a second, this is normally not a problem.  The error is minimal, and will not be noticeable in most applications.  In this case, we can just augment our functions from above to utilize acceleration:

x_vel += x_acc * time_span;
x_pos += x_vel * time_span;

In some cases, however, this incremental approach will not be sufficient.  These include functions that will be calculated at long or sporadic intervals, and applications where acceleration is a deciding factor in the game, such as racing games or multiplayer games that must use dead-reckoning to estimate player positions between updates.

In such cases, we must pull a bit of calculus out of the closet, and integrate the acceleration over time.  The final velocity is still calculated in the same manner, but we must calculate the final position differently:

x_pos += x_vel * time_span + x_acc * time_span * time_span * 0.5f;
x_vel += x_acc * time_span;

Note that in this case we wait to calculate the final velocity until after calculating position, as we need starting velocity in the distance calculation.

Next, we'll take a look at some real world uses of acceleration.

What Goes Up, Must Come Down

One use for acceleration in games is the simulation of gravity.  Though we may tend to think of the effect of gravity as applying weight, it is basically an accelerating force.  Normally it does not cause us to accelerate, as we are prevented from accelerating towards the Earth's core by solid objects that limit our downward movement - the floor, a chair, etc.

When we are falling, however, it becomes quite apparent that we are accelerating.  Gravity acts as a constant accelerating force, causing an acceleration of 9.8 meters per second / per second.

Simulating gravity is relatively simple.  Assuming an object is not blocked from falling, all we need to do is increment the velocity by a constant that provides the desired acceleration :

y_vel -= 9.8;    // assumes down to be negative, with 1 unit per meter

However, falling objects don't accelerate forever, assuming there is an atmosphere around them.  Wind resistance acts upon an object, providing a decelerating force that is proportionate to the velocity of the object.  Thus, the resistance increases as speed increases, to a point that it becomes equal to the acceleration of gravity.  This point is called "terminal velocity", and is the fastest that a falling object will achieve.

To provide for wind resistance, after accelerating the object we multiply it by a constant, equal to 1.0 minus a drag coefficient, which is the percentage of velocity that is encountered as a resisting force:

y_vel *= 0.9;    // assumes a 10% coefficient of drag

Wind resistance will also apply on the X and Z axis as well, which are not effected by gravity.  So, if we wished to act upon an object with an initial trajectory already established, we could combine these forces like so:

vel.y -= 9.8 * time_span;
float resist = 1.0 - 0.1 * time_span;
vel *= resist;
pos += vel * time_span;

Often, such an operation will be individually on a collection of small objects, in the case of particle systems.  For example, it we wanted an animated fountain, we can launch droplets at a random arc from a common point, and track their trajectory.  When a droplet reaches the ground, it is "recycled", launching it from the fountain nozzle again to keep a steady volume of particles in suspension.

Smooth Motion Control from User Input

One last caveat before we wrap this up....  Another great use for acceleration curves is in providing smooth motion from user input.  This is especially useful in scenarios such as keyboard or switch based gamepad  controls.  Simply setting a velocity when a key is pressed leads to very jerky motion.

Instead, we can set an acceleration while the control  is pressed, followed by a deceleration when the control is released.  For example :

if (right_pressed && vel<10.0) {

vel+=5.0 * time_span;

} else if (left_pressed && vel>-10.0) {

vel-=5.0 * time_span

} else {

double decel=((vel<0)?-5.0:5.0) * time_span;
if (fabs(decel)>fabs(vel))
    vel=0.0;
else
    vel-=decel;

}

pos += vel;

The above code will provide acceleration for as long as the control is depressed, and then slow to stationary at a constant rate when the controls are released.

 

Related Articles of Interest:

Introduction to Catmull-Rom Splines
Writing the Game Loop

This site, created by DirectX MVP Robert Dunlop and aided by the work of other volunteers, provides a free on-line resource for DirectX programmers.

Special thanks to WWW.MVPS.ORG, for providing a permanent home for this site.

Visitors Since 1/1/2000: Hit Counter
Last updated: 07/26/05.