很久之前就想做一個坦克大戰,嘗試着用WPF做了一部分,后來放棄了,或者說失敗了。之前的失敗給了自己很多啟示,想要的內容太多,心太高,能力有限,結果只有失敗。從那之后養成了很多習慣:每回自己做東西都要仔細的計划一下設計一下,即使不規范的寫文檔心里也要有數;在大程序開始前,先做一些小程序驗證一下可能遇到的技術問題是不是都能解決,免得做了多一半才發現有些問題是根本不可能解決的;先把最基礎個功能實現,不急於實現那些不重要的細枝末節,驗證整體結構的可行性,細小的功能再以后的迭代中逐步添加上去,將每一步的風險都降低;架構先行,將不同功能的代碼分割開來,不同的模塊分別進化,減少風險。
放寒假回家就開始動手做這個項目,開學以后事情繁多,利用零零碎碎的時間終於弄完了,雖然還面存在許多問題,目前開來是一個可以用的版本。開發的目的不是做一個商用游戲,只是磨練自己的技術,所以游戲性方面表現很差,我也沒有改進的想法。
代碼已上傳:http://files.cnblogs.com/GhostZCH/TankWar.rar(筒子們,下了要回帖哦!)
開發面臨得挑戰:
1.整體結構設計,需要設計一個支持多種界面的架構,支持界面和數據模型單獨進化
2.運用設計模式,設計類之間的關系,這次類比較多,我也打算把類設計的多一些,這樣也可以更好的理解類之間的關系,我也見過有人用幾個類就實現了坦克大戰,不過這不是我追求的,通過小程序磨練自己才是目的。
3.C++開發,之前也寫了幾個C++的小程序,但是都沒有真么復雜,不過在那些小程序中已經檢驗過大部分要用的技術了。
4.內存管理,這可能是我的弱項了,從C#和Java轉過來一只都不習慣手動管理內存。
5. 解決方案下多個項目的環境配置,這也轉C++才遇到的問題,之前C#只要添加引用項目就可以了,這次還要遇到路徑宏,頭文件包含等問題。。
整體的思路將程序分成3層,四個大塊,3層好理解就是MVC的三次了,除了這三層還需要一個基礎數據模塊,模塊被三層公用,提供基礎功能,類似物理引擎吧,不過功能小得多。
並沒有使用一般桌面程序使用的事件驅動模式,而是使用了游戲中廣泛應用的幀循環模式,程序啟動時開始幀循環直到程序退出,用戶操作不是被立即相應的,而是被記錄下來等到下一個幀循環的時候處理。每一個幀循環首先處理用戶操作更新物理數據信息,然后使用更新后的數據重繪界面。
整體設計如下,程序在設計基礎上寫的,最終可能有些不同:
類比較多,分開看就容易了,首先從控制器開始。控制器的作用是連接數據模型和視圖,將用戶操作傳給后台,同時將后台數據傳給前台,通知前台顯示。如果說視圖類似一個飯店的餐廳,數據模型就是這個飯店的后廚,而控制器就像是一個大堂經理,餐廳里客人點菜吃菜,后廚忙着做菜,經理的任務就是告訴廚師需要在什么時候做什么菜,同時通知服務員將做好的菜端給點菜的客人,經理從不動手做飯也不親自接待客人只是發號施令,服務員不進后廚,后廚也不進大堂。如果不分開,就是在飯店大堂里直接開火做飯,想必這樣的館子也只能是路邊小攤。
為了讓整個程序的數據模型和界面可以獨立演化,設置了兩個接口IView 和IModel,Controller中用到的也是這兩個接口,這也符合面向接口編程而不是面向實現編程的原則,以后的Model和View們只需要實現這兩個接口就可以了,兩者互不相關,都可以獨自的發展。
Controller中提供了兩種不同的使用模式,一種是自啟動循環,另一種是由外界驅動幀循環。
第一種程序如下,在類似命令行這種不存在循環的程序中可以直接啟動一個循環:
1 void Controller::Go() 2 { 3 // Initialization 4 time_t last_time = clock(); 5 6 // run 7 while(_state!=STATE_ESC) 8 { 9 // time 10 time_t this_time = clock(); 11 int derta_time = this_time - last_time; 12 last_time = this_time; 13 14 //user operation 15 USER_OPERATION op = _view->GetUserOperation(); 16 LoopOperation(op, derta_time); 17 } 18 }
在Main 函數啟動后就啟動循環了
1 int _tmain(int argc, _TCHAR* argv[]) 2 { 3 Controller *game = new Controller(new ConsoleView(),new ModelA()); 4 game->Go(); 5 return 0; 6 }
另一種情況是界面架構就已經在循環中(如MFC),或者界面架構可以啟動循環如openGL,ORGE等,這時就使用外部循環驅動程序:
1 bool Controller::TimerTick( int dertaTime ) 2 { 3 USER_OPERATION op = _view->GetUserOperation(); 4 LoopOperation(op, dertaTime); 5 6 if(_state!=STATE_ESC) return true; 7 else return false;//用戶選擇退出 8 }
MFC的啟動如下:
1 void CTankWarMFCDlg::OnTimer(UINT_PTR nIDEvent) 2 { 3 if(!_gameController->TimerTick(TIMER_SPAN)) 4 this->OnCancel(); 5 6 __super::OnTimer(nIDEvent); 7 8 _userOP = USER_NONE; 9 }
整個Controller針對的都是接口IView和IModel,不依賴具體的實現,只要在初始化Controller實例的時侯傳入View與Model的實例即可。Controller的構造函數如下:
Controller(IView *view, IModel *model);
初始化時傳入不同的參數也就應用不同的視圖和模型如:
Controller *game = new Controller(new ConsoleView(),new ModelA());//命令行界面和模型A
// this 在 CTankWarMFCDlg 中使的,該類實現了IView接口是MFC的窗口
_gameController = new Controller(this,new ModelB());//MFC界面和模型B
LoopOperation( USER_OPERATION op, int derta_time )是控制器中重要的函數,執行幀循環,參數是用戶在兩幀中的操作,derta_time 是兩幀的間隔時間,通常是毫秒級的,函數首先處理系統操作,如果用戶選擇退出,將停止循環。接下來如果游戲處於進行狀態,通知模型更新數據,最后在界面中顯示模型中的數據。
1 void Controller::LoopOperation( USER_OPERATION op, int derta_time ) 2 { 3 HandleSysOperation(op);//handle sys op 4 5 string *alert = NULL; 6 if(_state==STATE_GO) 7 { 8 _model->FrameStart(derta_time,UserOpToTankOp(op));// frame go 9 if (_model->IsLose()) alert = new string("GAME OVER!"); 10 if (_model->IsWin()) alert = new string("YOU WIN!"); 11 if (_model->IsLose()||_model->IsWin()) StartOrStop(); 12 } 13 Information *information = _model->GetInformation(); 14 information->AlertMsg(alert); 15 16 _view->DisplayInformation(information); // information 17 _view->DisplayGrid(_model->GetGird()); // draw grid 18 19 delete information; 20 }
接口中完全是純虛函數構成,代碼如下:
1 class IView 2 { 3 public: 4 IView(void); 5 ~IView(void); 6 7 virtual void DisplayGrid(Grid* grid) = 0; 8 virtual void DisplayInformation(Information* information) = 0; 9 10 virtual USER_OPERATION GetUserOperation() = 0; 11 12 virtual void Initialization() = 0; 13 virtual void Clear() = 0; 14 };
1 class IModel 2 { 3 4 public: 5 IModel(void); 6 ~IModel(void); 7 8 virtual Grid* GetGird() = 0; 9 virtual Information* GetInformation()= 0; 10 11 virtual void Initialization()= 0; 12 virtual void Clear()= 0; 13 14 virtual bool IsWin()= 0; 15 virtual bool IsLose()= 0; 16 17 virtual void FrameStart(int dertaTime, TANK_OPERATION op)= 0; 18 };
控制器說完說下界面層,共開發了兩個界面,事實上我的開發順序是這樣的,先開發了ModelA 為了測試ModelA開發了控制台界面,控制台測試通過后又開發了MFC的界面,為了MFC界面取得更好的游戲效果,在ModelA的基礎上寫了ModelB,可以看到兩者很多代碼都是一樣的。這里有個小問題,看似這種復制粘貼的方法有違代碼復用的原則,但是這里我更多的考慮可以在不影響已有部分的情況下進行改進,代碼的獨立性更為重要,這也說明一個問題,復用率不是越高越好,復用也要考慮合理性。
界面的詳細代碼就不貼了,只顯示一下界面的定義:
class CTankWarMFCDlg : public CDialog,public IView
class ConsoleView :public IView
界面代碼的內容是用各自的形式將數據展示出來,ConsoleView 里使用的多是一些cout,CTankWarMFCDlg 則使用了復雜一些的GDI與窗口的重繪。只貼兩小段代碼示意一下。
1 void ConsoleView::DisplayGrid( Grid* grid ) 2 { 3 if(!grid) return; 4 5 int h = grid->Height(); 6 int w = grid->Width(); 7 char *data = new char[h*w]; 8 9 for (int i=0;i<h*w;i++) 10 data[i] = ' '; 11 12 for(list<Bullet*>::const_iterator iter = grid->BulletList()->begin(); iter!=grid->BulletList()->end();iter++) 13 { 14 int x = (*iter)->Position()->X(); 15 int y = (*iter)->Position()->Y(); 16 17 data[y*w+x] = '*'; 18 } 19 20 if (grid->User()) 21 { 22 int x = grid->User()->Position()->X(); 23 int y = grid->User()->Position()->Y(); 24 data[y*w+x] = 'A'; 25 } 26 27 for(list<AITank*>::const_iterator iter = grid->AiTankList()->begin(); iter!=grid->AiTankList()->end();iter++) 28 { 29 int x = (*iter)->Position()->X(); 30 int y = (*iter)->Position()->Y(); 31 32 data[y*w+x] = 'o'; 33 } 34 35 cout<<"======================================================"<<endl; 36 for (int i=0;i<h;i++) 37 { 38 cout<<"||"; 39 for (int j=0;j<w;j++) 40 { 41 printf("%c",data[i*w+j]); 42 } 43 cout<<"||"<<endl; 44 } 45 cout<<"======================================================"<<endl; 46 }
MFC
1 void CTankWarMFCDlg::DrawAiTank( CDC *pDc,float hStep,float wStep,list<AITank*> *tankList ) 2 { 3 pDc->SelectObject(_aiTank);//選擇畫筆 4 5 for(list<AITank*>::iterator iter = tankList->begin();iter!=tankList->end();iter++) 6 { 7 AITank *t = (*iter); 8 9 Vect2d *pos = t->Position(); 10 float r = t->Radius(); 11 12 float x = pos->X(); 13 float y = pos->Y(); 14 15 int x1 = (x-r)*wStep; 16 int x2 = (x+r)*wStep; 17 int y1 = (y-r)*hStep; 18 int y2 = (y+r)*hStep; 19 20 pDc->Ellipse(x1,y1,x2,y2); 21 } 22 }
界面如下所示:
數據模型中,是兩個實現IModel的類(每次啟動只使用一個),用ModelB進行說明:h文件的定義如下:
1 class ModelB: 2 public IModel 3 { 4 public: 5 const static int GIRD_HEIGHT = 250; 6 const static int GIRD_WIDTH = 250; 7 const static int TANK_RADIUS = 5; 8 const static int WAVE_COUNT = 3; 9 10 private: 11 Grid *_grid; 12 string *_msg; 13 14 int _wave; 15 int _score; 16 long _totalTime; 17 18 // 實現接口 19 public: 20 ModelB(void); 21 ~ModelB(void); 22 23 Grid* GetGird(); 24 Information* GetInformation(); 25 26 void Initialization(); 27 void Clear(); 28 29 bool IsWin(); 30 bool IsLose(); 31 32 void FrameStart( int dertaTime, TANK_OPERATION op ); 33 34 //內部方法 35 private: 36 void NewWave(); 37 bool NeedNewWave(); 38 39 void UserOperation( int dertaTime,TANK_OPERATION op); 40 void BulletsOperation(int dertaTime); 41 void TanksOperation(int dertaTime); 42 43 };
實現接口的公告方法是留給控制器調用的,內部方法是為了方便實現自己添加的,Model中最復雜的是void FrameStart( int dertaTime, TANK_OPERATION op );函數,它是每幀的操作,在游戲進行時每次幀循環都會被調用一次用來提示數據模型根據用戶操作更新數據。
1 void ModelB::FrameStart( int dertaTime, TANK_OPERATION op ) 2 { 3 if (_grid) 4 { 5 _totalTime+=dertaTime; 6 UserOperation(dertaTime,op); 7 BulletsOperation(dertaTime); 8 TanksOperation(dertaTime); 9 10 if(NeedNewWave()) NewWave(); 11 } 12 }
單獨看這個函數比較清晰簡單,但是他調用過的5個函數就比較復雜了,幾乎占了Model代碼量的多半,響應用戶操作,坦克的移動,發射子彈,碰撞檢測,子彈的擊中事件,敵人的添加和減少。。。是游戲邏輯的實現。
貼上一個函數示意一下過程:
1 void ModelB::TanksOperation( int dertaTime ) 2 { 3 UserTank* user_tank = _grid->User(); 4 list<AITank *> *tanklist = _grid->AiTankList(); 5 list<AITank *>::iterator i,j; 6 7 for (i = tanklist->begin();i!=tanklist->end();i++) 8 { 9 AITank *t = (*i); 10 t->GetAIOperation(); 11 12 if (t->IsNeedMove()) 13 { 14 Circle *c = t->GetNextPosition(dertaTime); 15 16 //if inside grid 17 if(!_grid->IsInside(c)) break; 18 19 // if impact user tank break 20 if (c->IsImpact(user_tank)) break; 21 22 // if impact other AI tank break 23 bool isImpact = false; 24 for (j = tanklist->begin();j!=tanklist->end();j++) 25 { 26 if(i!=j && c->IsImpact(*j)) 27 { 28 isImpact = true;break; 29 } 30 } 31 if(!isImpact)//move 32 t->Operation(dertaTime); 33 } 34 else 35 { 36 t->Operation(dertaTime); 37 } 38 39 Bullet *b = t->GetBullet(); 40 if(b) _grid->BulletList()->push_back(b); 41 42 // 子彈太多就去掉最老的 43 if (_grid->BulletList()->size()>50) 44 { 45 Bullet *b =*( _grid->BulletList()->begin()); 46 _grid->BulletList()->pop_front(); 47 delete b; 48 } 49 } 50 }
需要移動的坦克先檢查是否碰撞,不碰撞的向前移動,碰撞的停止,如果坦克的操作時開炮就要獲得它的炮彈,獲得炮彈使用的是工廠模式,后面再細細說來。
說完了這三部分就要談談本次開發最復雜的部分——基礎數據:
看似很復雜其實只要抓住主線就很簡單了,最基礎的類是Circle,代表一個圓,是碰撞基礎元素,MoveCircle繼承自Circle增加了速度和方向屬性,MoveCircle分開兩支為坦克和炮彈,坦克又分成用戶可操作的坦克與電腦控制的敵軍,兩者的區別是在Aitank有一個Ai屬性,這里用了一個策略模式,Aitank可以有不同類型的Ai(這次就只寫了一種,但是支持更多)。其他的類都是輔助這根主線。
兩個Factory類方便獲取子彈和坦克,簡化了Model的操作,也方便維護,以TankFactory為例說明一下:
頭文件
1 class TankFactory 2 { 3 private: 4 TankFactory(void); 5 ~TankFactory(void); 6 7 public: 8 static Tank *GetTank(TANK_TYPE type,Vect2d *pos,DIRCTION dir,float r=1); 9 10 private: 11 static UserTank *GetUserTank(Vect2d *pos,DIRCTION dir,float r=1); 12 static AITank *GetStdAITank(Vect2d *pos,DIRCTION dir,float r=1); 13 };
Cpp文件
1 Tank * TankFactory::GetTank( TANK_TYPE type,Vect2d *pos,DIRCTION dir,float r) 2 { 3 switch(type) 4 { 5 case TANK_USER: return GetUserTank(pos,dir,r);break; 6 case TANK_AI_STD:return GetStdAITank(pos,dir,r);break; 7 default:return NULL; 8 } 9 } 10 11 UserTank * TankFactory::GetUserTank( Vect2d *pos,DIRCTION dir,float r) 12 { 13 return new UserTank(USER_TANK_TOP_HP,USER_TANK_TOP_HP,BULLET_STD_USER,pos,dir,USERTANK_SPEED,r); 14 } 15 16 AITank * TankFactory::GetStdAITank( Vect2d *pos,DIRCTION dir,float r) 17 { 18 return new AITank(new StandardAI(),AI_TANK_TOP_HP,AI_TANK_TOP_HP,BULLET_STD,pos,dir,AITANK_SPEED,r); 19 }
使用:
TankFactory::GetTank(TANK_AI_STD,new Vect2d(x,y),DIR_UP,TANK_RADIUS)
第一個參數是一個枚舉
1 enum TANK_TYPE 2 { 3 TANK_USER, 4 TANK_AI_STD 5 };
如果以后需要更多類型的坦克只需要添加枚舉,添加坦克類,對Factory的switch家一項即可,不影響其他部分的代碼。當然在我這個微小的系統中,工廠模式的效果可能並不明顯,但如果在初始化實例時還有復雜的操作時,這個優勢就很明顯了。封裝,減少耦合是提高代碼穩定性的重要途徑。
剩下的類就只有Gird了,這各類的意思是游戲區域,是一個邏輯概念,並不是用戶在界面上看到的顯示區域,顯示區域可達可小,取決於View的設置,與底層邏輯無關。
事情還很多,就說的這吧,繼續看面試寶典。。。