闲谈策略模式——基于UI动画框架

15 篇文章 0 订阅
9 篇文章 0 订阅

  UI动画框架中,对动画的行为进行了良好的抽象,使得动画逻辑(就是各种动画效果)和动画主体(就是承载动画的窗口,CEGUI中的Window对象)最大限度的解耦。最终,自然而然的演变出来“策略模式”。那就先看看“策略模式”的相关概念。

1. 策略模式

  策略模式的定义如下:

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。Strategy模式使算法可以独立于使用它的客户而变化。

  再看一下类图:

在这里插入图片描述

  策略模式对一个业务使用不同的规则或算法,其目的是将对算法的选择和算法的实现分离,允许根据上下文进行选择。从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,导致的结果可能相同,也可能不同。

  Strategy类定义了算法的接口,其各种具体派生类分别封装不同的算法。Context类持有Strategy类句柄,两者相互协作以实现所选的算法,有时候Strategy需要查询Context,可以通过算法接口的参数传递Context句柄,供具体算法实现时回调Context的方法获取需要的数据。Context的Strategy引用可以在运行时刻被任意替换成其它的具体Strategy。为Context选择具体哪个Strategy,一般是客户代码来完成。

  如果发现一系列算法的实现步骤都差不多,只是在某些局部步骤上有所不同,那么抽象类Strategy里面可以定义算法实现的骨架,让具体的策略类实现变化的部分。这样的一个结构自然就变成了“策略模式”+“模板方法模式”了。

  在UI动画框架中有两个地方用到了策略模式:“动画策略”和“移动轨迹”。

2. 动画策略模式

  先看下动画策略类的结构图:

在这里插入图片描述

  一个Window需要完成什么样的动画,是由客户代码(Client)指定的,Window就是动画算法运行的Context,而动画效果各式各样:平滑、移动、淡入淡出、缩放、震动等等,如果不设法将Window和具体的动画算法分离,那么Window对象势必会掺入太多动画逻辑的代码,这对CEGUI的Window类是一个巨大的破坏,因此,我们需要封装!如类图所示,Window只需要持有一个IStrategy接口类的句柄,它不需要了解隐藏在IStrategy后面的具体是什么样的动画算法,在需要表现动画时,Window对象将自己的句柄作为参数传递给IStrategy的Run接口,剩下的就由具体Strategy去操控了:

void Window::updateSelf(float elapsed)
{
    ... ...

    // 动画处理
    if (d_aniStrategy && d_parent->isVisible())
    {
        d_aniStrategy->Run(*this);
        if (d_aniStrategy->IsFinished(*this))
        {        
            AnimationManager::GetSingleton().Restore(d_aniStrategy);
            d_aniStrategy = 0;
        }
    }
}

Window类所做的改变就是这些了(当然还是得有接口负责为Window对象安置d_aniStrategy)。具体的动画算法在运行时,通过参数取得Window对象的必要信息进行运算,比如“淡入淡出”策略:

bool FadeStrategy::Run(Window& aniWnd)
{
    float alpha = aniWnd.getAlpha();
    if ((delta < 1.0 && alpha >= minAlpha) || (delta > 1.0 && alpha < maxAlpha))
    {
        float xalpha = alpha * delta;
        aniWnd.setAlpha(xalpha > 1.0f ? 1.0f : xalpha);
    }
    return true;
}

该策略的算法从aniWnd参数取得动画窗口的alpha值,运算后再更新动画窗口的alpha,从而实现“淡入淡出”的效果。

  我们将动画策略(算法)称之为“逻辑”,将Window对象称之为“数据”,策略模式的意图就是将“逻辑”和“数据”分离,以彻底实现解耦的目的。动画窗口和动画算法解耦之后,可以轻而易举地在不修改动画窗口的前提下增加新的动画效果。比如,在MMORPG游戏中,玩家和好友聊天,当有好友的消息到来时,要求右下角的聊天信息小窗口闪烁几下提示玩家。于是“闪烁”的动画需求理所当然地被提出来了!我们来看看,怎样在动画框架中增加一个“闪烁”的效果。
  首先,我们实现一个闪烁的算法:

class FlickerStrategy : public IStrategy
{
private:
    virtual FlickerStrategy* Clone() const;
    virtual void Reset(void);

    virtual bool IsFinished(Window& aniWnd) const;
    virtual bool Run(Window& aniWnd)
    {
        float alpha = aniWnd.getAlpha();
        if ((delta > 0 && alpha >= maxAlpha) || /* 开始变暗 */
            (delta < 0 && alpha <= minAlpha))   /* 开始变亮 */
        {
            delta = 0 - delta; // 反转
        }
        aniWnd.setAlpha(alpha * (1.0f + delta));
        return true;
    }

private:
    float       delta;         // alpha的变化率
    const float maxAlpha;      // alpha上限
    const float minAlpha;      // alpha下限
};

然后,我们配置一条动画策略:

<strategys id = "234" type = "serial" param = "3" desc = "闪烁3次" >
    <strategy id = "frame" param = "40" /> <!-- 40ms一帧 -->
    <strategy id = "fliker" param = "0.4;0.1;1" /> <!-- 在0.1~1之间闪烁,每次0.4的变化率 -->
</strategys>

就这样了,我们不需要再做什么了,动画框架已经支持了闪烁效果。你应该已经想到了客户代码怎么使用它:

GuiWinMgr:RunAnimation(GuiWinMgr:getWindow("Root/FriendChatTips"), 234);

  在支持新增的“闪烁”效果的过程中,Window类似乎不存在一样,我们并没有提到它,因为,我们已经封装了变化,扩展的新功能(算法)被隐藏在IStrategy背后,Window(数据)不需要了解这些,因此它不需要作任何改动。我们似乎做到了“对修改关闭,对扩展开放”!

3. 移动策略模式

  再来看看“移动策略模式”。先看下移动动画策略:

class MoveStrategy : public IStrategy
{
private:
    bool Run(Window& aniWnd)
    {
        if (steps > 0)
        {	
            UVector2 curPos = aniWnd.getPosition();
            path->Run(curPos);
            aniWnd.setPosition(curPos);
            --steps;
        }
        return true;
    }
private:
    int      steps;     // 移动步数
    Locus*   path;      // 移动路线对象,实现type指定的路线
};

移动的轨迹有很多种:水平直线、垂直直线、斜线、二次曲线(抛物线、圆)等等。MoveStrategy类只负责处理和动画相关的数据,比如移动步数,至于具体的路线轨迹,它并不关心。因此路线策略(算法)的封装又闪亮登场了!Locus抽象类将各种移动轨迹纳入其抽象范围,不同移动轨迹的具体实现对Locus透明!因此扩展或替换移动轨迹,MoveStrategy不需要作任何改动!类图如下:

在这里插入图片描述

  看几个常用的轨迹算法:

class Locus
{
public:
    virtual ~Locus() = 0 {}
    virtual void Run(UVector2& pos) = 0;
};

// 水平移动,只有x坐标会变
class Horizontal : public Locus
{
public:
    virtual void Run(UVector2& pos)
    {
        pos.d_x += UDim(0, delta);
    }

private:
    const float   delta;     // 移动步长
};

// 斜线移动:y = kx + b
class Bias : public Locus
{
public:
    virtual void Run(UVector2& pos)
    {
        if (k > -1 && k < 1)
        {
            pos.d_x += UDim(0, dx);
            pos.d_y += UDim(0, dx * k);
        }
        else
        {
            pos.d_y += UDim(0, dy);
            pos.d_x += UDim(0, dy / k);
        }
    }

private:
    const float    k;       // 系数
    const float    dx;      // x步长
    const float    dy;      // y步长
};

// 抛物线移动:y = k(x - a)^2 + b,以x为自变量
class Parabola : public Locus
{
public:
    virtual void Run(UVector2& pos)
    {
        float dy = (2 * sx + dx - 2 * a) * dx * k;
        sx += dx;
        pos.d_x.d_offset += dx;
        pos.d_y.d_offset += dy;
    }

private:
    const float     k;     // 系数k
    const float     a;
    const float     dx;    // x步长
    mutable float   sx;    // x增量
};

  如果想增加一个圆形环绕的算法,很简单:

// 圆形环绕:x = x0 + r * cosθ,y = y0 + r * sinθ
class Circle : public Locus
{
    virtual void Run(UVector2& pos)
    {
        pos.d_x.d_offset += r * (cos(angle + da) - cos(angle));
        pos.d_y.d_offset += r * (sin(angle + da) - sin(angle));
        angle += da;
    }

private:
    const float     r;         // 半径
    const float     da;        // 角度步长,>0表示逆时针转,<0表示顺时针
    mutable float   angle;     // 角度
};

然后在创建MoveStrategy对象时,将Circle对象句柄赋予path字段,就实现了。我们再一次做到了“对修改关闭,对扩展开放”!

4. 结语

  策略类是封装算法的,但是在实践中,几乎可以封装任何类型的规则。策略模式会使程序中引入很多策略类,适合于扁平的算法结构。策略模式也是“逻辑”和“数据”解耦的典型方法,应用相当广泛。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值