Designing An Object Oriented Animation Engine
Introduction to Animation Engines
So far, we've covered many of the core elements of an animation,
including the Graphic Object, Animation Object, Camera, Frustum, Layers
and Layer Folders. This article (and the next) will bring them all
together to design an Object Oriented Animation Engine.
Video games and Flash are examples of animation engines. An animation
engine is the code in the animation application that drives the
animation. The animation engine has many responsibilities:
- Taking drawing data (animation objects, layer arrangements) and events as input
- Delegating the rendering tasks to animation objects
- Making sure that drawing is done in the order specified by layers and layer folders
- Scene graph management
- Collision detection (in games)
- Satisfying real time drawing requirements ( if any)
- Managing memory for animation objects, layers and layer folders.
A good animation engine should be able to handle all kinds of drawing
and animation implementations without knowing how they are implemented.
A good animation engine will not know whether the rendering is done
with OpenGL or Direct3D. It will not know which platform-specific
functions are used for keeping time; it shouldn't even know how memory
is allocated. It must deal only with abstractions that are not platform
or library dependent. It can then cater to a wide variety of animation
applications on a variety of platforms. Animation engine design
therefore is one of the ultimate challenges for an Object Oriented
designer/programmer.
Animation Object examples
Let us build some animation objects based on the discussions thus far. The objects discussed in this article are:
- An animation object that will move the camera (AnimCamera).
- An animation object that will draw a Graphic Object (class AnimFace), built on a simple Graphic Object, namely a rectangular GraphicFace.
- An animation object for moving a Graphic Object (class AnimTranslate). It will cause the Graphic Object in AnimFace to move.
Camera based on OpenGL API
Before discussing the class AnimCamera, let's see how the camera
works in a graphics API such as OpenGL (this discussion is based on the C
API for OpenGL). Imagine a scene that contains several objects. Each
object is drawn at a specific location in the virtual world. Let's call
the virtual world coordinates "world coordinates" and use the prefix
"world_". These world coordinates are mapped onto the screen to "screen
coordinates" using prefix "screen_". When I use a triplet (x,y,z) for
coordinates, that means world coordinates (since the real world has 3
dimensions). If the coordinates are described as a pair (x,y), these are
screen coordinates, since screens are flat.
By default, the scene is drawn with the viewer at the origin of the
virtual world's 3D Cartesian coordinate system, i.e. at
(world_x,world_y,world_z) = (0.0, 0.0, 0.0). The center of the screen
will represent the very original of the world, (screen_x,screen_y) =
(0.0, 0.0). In this case, the world X-axis increase as we move across
the screen to the right, and the world Y-axis increases as we move
higher up the screen. The world Z-axis increases "into" the screen, away
from the viewer.
As discussed in Article 1, only objects located inside the clipping
planes of the frustum get displayed. For purposes of this discussion,
consider the distance between the camera and the near face of the
frustum to be equal to 1.0 unit in world coordinates. The center of the
near face of the frustum has 3D world coordinates (0.0, 0.0, 1.0).
Imagine two points of a Graphic Object, one located at world coordinates
(0.0, 0.0, 2.0) and the other at (1.0,1.0, 1.0). (Choosing these
particular points allows us to avoid the details of matrix
transformations that occur when the camera is displaced or oriented.)
By choosing the point (0.0, 0.0, 2.0) in world coordinates, we take
advantage of the fact that points on the Z-axis all map to the screen
coordinates (screen_x,screen_y) = (0.0, 0.0), i.e. the center of the
screen. The screen coordinates of the point (0.0, 0.0, 2.0) are
therefore (0.0, 0.0).
By choosing the point (1.0, 1.0, 1.0) we take advantage of the face
that the near face of the frustum maps directly onto the screen. Points
with a Z-coordinates equal to the distance between the camera and the
frustum map to a 2D point whose X and Y coordinates are equal to the X
and Y coordinates of the 3D point. Therefore the screen coordinates of
the point (1.0, 1.0, 1.0) are (1.0, 1.0). Points whose z coordinate is
equal to the distance between the camera and the near face of the
frustum map to screen coordinates equal to the x and y world
coordinates. Points whose z coordinate is greater than this distance
will not retain their x and y coordinates on the screen due to
perspective division, which is not discussed here.
Now suppose the camera moves closer to these points on the screen.
For example, what happens if the camera moves to
(world_x,world_y,world_z) = (0.0, 0.0, 1.0)? The entire frustum moves
with the camera such that the frustum does not change its position,
orientation or shape relative to the camera point. When the camera moves
from (0.0, 0.0, 0.0) to (0.0, 0.0, 1.0), the center of the frustum
moves from (0.0, 0.0, 1.0) to (0.0, 0.0, 2.0).
Relative to the new camera position, the point (0.0, 0.0, 2.0) now
has the position (0.0, 0.0, 1.0). This means the point is now right in
the center of the near face of the frustum, so it is still visible on
the screen at screen coordinates (0.0, 0.0).
Relative to the new camera position, the point (1.0,1.0, 1.0) has the
position (1.0, 1.0, 0.0). This point is no longer inside the frustum
because the relative Z-coordinate of points in the frustum must be
greater than the distance between the camera and the near face of the
frustum i.e.greater than 1.0 as per our example frustum. Since the new
relative x-coordinate of the point is 0.0, the point falls outside the
frustum and does not get drawn to the screen.
Diagram 1.
The above camera operations are shown below with actual calls to the
OpenGL API. The code uses the functions gluLookAt() and glTranslatef()
to position and displace the camera. The function gluLookAt() could also
be used to displace the camera by computing the new position based on
the displacement. The choice is entirely up to the programmer.
Both gluLookAt() and glTranslatef()place a transformation matrix in
the list of matrix multiplications that OpenGL performs to compute the
screen coordinates of 3D points of the objects in the virtual world. For
the purposes of this discussion,you can think of it as the
transformation matrix causing a change to the scene. For example,
gluLookAt() places a transformation matrix in the list of matrix
transformations so that all 3D points drawn on the screen will undergo
the transformation based on the camera coordinates of the position,
direction and up vectors passed to gluLookAt(). To really get into the
details of the OpenGL API, you should the OpenGL API and Matrix Transformations.
The example points described above undergo transformations such that
the world point (0.0, 0.0, 2.0) maps to the screen point (0.0, 0.0) and
the world point (2.0, 2.0, 1.0) maps to the screen point (2.0, 2.0).
// position the camera at (0.0, 0.0, 0.0) // by default the camera is positioned at this origin // following call explicitly positions camera at origin gluLookAt( 0.0, 0.0, 0.0, // coordinate of camera position 0.0, 0.0, 1.0 // eye direction vector : looking down the Z axis 0.0, 1.0, 0.0 // up direction vector : Y-axis is the up direction ); glTranslatef(0.0, 0.0, 1.0); // translate camera by (0.0, 0.0, 1.0)
In this discussion, the screen coordinates are mapped from the "near"
face of the frustum. If the top-left corner of the frustum has
coordinates (world_x, world_y, world_z) = (-1.0, 1.0, 1.0), then the
screen coordinates of this point are (screen_x, screen_y) = (-1.0, 1.0).
Similarly, the top right corner (1.0, 1.0, 1.0) of the frustum maps to
(screen_x, screen_y) = (1.0, 1.0). The bottom right corner (1.0, -1.0,
1.0) of the frustum maps to (screen_x, screen_y) = (1.0, -1.0). The
bottom left corner (-1.0, -1.0, 1.0) of the frustum maps to (screen_x,
screen_y) = (-1.0, 1.0).
All points that get drawn on the screen first get mapped to screen
coordinates that lie within the screen coordinates of the frustum
through a transformation called the Perspective transformation. Then
they get mapped to actual integral pixel coordinates on the screen
through a transformation called the Viewport Transformation. These
transformations are explained in books on OpenGL in more detail.
Although the OpenGL API is outside the scope of this article, readers
are urged to study theOpenGL Programming Guide and OpenGL Reference pages found at OpenGL.org.
For purposes of this article, the above camera positioning and
displacement are relevant to understanding what is done by the method
Camera::displace() called from AnimCamera::get_next_frame() as described
in the next section. The class Camera discussed in An Object Oriented Approach to Animation Design
is expanded in pseudo-code below. The default constructor sets the
default position, direction and up vectors to the origin, Z-axis and
Y-axis respectively. It also calls gluLookAt() to initialize the camera.
The method Camera::reposition(Vector position) repositions the camera
and calls gluLookAt() again. The method Camera::displace(Vector
displacement) basically translates to a call to glTranslatef(). Since
glTranslatef() also results in a transformation of the scene, there is
no need to call gluLookAt() separately after glTranslatef().
class Camera { protected: Vector position; Vector direction; Vector up; public: Camera::Camera(); void reposition(Vector position); void displace(Vector displacement); . . . other methods inherited from GraphicObject . . . }; Camera::Camera() { // Set default POSITION to origin (0.0, 0.0, 0.0) this->position.x = 0.0; this->position.y = 0.0; this->position.z = 0.0; // Set default DIRECTION to Z-axis (0.0, 0.0, 1.0) this->direction.x = 0.0; this->direction.y = 0.0; this->direction.z = 1.0; // Set default UP to Y-axis (0.0, 1.0, 0.0) this->up.x = 0.0; this->up.y = 1.0; this->up.z = 0.0; gluLookAt( this->position.x, this->position.y, this->position.z, this->direction.x, this->direction.y, this->direction.z, this->up.x, this->up.y, this->up.z } } void Camera::reposition(Vector position) { this->position.x = position.x; this->position.y = position.y; this->position.z = position.z; gluLookAt( this->position.x, this->position.y, this->position.z, this->direction.x, this->direction.y, this->direction.z, this->up.x, this->up.y, this->up.z } } void Camera::displace(Vector displacement) { this->position.x += displacement.x; this->position.y += displacement.y; this->position.z += displacement.z; glTranslatef(displacement.x, displacement.y, displacement.z); }
AnimCamera
Consider the problem of creating a simple animation to move the
animation camera smoothly in a 3D virtual world. Suppose the camera
needs to move forward from its current position by x units over a
certain number of frames, in a smooth movement that avoids an abrupt
change of position by x units but instead has a uniform motion across
all frames. What kind of code must be written to achieve this smooth
camera movement?
From the discussion on the Animation Object abstraction in An Object Oriented Approach to Animation Design
we know that the animation of the camera must happen in discrete steps.
Because the Animation Object abstraction is an extension of a Graphic
Object over a finite set of frames, you can think of the Animation
Camera object as an extension of the Camera object over a set of finite
frames. In order to animate the camera from its current position to a
position that is directly ahead of the current position by x units over N
frames, the Animation Camera object must be setup so that successive
calls to the method AnimCamera::get_next_frame() move camera position a
fraction of the total displacement of x units, such that after N
successive calls to this method, the camera is now displaced forward by x
units. For simplicity, let each call to AnimCamera::get_next_frame()
displace the camera by dx = x/N units. The AnimCamera class is
illustrated in pseudo-code below. In the example below, the total number
of frames (N) is set through the constructor parameter int n_frames and
subsequently stored in the data member int total_frames of the parent
class AnimationObject.
class AnimCamera : public AnimationObject { protected: // other data inherited from AnimationObject Vector displacement; public: AnimCamera(Camera* camera, Vector displacement, int n_frames); FrameState get_next_frame(); }; AnimCamera::AnimCamera(Camera* camera, Vector displacement, int n_frames) { this->graphic_object_list->add(camera); // add Camera to list of graphic objects this->start_frame = 0; this->current_frame = this->start_frame; this->total_frames = n_frames; } FrameState AnimCamera::get_next_frame() { Camera* camera = (Camera*)(get_first_from_list(this->graphic_object_list)); camera->displace( (1.0/this->total_frames) * this->displacement ); if ( this->current_frame < this->total_frames - 1) { // not reached last frame yet return PLAY_NEXT_FRAME; } else { // reached last frame return STOP; } }
AnimBackground
This Animation Object is responsible for drawing the background for
each frame. A simple background is one which is a solid color (such as
solid blue). The implementation of the method
AnimationObject::get_next_frame() in the class AnimBackground simply
clears the screen and fills it with a uniform blue color. Although
clearing the screen and filling it with blue is done for each frame, a
frame rate of 48 frames per second ensures that the human eye cannot
make out individual frame refreshes, and therefore ensures that we
perceive a constant non-changing background.
The AnimBackground animation object is typically placed first in the
list of layers that contain contents drawn to the screen each frame.
Other animation objects go in layers after the layer with the
AnimBackground object so that their images are drawn after (and
therefore on top of) the background.
class AnimBackground : public virtual AnimationObject { protected: Color color; public: FrameState get_next_frame() { clear_screen(); // first clear the screen fill_screen_with_color(BLUE); // fill with BLUE return REPEAT; // tell engine to repeat this call } };
GraphicFace
Our GraphicObject is defined by its "up" and "right" vector. The
"position" of GraphicFace is inherited from the abstract parent class
GraphicObject. Diagram 2 illustrates GraphicFace. It implements the
abstract method draw() which will actually call a graphics API such as
OpenGL or Direct3D to draw the face.
Diagram 2 "GraphicFace"
class GraphicFace : public virtual GraphicObject { protected: Vector up; Vector right; public: void draw() { .. draw face .. } };
AnimFace
This class contains a pointer to a GraphicFace object and implements
the abstract method get_next_frame(), inherited from class
AnimationObject. This method will simply draw the GraphicFace object and
return a REPEAT state, so the animation engine will keep calling it
repeatedly.
class AnimFace : public virtual AnimationObject { protected: GraphicFace* face; public: FrameState get_next_frame() { this->face->draw(); // then simply draw the face return REPEAT; } };
AnimTranslate
This class causes a GraphicObject to be translated as instructed.
This class contains a pointer to a GraphicObject object. This could
point to any object of a class inherited from GraphicObject (for example
GraphicFace). It implements the abstract method get_next_frame(),
inherited from class AnimationObject. The instance of this object will
be initialized with
- The displacement vector
- The total number of frames that the translate operation must be completed in
The get_next_frame() implementation will make an incremental
displacement of the object. The displacement will be equal to the total
displacement divided by the total number of frames. If this displacement
was not the last one, this method returns a PLAY_NEXT_FRAME state, so
that the engine will call this method again via its Layer's
Layer::get_image_content(). After the last displacement in the last
frame, this method will return a DONE state, so the engine can remove
the layer it is placed in. Thus, starting from the first frame, if this
method is called as many times as the total number of frames, the object
will be displaced by the total displacement provided at initialization.
The get_next_frame() method does not do any actual drawing; it only
displaces the GraphicObject that it points to.
class AnimTranslate : public virtual AnimationObject { protected: GraphicObject* graphic_object; int total_frames; int current_frame; Vector total_displacement; public: AnimateTranslate(Vector displacement, int total_frames); FrameSet get_next_frame(); }; AnimTranslate::AnimTranslate(Vector d, int t_f) { this->total_frames = t_f; this->total_displacement = d; this->current_frame = 0; } void AnimTranslate::get_next_frame() { this->graphic_object->displace( total_displacement / total_frames ); If ( current_frame < total_frames - 1) { // if not the last frame return PLAY_NEXT_FRAME; } else { // last frame return DONE; } }
Animation Engine based on alternate Animation Object design
A video game or animation application will have many things happening
at the same time. For example, the player could be moving in different
directions, enemies could be moving in different directions, weapons
effects like explosions might be rescaling or reshaped and so on. Based
on the discussion of the Animation Object abstraction,
these animations would be best achieved through a variety of classes
derived from and implementing the interface of the abstract class
AnimationObject. You've seen how the method
AnimationObject::get_next_frame() provides incremental updates to the
graphics display and that successive calls to this method result in
animation of the Graphic Object. The Graphic Object abstraction, on the
other hand, has methods on it like GraphicObject::displace(), which
causes a change in the state of the Graphic Object, so that when
AnimationObject::get_next_frame() calls GraphicObject::draw(), the
Graphic Object will be drawn in a different location.
At this stage, it is worth considering an alternative design for the
Animation Object interface. Examining and contrasting this alternative
with the design described in Article 1 on Animation Object abstraction,
will help in understanding why I've chosen the design in Article 1 as
well as you give you a deeper appreciate for that design.
The alternative design is to let the Animation Object have methods
like AnimationObject::displace() or AnimationObject::rotate() and let
these methods call GraphicObject::displace() and GraphicObject::draw()
internally. For a programmer, this would apparently seem to be a more
natural design. For example, if the player presses the UP arrow key, the
event handling part of the game simply calls the method
AnimCamera::displace(), on the Animation Camera Object. That method in
turn will call Camera::displace() in small increments, followed by
Camera::draw() and a refresh of the video memory. After all the
incremental displacements are done, the method AnimCamera::displace()
will return to its caller. This is illustrated in pseudocode below.
void AnimCamera::displace(Vector displacement, int n_frames) { Camera* camera =(Camera*)(get_first_from_list(this->graphic_object_list)); Vector delta_displacement = (1.0/N) * displacement; for (int i = 0; i < n_frames; i++) { camera->displace(delta_displacement); camera->draw(); refresh_video_memory_of_display_device(); } return; }
You can see the problems with the above design by considering what happens
if another animation happened at the same time as the camera displacement
animation, such as a cube revolving while the camera is being moved. In this
alternate design, the animation object for the cube will have a method on it,
AnimationCube::rotate(), which would be similar to the
AnimCamera::displace(). It would go through a loop that increments
the cube's rotational position until the desired rotation is
achieved. For each incremental rotation, the video memory of the
display device would have to be refreshed. Given that both
AnimCamera::displace() and AnimationCube::rotate() do not return
until they are done, it would not be possible to run both animations
simultaneously. For them to run simultaneously, each method would
have to be called in a separate thread, meaning that each animation
would have to be assigned its own unique thread to run in. This might
sound fine, but this approach has a major issue - the threads would
have to coordinate themselves using costly system resources like
mutex locks. This situation is illustrated below for synchronous
animations of the camera and the revolving cube.
// called in separate thread void AnimCamera::displace(Vector displacement, int n_frames) { Camera* camera =(Camera*)(get_first_from_list(this->graphic_object_list)); Vector delta_displacement = (1.0/N) * displacement; for (int i = 0; i < n_frames; i++) { // block video refresh by other threads acquire_refresh_video_lock(); camera->displace(delta_displacement); camera->draw(); // unblock video refresh by other threads Release_refresh_video_lock(); refresh_video_memory_of_display_device(); } return; } // called in separate thread void AnimationCube::rotate(double angle, int n_frames) { Cube* cube = (Cube*)(get_first_from_list(this->graphic_object_list)); double delta_rotation = (1.0/N) * angle; for (int i = 0; i < n_frames; i++) { // block video refresh by other threads acquire_refresh_video_lock(); cube-<rotate(delta_rotation); cube-<draw(); // unblock video refresh by other threads release_refresh_video_lock(); refresh_video_memory_of_display_device(); } return; }
In addition to the problem of coordinating the threads of different
animations, the number of calls to refresh the video memory will
multiply rapidly as the number of simultaneously moving objects
increases. Further, having multiple threads introduces interactions with
thread scheduling that might distort the animations.
Due to the above issues, this alternate animation engine based on the
alternate design of the Animation Object is not a suitable design.
Video games or animation applications do not generally use this
alternate design.
The Animation Engine based on the design of Animation Object as
described in Article 1 does not face any of the above problems. The
following section will describe a simple animation engine design based
on the concepts discussed thus far.
Bringing it all together: Animation Engine design based on chosen Animation Object design
Now that we have the essential abstractions and example classes, can
put put together a very simple animation engine. This engine has a set
of very basic and essential components. It has a camera and a frustum.
It also has a list of layers
that will be displayed containing the animation objects. For now, this
list is just a simple list, and not a hierarchical tree structure of
layer folders and layers.
The animation application will create and provide the engine with the
Camera, Frustum and list of sequenced Layers containing Animation
Objects. The engine will go through each layer in sequence, calling
Layer::get_image_content(), which will cause
AnimationObject::get_next_frame() to be called for that layer's
Animation Object. Once all the Layers are processed this way, the video
memory will be refreshed. This loop keeps running until the application
stops the animation engine. As a contrast with the alternative
multi-threaded design above, this engine:
- Does not run separate animations in separate threads
- Calls video memory refresh only once all drawing for the next frame is done
- Does not need system resources like mutex locks
- Runs the entire engine loop in a single thread while allowing simultaneous animations
The animation application will use this engine to animate the camera
forward and animate a GraphicFace object sideways towards the right
synchronously. So the user will move towards the face as well as see the
face moving right as the animation progresses.
The engine will be initialized with Camera and Frustum objects. The
application will initialize a layer each for the camera animation,
graphic face movement animation and the graphic face animation.
The engine itself is illustrated in pseudo code by a class as follows.
// pseudocode class AnimationEngine { protected: Camera* camera; Frustum* frustum; List<Layer>* layer_list; public: AnimationEngine(Camera* c, Frustum* f); add_layer_to_end_of_layer_list(Layer* layer); add_layer_to_start_of_layer_list(Layer* layer); delete_layer(Layer* layer); run_loop(); }
The animation application creates instances of Frustum, Camera,
AnimCamera, GraphicFace, AnimFace and AnimTranslate. The AnimCamera
instance is initialized with 50 frames and a displacement vector of (x,
y, z) = (0, 0, 100), meaning a displacement of 100 units forward. The
AnimTranslate instance is initialized with 50 frames and a displacement
vector of (x, y, z) == (100, 0, 0), meaning a displacement of 100 units
to the right. Note: these units do not have any particular significance
and are used just as an example.
Further, it creates three instances of class Layer, one for
containing the instance of AnimCamera, another containing the instance
of AnimFace, and another for containing the instance of AnimTranslate.
It places the Layer with AnimCamera first in the list, followed by the
Layer with AnimTranslate, followed by the Layer with AnimFace.
The Layer with AnimCamera can go anywhere in the list of layers. By
convention, it is generally placed first in this list. The calls to
gluLookAt() and glTranslatef() display the scene from the camera's
position and orientation. By placing the camera first, each frame first
does a "look" through the camera and then "draws" the objects in the
scene. If we placed the camera last in the layer's list, then the
objects in the scene would have been drawn first, followed by a "look"
through the camera. The ordering of the "look" and "draw" operations
does not really matter though, but as it is convention to put it in the
first player, so will I.
Diagram 3 below illustrates the engine with two layers - one for
moving the graphic object and one for drawing the graphic object. For
simplicity and to avoid clutter in the diagram, the layer with the
camera movement is left out. The diagram shows the generation of 3
sequential frames .
Diagram 3: Frames, Layers, Animation Objects and Graphic Objects in an Animation Engine
The application would be something like this in pseudo code.
int main(int argc, char** argv) { Camera camera; // init camera Frustum frustum; // init frustum AnimBackground anim_background; Layer anim_background_layer(&anim_background); AnimCamera anim_camera(50 frames, displacement = (0, 0, 100); // init AnimCamera Layer anim_camera_layer(&anim_camera); // Place AnimCamera in layer GraphicFace face(0, 0, 0); // init GraphicFace at location {0, 0, 0} AnimFace anim_face; // init AnimFace Layer anim_face_layer(&anim_face); // Place AnimFace in layer AnimTranslate anim_translate(50 frames, displacement = {100, 0, 0}); // init AnimTranslate Layer anim_translate_layer(&anim_translate); // place AnimTranslate in layer List<Layer> layer_list; // init layer list // init engine with camera, frustum and layer list AnimationEngine engine(&camera, &frustum, &layer_list); // place AnimCamera first in layer list engine. add_layer_to_end_of_layer_list (anim_camera_layer); // place AnimBackground second in the layer list engine.add_layer_to_end_of_layer_list (anim_background_layer); // place AnimTranslate third in layer list engine. add_layer_to_end_of_layer_list (anim_translate_layer); // place AnimFace fourth in layer list engine.add_layer_to_end_of_layer_list(anim_face_layer); // run animation engine loop engine.run_loop(); }
Finally, the loop that runs it all is AnimationEngine::run_loop(). This method has these steps:
- This method asks each layer in the list, in order, to call Layer::get_image_content(), which in turn asks the AnimationObject that it contains to call AnimationObject::get_next_frame().
- After each of the layers finish the call to Layer::get_image_content(), the method AnimationEngine::run_loop() updates the video buffer.
The above two steps are then repeated as long as the engine is running. This plays out as follows:
- First, for the first 50 runs of the above loop, the method Layer::get_image_content() gets called on the layer with the AnimCamera object. Thus AnimCamera::get_next_frame() gets called first. This method displaces the camera by the incremental displacement vector ( 0/50, 0/50, 100/50 ) = ( 0, 0, 2 ). This is because the AnimCamera::get_next_frame divides the total displacement which is (0, 0, 100) by the total number of frames, which is 50. This causes each frame to be viewed from a new position slightly incremented over the previous frame's position, which results in the effect of the animation of the camera.
- Second, for each of the above 50 runs of the loop, the method Layer::get_image_content() gets called on the layer with the AnimBackground object. Thus AnimBackground::get_next_frame() gets called second. This method simply clears the screen and fills it with blue color. When other layers get drawn after this layer, they draw over the background.
- Third, for each of the above 50 runs of the loop, the method Layer::get_image_content() gets called on the layer with the AnimTranslate object. Thus AnimTranslate::get_next_frame() gets called third. This method changes the coordinates of the GraphicFace object contained in the AnimFace object by the vector {100/50, 0/50, 0/50} = {2, 0, 0}. This is because AnimTranslate::get_next_frame() divides the total displacement which is {100, 0, 0} by the total number of frames, which is 50. This causes the object to be positioned at a new position slightly incremented over the object's position in the previous frame, which results in the effect of the animation of the object.
- Fourth, for each of the above 50 runs of the loop, the method Layer::get_image_content() gets called on the layer with the AnimFace object. Thus AnimFace::get_next_frame() gets called fourth. The method AnimFace::get_next_frame() simply draws its GraphicFace object at its location, which means the first time in the AnimationEngine::run_loop(), the face gets drawn at {2, 0, 0}, second time in the loop, it gets drawn at {4, 0, 0} and so on. Finally in the 50th run of the loop, the face gets drawn at {100, 0, 0}.
- After each set of calls (AnimCamera::get_next_frame(), AnimBackground::get_next_frame(), AnimTranslate::get_next_frame() and AnimFace::get_next_frame()), the method AnimationEngine::run_loop() updates the video memory of the display device.
- After the video memory refresh, AnimationEngine::run_loop() goes back to calling Layer::get_image_content() on its layers list.
- The 50th call to AnimTranslate::get_next_frame() detects that it is the last frame for that AnimTranslate object. Therefore, it returns the DONE status to Layer::get_image_content() which relays it to AnimationEngine::run_loop(). This tells AnimationEngine::run_loop() that the layer is done and can be removed, which is done by AnimationEngine::run_loop(). Thus on the 51st call to AnimationEngine::run_loop(), the only layer left in the engine is the layer containing AnimFace, resulting in the rest of the calls to AnimationEngine::run_loop() to simply drawing the face at {100, 0, 0}
- The first 50 steps resulted in a gradual movement of the face from {0, 0, 0} to {100, 0, 0}, which is the animation effect desired. After this effect is done, the face stays put at the new location.
void AnimationEngine::run_loop() { do { for each layer in layers_list { // get image content from next layer // and return status // Layer::get_image_content() will communicate // status from AnimationObject::get_next_frame() // to this engine loop status = Layer::get_image_content(); if (status == DONE) { mark layer for deletion; } } For each layer marked for deletion { Delete that layer; } Refresh video memory } while (true); // run forever }
That's it! We have a design for an extremely simple, but workable
animation engine design. The engine does not have event handling, scene
graph management, hierarchical animation object tree structure,
collision detection and so on. But it is a skeleton design upon which
the rest of these features could be built.
In a real game, the animation objects will be organized not in a flat
list of layers, but in a hierarchical tree structure of layers and
layer folders. The method AnimationEngine::run_loop() would traverse
this tree structure and call Layer::get_image_content() or
LayerFolder::get_image_content() on each of the nodes of the tree. The
game logic and event handling logic of the game would then be able to
manage not just individual animation objects, but composite animation
objects as well. More complex game engines may organize the layers into a
combined tree-graph structure so that the child nodes of a parent node
may be connected by a graph for advantage to scene graph management as
discussed in the next article.
Summary
We discussed example objects for the abstractions of GraphicObject
(Camera, GraphicFace) and AnimationObject (AnimCamera, AnimTranslate),
as well as an inferior alternative to the Animation Object design described in the first article of this series.
You saw how a skeletal animation engine design would work with some
example objects and Layer objects. You saw an example of this design in
action with a rectangular face moving the right while simultaneously
the camera moved towards the rectangular face.
The next article discusses First Person Shooter (FPS) game design
with special consideration for event handling. It will be shown how user
generated events like mouse and keyboard events get mapped to
animations in the game.
No comments:
Post a Comment
if You Need Any Help Then Write Us:We Come Back With Solution