Lab 16-1: 節省系統資源的物件架構 Flyweight

 

先下載執行一下這個 Marquee 範例程式, 解壓縮以後裡面有三個程式

Marquee0

Marquee1

Marquee2

第一個應用程式是靜態的顯示不同大小、不同顏色的字元符號; 第二個應用程式讓它們有一些動作, 三排的文字第一及第三排以同樣速度向左移動, 第二排每個字母翻轉後向右移動, 第一排向左移動就移出畫面而已, 第二排移出去以後會由第一排的右邊移入, 第三排的動作類似第二排, 移出去以後會由第二排的左邊移入; 第三個應用程式只有一排, 變成簡單的跑馬燈動畫, 當然一般的 Video game 隨著主角移動時場景的改變也是很像這樣的效果

上面這三個執行程式主要是展示一下 SFML 的貼圖, 給下面要製作的程式一個應用的環境: 在貼出一張圖片的時候, 圖片本身對照到一張影像圖, 這張影像需要載入繪圖卡, 繪圖卡上的記憶體以及由主記憶體搬移到繪圖卡記憶體時的頻寬是比較重要、比較搶手的資源, 所以我們如果要在視窗中貼出同一張圖片時, 影像應該只要載入繪圖卡一次, 不要重複地載入相同的圖片...

原本的影像圖片如下

在程式裡需要載入整張影像, 將個別字母影像, 在不同的位置上以不同的大小, 不同的角度, 不同的顏色繪出, 例如上面第二張圖中左上角的 V 是綠色, 右下角的 V 是橙色, 他們的大小和位置也都不一樣, 但是對照到的基本影像是一樣的, 你可以想像當應用程式裡要重複繪製某個字母圖片很多次的時候, 如果在顯示卡上每一個顯示的字母都對應到不同的繪圖物件的話, 會很浪費記憶體資源。(另一個類似的狀況是瀏覽器裡顯示下載的影像, 如果某一個網頁裡有多張重複的圖片, 瀏覽器需要重複下載相同的圖片嗎? 還是每一張圖片只下載一次, 但是重複顯示很多次?)

我們可以設計一下物件的架構, 使每一個字母的繪圖物件都可以重複使用多次, 主要的影像只載入一次, 顯示的位置、縮放大小、角度、顏色等等參數則每次使用這個物件的時候調整。如果像上面這張圖裡的字型影像還有很多很多組 (不同的字型), 應用程式允許使用者指定任一字母以某一字型、某一位置、某一大小、某一顏色來顯示,基本上程式把整張用到的影像由檔案中載入以後, 需要用到的字元才載入顯示卡產生繪圖物件, 沒有用到的字型影像完全不需要載入, 載入的字型中如果沒有使用到的字母也不需要產生繪圖物件。

在 SFML 環境中我們可以考慮設計下圖的類別架構:

上圖中最上面和最下面淺灰色的 sf::Texturesf::Sprite 是 SFML 中提供的類別, sf::Texture 是用來載入影像圖片的, sf::Sprite 則是可以指定位置、大小、顏色、角度的繪圖物件, 不過大家還不熟悉 SFML, 而且真的放繪圖物件進來以後上面的類別功能解釋起來繁瑣多了, 所以我們先簡化一下, 先不要用 SFML (不要失望, 後續你多練習一下 SFML, 很快就可以構思如何有效率地設計一開始的那幾個 demo 程式), 考慮只有 CharSpriteFactory 和 CharSprite 兩個類別, 你先假設「建構 CharSprite 類別的物件」是很花費資源的, 上面這個設計的精神著重於: 「節省資源, 整個系統中不同的 font 和 symbol 的 CharSprite 物件只保留一個」, 請根據下面簡化過的類別圖、類別的功能說明、還有部份的程式碼來完成相關的設計

首先下面的 main() 函式就代表上面類別圖中的 Client, 也就是運用 CharSpriteFactory 和 CharSprite 兩個類別的客戶, 定義下列的 Character 結構來整合記錄字型和符號 (下面的 Doodle, Cartoon, Tatoo 是假想的字型名稱)

struct Character
{
    string font;
    char symbol;
};

int main()
{
    Character documentData[] = 
                               {{"Doodle", 'R'}, {"Doodle" , 'A'}, 
                                {"Doodle", 'C'}, {"Cartoon", 'E'},
                                {"Tatoo" , 'C'}, {"Tatoo"  , 'A'}, 
                                {"Tatoo" , 'R'}, {"Tatoo"  , 'M'},
                                {"Doodle", 'A'}, {"Tatoo"  , 'D'}, 
                                {"Doodle", 'A'}, {"Tatoo"  , 'M'}};
    vector<Character> document(documentData, documentData+12);

    // 上面這個 document vector 物件裡面紀錄要顯示文件的內容, 其中有 12 個文字, 
    // "RACECARMADAM", 每個文字還有指定顯示的字型, 目前假設有 Doodle 
    // Cartoon 和 Tatoo 三種字型

    // 下面的 factory 物件是用來產生管理所有 CharSprite 物件的, 客戶端使用
    // getSprite() 介面來取得指定 font 和 symbol 的 CharSprite 物件的指標, 
    // factory 物件需要檢查參數 font 和 symbol, 看看是不是已經產生過了, 
    // 如果是的話就直接回傳那個 CharSprite 物件的指標, 否則就用指定的 font 
    // 和 symbol 產生一個新的 CharSprite 物件, 再傳回新產生物件的指標

    CharSpriteFactory* factory = new CharSpriteFactory;

    // 下面的迴圈中, 客戶端程式就是運用這個 factory 傳回需要的 CharSprite
    // 物件的指標 s, 然後運用 CharSprite::draw() 介面來以指定的縮放比例和位置
    // 繪出那個字, 因為我們已經把 Sprite 和 Texture 拿掉了, 所以 draw 介面
    // 裡面不要真的繪製圖片了, 只要把 font, symbol, scale, xPosition, yPosition
    // 印出來就好了

    float scale = 0.9f;
    int xPosition = 0, yPosition = 0;
    for (int i=0; i<document.size(); i++)
    {
        CharSprite *s = factory->getSprite(document[i].font, document[i].symbol);
        s->draw(scale, xPosition, yPosition);
        scale*=1.01;
        xPosition+=30;
        yPosition+=40;
    }
    
    delete factory;

  return 0;
}

程式輸出

(Doodle,R,0.9,0,0)
(Doodle,A,0.909,30,40)
(Doodle,C,0.91809,60,80)
(Cartoon,E,0.927271,90,120)
(Tatoo,C,0.936544,120,160)
(Tatoo,A,0.945909,150,200)
(Tatoo,R,0.955368,180,240)
(Tatoo,M,0.964922,210,280)
(Doodle,A,0.974571,240,320)
(Tatoo,D,0.984317,270,360)
(Doodle,A,0.99416,300,400)
(Tatoo,M,1.0041,330,440)

上面這段程式碼還缺 CharSprite 還有 CharSpriteFactory 兩個類別還沒定義, 所以還沒有辦法編譯, 讓我們加上最少的程式碼來編譯這一段程式

class CharSprite
{
public:
    void draw(float scale, int xPosition, int yPosition) 
    { 
        cout << '(' << scale << ')' << endl; 
    }
};

class CharSpriteFactory
{
public:
    CharSprite* getSprite(string font, char symbol) { return &tmp; }
private:
    CharSprite tmp;
};

上面這段程式只要加入需要的 #include 敘述就可以編譯執行了, 其中藍色的部份只是暫時的, 只是為了符合介面的型態, 同時讓程式可以暫時執行而已, 下面的步驟裡你應該會把它們改掉。

請完成 CharSprite 類別的設計

  1. 根據上面的類別圖需要設計兩個資料成員 m_font 和 m_symbol

  2. 需要設計建構元函式 CharSprite(string font, char symbol);

  3. 需要完成 void draw(float scale, int xPosition, int yPosition); 函式 (只需要像上面範例印出 font, symbol, scale, xPosition, yPosition 就好了)

程式應該還是可以編譯執行的, 會印出多一點東西了, 建構元函式只需要把傳入的資料記錄下來就可以, 建構元函式是給 CharSpriteFactory 物件使用的, 並不是給客戶端使用的

請完成 CharSpriteFactory 類別的設計

目標: CharSpriteFactory 產生出來的 factory 物件是用來產生管理所有 CharSprite 物件的, 客戶端使用 CharSpriteFactory::getSprite() 介面來取得指定 font 和 symbol 的 CharSprite 物件的指標, factory 物件需要檢查參數 font 和 symbol, 看看指定的 CharSprite 物件是不是已經產生過了, 如果是的話就直接回傳那個 CharSprite 物件的指標, 否則就用指定的 font 和 symbol 產生一個新的 CharSprite 物件, 紀錄在 m_sprites 容器裡面, 再傳回新產生物件的指標, 客戶端並不擁有這些 CharSprite 物件, 所以客戶端用完以後不要刪除這些物件, 要留給 factory 物件來刪除

  1. 根據類別圖請設計 m_sprites 這個容器, 需要紀錄產生出來的 CharSprite 物件的指標, 需要能夠查詢指定的 font 和 symbol 的 CharSprite 物件是不是存在於這個容器裡面

    請使 用 STL 裡面的 map 來實作 , map 是一個 binary search tree, 查詢的時候最迅速, 而且它的介面很簡單

    不管你打算用哪一種容器實作, 因為查詢的關鍵字有兩個欄位 font 和 symbol, 請定義下面的結構來輔助

    struct FontSymbol
    {
        FontSymbol(string f, char s);
        string font;
        char symbol;
    };

    請定義並且實作成員函式來比對 font 和 symbol 兩個不同型態的關鍵字

    bool operator<(const FontSymbol &rhs) const;

    如此以使用 map<FontSymbol, CharSprite*> 來實作
    m_sprites

  2. 要查詢 map 物件裡面存不存在某一個鍵值, 你不能用 if (m_sprites[FontSymbol(font, symbol)] == ???) 來測試, 這樣子做的話當想要查詢的 FontSymbol 不在容器裡的時候,map::operator[] 會直接放進去一個新的資料, 你需要用 if (m_sprites.find(FontSymbol(font, symbol) == m_sprites.end()) 來測試, 請完成 getSprite() 介面函式, 在 m_sprites 中沒有指定的物件時, 動態地產生 CharSprite 物件

  3. 請運用 map<FontSymbol, CharSprite*>::iterator 來完成 CharSpriteFactory 類別的解構元

到這裡應該可以得到需要的輸出了

請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\資料匣下的所有內容) 壓縮起來, 選擇 Lab16-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式

後續:

  1. 在 SFML 的環境中, 你可以運用 sf::Texture 類別來載入影像, sf::Sprite 類別來作出每一個字母的繪圖物件, 你可以用最前面下載的 Marquee 應用程式裡面的影像檔案 graffitiAlphabetFacialEmotion.png 和資料檔案 graffitiAlphabetFacialEmotion.txt 來設計, 資料檔案裡面每一個字母有一列來描述, 例如 A 9 11 49 93 24, 其中 (9,11) 是左上角, (49,93) 是寬度和高度, 24 是字型基準線以下的高度

  2. 上面這個是 Flyweight 樣版的一個特例, 一般化的類別圖如下


    設計 Factory  類別除了管理所有共享的 Flyweight 物件之外, 是為了去除 Client 和真正 ConcreteFlyweight 類別的產生程式碼之間的相依性; 設計 Flyweight 抽象類別是為了完全隔絕 Factory 和 Client 以及 ConcreteFlyweight 類別的相依性; Flyweight 類別裡面的 intrinsicState 是那些固定不動的資料, 以上面例子來說就是影像的資料, extrinsicState 是那些需要調整的資料, 以上面的例子來說就是圖片的顯示位置、以及縮放大小。

C++ 物件導向程式設計課程 首頁

製作日期: 05/18/2017 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon