這個作業基本上是延續第一個作業, 原先的作業大家用程序化的 C 來設計, 並且用文字作為介面, 在這個作業裡希望大家將原來的貪食蛇程式改為物件化的設計, 原先程式中的貪食蛇, 迷宮, 食物, 計分版都只是資料的組合, 並沒有很清楚的物件邊界與介面, 在這個作業裡希望你能夠把這些物件刻劃出來。為了讓你能夠進一步地體會物件的好處, 我也準備了一個簡單的圖形介面, 希望你能夠把你的物件結合到這個圖形介面上, 並且運用繼承的概念來整合整個系統。
這個作業的完成圖應該類似
在這個作業中不希望你去修改這些類別裡頭的實作, 所以基本上你應該不用擔心說看不懂這些類別各自完成了什麼功能, 你的類別裡的功能和這些類別裡的功能也應該沒有什麼關係, 不過為了謹慎起見, 你在逐步將你的程式加入這個 project 的時候, 應該要常常 compile, 執行, 並且檢查是否有記憶體的錯誤。
注意: 這個說明檔案無可避免地相當長, 因為整個程式的架構都不是你很熟悉的東西, 必須要有適當的說明, 不過你下載的程式已經運用繼承的架構做成可以直接編譯執行的樣子了, 希望你製作的部份也已經限制在一個小框框裡, 不要太害怕不熟悉的東西, 你可以一步一步地修改現有的程式, 如此就不致於等到最後發現完全不知道該怎樣偵錯了。
在這個作業中希望你不要去修改這兩個類別的定義, 但是你必須看懂這兩個介面所定義的各種操作方法。
CSnakeGame::CSnakeGame(IGraphicOut * const pGO) : m_pGO(pGO), m_xSize(14), m_ySize(14) { pGO->setDisplaySize(m_xSize, m_ySize); }
virtual void changeSpeed(int speed) {} virtual void leftMove() {} virtual void rightMove() {} virtual void upMove() {} virtual void downMove() {} virtual void timeUp() {} virtual void exit() {} virtual void draw() {}以及前面的建構元函式, 應該就可以做最簡單的測試了。 注意程式還是可以執行的, 這是 interface reuse, old code call new code 的範例, 雖然沒有要求你去寫, 但是你可以從 MFC UI\CSnakeView 類別裡 看到很多這樣的應用。
void CSnakeGame::draw() { ... m_pGO->drawHorzWall(3, 5, 2); m_pGO->drawVertWall(8, 0, 5); ... }上面的程式顯示如下圖:
這個函式可以在指定的格子內畫出蛇的頭, 如下圖:
為了方便作出動畫的效果, 呼叫的時候可以在 type 的格子內指定蛇頭朝向的方位 (left: 0, top: 1, right: 2, down:3) 以及張口的蛇頭 (leftOpen: 10, topOpen: 11, rightOpen: 12, downOpen: 13) 依序如下圖所示:
上面這些常數可以用 IActionHandler::left 或是 IActionHandler::leftOpen 等等 enum IActionHandler::Direction 型態的常數來指定。
這個函式可以在指定的格子內畫出蛇的身體, 如下圖:
這個函式可以在指定的格子內畫出食物, 如下圖:
食物有兩種, 可以用 IGraphicOut::fruit 或是 IGraphicOut::flower 兩個 enum IGraphicOut::FoodType 型態的常數來指定。
呼叫這個函式會在視窗中顯示目前的成績如下圖:
成績的範圍為 0-231-1 (受限於 int 參數)
在一個具有圖形使用者介面 (GUI) 的應用程式中, 光光呼叫上面這些輸出函式時, 在視窗內並不一定能立即看到結果, 所有這些函式的呼叫都需要放在由 IActionHandler 類別中繼承下來的 draw() 函式內, (請參考 CSnakeGame::draw() 函式的實作), 如此系統在需要重新繪製視窗內容時才能夠找到適當的程式碼來完成這個工作; 而這個 draw() 函式究竟在什麼時候會被呼叫到呢? (什麼時候會有 draw 訊息送過來呢?) 主要有兩個時機:
當你的程式發現視窗的內容需要更新時, 應該要呼叫 redraw() 函式來強迫系統呼叫 draw() 函式 (請參考 CSnakeGame::timeUp() 函式之實作)。
呼叫這個函式會在一個獨立的視窗中顯示所有的成績資訊如下圖:
成績的資訊由字元陣列 scoreMessages 傳遞進去。 通常可以在遊戲結束的時候呼叫這個函式, (請參考 CSnakeGame::exit() 函式之範例)。 你的應用程式如何得知遊戲結束了呢? 有兩種情況是代表遊戲結束了:
系統不管在上面兩種狀況中哪一種都會自動呼叫 IActionHandler::exit() 來通知應用程式該結束了。
當應用程式發現蛇已經死掉了, 不打算讓使用者繼續玩下去的時候就呼叫這個函式來要求視窗關閉,
這個類別和剛才的 IGraphicOut 類別一樣都是抽象的類別, IGraphicOut 這個類別最主要是定義一些圖形化的輸出界面來讓你的應用程式使用, 本來可以不需要定義這個類別的, 例如在骨架程式中其實所有的圖形化輸出界面都是由 CSnakeView 這個類別所提供的, 你可以打開這個類別來看一看, 應該可以發現這個類別所定義的函式不只 IGraphicOut 抽象類別定義的那些, 還有很多不太知道是什麼作用的成員函式, 看起來有一點頭昏眼花, 這也就是為什麼定義一個 IGraphicOut 抽象類別給大家使用的原因, 你需要的圖形化輸出界面都定義在這個 IGraphicOut 類別中, 你不需要去看 CSnakeView 類別內和圖形化輸出沒有關係的細節才對。IActionHandler 則是一個掌握圖形化輸入介面的抽象類別, 在程序化的程式製作裡, 輸入通常是由應用程式發出讀取的請求, 例如程式執行 scanf, cin >>, getline, getchar, ... 在圖形化介面的程式中輸入通常是由系統送給你訊息的, 例如在我們的應用程式裡如果使用者按下滑鼠上下左右按鍵時, 視窗系統會送給你的應用程式一個訊息, 這種訊息和我們描述物件運作時所說的物件送給物件的訊息是一致的, 所以視窗系統在送給你的應用程式物件訊息時, 會希望你的物件提供一個成員函式讓系統可以呼叫, 以滑鼠訊息來說就是 IActionHandler::leftMove(), IActionHandler::upMove(), IActionHandler::rightMove(), 及 IActionHandler::downMove() 四個函式, 只要視窗系統發現使用者產生適當的訊息, 視窗系統就會呼叫適當的函式。
在我們這個貪食蛇的應用程式裡, 應用程式並不需要處理所有可能發生的訊息 (例如, 各種按鍵...), 因此我們定義了 IActionHandler 這個抽象的類別來把需要的訊息處理函式集中起來, 你自己的貪食蛇物件需要繼承這個抽象類別, 並且實作下列的這些函式來處理各種輸入狀況:
請參考 CSnakeGame::getSpeed() 的實作, 這個函式應該要傳回一個 1 到 10 之間的整數代表目前遊戲的速度, 如此使用者在按下右鍵選單
更改遊戲的速度時可以看到目前的速度, 也就是在下列對話盒中會顯示目前的遊戲速度。
當使用者在上面的對話盒中更改了遊戲的速度以後, 這個函式會被呼叫到, 參數 speed 則是使用者在介面中所選到的遊戲速度, 請參考 CSnakeGame::changeSpeed() 的實作。
每當使用者按下上下左右按鍵來操作貪食蛇的動作時, 相對應的函式就會被呼叫到, 通常有兩種處理方式, 第一種是像 CSnakeGame 中一樣, 只修改行進的方向, 等待 timeUp() 函式呼叫時才修改蛇的位置, 另一種則是直接修改蛇的位置。 請參考 CSnakeGame::xxxMove() 的實作。
這是一個很重要的函式, 視窗系統每 5 ms (毫秒) 會呼叫一次這個函式, 所以你可以在這個函式中安排貪食蛇的移動, 當你希望遊戲的速度變慢時, 應用程式只要忽略掉一些 timeUp 訊息就可以做到。 請參考 CSnakeGame::timeUp() 的實作。
這個函式是當視窗系統確定應用程式的視窗即將關閉時呼叫的, 這個訊息代表說程式將要結束了。 請參考 CSnakeGame::exit() 的實作。
這個函式中應該要包含所有畫出整個畫面的圖形輸出程式碼, 請參考 CSnakeGame::draw() 的實作。
注意:
回
C++ 程式設計課程
首頁
製作日期: 05/20/2003
by 丁培毅 (Pei-yih Ting)
E-mail: pyting@cs.ntou.edu.tw
TEL: 02 24622192x6615
海洋大學
理工學院
資訊科學系