俄罗斯方块:win32api开发
生活随笔
收集整理的這篇文章主要介紹了
俄罗斯方块:win32api开发
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
本文簡述一門課程,演示win32api開發(fā)俄羅斯方塊的開發(fā)過程。如果學生學習過C語言,沒學過或者學習C++不好,剛剛開始學習win32api程序設計,還不懂消息循環(huán)和注冊窗體類。
我這學期講一門課,本科三年級,學生滿員17人。一般接近滿員,最低一次5人,那天據(jù)林同學說,其它的同學都去看足球賽了。
課程名字叫做算法與程序設計實踐3。第一堂課我照例要解釋:到了"3"這個階段,就不講算法了,僅僅有實踐。只是,后來看看算法也還是有一點應用,比方從一個線性表里刪除符合條件的元素們,在線性表里查找符合條件的元素,這樣的難度的。
課是在機房上的,大部分時間學生和教師都看著顯示,所以一學期下來,好多同學和我見面可能都不太認識。只是我們對代碼的形成過程更熟悉一些。
我試圖貫徹下述原則:學生應該看到教師編程的過程,而不不過結(jié)果;學生應該看到在編輯器和編譯器中的代碼,而不是WORD或PPT里的;學生應該先學會臨模教師的編程過程,而沒有能力直接臨模結(jié)果;學生甚至應該看到教師的錯誤及錯誤的解決過程、教師的無知及檢索過程,學生不應該看到事先排練的完美的編程過程和全知全能的教師,那樣的過程和專家,學生模仿時無從下手。
所以,我課前不準備,在課堂上無意犯各種錯誤--偶爾演示學生們easy犯的錯誤--及解決。在LOG文件里記錄我們的計劃和當前的進度,在繪圖里畫下原型。
所以,我假裝對某些API和函數(shù)不熟悉,演示在MSDN和互聯(lián)網(wǎng)中查找手冊和解決方式的步驟。單獨做一些技術(shù)原型驗證對API的調(diào)用結(jié)果的猜想,而不是在project的過程中在項目代碼中測試技術(shù)。有時,我知道問題在哪里,可是要先列出各種可能,然后一一驗證猜想(而不是直接解決,這似乎是計算機本科生很easy犯的錯誤,假設攻克了就認定那是問題的解決辦法)。除了這兩點,其余的時間我應該盡可能誠實。
有時候,學生會告訴我哪里錯了,先于我發(fā)現(xiàn)問題的解決辦法。這令我享受這種教學過程。
終于,我們--以我編碼為主--實現(xiàn)了WIN32API開發(fā)的俄羅斯方塊。
選擇俄羅斯方塊的原因,是由于小游戲的業(yè)務邏輯足夠復雜,保證學生了解在相對復雜的業(yè)務邏輯時的面臨的問題和編程行為與toy作品不同;所使用的到技術(shù)較少,避免過多的機制 (數(shù)據(jù)庫、網(wǎng)絡等)分散學生的注意力,保證學生把精力集中在對業(yè)務邏輯上。
選擇win32api是課堂上投票的結(jié)果。選擇C語言而沒有使用C++有兩個原因。一是學生的C++掌握通常并不熟練;二是我希望學生能在項目中發(fā)現(xiàn)面向?qū)ο蟮谋匾院烷L處,而不是僅由于學習過哪些語言而在project中選用;三是希望演示用C也能夠?qū)崿F(xiàn)基于對象的程序設計 (不是面向?qū)ο?#xff0c;不包含繼承,僅包含方法與數(shù)據(jù)的內(nèi)聚)。
2. 技術(shù)原型
涉及到的技術(shù)原型,要在project開始前建立小項目,以驗證對這些技術(shù)的掌握和對效果的猜想。
要實驗的技術(shù)列表,來源于需求。我們先不寫代碼,口頭描寫敘述需求,然后分解需求到所需的技術(shù)。這樣就形成了技術(shù)列表。這個過程中,同一時候也形成了定義,包含名詞和動詞表。
這些技術(shù)原型也限定了除C語言以外須要掌握的技術(shù),在這次開發(fā)其中。
技術(shù)原型包含:
* 使用GDI繪圖、擦除。用于畫小塊和移動小塊。移動是依據(jù)視覺暫留在新的位置上繪圖,并把舊位置上的小塊以底色重畫。
* 鍵盤消息響應。用于在不暫停小塊下落的情況下接受玩家通過按鍵操縱小塊左移、右移、旋轉(zhuǎn)、高速下落。
* 特定范圍的隨機數(shù)生成。用于在創(chuàng)建新的小塊時,決定是哪個類型。類型計有S型、L型、凸形、田形,及它們的旋轉(zhuǎn)。
* 計時器 (timer),用于驅(qū)動小塊定時下落,推斷是否該清除一行,計分,刷新工作區(qū) (重畫) 等。
* 在工作區(qū)輸出文字。用于調(diào)試和顯示分數(shù)。
終于形成的原型部分代碼量例如以下。代碼在附件中的 prototype文件夾下
繪圖 (及消息循環(huán)) ,draw,226行
擦除,eraser,263行
在工作區(qū)輸出文字,textout,201行
按鍵消息響應,key,207行
隨機數(shù),random, 31行
計時器,timer,214行
3. 開發(fā)過程的里程碑
技術(shù)原型確定以后,再又一次回到需求,并把需求排期。爭取每次課程限定完畢一個功能。
需求排期遵循的原則是:優(yōu)先完畢對其它功能無信賴的部分;優(yōu)先完畢核心功能。
下面是開發(fā)過程中的里程碑。
1) 生成塊。
2) 計時器驅(qū)動,塊自己主動下降
3) 鍵盤控制塊 旋轉(zhuǎn)、高速下降、左移、右移
4) 落究竟或粘在底部已存在塊上 (if (conficted || touch_bottom) stick)
5) 刪除一行:刪除一行,把之上的行下降一行
6) 計分:消除一行和多行分值不同
下面功能在本學期沒有實現(xiàn)。
7) 生成新塊前,在預覽區(qū)顯示下一個塊
8) 分數(shù)積累到一定程度 (?),加快塊下落的速度
開發(fā)過程以git版本號控制方式記錄了歷史,每一個重要功能一次commit,以日期作為message。
4. 定義
我們在開發(fā)前用示意圖約定了一些定義,作為詞匯表。排版原因,我在這里有文字解釋一下。
俄羅斯方塊元素:工作區(qū)上畫圖的最小單位,是一個小方格。俄羅斯方塊的名字 Terris 即四元素,由于每一個當前塊由4個元素組成。
數(shù)組元素:即C語言中的數(shù)組元素,數(shù)組中的某一個。提出這個定義是為了差別于俄羅斯方塊的元素。
當前塊 (current block) :正在移動的由四個元素構(gòu)成的塊。有S型、L型、田字型等類型。
已存在的塊 (exist block) :堆積在工作區(qū)底部的,已經(jīng)粘成一團的元素。
像素坐標,世界坐標。像素坐標是由GDI畫圖定義的,世界坐標由我們定義,以元素為單位,左上是原點 (0,0) ,向右向下遞增。
stick。當前塊接觸到已存在的塊,或者當前塊接觸到工作區(qū)底部,此時應該把當前塊增加到已存在的塊中,然后生成新的當前塊;假設導致已存在的塊中某一行充滿元素,須要按游戲規(guī)則刪除此行,然后把已存在的塊中此行以上的元素降落一行。
5. 數(shù)據(jù)結(jié)構(gòu)及流程
下面介紹當前塊、已存在塊、鍵盤操作、刪除已存在塊中的一行的數(shù)據(jù)結(jié)構(gòu)和流程。
5.1 當前塊
當前塊中,包含當前塊的下面數(shù)據(jù):當前坐標,上一次的坐標 (用以擦除) ,當前類型 (接下來會解釋),上一次的類型 (用于旋轉(zhuǎn))。結(jié)構(gòu)體例如以下,整個程序中僅僅有這個結(jié)構(gòu)體的唯一實例。
struct struct_block{
int x;
int y; /* row 0, col 0 */
int old_x;
int old_y;
int* type;
int* old_type;
};
當前塊的類型使用數(shù)組實現(xiàn),例如以下,各自是一字型、田字型、凸字型。
int line_v_block[]={0, 0, 0, 1, 0, 2, 0, 3};
int line_h_block[]={0,0,1,0,2,0,3,0};
int tian_block[]={0, 0, 0, 1, 1, 0, 1, 1};
int tu_v_block[]={0,1,1,0,1,1,2,1};
int tu_h_block[]={0,1,1,0,1,1,1,2};
數(shù)組中的每兩個數(shù)值 (數(shù)據(jù)中的元素)代表一個當前塊中的元素的坐標,計8個數(shù)值代表4個元素。
生成塊時,
current_block.type = line_v_block;
指定了當前塊的元素。
畫圖時,遍歷"類型數(shù)組",把每一個元素繪出。不管何種類型,都遵循這一流程,從而實現(xiàn)"以數(shù)據(jù)作為代碼":類型數(shù)組即數(shù)據(jù),遍歷"類型數(shù)組"、在旋轉(zhuǎn)時改變類型等即為引擎。
旋轉(zhuǎn)的代碼演示樣例,改變類型 (的指針) :
if(current_block.type == line_v_block)
{
current_block.type = line_h_block;
}
平移的代碼演示樣例,改變橫坐標:
current_block.x -= 1;
自己主動下降的代碼演示樣例,改變上一次的縱坐標和當前縱坐標。
if(! is_conflicted() && ! is_touch_bottom())
{
current_block.old_y = current_block.y;
current_block.y = current_block.y + 1;
}
else
{
stick();
generate_block();
}
高速下降:
縱坐標 添加 全部元素中到達底部 (或已存在塊中同一橫坐標的頂) 的最短距離。
貌似題外話,helper函數(shù):is_conflicted(),推斷當前塊是否接觸到已存在塊;is_touch_bottom(),推斷當前塊是否觸底;匹配橫坐標,給出當前塊的底坐標;求當前塊距離底部的最短距離。等等。
開發(fā)helper函數(shù)的目的,是為了使程序總體流程清晰。保障總體清晰的方法之中的一個,是要求每一個函數(shù)內(nèi)容不得超過一屏。假設超過了,就須要折解出 helper 函數(shù)。在主流程中調(diào)用 helper 函數(shù),而把helper函數(shù)體移出主流程,這樣主流程代碼長度就下降了。這和小學寫作文的時候,老師要求先拉大綱是一個道理。常常有同學說,在開發(fā)過程中會發(fā)現(xiàn)新的功能,在開發(fā)遇到新的技術(shù),沒有做原型的,因此難以把握大綱。這都說明把握大綱和做計劃的能力還差,須要通過練習來訓練。這和小學生寫著寫著作文發(fā)現(xiàn)須要查字典,或者寫跑題了,是一個道理。我們的成長并不是認識的字多了,而是能預見到將會用到哪些字 (甚至表達手法、寫作素材)。
此外,在面向?qū)ο笾?#xff0c;有些的函數(shù)會成為game (或者 current block 或者 exist block )的成員函數(shù)。這在開發(fā)中會認識到,假設它們與數(shù)據(jù)能內(nèi)聚在一個類中,該是多么方便,因此了解面向?qū)ο蟮脑谛畔㈦[藏方面的優(yōu)勢。這些函數(shù)應歸屬于哪個類,是由哪個類承擔這個責任決定的。
5.2 已存在塊
已存在塊中包含下面數(shù)據(jù)結(jié)構(gòu):塊的長度 (其實,是塊的長度*2,代碼中以橫坐標和縱坐標作為兩個數(shù)組元素) ,已存在塊數(shù)組。例如以下。
int exist_block_size=0;
int exist_block[(maxx+1)*(maxy+1)];
這樣的數(shù)據(jù)結(jié)構(gòu),及當前塊的數(shù)據(jù)結(jié)構(gòu),把橫縱坐標無區(qū)別地,不以結(jié)構(gòu)體地方式放在數(shù)組中,在興許開發(fā)中帶來了麻煩。只是因為課程時間有限,后來,我未對此做出改動。應該逐漸演化程序結(jié)構(gòu),形成以元素作為結(jié)構(gòu)體的數(shù)組。再開發(fā)出一些helper甚至成員函數(shù),遍歷時以俄羅斯方塊元素為單位,而不是當前代碼中的以數(shù)組元素為單位。
對已存在塊數(shù)據(jù)結(jié)構(gòu)操作的函數(shù)之中的一個是 stick,用于在當前塊觸底 (或觸及已存在塊)時,把當前塊中的元素移到已存在塊中。
有不少helper函數(shù),基本都是通過遍歷 exist_block,按匹配條件讀當中的坐標。包含:匹配橫坐標,給出已存在塊的頂坐標 int get_exist_block_top(int x)。
5.3 鍵盤操作 & 動作序列
玩家操作塊這一操作,由鍵盤消息響應開始。我們不在鍵盤響應中處理這一事件,而是僅僅在這里記住這個動作,增加動作序列中。這是后來的版本號。最初的版本號,我們也不在鍵盤響應中處理事件,而是調(diào)用 block.cpp 中的函數(shù)。原則是:凡依賴win32api的,放在 tetris.cpp 中,如 timer, 鍵盤響應,畫圖;凡是與業(yè)務邏輯有關(guān),平臺無關(guān)的,放在 block.cpp 中。接收向上箭頭,是鍵盤響應,平臺相關(guān),所以放在 tetris.cpp 中;此時調(diào)用的 rotate,用于改變當前塊的類型或坐標,平臺無關(guān),所以放在 block.cpp 中。
動作序列的數(shù)據(jù)結(jié)構(gòu)例如以下。在動作序列數(shù)組buffer_action_seq中,數(shù)組動作元素
(動作) 的類型是 枚舉 action。
enum action{ action_left=1, action_right=2, action_speed_down=3, action_rotate=4, action_down_auto=5, action_na=0};
action buffer_action_seq[action_size]={action_na};
int buffer_action_cursor = 0;
由玩家觸發(fā)鍵盤消息開始,流程例如以下。
1)鍵盤消息響應:
buffer_action_seq[buffer_action_cursor++] = action_rotate;在動作序列中增加一個動作。這相應于設計模式中的 commander 模式要解決的問題。
2)在timer中自己主動下降
timer中 buffer_action_seq[buffer_action_cursor++] = action_down_auto; 在動作序列中增加一個動作。
3)在timer中觸發(fā)WM_PAINT
timer 中 InvalidateRect 觸發(fā) WM_PAINT
4)WM_PAINT中運行動作序列
erase_old_block_seq(hdc);
erase_old_block_seq (hdc) 遍歷動作序列,按每一個動作改變當前塊坐標,然后擦除因為動作產(chǎn)生的舊塊。遍歷動作序列以后,就完畢了自上個 timer 周期以來全部的動作,擦除了這期間產(chǎn)生的全部舊塊。
void erase_old_block_seq(HDC hdc) 片斷例如以下:
for (i = 0; i < buffer_action_cursor; i++)
{
switch (buffer_action_seq[i])
{
case action_left:
move_left();
erase_old_block(hdc);
break;
在序列里的每一個動作中,move_left 改坐標, erase_old_block(hdc) 擦除舊塊.
5)WM_PAINT畫新的當前塊和已存在塊
draw_current_block(hdc);
draw_exist_block(hdc);
由于重繪比計算花費的時間要多,作為性能優(yōu)化,假設當前塊與舊塊坐標全然同樣,不重畫。
另,還有一個版本號的動作序列,不使用枚舉和swtich-case,通過把函數(shù)作為消息傳遞給責任者,實現(xiàn)disptach:
void (*next_action)() = move_still;?
next_action = move_left
當中 move_left是一個函數(shù)。next_action這種元素 (類型是函數(shù)) 組成一個數(shù)組,作為動作序列。運行動作序列時,用以下這種代碼:
while ( next_action++ != action_termination )
? ? ? next_action;
因為 next_action 既是函數(shù),也是數(shù)組元素的指針,因此上述代碼不是偽代碼,而是能夠運行的。這類似于 jump table 技術(shù),數(shù)組元素的類型函數(shù),能夠遍歷數(shù)組,運行元素相應的函數(shù)。
5.4 刪除一行 & 計分數(shù)
每一個 timer 中,都調(diào)用 void kill_all_full_lines()。它遍歷 exist block,凡符合滿行條件的,調(diào)用 kill_block_in_line 刪除該行,調(diào)用move_exist_block_down_line 把該行以上的 exist_block 下降一行。
這三個 helper 函數(shù)都是通過遍歷 exist block 中的每一個元素,匹配坐標條件,然后刪除數(shù)組元素或者改變數(shù)組元素的值。如前所述,因為 exist block 封裝中未使用 俄羅斯方塊元素,所以這些遍歷都寫得很丑陋。
刪除一行以后,累積刪除的行數(shù)。全刪以后,依據(jù)刪除的行數(shù)進行 switch-case,向全局變量 score 累加分數(shù)。在下個timer中,把 score 用 textout 輸出到工作區(qū)。
6. 回想和檢討
6.1 數(shù)據(jù)結(jié)構(gòu),封裝,循環(huán)條件
因為最初的 (也是終于的)數(shù)據(jù)結(jié)構(gòu)設計偷了懶,后來又沒有足夠的時間改動,此前已經(jīng)提及兩次,exist block的結(jié)構(gòu)過于貼近平臺,而遠離需求。exist block的顆粒度太低,是以 int 為類型的 數(shù)組元素,相應于需求中的 俄羅斯方塊元素 中的橫縱坐標之中的一個。某個數(shù)組元素究竟是橫坐標還是縱坐標,究竟是第幾個俄羅斯方塊元素,這些都須要由代碼實現(xiàn)。這樣,按需求寫helper函數(shù)的時候,遍歷的元素選取、終止條件,都遇到了麻煩。我在課堂上寫作時須要考慮,有時還會錯。經(jīng)驗說明,當我須要細致考慮,或者講述時間較長時,學生聽懂可能已經(jīng)有相當難度了。終止條件錯誤的bug,在代碼中存在兩三處,導致在 exist block夠多時,即游戲進行一段時間,工作區(qū)中會出現(xiàn)莫名其妙的俄羅斯方塊元素。這個bug在最后階段才解決。
這個故事告訴我們,設計不好,對編碼實現(xiàn)的難度要求就會提高。戰(zhàn)略失誤,戰(zhàn)役和戰(zhàn)斗就不easy打。領導決策膚淺,要求下屬跑死,結(jié)果也是白扯。道理都是一樣的。
6.2 不要對付過去
在開發(fā)中間的某堂課,我們發(fā)現(xiàn)當前塊移動時后面留了尾跡,擦得不干凈。這些那堂課快結(jié)束了。為了能讓學生在課后反復我課堂上的工作,所以我"對付"了代碼,由局部刷新改為刷新整個工作區(qū),包含背景。這樣尾跡表面上清除掉了。
之后,延續(xù)了這段"對付"的代碼。直到期末將至,我才發(fā)現(xiàn)這段"對付"掩蓋了還有一些bug,坐標移動的bug導致除非刷新整個工作區(qū)就有尾跡。這個bug在最后階段才解決。
6.3 并行,timer
有文章指出,剛開始學習的人很不easy理解的程序概念包含:賦值、遞歸和迭代、并行。本程序中有幾個埋得比較深的bug,是因為我對并行沒有足夠警惕造成的。
timer, 鍵盤響應,WM_PAINT會并行發(fā)生。當當中一個未處理完的時候,還有一個可能就開始運行;甚至timer未處理完的時候,還有一個timer也可能會開始。而這些并行的代碼,都調(diào)用了 block.cpp。比方有時導致當中一個正改坐標尚未完畢,還有一個開始刷新工作區(qū),這樣工作區(qū)里就出現(xiàn)個元素,位置是亂七八糟的。
并行的處理,須要 原子操作、進程間通信、避免重入 等概念。上述提到的動作序列,目的之中的一個就是希望擦除舊的當前塊這一動作僅僅在 timer 中發(fā)生。
在本課程中,應該不期待學生具備這些操作系統(tǒng)中的知識。只是我還沒有想到該怎樣設計才干規(guī)避這些知識。只是我猜應該類似于不用線程也能設計出貪吃蛇,應該有依賴更淺顯知識的設計手段,比方單純輪詢,而不用事件響應、消息循環(huán)。有哪位知道,請賜教,謝謝。
6.4 猜想后,應該先驗證,然后再改動
學生們通常把驗證猜想和實施解決歸約成了一步,我也常常如此。下文中的他們,包含我。
他們觀察到問題,然后做出猜想。這是正常步驟。
可是他們不以實驗驗證猜想是正確的,急急按猜想改動代碼。假設問題消失了,好,他們假設抓住了問題的解決辦法;假設問題還在,就再做個猜想,然后又立即改動。甚至更糟糕,沒有退回到上一步的起點,就在當前工作代碼上"繼續(xù)"改動,讓各個猜想累加起來,終于問題解決的時候甚至不知道是什么原因。
應該先設計實驗,按猜想的模型,假設如何就會如何。驗證猜想以后,再去解決。比方假設因為 timer 和 keyboard事件響應 同步導致繪圖混亂,那么,不應該著爭寫進程通信,而是 應該先選用簡單粗暴的手段 去除同步,以更大的顆粒度作為原子操作,驗證猜想。假設猜想正確,現(xiàn)象應該有所改變。盡管影響性能和效果,但這并非準備終于採用的代碼,僅僅是用來驗證猜想的。當猜想驗證以后,再去想效果更好的方案真正解決,比方建立個變量作為信號燈。
6.5 不要輕易更換技術(shù)方案,試圖繞過問題
這個方面,我最初是發(fā)現(xiàn)計算機本科的同學傾向強烈。常常有方案,明明再向前一步就能解決,他們卻在此時換了方案。問為什么。答:由于這個技術(shù)解決不了這個問題。
確定"不"是極其困難的,甚至比確定"能"要難上非常多。你不能,并不是就能確定這個方法不能。
須要充分了解你所使用的技術(shù),對它可以完畢的任務有足夠和明白的自信。同一時候,對用來替換的方案能解決何種問題,也應該明白。做原型驗證,依據(jù)理論推論,這些都是解決之道。見到工具,拿來就用,偏聽偏信別人的評論,就太草率了;一旦發(fā)現(xiàn)并不是萬能良藥,轉(zhuǎn)身就去尋找就的手段,這就更草率了。
6.6 版本號控制
為了讓學生能看到開發(fā)的過程,我上課時用文件系統(tǒng)做了版本號控制,每次課一個文件夾,有時壓縮成zip。課程結(jié)束以后,一個版本號一個版本號增加git,然后commit,操作了兩個小時(?),其間又操心整錯了,苦不堪言。
下次一定要從最開始就做版本號控制。還要在 commit 前把 debug, pch, sdf 等二進制垃圾手動刪除。
7. 附件
附件是以git版本號控制的代碼及日志,在這里[http://download.csdn.net/detail/younggift/7499881]。
protype下是技術(shù)原型。
tetris下的是俄羅斯方塊項目本身。早先的版本號是VS2010的,最后一天的是VS2012的。你能夠僅代碼部分加入進win32project,以適應你的VS版本號,或者dev c++版本號。
log0.txt是課堂上的日志。log1.txt是最后一天前期的日志。log2.doc是最后一天后期的日志,由于須要截圖,所以改成用word。
pic.bmp是圖片,用來說明定義的。
branch是一個分支,我忘了它是否增加了 trunk,留在那里備用,以防遺漏。
--------------------
博客會手工同步到下面地址:
[http://giftdotyoung.blogspot.com]
[http://blog.csdn.net/younggift]
??
近期的照片在這里 [http://www.douban.com/photos/album/132796665/] 和 [http://www.douban.com/photos/album/133241544/]。
1. 背景和原則我這學期講一門課,本科三年級,學生滿員17人。一般接近滿員,最低一次5人,那天據(jù)林同學說,其它的同學都去看足球賽了。
課程名字叫做算法與程序設計實踐3。第一堂課我照例要解釋:到了"3"這個階段,就不講算法了,僅僅有實踐。只是,后來看看算法也還是有一點應用,比方從一個線性表里刪除符合條件的元素們,在線性表里查找符合條件的元素,這樣的難度的。
課是在機房上的,大部分時間學生和教師都看著顯示,所以一學期下來,好多同學和我見面可能都不太認識。只是我們對代碼的形成過程更熟悉一些。
我試圖貫徹下述原則:學生應該看到教師編程的過程,而不不過結(jié)果;學生應該看到在編輯器和編譯器中的代碼,而不是WORD或PPT里的;學生應該先學會臨模教師的編程過程,而沒有能力直接臨模結(jié)果;學生甚至應該看到教師的錯誤及錯誤的解決過程、教師的無知及檢索過程,學生不應該看到事先排練的完美的編程過程和全知全能的教師,那樣的過程和專家,學生模仿時無從下手。
所以,我課前不準備,在課堂上無意犯各種錯誤--偶爾演示學生們easy犯的錯誤--及解決。在LOG文件里記錄我們的計劃和當前的進度,在繪圖里畫下原型。
所以,我假裝對某些API和函數(shù)不熟悉,演示在MSDN和互聯(lián)網(wǎng)中查找手冊和解決方式的步驟。單獨做一些技術(shù)原型驗證對API的調(diào)用結(jié)果的猜想,而不是在project的過程中在項目代碼中測試技術(shù)。有時,我知道問題在哪里,可是要先列出各種可能,然后一一驗證猜想(而不是直接解決,這似乎是計算機本科生很easy犯的錯誤,假設攻克了就認定那是問題的解決辦法)。除了這兩點,其余的時間我應該盡可能誠實。
有時候,學生會告訴我哪里錯了,先于我發(fā)現(xiàn)問題的解決辦法。這令我享受這種教學過程。
終于,我們--以我編碼為主--實現(xiàn)了WIN32API開發(fā)的俄羅斯方塊。
選擇俄羅斯方塊的原因,是由于小游戲的業(yè)務邏輯足夠復雜,保證學生了解在相對復雜的業(yè)務邏輯時的面臨的問題和編程行為與toy作品不同;所使用的到技術(shù)較少,避免過多的機制 (數(shù)據(jù)庫、網(wǎng)絡等)分散學生的注意力,保證學生把精力集中在對業(yè)務邏輯上。
選擇win32api是課堂上投票的結(jié)果。選擇C語言而沒有使用C++有兩個原因。一是學生的C++掌握通常并不熟練;二是我希望學生能在項目中發(fā)現(xiàn)面向?qū)ο蟮谋匾院烷L處,而不是僅由于學習過哪些語言而在project中選用;三是希望演示用C也能夠?qū)崿F(xiàn)基于對象的程序設計 (不是面向?qū)ο?#xff0c;不包含繼承,僅包含方法與數(shù)據(jù)的內(nèi)聚)。
2. 技術(shù)原型
涉及到的技術(shù)原型,要在project開始前建立小項目,以驗證對這些技術(shù)的掌握和對效果的猜想。
要實驗的技術(shù)列表,來源于需求。我們先不寫代碼,口頭描寫敘述需求,然后分解需求到所需的技術(shù)。這樣就形成了技術(shù)列表。這個過程中,同一時候也形成了定義,包含名詞和動詞表。
這些技術(shù)原型也限定了除C語言以外須要掌握的技術(shù),在這次開發(fā)其中。
技術(shù)原型包含:
* 使用GDI繪圖、擦除。用于畫小塊和移動小塊。移動是依據(jù)視覺暫留在新的位置上繪圖,并把舊位置上的小塊以底色重畫。
* 鍵盤消息響應。用于在不暫停小塊下落的情況下接受玩家通過按鍵操縱小塊左移、右移、旋轉(zhuǎn)、高速下落。
* 特定范圍的隨機數(shù)生成。用于在創(chuàng)建新的小塊時,決定是哪個類型。類型計有S型、L型、凸形、田形,及它們的旋轉(zhuǎn)。
* 計時器 (timer),用于驅(qū)動小塊定時下落,推斷是否該清除一行,計分,刷新工作區(qū) (重畫) 等。
* 在工作區(qū)輸出文字。用于調(diào)試和顯示分數(shù)。
終于形成的原型部分代碼量例如以下。代碼在附件中的 prototype文件夾下
繪圖 (及消息循環(huán)) ,draw,226行
擦除,eraser,263行
在工作區(qū)輸出文字,textout,201行
按鍵消息響應,key,207行
隨機數(shù),random, 31行
計時器,timer,214行
3. 開發(fā)過程的里程碑
技術(shù)原型確定以后,再又一次回到需求,并把需求排期。爭取每次課程限定完畢一個功能。
需求排期遵循的原則是:優(yōu)先完畢對其它功能無信賴的部分;優(yōu)先完畢核心功能。
下面是開發(fā)過程中的里程碑。
1) 生成塊。
2) 計時器驅(qū)動,塊自己主動下降
3) 鍵盤控制塊 旋轉(zhuǎn)、高速下降、左移、右移
4) 落究竟或粘在底部已存在塊上 (if (conficted || touch_bottom) stick)
5) 刪除一行:刪除一行,把之上的行下降一行
6) 計分:消除一行和多行分值不同
下面功能在本學期沒有實現(xiàn)。
7) 生成新塊前,在預覽區(qū)顯示下一個塊
8) 分數(shù)積累到一定程度 (?),加快塊下落的速度
開發(fā)過程以git版本號控制方式記錄了歷史,每一個重要功能一次commit,以日期作為message。
4. 定義
我們在開發(fā)前用示意圖約定了一些定義,作為詞匯表。排版原因,我在這里有文字解釋一下。
俄羅斯方塊元素:工作區(qū)上畫圖的最小單位,是一個小方格。俄羅斯方塊的名字 Terris 即四元素,由于每一個當前塊由4個元素組成。
數(shù)組元素:即C語言中的數(shù)組元素,數(shù)組中的某一個。提出這個定義是為了差別于俄羅斯方塊的元素。
當前塊 (current block) :正在移動的由四個元素構(gòu)成的塊。有S型、L型、田字型等類型。
已存在的塊 (exist block) :堆積在工作區(qū)底部的,已經(jīng)粘成一團的元素。
像素坐標,世界坐標。像素坐標是由GDI畫圖定義的,世界坐標由我們定義,以元素為單位,左上是原點 (0,0) ,向右向下遞增。
stick。當前塊接觸到已存在的塊,或者當前塊接觸到工作區(qū)底部,此時應該把當前塊增加到已存在的塊中,然后生成新的當前塊;假設導致已存在的塊中某一行充滿元素,須要按游戲規(guī)則刪除此行,然后把已存在的塊中此行以上的元素降落一行。
5. 數(shù)據(jù)結(jié)構(gòu)及流程
下面介紹當前塊、已存在塊、鍵盤操作、刪除已存在塊中的一行的數(shù)據(jù)結(jié)構(gòu)和流程。
5.1 當前塊
當前塊中,包含當前塊的下面數(shù)據(jù):當前坐標,上一次的坐標 (用以擦除) ,當前類型 (接下來會解釋),上一次的類型 (用于旋轉(zhuǎn))。結(jié)構(gòu)體例如以下,整個程序中僅僅有這個結(jié)構(gòu)體的唯一實例。
struct struct_block{
int x;
int y; /* row 0, col 0 */
int old_x;
int old_y;
int* type;
int* old_type;
};
當前塊的類型使用數(shù)組實現(xiàn),例如以下,各自是一字型、田字型、凸字型。
int line_v_block[]={0, 0, 0, 1, 0, 2, 0, 3};
int line_h_block[]={0,0,1,0,2,0,3,0};
int tian_block[]={0, 0, 0, 1, 1, 0, 1, 1};
int tu_v_block[]={0,1,1,0,1,1,2,1};
int tu_h_block[]={0,1,1,0,1,1,1,2};
數(shù)組中的每兩個數(shù)值 (數(shù)據(jù)中的元素)代表一個當前塊中的元素的坐標,計8個數(shù)值代表4個元素。
生成塊時,
current_block.type = line_v_block;
指定了當前塊的元素。
畫圖時,遍歷"類型數(shù)組",把每一個元素繪出。不管何種類型,都遵循這一流程,從而實現(xiàn)"以數(shù)據(jù)作為代碼":類型數(shù)組即數(shù)據(jù),遍歷"類型數(shù)組"、在旋轉(zhuǎn)時改變類型等即為引擎。
旋轉(zhuǎn)的代碼演示樣例,改變類型 (的指針) :
if(current_block.type == line_v_block)
{
current_block.type = line_h_block;
}
平移的代碼演示樣例,改變橫坐標:
current_block.x -= 1;
自己主動下降的代碼演示樣例,改變上一次的縱坐標和當前縱坐標。
if(! is_conflicted() && ! is_touch_bottom())
{
current_block.old_y = current_block.y;
current_block.y = current_block.y + 1;
}
else
{
stick();
generate_block();
}
高速下降:
縱坐標 添加 全部元素中到達底部 (或已存在塊中同一橫坐標的頂) 的最短距離。
貌似題外話,helper函數(shù):is_conflicted(),推斷當前塊是否接觸到已存在塊;is_touch_bottom(),推斷當前塊是否觸底;匹配橫坐標,給出當前塊的底坐標;求當前塊距離底部的最短距離。等等。
開發(fā)helper函數(shù)的目的,是為了使程序總體流程清晰。保障總體清晰的方法之中的一個,是要求每一個函數(shù)內(nèi)容不得超過一屏。假設超過了,就須要折解出 helper 函數(shù)。在主流程中調(diào)用 helper 函數(shù),而把helper函數(shù)體移出主流程,這樣主流程代碼長度就下降了。這和小學寫作文的時候,老師要求先拉大綱是一個道理。常常有同學說,在開發(fā)過程中會發(fā)現(xiàn)新的功能,在開發(fā)遇到新的技術(shù),沒有做原型的,因此難以把握大綱。這都說明把握大綱和做計劃的能力還差,須要通過練習來訓練。這和小學生寫著寫著作文發(fā)現(xiàn)須要查字典,或者寫跑題了,是一個道理。我們的成長并不是認識的字多了,而是能預見到將會用到哪些字 (甚至表達手法、寫作素材)。
此外,在面向?qū)ο笾?#xff0c;有些的函數(shù)會成為game (或者 current block 或者 exist block )的成員函數(shù)。這在開發(fā)中會認識到,假設它們與數(shù)據(jù)能內(nèi)聚在一個類中,該是多么方便,因此了解面向?qū)ο蟮脑谛畔㈦[藏方面的優(yōu)勢。這些函數(shù)應歸屬于哪個類,是由哪個類承擔這個責任決定的。
5.2 已存在塊
已存在塊中包含下面數(shù)據(jù)結(jié)構(gòu):塊的長度 (其實,是塊的長度*2,代碼中以橫坐標和縱坐標作為兩個數(shù)組元素) ,已存在塊數(shù)組。例如以下。
int exist_block_size=0;
int exist_block[(maxx+1)*(maxy+1)];
這樣的數(shù)據(jù)結(jié)構(gòu),及當前塊的數(shù)據(jù)結(jié)構(gòu),把橫縱坐標無區(qū)別地,不以結(jié)構(gòu)體地方式放在數(shù)組中,在興許開發(fā)中帶來了麻煩。只是因為課程時間有限,后來,我未對此做出改動。應該逐漸演化程序結(jié)構(gòu),形成以元素作為結(jié)構(gòu)體的數(shù)組。再開發(fā)出一些helper甚至成員函數(shù),遍歷時以俄羅斯方塊元素為單位,而不是當前代碼中的以數(shù)組元素為單位。
對已存在塊數(shù)據(jù)結(jié)構(gòu)操作的函數(shù)之中的一個是 stick,用于在當前塊觸底 (或觸及已存在塊)時,把當前塊中的元素移到已存在塊中。
有不少helper函數(shù),基本都是通過遍歷 exist_block,按匹配條件讀當中的坐標。包含:匹配橫坐標,給出已存在塊的頂坐標 int get_exist_block_top(int x)。
5.3 鍵盤操作 & 動作序列
玩家操作塊這一操作,由鍵盤消息響應開始。我們不在鍵盤響應中處理這一事件,而是僅僅在這里記住這個動作,增加動作序列中。這是后來的版本號。最初的版本號,我們也不在鍵盤響應中處理事件,而是調(diào)用 block.cpp 中的函數(shù)。原則是:凡依賴win32api的,放在 tetris.cpp 中,如 timer, 鍵盤響應,畫圖;凡是與業(yè)務邏輯有關(guān),平臺無關(guān)的,放在 block.cpp 中。接收向上箭頭,是鍵盤響應,平臺相關(guān),所以放在 tetris.cpp 中;此時調(diào)用的 rotate,用于改變當前塊的類型或坐標,平臺無關(guān),所以放在 block.cpp 中。
動作序列的數(shù)據(jù)結(jié)構(gòu)例如以下。在動作序列數(shù)組buffer_action_seq中,數(shù)組動作元素
(動作) 的類型是 枚舉 action。
enum action{ action_left=1, action_right=2, action_speed_down=3, action_rotate=4, action_down_auto=5, action_na=0};
action buffer_action_seq[action_size]={action_na};
int buffer_action_cursor = 0;
由玩家觸發(fā)鍵盤消息開始,流程例如以下。
1)鍵盤消息響應:
buffer_action_seq[buffer_action_cursor++] = action_rotate;在動作序列中增加一個動作。這相應于設計模式中的 commander 模式要解決的問題。
2)在timer中自己主動下降
timer中 buffer_action_seq[buffer_action_cursor++] = action_down_auto; 在動作序列中增加一個動作。
3)在timer中觸發(fā)WM_PAINT
timer 中 InvalidateRect 觸發(fā) WM_PAINT
4)WM_PAINT中運行動作序列
erase_old_block_seq(hdc);
erase_old_block_seq (hdc) 遍歷動作序列,按每一個動作改變當前塊坐標,然后擦除因為動作產(chǎn)生的舊塊。遍歷動作序列以后,就完畢了自上個 timer 周期以來全部的動作,擦除了這期間產(chǎn)生的全部舊塊。
void erase_old_block_seq(HDC hdc) 片斷例如以下:
for (i = 0; i < buffer_action_cursor; i++)
{
switch (buffer_action_seq[i])
{
case action_left:
move_left();
erase_old_block(hdc);
break;
在序列里的每一個動作中,move_left 改坐標, erase_old_block(hdc) 擦除舊塊.
5)WM_PAINT畫新的當前塊和已存在塊
draw_current_block(hdc);
draw_exist_block(hdc);
由于重繪比計算花費的時間要多,作為性能優(yōu)化,假設當前塊與舊塊坐標全然同樣,不重畫。
另,還有一個版本號的動作序列,不使用枚舉和swtich-case,通過把函數(shù)作為消息傳遞給責任者,實現(xiàn)disptach:
void (*next_action)() = move_still;?
next_action = move_left
當中 move_left是一個函數(shù)。next_action這種元素 (類型是函數(shù)) 組成一個數(shù)組,作為動作序列。運行動作序列時,用以下這種代碼:
while ( next_action++ != action_termination )
? ? ? next_action;
因為 next_action 既是函數(shù),也是數(shù)組元素的指針,因此上述代碼不是偽代碼,而是能夠運行的。這類似于 jump table 技術(shù),數(shù)組元素的類型函數(shù),能夠遍歷數(shù)組,運行元素相應的函數(shù)。
5.4 刪除一行 & 計分數(shù)
每一個 timer 中,都調(diào)用 void kill_all_full_lines()。它遍歷 exist block,凡符合滿行條件的,調(diào)用 kill_block_in_line 刪除該行,調(diào)用move_exist_block_down_line 把該行以上的 exist_block 下降一行。
這三個 helper 函數(shù)都是通過遍歷 exist block 中的每一個元素,匹配坐標條件,然后刪除數(shù)組元素或者改變數(shù)組元素的值。如前所述,因為 exist block 封裝中未使用 俄羅斯方塊元素,所以這些遍歷都寫得很丑陋。
刪除一行以后,累積刪除的行數(shù)。全刪以后,依據(jù)刪除的行數(shù)進行 switch-case,向全局變量 score 累加分數(shù)。在下個timer中,把 score 用 textout 輸出到工作區(qū)。
6. 回想和檢討
6.1 數(shù)據(jù)結(jié)構(gòu),封裝,循環(huán)條件
因為最初的 (也是終于的)數(shù)據(jù)結(jié)構(gòu)設計偷了懶,后來又沒有足夠的時間改動,此前已經(jīng)提及兩次,exist block的結(jié)構(gòu)過于貼近平臺,而遠離需求。exist block的顆粒度太低,是以 int 為類型的 數(shù)組元素,相應于需求中的 俄羅斯方塊元素 中的橫縱坐標之中的一個。某個數(shù)組元素究竟是橫坐標還是縱坐標,究竟是第幾個俄羅斯方塊元素,這些都須要由代碼實現(xiàn)。這樣,按需求寫helper函數(shù)的時候,遍歷的元素選取、終止條件,都遇到了麻煩。我在課堂上寫作時須要考慮,有時還會錯。經(jīng)驗說明,當我須要細致考慮,或者講述時間較長時,學生聽懂可能已經(jīng)有相當難度了。終止條件錯誤的bug,在代碼中存在兩三處,導致在 exist block夠多時,即游戲進行一段時間,工作區(qū)中會出現(xiàn)莫名其妙的俄羅斯方塊元素。這個bug在最后階段才解決。
這個故事告訴我們,設計不好,對編碼實現(xiàn)的難度要求就會提高。戰(zhàn)略失誤,戰(zhàn)役和戰(zhàn)斗就不easy打。領導決策膚淺,要求下屬跑死,結(jié)果也是白扯。道理都是一樣的。
6.2 不要對付過去
在開發(fā)中間的某堂課,我們發(fā)現(xiàn)當前塊移動時后面留了尾跡,擦得不干凈。這些那堂課快結(jié)束了。為了能讓學生在課后反復我課堂上的工作,所以我"對付"了代碼,由局部刷新改為刷新整個工作區(qū),包含背景。這樣尾跡表面上清除掉了。
之后,延續(xù)了這段"對付"的代碼。直到期末將至,我才發(fā)現(xiàn)這段"對付"掩蓋了還有一些bug,坐標移動的bug導致除非刷新整個工作區(qū)就有尾跡。這個bug在最后階段才解決。
6.3 并行,timer
有文章指出,剛開始學習的人很不easy理解的程序概念包含:賦值、遞歸和迭代、并行。本程序中有幾個埋得比較深的bug,是因為我對并行沒有足夠警惕造成的。
timer, 鍵盤響應,WM_PAINT會并行發(fā)生。當當中一個未處理完的時候,還有一個可能就開始運行;甚至timer未處理完的時候,還有一個timer也可能會開始。而這些并行的代碼,都調(diào)用了 block.cpp。比方有時導致當中一個正改坐標尚未完畢,還有一個開始刷新工作區(qū),這樣工作區(qū)里就出現(xiàn)個元素,位置是亂七八糟的。
并行的處理,須要 原子操作、進程間通信、避免重入 等概念。上述提到的動作序列,目的之中的一個就是希望擦除舊的當前塊這一動作僅僅在 timer 中發(fā)生。
在本課程中,應該不期待學生具備這些操作系統(tǒng)中的知識。只是我還沒有想到該怎樣設計才干規(guī)避這些知識。只是我猜應該類似于不用線程也能設計出貪吃蛇,應該有依賴更淺顯知識的設計手段,比方單純輪詢,而不用事件響應、消息循環(huán)。有哪位知道,請賜教,謝謝。
6.4 猜想后,應該先驗證,然后再改動
學生們通常把驗證猜想和實施解決歸約成了一步,我也常常如此。下文中的他們,包含我。
他們觀察到問題,然后做出猜想。這是正常步驟。
可是他們不以實驗驗證猜想是正確的,急急按猜想改動代碼。假設問題消失了,好,他們假設抓住了問題的解決辦法;假設問題還在,就再做個猜想,然后又立即改動。甚至更糟糕,沒有退回到上一步的起點,就在當前工作代碼上"繼續(xù)"改動,讓各個猜想累加起來,終于問題解決的時候甚至不知道是什么原因。
應該先設計實驗,按猜想的模型,假設如何就會如何。驗證猜想以后,再去解決。比方假設因為 timer 和 keyboard事件響應 同步導致繪圖混亂,那么,不應該著爭寫進程通信,而是 應該先選用簡單粗暴的手段 去除同步,以更大的顆粒度作為原子操作,驗證猜想。假設猜想正確,現(xiàn)象應該有所改變。盡管影響性能和效果,但這并非準備終于採用的代碼,僅僅是用來驗證猜想的。當猜想驗證以后,再去想效果更好的方案真正解決,比方建立個變量作為信號燈。
6.5 不要輕易更換技術(shù)方案,試圖繞過問題
這個方面,我最初是發(fā)現(xiàn)計算機本科的同學傾向強烈。常常有方案,明明再向前一步就能解決,他們卻在此時換了方案。問為什么。答:由于這個技術(shù)解決不了這個問題。
確定"不"是極其困難的,甚至比確定"能"要難上非常多。你不能,并不是就能確定這個方法不能。
須要充分了解你所使用的技術(shù),對它可以完畢的任務有足夠和明白的自信。同一時候,對用來替換的方案能解決何種問題,也應該明白。做原型驗證,依據(jù)理論推論,這些都是解決之道。見到工具,拿來就用,偏聽偏信別人的評論,就太草率了;一旦發(fā)現(xiàn)并不是萬能良藥,轉(zhuǎn)身就去尋找就的手段,這就更草率了。
6.6 版本號控制
為了讓學生能看到開發(fā)的過程,我上課時用文件系統(tǒng)做了版本號控制,每次課一個文件夾,有時壓縮成zip。課程結(jié)束以后,一個版本號一個版本號增加git,然后commit,操作了兩個小時(?),其間又操心整錯了,苦不堪言。
下次一定要從最開始就做版本號控制。還要在 commit 前把 debug, pch, sdf 等二進制垃圾手動刪除。
7. 附件
附件是以git版本號控制的代碼及日志,在這里[http://download.csdn.net/detail/younggift/7499881]。
protype下是技術(shù)原型。
tetris下的是俄羅斯方塊項目本身。早先的版本號是VS2010的,最后一天的是VS2012的。你能夠僅代碼部分加入進win32project,以適應你的VS版本號,或者dev c++版本號。
log0.txt是課堂上的日志。log1.txt是最后一天前期的日志。log2.doc是最后一天后期的日志,由于須要截圖,所以改成用word。
pic.bmp是圖片,用來說明定義的。
branch是一個分支,我忘了它是否增加了 trunk,留在那里備用,以防遺漏。
--------------------
博客會手工同步到下面地址:
[http://giftdotyoung.blogspot.com]
[http://blog.csdn.net/younggift]
轉(zhuǎn)載于:https://www.cnblogs.com/mengfanrong/p/3820473.html
總結(jié)
以上是生活随笔為你收集整理的俄罗斯方块:win32api开发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java的登陆验证问题
- 下一篇: 浅谈递归