Time and the measurements that are based on
it are very important in video games. When it comes to animation,
time-based calculations allow for consistent updates to
occur in real time. When calculations are based on the frame rate, the
simulation can slow down or speed up based on the frame rate change. On
PCs there is no way to guarantee that a game will run at the same
performance rate from one machine to another.
The term frame-based calculation
refers to calculating some value every frame—for example, if the
position of an object was moved every frame like the following.
Therefore, the speed at which the object moves is
solely dependent on the frame rate. If there is a sudden drop in the
frame rate, the object will appear to move slower. Usually this is not
the effect developers have in mind, especially when they intend for an
object or some other calculation to be updated at a specific rate of
speed.
If you use time-based calculations, it does not
matter if a game’s frame rate suddenly drops to a crawl or jumps to very
high levels because the simulation updates consistently. For example,
if an object is being moved five units per second, it doesn’t matter
what the frame rate is because a second is still a second, regardless of
frame rate. Therefore, when the next update call is made, the elapsed
time is examined and the object is moved the distance it would have
moved during the time frame.
If too much time goes by between update calls, the
object can appear to jump from one location to another. This can also
be seen in online games where a lot of lag causes the updates of some
players to occur so far apart that a jumping effect can be observed.
However, by using time-based calculations, we can keep updates
consistent and accurate.
Line Path Animations
The first type of animation path we will look at is a
simple straight-line path. A straight line is made up of two end points
that go from point A to point B. When we move an object, we move the
object starting at point A, and it continues until it reaches point B.
We use linear interpolation to gradually move the object’s position from
point A to point B. For example, if an object has been linearly
interpolated between two points at 50%, then the object’s position is
midway between point A and point B. Take a look at Figure 1 for an example of this.
To
interpolate between two numbers, we can use linear interpolation.
Linear interpolation has been used for many things, from moving objects
to character animation. To perform linear interpolation, we need three
pieces of information. The first two pieces of information are the two
numbers between which we are interpolating. The third piece of
information needed is a scalar value that is a value from 0 to 1, with 1
being 100%. Using 0% will basically say we are at point A, and using
100% means we are at point B. Any value between 0% and 100% will place
us somewhere between point A and point B. The following equation makes
up linear interpolation, where Final is the interpolated value, A is
point A, B is point B, and dt is a percentage to interpolate between A
and B.
Final = (B - A) * dt + A;
To perform straight-line animation, we need point
A, point B, and a percentage. We can have a bunch of straight lines
that form a complete path, such a guard patrolling an area or a car
driving around a neighborhood block. For the mathematics we can use 3D
vectors for the end points and a floating-point value for the percent of
interpolation. Interpolating between a 3D vector is not that much
different than what we did for single numbers. The only real difference
is that we are interpolating three values (X, Y, and Z axes) instead of
just the one. An example of this can be seen as follows:
Final.x = (B.x - A.x) * Scalar + A.x;
Final.y = (B.y - A.y) * Scalar + A.y;
Final.z = (B.z - A.z) * Scalar + A.z;
Curve Path Animations
The
next type of animation path we will look at is the curve path. This
path goes from point A to point B in a curve instead of a straight line.
To create a curve with straight lines would take a lot of very small
lines connected to each another. The more lines you use, the smoother
the curve will look. The problem is that this is still not perfect and
would take a huge amount of data. A different way to create a curve path
is to use two points for point A to point B and two control points that
define the curve, giving it a total of four points. This type of curve
is known as a cubic Bezier curve. The control points are used to bend
the line into a curve, so these two values determine how the curve will
appear, while the end points specify the starting and ending locations.
Take a look at Figure 2 for an example of a cubic Bezier curve.
The straight line uses two points and a scalar
value to calculate the final position of the object. For the cubic
Bezier curve we use four points and a scalar value. The curve can bend
and twist in any way based on the position of the control points. Like
the straight-line path, a value of 0% places our object at point A, and a
value of 100% places our object at point B. The equation used to
calculate a position along this curve is not as simple as it is with a
straight line. The equation is shown as follows, where the points in the
equation are A for point A, B for point B, C1 for control point 1, and
C2 for control point 2. Also in the equation is S for the scalar, S2 for
(scalar * scalar) and S3 for (scalar * scalar * scalar).
Final = A * (1 – S)3 + C1 * 3 * S * (1 – S)2 + C2 * 3 * S2 * (1 – s) + B * S3
Routes
So
far we’ve look at a few different paths that can be used for moving
objects in a 3D scene. The paths by themselves are not really helpful in
representing a lot of different movements. It is only when we string
them together that we get something useful for our games. Using multiple
paths allows us to define an entire route of animation along which an
object can travel. This route can be made up of a lot of straight lines
or curve paths and, when character animation is applied, can add a huge
amount of detail and believability to our 3D games. Additional types of
paths that can be created to add to the mix include:
To create a route, we need to list the paths that
make up that route, and we then travel through each path. Once we hit
that end of a specific path, we move to the next path, reset the start
time, and travel along that until we hit the end of it. We keep doing
this until we’ve hit the last path, which would complete the route. At
this point we can either stop the movement, or we can loop and start it
all over again from the beginning. Starting from the beginning is the
best bet if we want to have our patrolling guards or other characters
moving around nonstop until something forces them to perform another
action—for example, if the gamer shoots at the character or anything
else that can cause the AI to take action.
Once we have a route system, we can take things
one step further by allowing the route information to be read in by a
file, and we can apply a route to our camera.Creating routes this
way will move us toward creating cut-scenes, which is something that is
very popular in today’s games.
Animation Paths Demo
Animation paths are predefined paths that, when
put together, create a route from one location to another along which a
character or object can travel. A collection of routes for a scene can
constitute a cut-scene if the routes are used for story-telling
purposes. Add a walking animation to characters when using animation
paths, and you will have the type of behavior that we see in many games.
An example of this can be seen in Call of Duty 4 for the Xbox 360 and PlayStation 3, where enemy characters
walk around patrolling an area. When you walk in their field of view,
the enemies react. This reaction is mostly aggression on the part of the
enemy AI character, but the reaction can be anything such as running
away, running from the character’s current location to the location of
the nearest alarm button, taking cover from possible enemy gun fire, and
so on.
We will create a class for each type of animation
path in our system. We will then create a class that will store a list
of paths that make up an entire route. The types of paths we will be
creating are straight-line paths and curve paths.
The Animation Path Classes
The demo specifies a base class called Path that has two classes that derive from it called StraightLinePath and CurvePath. The Path base class has two functions and three variables.
The Release() function from the Path
class is used to delete the next path in the list. The paths are
specified by using a simple link list setup, where each path has a
pointer to the next path in the list. The Release() function’s
purpose is to delete the path that is next in the link. Since we are not
using arrays, we need a way to release these objects from memory, and
in a link list this is done by traversing the nodes one at a time and
deleting them. Also, using a link list allows us to add paths to the
list without having to allocate or re-allocate an array every time the
list grows.
The GetPathPos() function from the Path class is used to get the position within the path based on the percentage (dt)
that is passed in the parameter. This is used to return to the caller
the time-based position that marks where the object is while it travels
along the current path.
The variables of the Path class are
straightforward. The first variable is the next pointer, which is used
for the link list behavior. The second variable is the start time of the
animation for the path. The third variable is the total time it takes
to travel along the animation path. The start time is equal to 0 if this
is the first path in the list, but if it is not the first in the list,
the start time equals the last path’s start time plus the last path’s
total time. In other words, the path, assuming it’s not the first in the
list, has a start time that equals the end time of the path that came
before it.
The Path base class is shown in Listing 1. Listing 2 shows the Path class’s constructor, destructor, and Release() functions. The GetPathPos() function is a virtual function that is to be implemented by each class that derives from the Path base class.
Listing 1. The Path Base Class
class Path
{
public:
Path();
~Path();
void Release()
virtual Vector3D GetPathPos(float dt) = 0;
public:
float m_start;
float m_total;
Path *m_next;
};
|
Listing 2. The Path Class’s Functions
path::path()
{
m_start = 0;
m_total = 0;
m_next = NULL;
}
Path::~Path()
{
Release()
}
void Path::Release()
{
if(m_next)
{
m_next->Release()
delete m_next;
m_next = NULL;
}
}
|
The StraightLinePath class has a member variable for the start position and one for the end position, and it implements the GetPathPos() function from the Path base class. The StraightLinePath class declaration is shown in Listing 3. Listing 4 shows the class’s constructor, which sets the two member variables, and the GetPathPos() function. In the GetPathPos() function, the equation we looked at earlier for the linear interpolation is used for the line.
Listing 3. The StraightLinePath Class Declaration
class StraightLinePath : public Path
{
public:
StraightLinePath(Vector3D start, Vector3D end);
Vector3D GetPathPos(float dt);
public:
Vector3D m_startPos;
Vector3D m_endPos;
};
|
Listing 4. The StraightLinePath Class Functions
StraightLinePath::StraightLinePath(Vector3D start, Vector3D end)
{
m_startPos = start;
m_endPos = end;
}
Vector3D StraightLinePath::GetPathPos(float dt)
{
return ((m_endPos - m_startPos) * dt + m_startPos);
}
|
The CurvePath
class has a member variable for the start position, one for the end
position, and two positions for each control point, and it implements
the GetPathPos() function from the Path base class. The CurvePath class declaration is shown in Listing 5, and the class’s functions are shown in Listing 6.
Listing 5. The CurvePath Class Declaration
class CurvePath : public Path
{
public:
CurvePath(Vector3D p1, Vector3D c1,
Vector3D c2, Vector3D p2);
Vector3D GetPathPos(float dt);
public:
Vector3D m_p1;
Vector3D m_control1;
Vector3D m_control2;
Vector3D m_p2;
};
|
Listing 6. The CurvePath Class Functions
CurvePath::CurvePath(Vector3D p1, Vector3D c1,
Vector3D c2, Vector3D p2)
{
m_p1 = p1;
m_control1 = c1;
m_control2 = c2;
m_p2 = p2;
}
Vector3D CurvePath::GetPathPos(float dt)
{
return (m_p1 * (1.0f - dt) * (1.0f - dt) * (1.0f - dt) +
m_control1 * 3.0f * dt * (1.0f - dt) * (1.0f - dt) +
m_control2 * 3.0f * dt * dt * (1.0f - dt) +
m_p2 * dt * dt * dt);
}
|
Each of these classes can be found in Route.h and Route.cpp of the Animation Paths demo’s source files.