使用Qt动画框架设计角色的二维动画

使用Qt动画框架设计角色的二维动画

Qt的动画框架是Qt4.6新添加的一个重要的特性,有了它,开发人员可以制作激动人心的动画界面,而不必局限于单调的固定窗口了,可以说,Qt动画框架是其它界面库少见的功能,它带来的是Qt的一大优势。最近我花了大概三、四天的时间研究Qt动画框架的内容,这让我感到Qt人员精心的设计给了我们一套非常规范并且易懂的代码,如果另外的一名开发者也对Qt动画框架有所了解,那么他可以毫不费力地看懂我的代码,并且从代码中了解设计思想。

演示程序的下载地址:这里

源代码的下载地址:这里

下面是程序的截图:



这里我使用了大家非常熟知的初音ミク来演示,我们可以通过【w】【s】【a】【d】键控制它的行动,不过呢,这个程序还是有一些小问题的,以后有时间再来改进。

该如何介绍呢?首先为了了解角色动画是如何进行的,还是了解一下Qt状态机框架。这里是英文的介绍,而这里呢,是英文介绍的翻译。

在初步了解Qt状态框架之后,我介绍一下最简单的二维动画:行走动画。其实演示程序所用的是初音ミク的行走图:如下图所示:


我们应用Qt的场景–视图框架(Scene–ViewFramework)来绘制二维图形,这很简单。在成功之后我就想,如何才能让初音ミク动起来呢?在多天的尝试和失败后,我绘制了一个有限状态图。


在这个图中,站立和行走是状态机中的两个状态,它们之间可以相互转换。这样的转换通过按键的响应来完成。在Qt中有一个专门负责按键响应并且用于状态之间转换的类:QKeyEventTransition。它通过绑定一个QWidget来获取它的按键事件,并且一旦满足相应的按键条件就开始进行状态之间的转换。所以角色的行走动画可以这样制作:首先角色处于站立状态,当它捕获到了上下左右键,一个QkeyEventTransition的对象指针负责将其转换成行走状态。转换附带了两个动画,一个是角色的帧动画,也就是初音ミク的一帧一帧的运动,另一个是角色的位置动画,让角色的位置发生变化。不可能让我们的ミク原地踏步吧!转换完毕后角色实际上进入了行走的状态,通过设定另外一个QAbstractTransition的指针来让角色从行走状态迅速地回到站立状态,这样就可以等待下一次按键的响应。

在我的实现中,让Character类来实现这样的功能,它是GameObject的子类,而GameObject类则是所有游戏对象的基类。一般来说角色(Character)基本上都有行走的功能,而同属于GameObject的其它物体则没有,比如说树木。

下面是Character.h的内容:

#ifndef CHARACTER_H
#define CHARACTER_H
#include 
#include 
#include 
#include 
#include "GameObject.h"
/*---------------------------------------------------------------------------*/
class Character: public GameObject
{
    Q_OBJECT
    Q_PROPERTY( QRect FrameRect READ FrameRect WRITE setFrameRect )
public:
    explicit Character( const QString& fileName, GameObject* parent,
                        QWidget* pWidget = 0 );
    // QGraphicsItem必须实现的函数
    QRectF boundingRect( void ) const;
    void paint( QPainter* pPainter, const QStyleOptionGraphicsItem* pOption,
                QWidget* pWidget = 0 );
    // 子类需要实现的函数
    virtual void Init( void );
public slots:
    void SlotSetDirection( void );
protected:
    // 枚举
    enum Direction// 即控制的按键
    {
        _Up_            = Qt::Key_W,
        _Down_          = Qt::Key_S,
        _Left_          = Qt::Key_A,
        _Right_         = Qt::Key_D
    };
    // 本类的函数
    void SetDirection( int dir );
    void SetFrameAnimation( int dir );
    void SetPosAnimation( int dir );
    // 本类的成员以及读/写函数
    DECLRARE_PROPERTY_WITH_GETTERS_AND_SETTERS( QRect, FrameRect )
    DECLRARE_PROPERTY_WITH_GETTERS_AND_SETTERS( int, AnimateTime )
    QState                     *m_pStandState, *m_pWalkState;
    QPropertyAnimation*         m_pPA;
    QPropertyAnimation*         m_pFPA[4];
    QWidget*                    m_pWidget;
};
/*---------------------------------------------------------------------------*/
#endif // CHARACTER_H

下面是Character.cpp的内容:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Character.h"
/*---------------------------------------------------------------------------*/
Character::Character( const QString& fileName, GameObject* parent,
                      QWidget* pWidget ): GameObject( parent )
{
    m_AnimateTime = 1000;// 即1秒
    m_pWidget = pWidget;
    m_Image.load( fileName );       // 载入图片
    m_FrameRect.setRect( 0, 0, m_Image.width( ) / 4, m_Image.height( ) / 4 );
    SetDirection( _Right_ );        // 朝向右
    Init( );
}
/*---------------------------------------------------------------------------*/
void Character::paint( QPainter* pPainter, const QStyleOptionGraphicsItem* pOption,
                       QWidget* pWidget )
{
    pPainter->drawPixmap( QPoint( 0, -16 ), m_Image, m_FrameRect );
    GameObject::paint( pPainter, pOption, pWidget );
}
/*---------------------------------------------------------------------------*/
QRectF Character::boundingRect( void ) const
{
    return QRect( 0, -16, 32, 48 );// 没有其它特殊的效果将返回帧框
}
/*---------------------------------------------------------------------------*/
void Character::SetDirection( int dir )
{
    int rectY = 2;
    switch ( dir )
    {
    case _Up_: rectY = 3; break;
    case _Down_: rectY = 0; break;
    case _Left_: rectY = 1; break;
    case _Right_: rectY = 2; break;
    }
    m_FrameRect = QRect( m_FrameRect.x( ),
                         rectY * m_Image.height( ) / 4,
                         m_FrameRect.width( ),
                         m_FrameRect.height( ) );
}
/*---------------------------------------------------------------------------*/
static qreal OneEasingFunction( qreal progress )
{
    Q_UNUSED( progress )
    return 1.0;
}
/*---------------------------------------------------------------------------*/
void Character::SetFrameAnimation( int dir )
{
    int rectY = 2;
    switch ( dir )
    {
    case _Up_: rectY = 3; break;
    case _Down_: rectY = 0; break;
    case _Left_: rectY = 1; break;
    case _Right_: rectY = 2; break;
    }
    QRect rect[4];
    QEasingCurve curve( QEasingCurve::Custom );
    curve.setCustomType( OneEasingFunction );
    for ( int i = 0; i < 4; ++i )
    {
        rect[i] = QRect( m_FrameRect.width( ) * i, rectY * m_Image.height( ) / 4,
                         m_FrameRect.width( ), m_FrameRect.height( ) );
    }
    for ( int i = 0; i < 4; ++i )
    {
        m_pFPA[i]->setStartValue( rect[i] );
        m_pFPA[i]->setEndValue( rect[( i + 1 ) % 4] );
        m_pFPA[i]->setEasingCurve( curve );
        m_pFPA[i]->setDuration( m_AnimateTime / 4 );
    }
}
/*---------------------------------------------------------------------------*/
void Character::SetPosAnimation( int dir )// 设定位置动画
{
    QPointF point = pos( ), deltaPt;
    switch ( dir )
    {
    case _Up_:
        deltaPt = QPointF( 0.0, -32.0 ); break;
    case _Down_:
        deltaPt = QPointF( 0.0, 32.0 ); break;
    case _Left_:
        deltaPt = QPointF( -32.0, 0.0 ); break;
    case _Right_:
        deltaPt = QPointF( 32.0, 0.0 ); break;
    }
    m_pPA->setStartValue( point );
    point += deltaPt;
    m_pPA->setEndValue( point );
}
/*---------------------------------------------------------------------------*/
void Character::SlotSetDirection( void )
{
    QKeyEventTransition* eventTrans =
            qobject_cast( sender( ) );
    SetDirection( eventTrans->key( ) );
    SetFrameAnimation( eventTrans->key( ) );
    SetPosAnimation( eventTrans->key( ) );
}
/*---------------------------------------------------------------------------*/
void Character::Init( void )
{
    // 初始化站和走的状态
    m_pStandState = new QState( &m_Machine );
    m_pWalkState = new QState( &m_Machine );
    // 初始化和设定帧动画
    QSequentialAnimationGroup* pFSAG = new QSequentialAnimationGroup( this );
    for ( int i = 0; i < 4; ++i )
    {
        m_pFPA[i] = new QPropertyAnimation( this, "FrameRect", this );
        pFSAG->addAnimation( m_pFPA[i] );
    }
    // 初始化和设定位置动画
    m_pPA = new QPropertyAnimation( this, "pos", this );
    m_pPA->setDuration( m_AnimateTime );
    // 设定一个并发的动画组,同时调用这两个动画
    QParallelAnimationGroup* pPAG = new QParallelAnimationGroup( this );
    pPAG->addAnimation( pFSAG );
    pPAG->addAnimation( m_pPA );
    QKeyEventTransition* pKeyPressTrans[4];
    QKeyEventTransition* pKeyReleaseTrans[4];
    int keys[4] = { _Up_, _Down_, _Left_, _Right_ };
    for ( int i = 0; i < 4; ++i )
    {
        pKeyPressTrans[i] = new QKeyEventTransition( m_pWidget,
                                                     QEvent::KeyPress,
                                                     keys[i],
                                                     m_pStandState );
        pKeyPressTrans[i]->setTargetState( m_pWalkState );
        pKeyPressTrans[i]->addAnimation( pPAG );// “站立?行走”转换需要动画
        connect( pKeyPressTrans[i], SIGNAL( triggered( ) ),
                 this, SLOT( SlotSetDirection( ) ) );// 触发转换的时候调用槽
        pKeyReleaseTrans[i] = new QKeyEventTransition( m_pWidget,
                                                       QEvent::KeyRelease,
                                                       keys[i],
                                                       m_pWalkState );
        pKeyReleaseTrans[i]->setTargetState( m_pStandState );
    }
    // 启动有限状态机
    m_Machine.setInitialState( m_pStandState );
    m_Machine.start( );
}

注意,在我的草图中还有一点,就是当转换被触发(triggered)时,会向Character类实例发送一个信号,通过SlotSetDirection()槽函数接收,用来针对不同的按键来设定不同的帧显示和进行不同的位置移动,也可以说槽函数设定了角色的朝向(帧框)、帧动画和位置动画。

程序运行基本正常,只是会出现“QEasingCurve:Invalidcurvetype45”这样的错误(控制台下),看来是我自定义的插值函数搞的鬼。虽然我在论坛上提问了,但是目前还是没有人知道,这个问题就不了了之了。

你可能感兴趣的:(使用Qt动画框架设计角色的二维动画)