日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

俄罗斯方块:win32api开发

發布時間:2025/6/17 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 俄罗斯方块:win32api开发 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
本文簡述一門課程,演示win32api開發俄羅斯方塊的開發過程。如果學生學習過C語言,沒學過或者學習C++不好,剛剛開始學習win32api程序設計,還不懂消息循環和注冊窗體類。

??

近期的照片在這里 [http://www.douban.com/photos/album/132796665/] 和 [http://www.douban.com/photos/album/133241544/]。

1. 背景和原則

我這學期講一門課,本科三年級,學生滿員17人。一般接近滿員,最低一次5人,那天據林同學說,其它的同學都去看足球賽了。

課程名字叫做算法與程序設計實踐3。第一堂課我照例要解釋:到了"3"這個階段,就不講算法了,僅僅有實踐。只是,后來看看算法也還是有一點應用,比方從一個線性表里刪除符合條件的元素們,在線性表里查找符合條件的元素,這樣的難度的。

課是在機房上的,大部分時間學生和教師都看著顯示,所以一學期下來,好多同學和我見面可能都不太認識。只是我們對代碼的形成過程更熟悉一些。

我試圖貫徹下述原則:學生應該看到教師編程的過程,而不不過結果;學生應該看到在編輯器和編譯器中的代碼,而不是WORD或PPT里的;學生應該先學會臨模教師的編程過程,而沒有能力直接臨模結果;學生甚至應該看到教師的錯誤及錯誤的解決過程、教師的無知及檢索過程,學生不應該看到事先排練的完美的編程過程和全知全能的教師,那樣的過程和專家,學生模仿時無從下手。

所以,我課前不準備,在課堂上無意犯各種錯誤--偶爾演示學生們easy犯的錯誤--及解決。在LOG文件里記錄我們的計劃和當前的進度,在繪圖里畫下原型。

所以,我假裝對某些API和函數不熟悉,演示在MSDN和互聯網中查找手冊和解決方式的步驟。單獨做一些技術原型驗證對API的調用結果的猜想,而不是在project的過程中在項目代碼中測試技術。有時,我知道問題在哪里,可是要先列出各種可能,然后一一驗證猜想(而不是直接解決,這似乎是計算機本科生很easy犯的錯誤,假設攻克了就認定那是問題的解決辦法)。除了這兩點,其余的時間我應該盡可能誠實。

有時候,學生會告訴我哪里錯了,先于我發現問題的解決辦法。這令我享受這種教學過程。

終于,我們--以我編碼為主--實現了WIN32API開發的俄羅斯方塊。

選擇俄羅斯方塊的原因,是由于小游戲的業務邏輯足夠復雜,保證學生了解在相對復雜的業務邏輯時的面臨的問題和編程行為與toy作品不同;所使用的到技術較少,避免過多的機制 (數據庫、網絡等)分散學生的注意力,保證學生把精力集中在對業務邏輯上。

選擇win32api是課堂上投票的結果。選擇C語言而沒有使用C++有兩個原因。一是學生的C++掌握通常并不熟練;二是我希望學生能在項目中發現面向對象的必要性和長處,而不是僅由于學習過哪些語言而在project中選用;三是希望演示用C也能夠實現基于對象的程序設計 (不是面向對象,不包含繼承,僅包含方法與數據的內聚)。

2. 技術原型

涉及到的技術原型,要在project開始前建立小項目,以驗證對這些技術的掌握和對效果的猜想。

要實驗的技術列表,來源于需求。我們先不寫代碼,口頭描寫敘述需求,然后分解需求到所需的技術。這樣就形成了技術列表。這個過程中,同一時候也形成了定義,包含名詞和動詞表。

這些技術原型也限定了除C語言以外須要掌握的技術,在這次開發其中。

技術原型包含:

* 使用GDI繪圖、擦除。用于畫小塊和移動小塊。移動是依據視覺暫留在新的位置上繪圖,并把舊位置上的小塊以底色重畫。

* 鍵盤消息響應。用于在不暫停小塊下落的情況下接受玩家通過按鍵操縱小塊左移、右移、旋轉、高速下落。

* 特定范圍的隨機數生成。用于在創建新的小塊時,決定是哪個類型。類型計有S型、L型、凸形、田形,及它們的旋轉。

* 計時器 (timer),用于驅動小塊定時下落,推斷是否該清除一行,計分,刷新工作區 (重畫) 等。

* 在工作區輸出文字。用于調試和顯示分數。

終于形成的原型部分代碼量例如以下。代碼在附件中的 prototype文件夾下

繪圖 (及消息循環) ,draw,226行
擦除,eraser,263行
在工作區輸出文字,textout,201行
按鍵消息響應,key,207行
隨機數,random, 31行
計時器,timer,214行

3. 開發過程的里程碑

技術原型確定以后,再又一次回到需求,并把需求排期。爭取每次課程限定完畢一個功能。

需求排期遵循的原則是:優先完畢對其它功能無信賴的部分;優先完畢核心功能。

下面是開發過程中的里程碑。

1) 生成塊。
2) 計時器驅動,塊自己主動下降
3) 鍵盤控制塊 旋轉、高速下降、左移、右移
4) 落究竟或粘在底部已存在塊上 (if (conficted || touch_bottom) stick)
5) 刪除一行:刪除一行,把之上的行下降一行
6) 計分:消除一行和多行分值不同

下面功能在本學期沒有實現。

7) 生成新塊前,在預覽區顯示下一個塊
8) 分數積累到一定程度 (?),加快塊下落的速度

開發過程以git版本號控制方式記錄了歷史,每一個重要功能一次commit,以日期作為message。

4. 定義

我們在開發前用示意圖約定了一些定義,作為詞匯表。排版原因,我在這里有文字解釋一下。

俄羅斯方塊元素:工作區上畫圖的最小單位,是一個小方格。俄羅斯方塊的名字 Terris 即四元素,由于每一個當前塊由4個元素組成。

數組元素:即C語言中的數組元素,數組中的某一個。提出這個定義是為了差別于俄羅斯方塊的元素。

當前塊 (current block) :正在移動的由四個元素構成的塊。有S型、L型、田字型等類型。

已存在的塊 (exist block) :堆積在工作區底部的,已經粘成一團的元素。

像素坐標,世界坐標。像素坐標是由GDI畫圖定義的,世界坐標由我們定義,以元素為單位,左上是原點 (0,0) ,向右向下遞增。

stick。當前塊接觸到已存在的塊,或者當前塊接觸到工作區底部,此時應該把當前塊增加到已存在的塊中,然后生成新的當前塊;假設導致已存在的塊中某一行充滿元素,須要按游戲規則刪除此行,然后把已存在的塊中此行以上的元素降落一行。

5. 數據結構及流程

下面介紹當前塊、已存在塊、鍵盤操作、刪除已存在塊中的一行的數據結構和流程。

5.1 當前塊

當前塊中,包含當前塊的下面數據:當前坐標,上一次的坐標 (用以擦除) ,當前類型 (接下來會解釋),上一次的類型 (用于旋轉)。結構體例如以下,整個程序中僅僅有這個結構體的唯一實例。

struct struct_block{
int x;
int y; /* row 0, col 0 */
int old_x;
int old_y;
int* type;
int* old_type;
};

當前塊的類型使用數組實現,例如以下,各自是一字型、田字型、凸字型。

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};

數組中的每兩個數值 (數據中的元素)代表一個當前塊中的元素的坐標,計8個數值代表4個元素。

生成塊時,

current_block.type = line_v_block;

指定了當前塊的元素。

畫圖時,遍歷"類型數組",把每一個元素繪出。不管何種類型,都遵循這一流程,從而實現"以數據作為代碼":類型數組即數據,遍歷"類型數組"、在旋轉時改變類型等即為引擎。

旋轉的代碼演示樣例,改變類型 (的指針) :

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函數:is_conflicted(),推斷當前塊是否接觸到已存在塊;is_touch_bottom(),推斷當前塊是否觸底;匹配橫坐標,給出當前塊的底坐標;求當前塊距離底部的最短距離。等等。

開發helper函數的目的,是為了使程序總體流程清晰。保障總體清晰的方法之中的一個,是要求每一個函數內容不得超過一屏。假設超過了,就須要折解出 helper 函數。在主流程中調用 helper 函數,而把helper函數體移出主流程,這樣主流程代碼長度就下降了。這和小學寫作文的時候,老師要求先拉大綱是一個道理。常常有同學說,在開發過程中會發現新的功能,在開發遇到新的技術,沒有做原型的,因此難以把握大綱。這都說明把握大綱和做計劃的能力還差,須要通過練習來訓練。這和小學生寫著寫著作文發現須要查字典,或者寫跑題了,是一個道理。我們的成長并不是認識的字多了,而是能預見到將會用到哪些字 (甚至表達手法、寫作素材)。

此外,在面向對象中,有些的函數會成為game (或者 current block 或者 exist block )的成員函數。這在開發中會認識到,假設它們與數據能內聚在一個類中,該是多么方便,因此了解面向對象的在信息隱藏方面的優勢。這些函數應歸屬于哪個類,是由哪個類承擔這個責任決定的。

5.2 已存在塊

已存在塊中包含下面數據結構:塊的長度 (其實,是塊的長度*2,代碼中以橫坐標和縱坐標作為兩個數組元素) ,已存在塊數組。例如以下。

int exist_block_size=0;
int exist_block[(maxx+1)*(maxy+1)];

這樣的數據結構,及當前塊的數據結構,把橫縱坐標無區別地,不以結構體地方式放在數組中,在興許開發中帶來了麻煩。只是因為課程時間有限,后來,我未對此做出改動。應該逐漸演化程序結構,形成以元素作為結構體的數組。再開發出一些helper甚至成員函數,遍歷時以俄羅斯方塊元素為單位,而不是當前代碼中的以數組元素為單位。

對已存在塊數據結構操作的函數之中的一個是 stick,用于在當前塊觸底 (或觸及已存在塊)時,把當前塊中的元素移到已存在塊中。

有不少helper函數,基本都是通過遍歷 exist_block,按匹配條件讀當中的坐標。包含:匹配橫坐標,給出已存在塊的頂坐標 int get_exist_block_top(int x)。

5.3 鍵盤操作 & 動作序列

玩家操作塊這一操作,由鍵盤消息響應開始。我們不在鍵盤響應中處理這一事件,而是僅僅在這里記住這個動作,增加動作序列中。這是后來的版本號。最初的版本號,我們也不在鍵盤響應中處理事件,而是調用 block.cpp 中的函數。原則是:凡依賴win32api的,放在 tetris.cpp 中,如 timer, 鍵盤響應,畫圖;凡是與業務邏輯有關,平臺無關的,放在 block.cpp 中。接收向上箭頭,是鍵盤響應,平臺相關,所以放在 tetris.cpp 中;此時調用的 rotate,用于改變當前塊的類型或坐標,平臺無關,所以放在 block.cpp 中。

動作序列的數據結構例如以下。在動作序列數組buffer_action_seq中,數組動作元素
(動作) 的類型是 枚舉 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;

由玩家觸發鍵盤消息開始,流程例如以下。

1)鍵盤消息響應:

buffer_action_seq[buffer_action_cursor++] = action_rotate;在動作序列中增加一個動作。這相應于設計模式中的 commander 模式要解決的問題。

2)在timer中自己主動下降

timer中 buffer_action_seq[buffer_action_cursor++] = action_down_auto; 在動作序列中增加一個動作。

3)在timer中觸發WM_PAINT

timer 中 InvalidateRect 觸發 WM_PAINT

4)WM_PAINT中運行動作序列

erase_old_block_seq(hdc);

erase_old_block_seq (hdc) 遍歷動作序列,按每一個動作改變當前塊坐標,然后擦除因為動作產生的舊塊。遍歷動作序列以后,就完畢了自上個 timer 周期以來全部的動作,擦除了這期間產生的全部舊塊。

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);

由于重繪比計算花費的時間要多,作為性能優化,假設當前塊與舊塊坐標全然同樣,不重畫。

另,還有一個版本號的動作序列,不使用枚舉和swtich-case,通過把函數作為消息傳遞給責任者,實現disptach:

void (*next_action)() = move_still;?
next_action = move_left

當中 move_left是一個函數。next_action這種元素 (類型是函數) 組成一個數組,作為動作序列。運行動作序列時,用以下這種代碼:

while ( next_action++ != action_termination )
? ? ? next_action;

因為 next_action 既是函數,也是數組元素的指針,因此上述代碼不是偽代碼,而是能夠運行的。這類似于 jump table 技術,數組元素的類型函數,能夠遍歷數組,運行元素相應的函數。

5.4 刪除一行 & 計分數

每一個 timer 中,都調用 void kill_all_full_lines()。它遍歷 exist block,凡符合滿行條件的,調用 kill_block_in_line 刪除該行,調用move_exist_block_down_line 把該行以上的 exist_block 下降一行。

這三個 helper 函數都是通過遍歷 exist block 中的每一個元素,匹配坐標條件,然后刪除數組元素或者改變數組元素的值。如前所述,因為 exist block 封裝中未使用 俄羅斯方塊元素,所以這些遍歷都寫得很丑陋。

刪除一行以后,累積刪除的行數。全刪以后,依據刪除的行數進行 switch-case,向全局變量 score 累加分數。在下個timer中,把 score 用 textout 輸出到工作區。

6. 回想和檢討

6.1 數據結構,封裝,循環條件

因為最初的 (也是終于的)數據結構設計偷了懶,后來又沒有足夠的時間改動,此前已經提及兩次,exist block的結構過于貼近平臺,而遠離需求。exist block的顆粒度太低,是以 int 為類型的 數組元素,相應于需求中的 俄羅斯方塊元素 中的橫縱坐標之中的一個。某個數組元素究竟是橫坐標還是縱坐標,究竟是第幾個俄羅斯方塊元素,這些都須要由代碼實現。這樣,按需求寫helper函數的時候,遍歷的元素選取、終止條件,都遇到了麻煩。我在課堂上寫作時須要考慮,有時還會錯。經驗說明,當我須要細致考慮,或者講述時間較長時,學生聽懂可能已經有相當難度了。終止條件錯誤的bug,在代碼中存在兩三處,導致在 exist block夠多時,即游戲進行一段時間,工作區中會出現莫名其妙的俄羅斯方塊元素。這個bug在最后階段才解決。

這個故事告訴我們,設計不好,對編碼實現的難度要求就會提高。戰略失誤,戰役和戰斗就不easy打。領導決策膚淺,要求下屬跑死,結果也是白扯。道理都是一樣的。

6.2 不要對付過去

在開發中間的某堂課,我們發現當前塊移動時后面留了尾跡,擦得不干凈。這些那堂課快結束了。為了能讓學生在課后反復我課堂上的工作,所以我"對付"了代碼,由局部刷新改為刷新整個工作區,包含背景。這樣尾跡表面上清除掉了。

之后,延續了這段"對付"的代碼。直到期末將至,我才發現這段"對付"掩蓋了還有一些bug,坐標移動的bug導致除非刷新整個工作區就有尾跡。這個bug在最后階段才解決。

6.3 并行,timer

有文章指出,剛開始學習的人很不easy理解的程序概念包含:賦值、遞歸和迭代、并行。本程序中有幾個埋得比較深的bug,是因為我對并行沒有足夠警惕造成的。

timer, 鍵盤響應,WM_PAINT會并行發生。當當中一個未處理完的時候,還有一個可能就開始運行;甚至timer未處理完的時候,還有一個timer也可能會開始。而這些并行的代碼,都調用了 block.cpp。比方有時導致當中一個正改坐標尚未完畢,還有一個開始刷新工作區,這樣工作區里就出現個元素,位置是亂七八糟的。

并行的處理,須要 原子操作、進程間通信、避免重入 等概念。上述提到的動作序列,目的之中的一個就是希望擦除舊的當前塊這一動作僅僅在 timer 中發生。

在本課程中,應該不期待學生具備這些操作系統中的知識。只是我還沒有想到該怎樣設計才干規避這些知識。只是我猜應該類似于不用線程也能設計出貪吃蛇,應該有依賴更淺顯知識的設計手段,比方單純輪詢,而不用事件響應、消息循環。有哪位知道,請賜教,謝謝。

6.4 猜想后,應該先驗證,然后再改動

學生們通常把驗證猜想和實施解決歸約成了一步,我也常常如此。下文中的他們,包含我。

他們觀察到問題,然后做出猜想。這是正常步驟。

可是他們不以實驗驗證猜想是正確的,急急按猜想改動代碼。假設問題消失了,好,他們假設抓住了問題的解決辦法;假設問題還在,就再做個猜想,然后又立即改動。甚至更糟糕,沒有退回到上一步的起點,就在當前工作代碼上"繼續"改動,讓各個猜想累加起來,終于問題解決的時候甚至不知道是什么原因。

應該先設計實驗,按猜想的模型,假設如何就會如何。驗證猜想以后,再去解決。比方假設因為 timer 和 keyboard事件響應 同步導致繪圖混亂,那么,不應該著爭寫進程通信,而是 應該先選用簡單粗暴的手段 去除同步,以更大的顆粒度作為原子操作,驗證猜想。假設猜想正確,現象應該有所改變。盡管影響性能和效果,但這并非準備終于採用的代碼,僅僅是用來驗證猜想的。當猜想驗證以后,再去想效果更好的方案真正解決,比方建立個變量作為信號燈。

6.5 不要輕易更換技術方案,試圖繞過問題

這個方面,我最初是發現計算機本科的同學傾向強烈。常常有方案,明明再向前一步就能解決,他們卻在此時換了方案。問為什么。答:由于這個技術解決不了這個問題。

確定"不"是極其困難的,甚至比確定"能"要難上非常多。你不能,并不是就能確定這個方法不能。

須要充分了解你所使用的技術,對它可以完畢的任務有足夠和明白的自信。同一時候,對用來替換的方案能解決何種問題,也應該明白。做原型驗證,依據理論推論,這些都是解決之道。見到工具,拿來就用,偏聽偏信別人的評論,就太草率了;一旦發現并不是萬能良藥,轉身就去尋找就的手段,這就更草率了。

6.6 版本號控制

為了讓學生能看到開發的過程,我上課時用文件系統做了版本號控制,每次課一個文件夾,有時壓縮成zip。課程結束以后,一個版本號一個版本號增加git,然后commit,操作了兩個小時(?),其間又操心整錯了,苦不堪言。

下次一定要從最開始就做版本號控制。還要在 commit 前把 debug, pch, sdf 等二進制垃圾手動刪除。

7. 附件

附件是以git版本號控制的代碼及日志,在這里[http://download.csdn.net/detail/younggift/7499881]。

protype下是技術原型。

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]

轉載于:https://www.cnblogs.com/mengfanrong/p/3820473.html

總結

以上是生活随笔為你收集整理的俄罗斯方块:win32api开发的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。