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>
就這樣了,我們不需要再做什麼了,動畫架構已經支援了閃爍效果。你應該已經想到了客戶代碼怎麼使用它:
在支援新增的“閃爍”效果的過程中,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. 結語
政策類是封裝算法的,但是在實踐中,幾乎可以封裝任何類型的規則。政策模式會使程式中引入很多政策類,适合于扁平的算法結構。政策模式也是“邏輯”和“資料”解耦的典型方法,應用相當廣泛。