太阳神三国杀中,每当玩家出杀或吃桃子时,就会有一个动画效果,使界面非常生动绚丽.现在我们就分析一下QT中动画的原理,及实现方式,这里我们只分析吃桃子时的动画效果实现.由于三国杀有多个在线玩家同时游戏,因此当一个玩家吃了桃子,会将这个消息发送给服务器,服务器在分别通知每个在线玩家,使玩家界面出现吃桃子的动画.现在我们来跟踪一下代码的执行流程.

在NativeClientSocket::init()成员函数中,关联QtcpSocket的readyRead信号与其处理槽函数:connect(socket, SIGNAL(readyRead()), this, SLOT(getMessage()));在getMessage()函数中,每接收一条消息就触发message_got信号,Client类的构造函数中关联这个信号connect(socket, SIGNAL(message_got(char*)), this, SLOT(processServerPacket(char*)));在processServerPacket函数中,判断消息如果不是通知或请求,则调用processReply方法.在processReply方法中,判断消息字符串的首个字符,并根据消息内容查找对应的处理函数,此时消息内容为"animate",因此转而调用animate方法.这里有个小技巧,因为服务端和客户端之间的通信内容时字符串,那么如何根据发送的字符串找到相应的处理函数呢?将函数的名称和函数地址映射存入了一个QHash<QString, Callback> callbacks;成员变量中,在Client类的构造函数中进行初始化.

    callbacks["addHistory"] = &Client::addHistory;
    callbacks["animate"] = &Client::animate;
    callbacks["judgeResult"] = &Client::judgeResult;
    callbacks["setScreenName"] = &Client::setScreenName;
    callbacks["setFixedDistance"] = &Client::setFixedDistance;
    callbacks["transfigure"] = &Client::transfigure;
    callbacks["jilei"] = &Client::jilei;
    callbacks["cardLock"] = &Client::cardLock;
    callbacks["pile"] = &Client::pile;    ...... ......
其中Callback的定义为:typedef void (Client::*Callback)(const QString &);在根据函数名称查找函数地址时,只需要使用QHash类的value方法获取,而后调用:

        Callback callback = callbacks.value(method, NULL);
        if(callback){
            QString arg_str = arg;
            (this->*callback)(arg_str);//注意调用方式为(this->*callback)(参数列表),表示调用的是一个类实例(this)的成员函数,因为callback为函数指针,加星号降引用(好像可不加星号而直接调用,没有测试过).

现在就可以进入Client::animate成员函数了.解析出服务端发送的名称,及参数,触发animated信号.

void Client::animate(const QString &animate_str){
    QStringList args = animate_str.split(":");
    QString name = args.takeFirst();

    emit animated(name, args);
}

在RoomScene构造函数中,关联了animated信号及处理槽:connect(ClientInstance, SIGNAL(animated(QString,QStringList)), this, SLOT(doAnimation(QString,QStringList)));doAnimation函数首先定义了一个静态QMap变量,并在首次调用的时候进行设置,将一个字符串命令与一个函数地址向映射,原理与callbacks相同,实现延时加载的目标,提高软件启动速度.接着根据传入的name参数到map中查找对应的函数地址,并调用.

void RoomScene::doAnimation(const QString &name, const QStringList &args){
    static QMap<QString, AnimationFunc> map;
    if(map.isEmpty()){
        map["peach"] = &RoomScene::doAppearingAnimation;
        map["jink"] = &RoomScene::animatePopup;
        map["nullification"] = &RoomScene::doMovingAnimation;

        map["analeptic"] = &RoomScene::doAppearingAnimation;
        map["fire"] = &RoomScene::doAppearingAnimation;
        map["lightning"] = &RoomScene::doAppearingAnimation;
        map["typhoon"] = &RoomScene::doAppearingAnimation;

        map["lightbox"] = &RoomScene::doLightboxAnimation;
        map["huashen"] = &RoomScene::doHuashen;
        map["indicate"] = &RoomScene::doIndicate;

        map["hpChange"] = &RoomScene::animateHpChange;
    }

    AnimationFunc func = map.value(name, NULL);
    if(func)
        (this->*func)(name, args);
}

根据name的值可知,接下来调用了RoomScene::doAppearingAnimation成员函数,转而又调用了setEmotion函数.

    if(name == "analeptic"
            || name == "peach")
    {
        setEmotion(args.at(0),name);
        return;
    }

setEmotion首先根据参数who获取对应的座位对象photo,如果找到座位则在对手的座位上播放动画,否则在本地的控制区播放动画.

void RoomScene::setEmotion(const QString &who, const QString &emotion ,bool permanent){
    Photo *photo = name2photo[who];
    if(photo){
        photo->setEmotion(emotion,permanent);
        return;
    }
    PixmapAnimation * pma = PixmapAnimation::GetPixmapAnimation(dashboard,emotion);
    if(pma)
    {
        pma->moveBy(0,- dashboard->boundingRect().height()/2);
        pma->setZValue(8.0);
    }
}
先看setEmotion方法,参数emotion是动画名称,对应image/system/emotion/目录中的一个子目录名称,permanent指示是否永久播放动画.emotion_item是QGraphicsPixmapItem类型的指针,设置其显示,在其上播放动画,如果不是永久播放的动画则使用QTimer在2秒后隐藏动画图元emotion_item.接着就调用PixmapAnimation的静态成员函数GetPixmapAnimation来播放动画了.注意PixmapAnimation类是从QGraphicItem继承的(class PixmapAnimation : public QObject,public QGraphicsItem),动画的原理就是在桌位上创建一个PixmapAnimation实例,并使用计时器定时触发调用paint方法,在其上绘制不同的图片.

void Photo::setEmotion(const QString &emotion, bool permanent){
    this->permanent = permanent;

    if(emotion == "."){
        emotion_item->hide();
        return;
    }

    QString path = QString("image/system/emotion/%1.png").arg(emotion);//这个图片路径如果不存在,则emotion_item不生效.
    emotion_item->setPixmap(QPixmap(path));
    emotion_item->show();

    if(emotion == "question" || emotion == "no-question")
        return;

    if(!permanent)
        QTimer::singleShot(2000, this, SLOT(hideEmotion()));

    PixmapAnimation::GetPixmapAnimation(this,emotion);
}

GetPixmapAnimation是一个静态函数,首先创建一个新的PixmapAnimation实例pma,并设置其图片所在路径加载路径中的图片,设置动画播放的位置,最后调用基类QObject的startTimer函数设置计时器,每50毫秒触发一次,动画完成时触发QObject的deleteLater函数,延时删除自己(在对象退出消息循环后进行自我删除).

PixmapAnimation* PixmapAnimation::GetPixmapAnimation(QGraphicsObject *parent, const QString &emotion)
{
    PixmapAnimation *pma = new PixmapAnimation();
    pma->setPath(QString("image/system/emotion/%1/").arg(emotion));
    if(pma->valid())
    {
        if(emotion == "slash_red" ||
                emotion == "slash_black" ||
                emotion == "thunder_slash" ||
                emotion == "peach" ||
                emotion == "analeptic")
        {
            pma->moveBy(pma->boundingRect().width()*0.15,
                        pma->boundingRect().height()*0.15);
            pma->setScale(0.7);
        }
        else if(emotion == "no-success")
        {
            pma->moveBy(pma->boundingRect().width()*0.15,
                        pma->boundingRect().height()*0.15);
            pma->setScale(0.7);
        }

        pma->moveBy((parent->boundingRect().width() - pma->boundingRect().width())/2,
                (parent->boundingRect().height() - pma->boundingRect().height())/2);

        {
            if(emotion == "fire_slash")pma->moveBy(40,0);
        }
        pma->setParentItem(parent);
        pma->startTimer(50);
        connect(pma,SIGNAL(finished()),pma,SLOT(deleteLater()));
        return pma;
    }
    else
    {
        delete pma;
        return NULL;
    }
}

接着就进入了timerEvent函数,这是一个QObject定义的虚函数,如果对象调用了startTimer则会定时触发timerEvent,这里将其重写.

void PixmapAnimation::timerEvent(QTimerEvent *)
{
    advance(1);
}

advance成员函数累加current成员变量,记录当前帧,调用update方法触发paint函数的调用,如果帧数超过总帧数,则重置为0,并触发finished信号.

void PixmapAnimation::advance(int phase)
{
    if(phase)current++;
    if(current>=frames.size())
    {
        current = 0;
        emit finished();
    }
    update();
}

paint函数则直接根据current成员变量值获取对应的帧图片,进行绘制.frames的定义为QList<QPixmap> frames;在PixmapAnimation::setPath函数中,将指定路径中的所有png图片都加载到frames中.为了提高图片加载速度,setPath函数中加载图片是调用GetFrameFromCache从缓存中查找图片,如果没有则从磁盘上加载并存入缓存,QPixmapCache是提供这个功能的核心类.

void PixmapAnimation::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    painter->drawPixmap(0,0,frames.at(current));
}

QPixmap PixmapAnimation::GetFrameFromCache(const QString &filename){
    QPixmap pixmap;
    if(!QPixmapCache::find(filename, &pixmap)){
        pixmap.load(filename);
        if(!pixmap.isNull())
            QPixmapCache::insert(filename, pixmap);
    }

    return pixmap;
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐