天天看點

閑談政策模式——基于UI動畫架構1. 政策模式2. 動畫政策模式3. 移動政策模式4. 結語

  UI動畫架構中,對動畫的行為進行了良好的抽象,使得動畫邏輯(就是各種動畫效果)和動畫主體(就是承載動畫的視窗,CEGUI中的Window對象)最大限度的解耦。最終,自然而然的演變出來“政策模式”。那就先看看“政策模式”的相關概念。

1. 政策模式

  政策模式的定義如下:

定義一系列的算法,把它們一個個封裝起來,并且使它們可以互相替換。Strategy模式使算法可以獨立于使用它的客戶而變化。

  再看一下類圖:

閑談政策模式——基于UI動畫架構1. 政策模式2. 動畫政策模式3. 移動政策模式4. 結語

  政策模式對一個業務使用不同的規則或算法,其目的是将對算法的選擇和算法的實作分離,允許根據上下文進行選擇。從概念上來看,所有這些算法完成的都是相同的工作,隻是實作不同,導緻的結果可能相同,也可能不同。

  Strategy類定義了算法的接口,其各種具體派生類分别封裝不同的算法。Context類持有Strategy類句柄,兩者互相協作以實作所選的算法,有時候Strategy需要查詢Context,可以通過算法接口的參數傳遞Context句柄,供具體算法實作時回調Context的方法擷取需要的資料。Context的Strategy引用可以在運作時刻被任意替換成其它的具體Strategy。為Context選擇具體哪個Strategy,一般是客戶代碼來完成。

  如果發現一系列算法的實作步驟都差不多,隻是在某些局部步驟上有所不同,那麼抽象類Strategy裡面可以定義算法實作的骨架,讓具體的政策類實作變化的部分。這樣的一個結構自然就變成了“政策模式”+“模闆方法模式”了。

  在UI動畫架構中有兩個地方用到了政策模式:“動畫政策”和“移動軌迹”。

2. 動畫政策模式

  先看下動畫政策類的結構圖:

閑談政策模式——基于UI動畫架構1. 政策模式2. 動畫政策模式3. 移動政策模式4. 結語

  一個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不需要作任何改動!類圖如下:

閑談政策模式——基于UI動畫架構1. 政策模式2. 動畫政策模式3. 移動政策模式4. 結語

  看幾個常用的軌迹算法:

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. 結語

  政策類是封裝算法的,但是在實踐中,幾乎可以封裝任何類型的規則。政策模式會使程式中引入很多政策類,适合于扁平的算法結構。政策模式也是“邏輯”和“資料”解耦的典型方法,應用相當廣泛。

繼續閱讀