MFC實現桌面版Flappy Bird
一、開發背景:
flappy bird由一位來自越南河內的獨立游戲開發者阮哈東開發,是一款形式簡易但難度極高的休閑游戲。簡單但不粗糙的8比特像素畫面、超級馬里奧游戲中的水管、眼神有點呆滯的小鳥和幾朵白云便構成了游戲的一切。你需要不斷控制點擊屏幕的頻率來調節小鳥的飛行高度和降落速度,讓小鳥順利地通過畫面右端的通道,如果你不小心擦碰到了通道的話,游戲便宣告結束。
這款虐心的小游戲一經推出,便引起火爆的下載。然后先后出現了各種平臺的移植開發:IOS平臺PC和手機版、采用HTML5+Canvas及Javascript技術來實現的Flappy Bird電腦版、以網頁html5+JS技術完全克隆了原版native app的Web App版、實現了在微信朋友圈和QQ空間中的無縫運行的微信/QQ空間版、WindowsPhone版….但是唯一沒有的是直接可在windows操作系統下的單機版,于是當時突發奇想,不如我來填補這個漏洞吧!
二、開發語言及運行環境:
此PC版采用C++的MFC技術在VS2012開發平臺下寫成,支持windows 7\8環境,XP不知道為啥不行~
三、效果展示:
四、游戲框架說明:
整個游戲除了由MFC游戲基本框架CMyApp和CMainWindow外,這里特別封裝了以下幾個類:
- 1、 Bird類[專門處理鳥的飛行邏輯、碰撞檢測、音樂播放、貼圖]
- 2、 PipeList類[內嵌Pipe類,并用CList創建一個Pipe鏈表,用來處理游戲中管道的移動邏輯、碰撞檢測等]
- 3、 Panel類[主要是計分板的動畫效果邏輯和計分板的計分邏輯,數字貼圖,金幣種類運算等]
- 4、 Land類[主要處理陸地運動邏輯及貼圖]
- 5、 Button類[主要處理按鈕的動畫效果、貼圖及響應]
- 6、 Pic類[是圖片資源類,主要負責存儲、加載、全局調用游戲的圖片資源]
五、游戲狀態及邏輯說明:
這款游戲本身操作簡單、邏輯分明,大致可分為以下幾種狀態:
1、 初始態:基本上為靜態貼圖,只有鳥和陸地為簡單運動。由上往下依次為:
- [數字:0]
- [標志:Get Ready!]
- [圖標:操作方法]
- [鳥:上下飛行]
- [陸地:向左移動]
- [背景:隨機晝夜]
2、 游戲進行態:當點擊一下屏幕,鳥、柱子被解封,陸地依然保持原來運動狀態,背景不變,這里采用相對運動效果,其實背景是沒有運動的,而鳥也只是上下運動,根本就沒有向前飛一點!
- [鳥:向上躍起,然后以豎直上拋的邏輯使鳥運動;同時,還要專門為鳥的姿態設計合理的旋轉函數]
- [柱子:向柱子鏈表里加入新的柱子,并使鏈表里的所有柱子開始向左移動,當柱子完全超出最左邊界時,將該柱子刪除;同樣的,當最后一個柱子到達某一特定距離時,向鏈表里加入一個新的柱子,這樣既保證了剛開始的柱子出現效果的真實、有趣性,又保證了資源的合理回收,提高算法高效性]
- [分數:當柱子到達鳥所在的位置時就要進行碰撞檢測,如果沒有碰撞且鳥跨過柱子,就讓分數+1,并響鈴]
- [陸地:保持勻速運動邏輯,采用循環貼圖技術,產生無縫效果]
3、 死亡狀態:鳥的死亡狀態看似簡單,但是仔細分析并非如此。各種細節都要分別考慮:
- [直接撞地態:中止所有運動邏輯,同時留一定的時間間隔,產生畫面轉換的質感]
- [高撞柱子態:旋轉為垂直態,然后自由落體;撞擊時發出聲音,然后發出墜落的聲音,同時進行碰撞檢測,碰到陸地中止一切運動,進行時間停留]
- [低撞柱子態:和高撞柱子態的區別是,墜落的時間少了,音樂沒有完頁面就跳轉了,所以要控制時間停留長度,產生高仿的效果]
4、 死亡之后態:鳥撞地之后要有一定的時間逗留防止頁面跳轉過快不舒服的感覺。接下來首先貼上game_over的圖標,然后計分板從下往上飛來,接著開始計分并張貼是否為新紀錄和是否獲得金牌之類的,最后貼上兩個按鈕等待響應。
- [陸地:停止運動]
- [圖標:展示Game_Over]
- [計分板:動畫效果,從下往上飛來并帶有音效,當飛到指定位置時開始從0累計得分,并統計是否為新紀錄和是否獲得相應的獎牌]
- [按鈕:靜態貼圖,但是相應的時候有上下振動的效果]
#p#
六、經典算法說明:
1、 ON_WM_TIMER:時間消息映射:
主要控制全局邏輯運算的時間進程,根據當前的狀態做相應的邏輯運算;同時邏輯運算也會對全局的游戲狀態進行改變,實現全局操控邏輯實現:(與此相同的draw函數這里就不再詳細介紹)
- void CMainWindow::OnTimer(UINT nTimerID){
- switch(nTimerID){
- case bird_time:
- if(game_state==before_game)bird.logic(before_game,game_state);//開始前
- break;
- case land_time:
- if(game_state==before_game){//開始前
- land.logic();//路
- }else if(game_state==during_game){//游戲中
- if(bird.state!=bird_delay)land.logic();//路
- bird.logic(1,game_state);//鳥正常運動
- if(bird.state!=bird_delay)pipe.logic(goals,bird,game_state);//管道
- }else if(game_state==dying_game){//失敗中
- bird.logic(2,game_state);//垂直下落
- }else if(game_state==end_game){//顯示game-over+計分板+2個按鈕
- if(panel.state==finish)button.logic(game_state);
- if(last_state>=10)panel.logic(goals,best_goals);
- }else if(game_state==start_game){//重新開始
- restart();
- game_state=before_game;
- }
- break;
- default:break;
- }
- draw();
- }
2、 ON_WM_LEFTBUTTONDOWN:鼠標左鍵按下監聽映射:
每次單擊鼠標左鍵相應該函數,然后該函數根據不同的游戲狀態做出不同的邏輯操作:①、[當游戲處于0態,即:游戲開始之前時,點擊鼠標,狀態改為1態,柱子加入開始移動,鳥躍起開始飛翔][當處于游戲態時:每次點擊鳥都會躍起];②、[當處于結束態時:按鈕等待鼠標按動,并根據區域做出判斷是否按了按鈕,按了哪一個]
- void CMainWindow::OnLButtonDown(UINT nFlags, CPoint point){
- if(game_state==0){
- game_state=1;
- pipe.add();
- bird.jump();
- }else if(game_state==1){
- bird.jump();
- }else if(game_state==3){
- button.click(point);
- }
- }
3、PipeList::logic柱子邏輯函數,包括碰撞檢測!
為了簡化起見,我把音頻播放的部分刪去了:這里是遍歷整個鏈表,對于每一個柱子,由上到下每一個if為:①、[判斷鳥是否正好穿越一個柱子,如果是則分數加1];②、[判斷柱子是否出界,超出就不把該柱子放回鏈表,相當于刪除];③、[鳥與地面的碰撞檢測];④、[鳥與柱子的碰撞檢測]⑤、[最后一個if是判斷最后一個柱子是否到達指定位置,如果到達就向鏈表尾部加入一個新的柱子,從而保證了柱子連續且間距統一]
- //---------------------------------------------------------------
- void PipeList::logic(int &goals,Bird &bird,int &game_state){//邏輯函數
- int count=pipe.GetCount();
- for(int i=0;i<count;i++){
- Pipe temp=pipe.GetHead();
- pipe.RemoveHead();
- temp.logic();
- if(temp.pos_x==64){
- goals+=1;
- }
- if(temp.pos_x>=-70)pipe.AddTail(temp);
- //碰撞檢測
- if(23+bird.y+48-$d>400){//與地面
- bird.y=400-230-48+$d;
- bird.stop();
- game_state=2;
- }else if(!(65+48-$d < temp.pos_x || temp.pos_x+52<65+$d)){//與柱子
- if(!(230+bird.y+$d > temp.pos_y+320 && temp.pos_y+420 > 230+bird.y+48-$d)){
- game_state=2;//表示碰撞,游戲結束;
- }
- }
- }
- if((pipe.GetTail()).pos_x<=140){
- Pipe temp;
- pipe.AddTail(temp);
- }
- }//---------------------------------------------------------------
4、 Bird::logic鳥的運動邏輯,包括所有運動狀態(絕密算法!!!)
同樣的為了簡單我也把音頻部分的代碼刪去了。此函數是分別將鳥的運動的各個狀態做分別處理:①、[開始前:采用正弦函數波動飛行同時改變翅膀狀態];②、[正常飛行時:又把鳥的運動狀態劃分為向上、向下、旋轉、停留四個狀態分別處理];③、[下落死亡狀態:這里用了一個輔助時間變量,控制幀動畫播放]
- //---------------------------------------------------------------
- void Bird::logic(int ID,int &game_state){
- if(ID==0){//開始前
- y=4*sin(Time*PI);
- Time+=0.25;
- fly_state=(fly_state+1)%3;
- }else if(ID==1){//正常
- switch(state){
- case state_up:
- v+=a;
- y+=v;
- dis_state--;
- if(dis_state==0){
- state=state_turn;
- Time=0;
- }
- break;
- case state_turn:
- v+=a;
- y+=v;
- if(230+y+48-$d>=400){
- y=400-230-48+$d;
- stop();
- game_state=3;
- }
- dis_state++;
- if(dis_state==1 && Time<=0.4){
- Time+=0.1;
- dis_state=0;
- }
- if(dis_state==6){
- state=state_down;
- }
- break;
- case state_down:
- v+=a;
- y+=v;
- if(delay==0 && 230+y+48-$d>=400){
- y=400-230-48+$d;
- stop();
- state=state_delay;
- }
- break;
- case state_delay:
- delay++;
- if(delay==8){game_state=3;}
- break;
- default:break;
- }
- if(dis_state!=6)fly_state=(fly_state+1)%3;
- }else if(ID==2){//下落
- delay++;
- if(delay==8){//撞擊聲延時
- }
- if(delay<60){//下落運算
- y+=v;
- v+=a;
- if(dis_state!=6)dis_state++;
- if(230+y+48-$d>=400){//撞地檢測
- y=400-230-48+$d;
- stop();
- if(dis_state==6){delay=60;}
- }
- }else if(delay==66){//墜地后延時
- game_state=3;
- }
- }
- }//---------------------------------------------------------------
5、Button::按鈕識別和按鈕動畫邏輯實現:
通過鼠標所在的點判斷是否在按鈕所在的矩形區域內來判斷是否點了該按鈕:
- //---------------------------------------------------------------
- void Button::click(CPoint &point){
- if(point.x>=25 && point.x<=25+116 && point.y>=340 && point.y<=340+70){
- kind=play;
- move=true;
- }else if(point.x>=155 && point.x<=155+116 && point.y>=340 && point.y<=340+70){
- kind=score;
- move=true;
- LoadFromResource(IDR_HTML1);
- }else kind=none;
- }//---------------------------------------------------------------
- //這里主要解決顫動效果實現及按鈕狀態復原:
- //---------------------------------------------------------------
- void Button::logic(int &game_state){
- if(kind==play){//顫動控制
- if(move==true){
- play_y=2;
- move=false;
- }else{
- play_y=0;
- kind=none;
- game_state=4;
- }
- }else if(kind==score){
- if(move==true){
- score_y=2;
- move=false;
- }else{
- score_y=0;
- kind=none;
- }
- }
- }//---------------------------------------------------------------
七、重量級問題解決:
1、 飛翔弧度、旋轉狀態難題:
正如前面的鳥的飛翔邏輯代碼所示:鳥的飛翔過程并不是簡單的自由上拋就能解決的;我通過大量實驗發現必須把這個過程分為上面介紹的4步,然后每一步用更加詳細的數學公式計算鳥的運行邏輯[因為此處我們必須考慮鳥的旋轉效果和鳥的速度同步,所以這才是難點所在]。因為上面已經詳細說明了,這里就不再重復,但這是一大難點!
2、 混音效果、完美封裝處理:
本來音樂播放只要用PlaySound函數一句話就能產生音樂播放效果,但是當全部節點都放好音樂時,發現當鳥正好越過柱子發出加分的鈴聲時和鳥飛翔的聲音無法混合播放,而是出現了嚴重的打斷效果!導致聽起來很不舒服。難道只有重新用Direct-X來處理混音嗎?想想就冒汗….畢竟游戲已經接近尾聲了,沒必要再推翻MFC框架而用Direct-X來吧!于是發現用用2個不同的函數可以解決這個問題,即:其他部分不變還是用PlaySound函數,而分數增加的音樂用mciSendString函數來播放可以解決問題。
但是mciSendString只能加在特定路徑下的音頻,無法處理資源文件下的wav文件,這該怎么辦呢?難道要放棄資源的全封裝效果?那多不好,于是還是被我解決了!我采用的思路是:把資源文件讀到一個中間虛擬文件,然后把該中間文件加載金mciSendString就可以啦!下面是如何讀取資源文件并轉為中間文件的函數:
注:PlaySound(MAKEINTRESOURCE(ID),AfxGetResourceHandle(),SND_RESOURCE|SND_ASYNC);
- //---------------------------------------------------------------
- bool ExtractResource(LPCTSTR strDstFile, LPCTSTR strResType, LPCTSTR strResName)
- {
- // 創建文件
- HANDLE hFile = ::CreateFile(strDstFile, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- return false;
- // 查找資源文件中、加載資源到內存、得到資源大小
- HRSRC hRes = ::FindResource(NULL, strResName, strResType);
- HGLOBAL hMem = ::LoadResource(NULL, hRes);
- DWORD dwSize = ::SizeofResource(NULL, hRes);
- // 寫入文件
- DWORD dwWrite = 0; // 返回寫入字節
- ::WriteFile(hFile, hMem, dwSize, &dwWrite, NULL);
- ::CloseHandle(hFile);
- return true;
- }//--------------------------------------------------------------
3、 創建分享、窗口截屏技術:
其實已經解決上面幾個問題已經仿的差不多啦,但是還不完美!于是開始著手解決那個分享按鈕[要知道這對MFC來說難度不亞于不用引擎來做圖像處理!]可是這并不代表問題不可解。先不說,先看看效果!
知道難度了吧!這是分享按鈕自動創建的網頁,然后還有圖片信息,下載鏈接[這樣才會吸引更多的人玩]由于這里涉及到非基礎MFC知識,這里只提示一下:用到的技術是HTML+JS技術[也就是網頁編程+腳本設計]
八、開發感想:
實踐出真知,通過開發這款簡單的像素游戲,遇到了很多問題,也學到了很多,如今將近一年后拿出來還覺得當時做的這個是一個小奇跡~雖然這一年里也做了不少好玩的軟件、神奇的硬件、以及一些軟硬結合的小東西,但是都沒有這個讓人感覺充實。仔細想想,我覺得之所以它能讓開發它的人如此留念,很大一部分原因是因為對它的反復斟酌修改與追求完美的過程中所積淀的解決問題、享受成果的樂趣吧!如今大三上也快GAME OVER了。這一年可能太過于浮躁,一方面想施展下身手、另一方面又技藝不精,會的挺多但都淺嘗輒止,好的想法要很長時間才能實現,遇到優化又不能靜下心來,整天忙忙碌碌基本1~2點休息,可是很少出這種精致的作品~時間很快,暑假實習過后留在大學里的日子就不多啦,且行且珍惜~