C++坦克大战
很久之前就想做一个坦克大战,尝试着用WPF做了一部分,后来放弃了,或者说失败了。之前的失败给了自己很多启示,想要的内容太多,心太高,能力有限,结果只有失败。从那之后养成了很多习惯:每回自己做东西都要仔细的计划一下设计一下,即使不规范的写文档心里也要有数;在大程序开始前,先做一些小程序验证一下可能遇到的技术问题是不是都能解决,免得做了多一半才发现有些问题是根本不可能解决的;先把最基础个功能实现,不急于实现那些不重要的细枝末节,验证整体结构的可行性,细小的功能再以后的迭代中逐步添加上去,将每一步的风险都降低;架构先行,将不同功能的代码分割开来,不同的模块分别进化,减少风险。
放寒假回家就开始动手做这个项目,开学以后事情繁多,利用零零碎碎的时间终于弄完了,虽然还面存在许多问题,目前开来是一个可以用的版本。开发的目的不是做一个商用游戏,只是磨练自己的技术,所以游戏性方面表现很差,我也没有改进的想法。
代码已上传:https://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的设置,与底层逻辑无关。
事情还很多,就说的这吧,继续看面试宝典。。。