zoukankan      html  css  js  c++  java
  • Simple2D-25 精灵动作

      精灵动画作用在精灵上,使精灵表现出动画效果。本文将详细说明如何创建一个简单的动作系统,暂时只有 4 中基本的动作——平移、旋转、缩放和 Alpha 变化,并且这些动作能够自由组合,组成串行动作或并行动作。下图是动作系统的类图:

      动作就是进行插值的过程,需要在每一帧被调用。FrameCall 顾名思义是一个帧调用对象,将 FrameCall 添加到帧调用管理器 FrameCallManager 中,FrameCall 的 Step( float frame_time ) 函数则在每一帧中被调用。

      如果将一个球从 P1 移动到 P2,只给出起点 P1 和终点 P2 的坐标,中间的坐标可以通过线性插值进行计算:

       t 是一个 0 到 1 范围的数,动作系统基于此进行设计。AbstractAction 是一个动作基类,继承于 FrameCall,通过重写 Step( float frame_time ) 函数,累计当前动作执行的累计时间。动作有一个执行总时间 DurationTime 表示经过这个时间长度后动作执行结束;还有一个动作执行的持续时间 ElapsedTime 表示动作执行了多长时间。此时 t = ElapsedTime / DurationTime,AbstractAction 主要进行 t 的计算:

        void AbstractAction::Step(float dt)
        {
            this->BeginAction();
    
            fElapsedTime += dt;
            fElapsedTime = fminf(fElapsedTime, fDurationTime);
    
            /* 缓动动画插值 */
            this->Update(Tween::tween(fElapsedTime, fDurationTime, tweenType));
    
            if ( fElapsedTime >= fDurationTime ) {
                fElapsedTime = 0;
                SigActionFinishedCallback(this);
    
                this->EndAction();
            }
        }

      如果进行线性插值的话,小球的移动动作的速度是一直不变的,有些情况下,可能需要速度发生正弦变化、余弦变化或其他变化。Tween 中封装了了一些常用的缓动曲线,下面的动图是 Tween 缓动动画的演示:

      使用不同的缓动动画,小球会以不同的速度进行移动。将计算出来的 t 传递给 Update( float ratio ) 函数,由继承 AbstractAction 的子类重写 Update( float ratio ) 函数进行响应的插值工作。开始动作前需要调用 BeginAction( ) 函数进行相关的初始化工作,例如重新设置起始值、将 ElapsedTime 清零。

        void AbstractAction::BeginAction()
        {
            if ( bFirstTick == false ) return;
    
            this->Reset();
    
            bFirstTick = false;
            bActionFinished = false;
        }

      由于只需进行一次调用即可,所以设置一个开关,保证它只有第一次才会执行函数内的内容,其中 Reset( ) 函数由子类实现进行初始化工作。在动作结束后要停止 FrameCall 的 Step( ) 函数被调用,表示停止动作。

        void AbstractAction::EndAction()
        {
            bFirstTick = true;
            bActionFinished = true;
    
            if ( HasFrameCallManager() ) {
                this->Stop();
            }
        }

      基本动作

      MoveTo 动作,使精灵从起始位置移动到目标位置。MoveTo 有起始位置和结束位置的属性:

            int nSrcx;
            int nSrcy;
    
            int nDestx;
            int nDesty;

      在重写的 Reset( ) 函数中设置起始位置:

        void MoveTo::Reset()
        {
            AbstractAction::Reset();
    
            nSrcx = pTarget->GetPosition().x;
            nSrcy = pTarget->GetPosition().y;
        }

      然后在 Update( ) 函数中对位置进行插值:

        void MoveTo::Update(float rate)
        {
            pTarget->SetPosition(nSrcx + (nDestx - nSrcx) * rate, nSrcy + (nDesty - nSrcy) * rate);
        }

      除了 MoveTo 动作,还有其他的 MoveBy、ScaleTo、ScaleBy、RotateTo、RotateBy、AlphaTo、AlphaBy 的动作,和 MoveTo 的实现类似。

      组合动作

      很多时候,并不是单纯的执行一个动作,而是一次执行一系列动作或同时执行一系列动作,称为串行动作或并行动作。要实现组合动作,需要一个容器来保存这些组合的动作。ActionGroup 继承于 AbstractAction,是串行动作容器和并行动作容器的父类。ActionGroup 单纯是一个容器,封装了添加动作的方法。但要组合的动作的数量是不确定的,需要一个合理的方法来添加这些动作。在 Signal-Slot 的文章中介绍过 C++ 可变参模板的使用,使用变参模板就可以接收任意数量的动作对象了。

            template<class... Args>
            void AppendAction(AbstractAction* head, Args... args)
            {
                vActions.push_back(head);
                AppendAction(args...);
            }
    
            void AppendAction(AbstractAction* Action)
            {
                vActions.push_back(Action);
            }

      你可以使用方法 AppendAction 来添加动作,也可以在新建对象时添加动作

            template<class... Args>
            ActionGroup(Args... args) : AbstractAction(0)
            {
                this->AppendAction(args...);
            }

      

      SequenceActionGroup 是串行动作组对象,添加到该容器中的动作会被依次执行:

        void SequenceActionGroup::Step(float dt)
        {
            this->BeginAction();
    
            auto Action = vActions[nCurrentActionIndex];
            Action->Step(dt);
    
            if ( Action->IsFinishedAction() ) {
                nCurrentActionIndex++;
            }
    
            if ( nCurrentActionIndex >= vActions.size() ) {
                this->EndAction();
            }
        }

      ParallelActionGroup 是并行动作组对象,添加到该容器中的动作会被同时执行:

        void ParallelActionGroup::Step(float dt)
        {
            this->BeginAction();
    
            for ( int i = 0; i < vActions.size(); i++ ) {
                if ( vFinishedIndices[i] ) continue;
    
                auto action = vActions[i];
                action->Step(dt);
    
                if ( action->IsFinishedAction() ) {
                    vFinishedIndices[i] = true;
                    nRemainActionCount--;
                }
            }
    
            if ( nRemainActionCount == 0 ) {
                for ( auto& ele : vFinishedIndices ) ele = false;
                this->EndAction();
            }
        }

      并行动作有个问题,就是这些动作的执行时长并不一致。有些时长短的动作在动作结束后,由于时长长的动作还没有结束,这就要求已经结束的动作的 Step( ) 不会被调用。所以还需要一个 bool 数组来标志哪些动作已经执行完,不需要调用 Step( )

      其它动作

      除了那些基本的动作,还需要一些特殊用途的动作,类似于容器,通过包装基本动画实现例如重复动作、延时动作和空动作(什么都不干)的功能。

      这些的实现比较简单,重复动作 RepeatAction 记录动作的执行次数,有重复一定次数的,有无限重复的:

        void RepeatAction::Step(float dt)
        {
            this->BeginAction();
    
            pAction->Step(dt);
    
            if ( pAction->IsFinishedAction() ) {
                if ( nLoopCount == -1 ) {
                    return;
                }
                nCurrentLoopCount--;
            }
    
            if ( nLoopCount != -1 && nCurrentLoopCount <= 0 ) {
                this->EndAction();
            }
        }

      延时动作 DelayAction 在一段时间后才进行 Step( ) 的调用:

        void DelayAction::Step(float dt)
        {
            this->BeginAction();
    
            fElapsedTime += dt;
            if ( fElapsedTime <= fDelayTime ) return;
    
            pAction->Step(dt);
    
            if ( pAction->IsFinishedAction() ) {
                this->EndAction();
            }
        }

      而空动作 DummyAction 只继承与 AbstractAction 即可。

      Sprite 使用动作,调用 StartAction( ) 函数将开始动作:

        void RectTransform::StartAction(AbstractAction* action)
        {
            pAction = action;
            pAction->SetTarget(this);
            pAction->Start();
        }

      动作系统就结束了,下面给出一个动图,图中的正方形和圆形执行的都是组合动作:

      详细内容请参考源码。

      源码下载:Simple2D-20.rar

  • 相关阅读:
    element-ui 中 el-table 根据scope.row行数据变化动态显示行内控件
    vue.js 父组件主动获取子组件的数据和方法、子组件主动获取父组件的数据和方法
    把json1赋值给json2,修改json2的属性,json1的属性也一起变化
    win10下当前目录右键添加CMD快捷方式
    element-ui
    vscode 头部注释插件
    IE浏览器new Date()带参返回NaN解决方法
    常用css
    使用DataGridView控件显示数据
    第四章 ADO.NET
  • 原文地址:https://www.cnblogs.com/ForEmail5/p/7468532.html
Copyright © 2011-2022 走看看