第20课 - 动画


更新时间:2014年2月13日


直到现在,我们已经仅仅是在和静态图像打交道。本课教程将通过实现一个在屏幕上行走的火柴人,来教你精灵动画的基本技术。

动画的基本概念是先拿来一连串的图像,就像下面这张精灵图中的火柴人。

然后从左往右逐个展示,来创造运动的幻觉。

所以当你在SDL中使用动画时,实际上你就是在展示一系列的SDL_Surface.
//火柴人
class Foo
{
    private:
    //坐标
    int offSet;
    
    //移动速度
    int velocity;
    
    //当前帧
    int frame;
    
    //动画状态
    int status;

    public:
    //初始化变量
    Foo();
    
    //处理输入
    void handle_events();
    
    //移动火柴人
    void move();    
    
    //显示火柴人
    void show();    
};

这是我们将要在屏幕上移动的火柴人的类。

首先,我们有"offSet" 和 "velocity"变量。因为我们仅仅将火柴人向左或向右移动,所以我们仅仅跟踪x方向的坐标和速度。

然后,我们有"frame" 和 "status"变量。"frame" 指示动画中显示的是哪一帧。"status"指示了显示哪个动画,即Foo向左走的动画或Foo向右走的动画。

接着,我们当然有构造器、事件处理函数和移动及显示火柴人的函数。
void set_clips()
{
    //剪切子图
    clipsRight[ 0 ].x = 0;
    clipsRight[ 0 ].y = 0;
    clipsRight[ 0 ].w = FOO_WIDTH;
    clipsRight[ 0 ].h = FOO_HEIGHT;
    
    clipsRight[ 1 ].x = FOO_WIDTH;
    clipsRight[ 1 ].y = 0;
    clipsRight[ 1 ].w = FOO_WIDTH;
    clipsRight[ 1 ].h = FOO_HEIGHT;
    
    clipsRight[ 2 ].x = FOO_WIDTH * 2;
    clipsRight[ 2 ].y = 0;
    clipsRight[ 2 ].w = FOO_WIDTH;
    clipsRight[ 2 ].h = FOO_HEIGHT;
    
    clipsRight[ 3 ].x = FOO_WIDTH * 3;
    clipsRight[ 3 ].y = 0;
    clipsRight[ 3 ].w = FOO_WIDTH;
    clipsRight[ 3 ].h = FOO_HEIGHT;
    
    clipsLeft[ 0 ].x = 0;
    clipsLeft[ 0 ].y = FOO_HEIGHT;
    clipsLeft[ 0 ].w = FOO_WIDTH;
    clipsLeft[ 0 ].h = FOO_HEIGHT;
    
    clipsLeft[ 1 ].x = FOO_WIDTH;
    clipsLeft[ 1 ].y = FOO_HEIGHT;
    clipsLeft[ 1 ].w = FOO_WIDTH;
    clipsLeft[ 1 ].h = FOO_HEIGHT;
    
    clipsLeft[ 2 ].x = FOO_WIDTH * 2;
    clipsLeft[ 2 ].y = FOO_HEIGHT;
    clipsLeft[ 2 ].w = FOO_WIDTH;
    clipsLeft[ 2 ].h = FOO_HEIGHT;
    
    clipsLeft[ 3 ].x = FOO_WIDTH * 3;
    clipsLeft[ 3 ].y = FOO_HEIGHT;
    clipsLeft[ 3 ].w = FOO_WIDTH;
    clipsLeft[ 3 ].h = FOO_HEIGHT;
}
这个函数设置了精灵图中每个单独子图的剪切范围。

我们有两组子图,其中clipsRight剪切的子图是Foo向右走动画的帧,clipsLeft剪切的子图是Foo向左走动画的帧。
Foo::Foo()
{
    //初始化移动参数
    offSet = 0;
    velocity = 0;
    
    //初始化动画参数
    frame = 0;
    status = FOO_RIGHT;
}
在Foo类的构造器中,我们首先初始化坐标和速度。

然后我们设置动画在帧0的状态,并且我们将status设置为FOO_RIGHT,这样默认的动画就是火柴人向右走的动画。
void Foo::move()
{
    //移动
    offSet += velocity;
    
    //确保火柴人在屏幕范围内
    if( ( offSet < 0 ) || ( offSet + FOO_WIDTH > SCREEN_WIDTH ) )
    {
        offSet -= velocity;    
    }
}
现在在move()函数中,我们首先采用一贯的做法移动火柴人并确保它在屏幕内。
void Foo::show()
{
    //如果Foo向左走
    if( velocity < 0 )
    {
        //设置动画为向左走的动画
        status = FOO_LEFT;
        
        //动画进入下一帧
        frame++;
    }
    //如果Foo向右走
    else if( velocity > 0 )
    {
        //设置动画为向右走的动画
        status = FOO_RIGHT;
        
        //动画进入下一帧
        frame++;
    }
    //如果Foo站着不动
    else
    {
        //重新启动动画
        frame = 0;    
    }
火柴人移动后,是时候实现真正的动画了。首先我们检查它移动的方向。

如果它向左走,我们将status设为FOO_LEFT,然后增加帧计数器的值,这样动画中的下一个子图将被显示出来。

如果它向右走,我们将status设为FOO_RIGHT,然后增加帧计数器的值,这样动画中的下一个子图将被显示出来。

如果火柴人是静止的,我们将frame设为0来重新启动动画。这样火柴人在静止站立时就不会看上去像迈了一半的步子了。
    //动画循环
    if( frame >= 4 )
    {
        frame = 0;
    }
之后,我们检查帧计数器是否已经达到第四帧,由于动画中仅有4帧,如果帧计数器超过这个数,我们就重启动画以使火柴人在移动时保持动画的循环。
    //显示火柴人
    if( status == FOO_RIGHT )
    {
        apply_surface( offSet, SCREEN_HEIGHT - FOO_HEIGHT, foo, screen, &clipsRight[ frame ] );
    }
    else if( status == FOO_LEFT )
    {
        apply_surface( offSet, SCREEN_HEIGHT - FOO_HEIGHT, foo, screen, &clipsLeft[ frame ] );
    }
}
最后,我们在屏幕上显示正确的子图。

如果火柴人在向右走,我们从向右走的动画中应用正确的子图;如果火柴人在向左走,我们从向左走的动画中应用正确的子图。
    //设置精灵图的剪切
    set_clips();

    //帧率校准器
    Timer fps;
    
    //创建火柴人
    Foo walk;
在我们的main函数中,初始化和文件加载完毕后,我们设置精灵图的剪切,然后声明一个FPS计时器,接着声明一个火柴人对象。
    //当用户还没退出
    while( quit == false )
    {
        //启动帧计时器
        fps.start();
    
        //当有事件需要处理
        while( SDL_PollEvent( &event ) )
        {
            //处理火柴人的事件
            walk.handle_events();
            
            //如果用户还没有叉掉窗口
            if( event.type == SDL_QUIT )
            {
                //退出程序
                quit = true;
            }
        }
        
        //移动火柴人
        walk.move();
        
        //用白色填充屏幕
        SDL_FillRect( screen, screen->clip_rect, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF ) );
        
        //在屏幕上显示火柴人
        walk.show();
        
        //更新屏幕
        if( SDL_Flip( screen ) == -1 )
        {
            return 1;    
        }
        
        //捕获帧率
        if( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
        {
            SDL_Delay( ( 1000 / FRAMES_PER_SECOND ) - fps.get_ticks() );
        }
    }
这里我们有main循环。它和前面课程中和Dot类一起使用的main循环基本一致。

因此,就像你看到的那样,对于一个动画引擎,你需要做的仅仅是跟踪你要使用的是哪个动画以及你要blit哪一帧。

本课所用的多媒体文件和源代码可从此处下载

下一课:滚动