一、开发背景:
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点休息,可是很少出这种精致的作品~时间很快,暑假实习过后留在大学里的日子就不多啦,且行且珍惜~