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