2016年9月7日 星期三

Android SurfaceView实战 带你玩转flabby bird (下)

Ref : http://blog.csdn.net/lmj623565791/article/details/43063331



就是这个效果图了。
首先我们明确下,当然我们的状态与上图的差距:
1、我们的管道只有一个,现需要动态生成,以及动态移除;
2、我们的鸟静止,现需要默认下落,按下屏幕上升一段距离;
3、现需要,判断鸟在飞翔过程中与管道以及地面的接触情况,判断是否Gameover;
4、现需要,当鸟每穿过一个管道的时候,我们能够进行计分+1 ;

ok,明确了目标~~~
那么首先,根据上述,我们发现我们的游戏缺少一个什么?
嗯,是状态,我们得知道游戏什么时候正在运行,什么时候准备运行,什么时候GameOver吧。所以引入一个枚举变量:
  1. /** 
  2.      * 游戏的状态 
  3.      *  
  4.      * @author zhy 
  5.      *  
  6.      */  
  7.     private enum GameStatus  
  8.     {  
  9.         WAITING, RUNNING, OVER  
  10.     }  
1、默认情况下,是WAITING状态,屏幕静止,上面就一只静止的鸟~~
2、当用户触摸屏幕时:进入RUNNING状态,游戏开始根据用户的触摸情况进行交互;
3、当鸟触碰到管道或者落到地上,那么进入GAMEOVER状态,OVER时,如果触碰的是管道,则让鸟落到地上以后,立即切换为WAITING状态。
好了,这样,我们的三个状态就搞定了~~这才像个游戏么~

  1. /** 
  2.      * 游戏的状态 
  3.      *  
  4.      * @author zhy 
  5.      *  
  6.      */  
  7.     private enum GameStatus  
  8.     {  
  9.         WAITING, RUNNING, OVER  
  10.     }  


1、默认情况下,是WAITING状态,屏幕静止,上面就一只静止的鸟~~
2、当用户触摸屏幕时:进入RUNNING状态,游戏开始根据用户的触摸情况进行交互;
3、当鸟触碰到管道或者落到地上,那么进入GAMEOVER状态,OVER时,如果触碰的是管道,则让鸟落到地上以后,立即切换为WAITING状态。
好了,这样,我们的三个状态就搞定了~~这才像个游戏么~

有了状态 ,我们再考虑如何处理用户交互,我们的交互主要就是触摸了,那么我们去重写View的onTouchEvent方法即可。
我们在onTouchEvent里面,根据用户的触摸游戏的状态、一些变量等;改变了的变量,会在绘制的时候进行体现,这样才形成了交互效果。
然后呢?为了我们代码的清晰,我们本来在线程中只有一个draw()方法,现在我们增加一个方法:logic();处理在游戏过程中分数的计算、管道的生成移除等。
这样可以把逻辑分开,logic专心做一些逻辑上的事,draw只管绘制;
说了这么多,如果你木有消化,没事,下面我们开始进入代码阶段了~~

2、动态生成和移除管道

首先我们增加一个mStatus变量:private GameStatus mStatus = GameStatus.WAITING;
然后添加logic方法以及复写onTouchEvent方法。
经过筛检后的代码:

  1. public class CopyOfGameFlabbyBird extends SurfaceView implements Callback,  
  2.         Runnable  
  3. {  
  4.   
  5.     //省略了一些代码  
  6.       
  7.     private enum GameStatus  
  8.     {  
  9.         WAITTING, RUNNING, STOP;  
  10.     }  
  11.   
  12.     /** 
  13.      * 记录游戏的状态 
  14.      */  
  15.     private GameStatus mStatus = GameStatus.WAITTING;  
  16.   
  17.     /** 
  18.      * 触摸上升的距离,因为是上升,所以为负值 
  19.      */  
  20.     private static final int TOUCH_UP_SIZE = -16;  
  21.     /** 
  22.      * 将上升的距离转化为px;这里多存储一个变量,变量在run中计算 
  23.      *  
  24.      */  
  25.     private final int mBirdUpDis = Util.dp2px(getContext(), TOUCH_UP_SIZE);  
  26.   
  27.     private int mTmpBirdDis;  
  28.     /** 
  29.      * 鸟自动下落的距离 
  30.      */  
  31.     private final int mAutoDownSpeed = Util.dp2px(getContext(), 2);  
  32.   
  33.     /** 
  34.      * 处理一些逻辑上的计算 
  35.      */  
  36.     private void logic()  
  37.     {  
  38.         switch (mStatus)  
  39.         {  
  40.         case RUNNING:  
  41.   
  42.             // 管道移动  
  43.             for (Pipe pipe : mPipes)  
  44.             {  
  45.                 pipe.setX(pipe.getX() - mSpeed);  
  46.             }  
  47.             // 更新我们地板绘制的x坐标,地板移动  
  48.             mFloor.setX(mFloor.getX() - mSpeed);  
  49.             break;  
  50.   
  51.         case STOP: // 鸟落下  
  52.   
  53.             break;  
  54.         default:  
  55.             break;  
  56.         }  
  57.   
  58.     }  
  59.   
  60.     @Override  
  61.     public boolean onTouchEvent(MotionEvent event)  
  62.     {  
  63.   
  64.         int action = event.getAction();  
  65.   
  66.         if (action == MotionEvent.ACTION_DOWN)  
  67.         {  
  68.             switch (mStatus)  
  69.             {  
  70.             case WAITTING:  
  71.                 mStatus = GameStatus.RUNNING;  
  72.                 break;  
  73.             case RUNNING:  
  74.                 mTmpBirdDis = mBirdUpDis;  
  75.                 break;  
  76.             }  
  77.   
  78.         }  
  79.   
  80.         return true;  
  81.   
  82.     }  
  83.   
  84.     @Override  
  85.     public void run()  
  86.     {  
  87.         while (isRunning)  
  88.         {// 省略了一些代码  
  89.             logic();  
  90.             draw();  
  91.             // 省略了一些代码  
  92.         }  
  93.   
  94.     }  
  95.   
  96. }  

可以看到,我们在run中增加调用了logic()方法,在onTouch中根据用户DOWN,改变状态或者设置mTmpBirdDis即为每次用户点击时,鸟上升的距离,接下来会实现。
还有一点,我们把更新管道的x坐标,从drawFloor中提取了出来;以及更新mFloor的x坐标从draw中提取到logic();draw目前,只管绘制,不管任何事。
现在我们的游戏,启动后画面静止,用户触摸后开始移动;
当然了,现在依旧是一个管道,接下来,我们来动态添加管道:
管道的添加:
对于管道的添加,我准备每隔300dp生成一个管道;
当管道移动出屏幕,我们将其从List中移除,避免不必要的绘制;
那么怎么做呢?

  1. /** 
  2.      * 两个管道间距离 
  3.      */  
  4.     private final int PIPE_DIS_BETWEEN_TWO = Util.dp2px(getContext(), 300);  
  5.     /** 
  6.      * 记录移动的距离,达到 PIPE_DIS_BETWEEN_TWO 则生成一个管道 
  7.      */  
  8.     private int mTmpMoveDistance;  
  9.   
  10.       
  11.     /** 
  12.      * 处理一些逻辑上的计算 
  13.      */  
  14.     private void logic()  
  15.     {  
  16.         switch (mStatus)  
  17.         {  
  18.         case RUNNING:  
  19.                
  20.             // 管道  
  21.             mTmpMoveDistance += mSpeed;  
  22.             // 生成一个管道  
  23.             if (mTmpMoveDistance >= PIPE_DIS_BETWEEN_TWO)  
  24.             {  
  25.                 Pipe pipe = new Pipe(getContext(), getWidth(), getHeight(),  
  26.                         mPipeTop, mPipeBottom);  
  27.                 mPipes.add(pipe);  
  28.                 mTmpMoveDistance = 0;  
  29.             }  
  30.   
  31.             break;  
  32.   
  33.         case STOP: // 鸟落下  
  34.   
  35.             break;  
  36.         default:  
  37.             break;  
  38.         }  
  39.   
  40.     }  











可以看到,一开始状态WAITTING,当我们点击后,地板开始移动,管道开始动态添加并移动~~~
那么,现在有一个问题,我们的管道现在动态添加了,随着游戏的运行,我们的管道肯定无限多呀,当然了,我这种超不过10分的渣渣,这个问题是不会出现的。
无限多,即使不崩,估计也卡,那么多管道看不到了,干嘛绘制呢?
那么我们该如何移除这些不在屏幕上的管道呢
管道的移除:
很简单,看代码:
  1. /** 
  2.      * 记录需要移除的管道 
  3.      */  
  4.     private List<Pipe> mNeedRemovePipe = new ArrayList<Pipe>();  
  5.   
  6.     /** 
  7.      * 处理一些逻辑上的计算 
  8.      */  
  9.     private void logic()  
  10.     {  
  11.         switch (mStatus)  
  12.         {  
  13.         case RUNNING:  
  14.   
  15.             // 更新我们地板绘制的x坐标,地板移动  
  16.             mFloor.setX(mFloor.getX() - mSpeed);  
  17.   
  18.             // 管道移动  
  19.             for (Pipe pipe : mPipes)  //  所有的pipe....
  20.             {  
  21.                 if (pipe.getX() < -mPipeWidth)  
  22.                 {  
  23.                     mNeedRemovePipe.add(pipe);  
  24.                     continue;  
  25.                 }  
  26.                 pipe.setX(pipe.getX() - mSpeed);  
  27.             }  
  28.             //移除管道  
  29.             mPipes.removeAll(mNeedRemovePipe);  
  30.   
  31.             Log.e("TAG""现存管道数量:" + mPipes.size());  
  32.               
  33.               
  34.             // 管道  
  35.             mTmpMoveDistance += mSpeed;  
  36.             // 生成一个管道  
  37.             if (mTmpMoveDistance >= PIPE_DIS_BETWEEN_TWO)  
  38.             {  
  39.                 Pipe pipe = new Pipe(getContext(), getWidth(), getHeight(),  
  40.                         mPipeTop, mPipeBottom);  
  41.                 mPipes.add(pipe);  
  42.                 mTmpMoveDistance = 0;  
  43.             }  
  44.   
  45.             break;  
  46.   
  47.         case STOP: // 鸟落下  
  48.   
  49.             break;  
  50.         default:  
  51.             break;  
  52.         }  
  53.   
  54.     }  

其实就增加了几行代码,为了好理解,贴出代码较多;我们增加了一个变量mNeedRemovePipe,在遍历Pipes的时候,如果x左边已经小于 -mPipeWidth时候,说明看不到了,那么就防到mNeedRemovePipe中;
最后统一移除mNeedRemovePipe。 
有人会说,为啥要多创建个mNeedRemovePipe呢?你for循环移除不就行了~嗯,这样是不行的,会报错;
又有人说,我知道那样会报错,但是你可以用CopyOnWriteArrayList这类安全的List,就能for循环时,移除了~~嗯,这样是可以,但是这类List的方法中为了安全,各种clone,势必造成运行速度慢~~我们这里是游戏,千万要避免不必要的速度丢失~~~
好了~~现在管道,彻底没什么问题了~~~
接下来,我们的鸟该动了~~

  1. private void logic()  
  2.     {  
  3.         switch (mStatus)  
  4.         {  
  5.         case RUNNING:  
  6.   
  7.             // 更新我们地板绘制的x坐标,地板移动  
  8.             mFloor.setX(mFloor.getX() - mSpeed);  
  9.               
  10.             logicPipe();  
  11.   
  12.             //默认下落,点击时瞬间上升  
  13.             mTmpBirdDis += mAutoDownSpeed;  
  14.             mBird.setY(mBird.getY() + mTmpBirdDis);  
  15.             break;  
  16.   
  17.         case STOP: // 鸟落下  
  18.   
  19.             break;  
  20.         default:  
  21.             break;  
  22.         }  
  23.   
  24.     }  

logic中添加两行就行了~~
mTmpBirdDis += mAutoDownSpeed;
mBird.setY(mBird.getY() + mTmpBirdDis);
默认情况越降越快,当用户点击的时候瞬间上升一段距离,继续往下掉~
我把管道相关的抽取出去了~~
现在的效果:

4、GameOver or Grades++

 对于失败的判断,我觉得很简单呀,直接在遍历管道的时候,去判断管道的和鸟是否触碰~~或者鸟的y坐标是否触地~~
在logic中调用checkGameOver()

  1. private void checkGameOver()  
  2.     {  
  3.   
  4.         // 如果触碰地板,gg  
  5.         if (mBird.getY() > mFloor.getY() - mBird.getHeight())  
  6.         {  
  7.             mStatus = GameStatus.STOP;  
  8.         }  
  9.         // 如果撞到管道  
  10.         for (Pipe wall : mPipes)  
  11.         {  
  12.             //已经穿过的  
  13.             if (wall.getX() + mPipeWidth < mBird.getX())  
  14.             {  
  15.                 continue;  
  16.             }  
  17.             if (wall.touchBird(mBird))  
  18.             {  
  19.                  mStatus = GameStatus.STOP;  
  20.                 break;  
  21.             }  
  22.         }  
  23.     }  

如果碰到地面gg,如果和管道碰到gg;

  1. public class Pipe  
  2. {  
  3.     //...  
  4.   
  5.     /** 
  6.      * 判断和鸟是否触碰 
  7.      * @param mBird 
  8.      * @return 
  9.      */  
  10.     public boolean touchBird(Bird mBird)  
  11.     {  
  12.         /** 
  13.          * 如果bird已经触碰到管道 
  14.          */  
  15.         if (mBird.getX() + mBird.getWidth() > x  
  16.                 && (mBird.getY() < height || mBird.getY() + mBird.getHeight() > height  
  17.                         + margin))  
  18.         {  
  19.             return true;  
  20.         }  
  21.         return false;  
  22.           
  23.     }  
  24.   
  25. }  

我们在管道中添加了touchBird用于进行判断~~很简单,鸟如果在管道的范围内,如果不在管道的空隙中则为true~~
好了,现在运行代码,发现我们的鸟如果碰到管道或者落地就OVER了~~但是OVER以后,再也不会动了~~
维萨呢?因为OVER后,我们的状态是STOP,而STOP我们没有做任何处理~~
我们应该在STOP中去判断,如果没有落地让鸟落地,然后切换状态为WAITTING~

//  Y 是往下越大....
if (mBird.getY() < mFloor.getY() - mBird.getWidth()) 

成立的話代表還沒有到最下面...也就是空中...  所以讓他慢慢的降下來

else

成立的話代表到最下面...改變狀態到WAITTING....

然後呼叫initPos()

  1. private void logic()  
  2.     {  
  3.         switch (mStatus)  
  4.         {  
  5.         case RUNNING:  
  6.   
  7.               
  8.   
  9.         case STOP: // 鸟落下  
  10.             // 如果鸟还在空中,先让它掉下来  
  11.             if (mBird.getY() < mFloor.getY() - mBird.getWidth())  
  12.             {  
  13.                 mTmpBirdDis += mAutoDownSpeed;  
  14.                 mBird.setY(mBird.getY() + mTmpBirdDis);  
  15.             } else  
  16.             {  
  17.                 mStatus = GameStatus.WAITTING;  
  18.                 initPos();  
  19.             }  
  20.             break;  
  21.         default:  
  22.             break;  
  23.         }  
  24.   
  25.     }  
  26.   
  27.     /** 
  28.      * 重置鸟的位置等数据 
  29.      */  
  30.     private void initPos()  
  31.     {  
  32.         mPipes.clear();  
  33.         mNeedRemovePipe.clear();  
  34.         //重置鸟的位置  
  35.         mBird.setY(mHeight * 2 / 3);  
  36.         //重置下落速度  
  37.         mTmpBirdDis = 0;                                                                                                                                          mTmpMoveDistance = 0 ;  
  38.     }  

可以看到,增加了几行计算分数的代码~我们的分数,每次都是置0,然后加上已经看不到的管道数量(都看不到了,肯定是通过的),然后再加上屏幕上在鸟左边的管道数量~
这就是你获得的分数了~~
所以记得移除管道的时候,通过下mRemovedPipe

  1. private int mRemovedPipe 0;  
  2.     private void logicPipe()  
  3.     {  
  4.         // 管道移动  
  5.         for (Pipe pipe : mPipes)  
  6.         {  
  7.             if (pipe.getX() < -mPipeWidth)  
  8.             {  
  9.                 mNeedRemovePipe.add(pipe);  
  10.                 mRemovedPipe++;  
  11.                 continue;  
  12.             }  
  13.             pipe.setX(pipe.getX() - mSpeed);  
  14.         }  
且,在OVER以后,在initPos中将mRemovedPipe 置为 0 ;这样重新开始以后,又从0分开始了~~~
好了,到此结束~~至于,管道间的距离,管道宽度,鸟下降速度各种常量,大家如果觉得不适,自行修改~~~
请允许我再贴一次最终效果~:
看着结果的同时,我们总结下:
其实游戏总体来说不难,别看写了两篇这么长,你站远点看,其实还是我们最初的SurfaceViewTemplate,无非多了个重写onTouchEvent和logic()方法,onTouchEvent是交互必须要写的,其实没什么代码~logic方法进行一些重绘时需要的计算~~而draw就安心的draw进行了~~也就是说,这个游戏,其实和绘制个小鸟,点击上升,没什么区别~~~
值得祝贺的是,我们的SurfaceView经过这三篇博客(还有个转盘),基本涵盖的知识点都覆盖了,说不定哪天,大家想出个虐心的简单的游戏就富了呢~~



沒有留言:

張貼留言