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


    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43063331,本文出自:【张鸿洋的博客】

    1、概述

    Android SurfaceView实战 带你玩转flabby bird (上)中,我们完成了在游戏所需的所有的元素的绘制,包括 Bird鸟、 Floor地板、Pipe 管道 、背景图以及分数等。

    本篇博客将在上篇的基本上,继续带领大家向我们的目标进发,那么问题来了,我们的目标是:


    就是这个效果图了。

    首先我们明确下,当然我们的状态与上图的差距:

    1、我们的管道只有一个,现需要动态生成,以及动态移除;

    2、我们的鸟静止,现需要默认下落,按下屏幕上升一段距离;

    3、现需要,判断鸟在飞翔过程中与管道以及地面的接触情况,判断是否Gameover;

    4、现需要,当鸟每穿过一个管道的时候,我们能够进行计分+1 ;


    ok,明确了目标~~~

    那么首先,根据上述,我们发现我们的游戏缺少一个什么?

    嗯,是状态,我们得知道游戏什么时候正在运行,什么时候准备运行,什么时候GameOver吧。所以引入一个枚举变量:

    /**
    	 * 游戏的状态
    	 * 
    	 * @author zhy
    	 * 
    	 */
    	private enum GameStatus
    	{
    		WAITING, RUNNING, OVER
    	}

    1、默认情况下,是WAITING状态,屏幕静止,上面就一只静止的鸟~~

    2、当用户触摸屏幕时:进入RUNNING状态,游戏开始根据用户的触摸情况进行交互;

    3、当鸟触碰到管道或者落到地上,那么进入GAMEOVER状态,OVER时,如果触碰的是管道,则让鸟落到地上以后,立即切换为WAITING状态。

    好了,这样,我们的三个状态就搞定了~~这才像个游戏么~


    有了状态 ,我们再考虑如何处理用户交互,我们的交互主要就是触摸了,那么我们去重写View的onTouchEvent方法即可。

    我们在onTouchEvent里面,根据用户的触摸游戏的状态、一些变量等;改变了的变量,会在绘制的时候进行体现,这样才形成了交互效果。

    然后呢?为了我们代码的清晰,我们本来在线程中只有一个draw()方法,现在我们增加一个方法:logic();处理在游戏过程中分数的计算、管道的生成移除等。

    这样可以把逻辑分开,logic专心做一些逻辑上的事,draw只管绘制;

    说了这么多,如果你木有消化,没事,下面我们开始进入代码阶段了~~


    2、动态生成和移除管道

    首先我们增加一个mStatus变量:private GameStatus mStatus = GameStatus.WAITING;

    然后添加logic方法以及复写onTouchEvent方法。

    经过筛检后的代码:

    public class CopyOfGameFlabbyBird extends SurfaceView implements Callback,
    		Runnable
    {
    
    	//省略了一些代码
    	
    	private enum GameStatus
    	{
    		WAITTING, RUNNING, STOP;
    	}
    
    	/**
    	 * 记录游戏的状态
    	 */
    	private GameStatus mStatus = GameStatus.WAITTING;
    
    	/**
    	 * 触摸上升的距离,因为是上升,所以为负值
    	 */
    	private static final int TOUCH_UP_SIZE = -16;
    	/**
    	 * 将上升的距离转化为px;这里多存储一个变量,变量在run中计算
    	 * 
    	 */
    	private final int mBirdUpDis = Util.dp2px(getContext(), TOUCH_UP_SIZE);
    
    	private int mTmpBirdDis;
    	/**
    	 * 鸟自动下落的距离
    	 */
    	private final int mAutoDownSpeed = Util.dp2px(getContext(), 2);
    
    	/**
    	 * 处理一些逻辑上的计算
    	 */
    	private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			// 管道移动
    			for (Pipe pipe : mPipes)
    			{
    				pipe.setX(pipe.getX() - mSpeed);
    			}
    			// 更新我们地板绘制的x坐标,地板移动
    			mFloor.setX(mFloor.getX() - mSpeed);
    			break;
    
    		case STOP: // 鸟落下
    
    			break;
    		default:
    			break;
    		}
    
    	}
    
    	@Override
    	public boolean onTouchEvent(MotionEvent event)
    	{
    
    		int action = event.getAction();
    
    		if (action == MotionEvent.ACTION_DOWN)
    		{
    			switch (mStatus)
    			{
    			case WAITTING:
    				mStatus = GameStatus.RUNNING;
    				break;
    			case RUNNING:
    				mTmpBirdDis = mBirdUpDis;
    				break;
    			}
    
    		}
    
    		return true;
    
    	}
    
    	@Override
    	public void run()
    	{
    		while (isRunning)
    		{// 省略了一些代码
    			logic();
    			draw();
    			// 省略了一些代码
    		}
    
    	}
    
    }
    

    可以看到,我们在run中增加调用了logic()方法,在onTouch中根据用户DOWN,改变状态或者设置mTmpBirdDis即为每次用户点击时,鸟上升的距离,接下来会实现。

    还有一点,我们把更新管道的x坐标,从drawFloor中提取了出来;以及更新mFloor的x坐标从draw中提取到logic();draw目前,只管绘制,不管任何事。

    现在我们的游戏,启动后画面静止,用户触摸后开始移动;

    当然了,现在依旧是一个管道,接下来,我们来动态添加管道:

    管道的添加:

    对于管道的添加,我准备每隔300dp生成一个管道;

    当管道移动出屏幕,我们将其从List中移除,避免不必要的绘制;

    那么怎么做呢?

    /**
    	 * 两个管道间距离
    	 */
    	private final int PIPE_DIS_BETWEEN_TWO = Util.dp2px(getContext(), 300);
    	/**
    	 * 记录移动的距离,达到 PIPE_DIS_BETWEEN_TWO 则生成一个管道
    	 */
    	private int mTmpMoveDistance;
    
    	
    	/**
    	 * 处理一些逻辑上的计算
    	 */
    	private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			
    			// 管道
    			mTmpMoveDistance += mSpeed;
    			// 生成一个管道
    			if (mTmpMoveDistance >= PIPE_DIS_BETWEEN_TWO)
    			{
    				Pipe pipe = new Pipe(getContext(), getWidth(), getHeight(),
    						mPipeTop, mPipeBottom);
    				mPipes.add(pipe);
    				mTmpMoveDistance = 0;
    			}
    
    			break;
    
    		case STOP: // 鸟落下
    
    			break;
    		default:
    			break;
    		}
    
    	}

    很简单,添加两个变量,然后在logic中,如果是RUNNING状态,记录移动的距离,达到我们预设的值300dp就增加一个。

    现在我们的效果(记得删除之前我们在onSizeChanged中添加的那个管道,没必要了,我们已经动态生成了)


    可以看到,一开始状态WAITTING,当我们点击后,地板开始移动,管道开始动态添加并移动~~~

    那么,现在有一个问题,我们的管道现在动态添加了,随着游戏的运行,我们的管道肯定无限多呀,当然了,我这种超不过10分的渣渣,这个问题是不会出现的。

    无限多,即使不崩,估计也卡,那么多管道看不到了,干嘛绘制呢?

    那么我们该如何移除这些不在屏幕上的管道呢?

    管道的移除:

    很简单,看代码:

    /**
    	 * 记录需要移除的管道
    	 */
    	private List<Pipe> mNeedRemovePipe = new ArrayList<Pipe>();
    
    	/**
    	 * 处理一些逻辑上的计算
    	 */
    	private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			// 更新我们地板绘制的x坐标,地板移动
    			mFloor.setX(mFloor.getX() - mSpeed);
    
    			// 管道移动
    			for (Pipe pipe : mPipes)
    			{
    				if (pipe.getX() < -mPipeWidth)
    				{
    					mNeedRemovePipe.add(pipe);
    					continue;
    				}
    				pipe.setX(pipe.getX() - mSpeed);
    			}
    			//移除管道
    			mPipes.removeAll(mNeedRemovePipe);
    
    			Log.e("TAG", "现存管道数量:" + mPipes.size());
    			
    			
    			// 管道
    			mTmpMoveDistance += mSpeed;
    			// 生成一个管道
    			if (mTmpMoveDistance >= PIPE_DIS_BETWEEN_TWO)
    			{
    				Pipe pipe = new Pipe(getContext(), getWidth(), getHeight(),
    						mPipeTop, mPipeBottom);
    				mPipes.add(pipe);
    				mTmpMoveDistance = 0;
    			}
    
    			break;
    
    		case STOP: // 鸟落下
    
    			break;
    		default:
    			break;
    		}
    
    	}

    其实就增加了几行代码,为了好理解,贴出代码较多;我们增加了一个变量mNeedRemovePipe,在遍历Pipes的时候,如果x左边已经小于 -mPipeWidth时候,说明看不到了,那么就防到mNeedRemovePipe中;

    最后统一移除mNeedRemovePipe。 

    有人会说,为啥要多创建个mNeedRemovePipe呢?你for循环移除不就行了~嗯,这样是不行的,会报错;

    又有人说,我知道那样会报错,但是你可以用CopyOnWriteArrayList这类安全的List,就能for循环时,移除了~~嗯,这样是可以,但是这类List的方法中为了安全,各种clone,势必造成运行速度慢~~我们这里是游戏,千万要避免不必要的速度丢失~~~

    好了~~现在管道,彻底没什么问题了~~~

    接下来,我们的鸟该动了~~


    3、让鸟飞起来

    其实特别简单,就两行代码,短的我都不好意思独立章节了~~

    private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			// 更新我们地板绘制的x坐标,地板移动
    			mFloor.setX(mFloor.getX() - mSpeed);
    			
    			logicPipe();
    
    			//默认下落,点击时瞬间上升
    			mTmpBirdDis += mAutoDownSpeed;
    			mBird.setY(mBird.getY() + mTmpBirdDis);
    			break;
    
    		case STOP: // 鸟落下
    
    			break;
    		default:
    			break;
    		}
    
    	}

    logic中添加两行就行了~~

    mTmpBirdDis += mAutoDownSpeed;
    mBird.setY(mBird.getY() + mTmpBirdDis);

    默认情况越降越快,当用户点击的时候瞬间上升一段距离,继续往下掉~

    我把管道相关的抽取出去了~~

    现在的效果:


    好了,可以看到我们的鸟终于可以交互了~~上面的动画比较快~~见谅,据说这样图能小一点~

    现在,我们已经实现了绝大部分功能了,只剩下一个判断GameOver和计分了~~~let's go !


    4、GameOver or Grades++

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

    在logic中调用checkGameOver()

    private void checkGameOver()
    	{
    
    		// 如果触碰地板,gg
    		if (mBird.getY() > mFloor.getY() - mBird.getHeight())
    		{
    			mStatus = GameStatus.STOP;
    		}
    		// 如果撞到管道
    		for (Pipe wall : mPipes)
    		{
    			//已经穿过的
    			if (wall.getX() + mPipeWidth < mBird.getX())
    			{
    				continue;
    			}
    			if (wall.touchBird(mBird))
    			{
    				 mStatus = GameStatus.STOP;
    				break;
    			}
    		}
    	}

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

    public class Pipe
    {
    	//...
    
    	/**
    	 * 判断和鸟是否触碰
    	 * @param mBird
    	 * @return
    	 */
    	public boolean touchBird(Bird mBird)
    	{
    		/**
    		 * 如果bird已经触碰到管道
    		 */
    		if (mBird.getX() + mBird.getWidth() > x
    				&& (mBird.getY() < height || mBird.getY() + mBird.getHeight() > height
    						+ margin))
    		{
    			return true;
    		}
    		return false;
    		
    	}
    
    }
    
    我们在管道中添加了touchBird用于进行判断~~很简单,鸟如果在管道的范围内,如果不在管道的空隙中则为true~~

    好了,现在运行代码,发现我们的鸟如果碰到管道或者落地就OVER了~~但是OVER以后,再也不会动了~~

    维萨呢?因为OVER后,我们的状态是STOP,而STOP我们没有做任何处理~~

    我们应该在STOP中去判断,如果没有落地让鸟落地,然后切换状态为WAITTING~

    private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			
    
    		case STOP: // 鸟落下
    			// 如果鸟还在空中,先让它掉下来
    			if (mBird.getY() < mFloor.getY() - mBird.getWidth())
    			{
    				mTmpBirdDis += mAutoDownSpeed;
    				mBird.setY(mBird.getY() + mTmpBirdDis);
    			} else
    			{
    				mStatus = GameStatus.WAITTING;
    				initPos();
    			}
    			break;
    		default:
    			break;
    		}
    
    	}
    
    	/**
    	 * 重置鸟的位置等数据
    	 */
    	private void initPos()
    	{
    		mPipes.clear();
    		mNeedRemovePipe.clear();
    		//重置鸟的位置
    		mBird.setY(mHeight * 2 / 3);
    		//重置下落速度
    		mTmpBirdDis = 0;                                                                                                                                          mTmpMoveDistance = 0 ;
    	}
    

    如果STOP,让鸟继续落,到地面了则直接切换为WAITING,同时记得重置一些必要的数据;

    最后就剩下分数的计算了~~~

    偶也~~

    分数的计算:

    还好分数的计算也比较简单~~~

    /**
    	 * 处理一些逻辑上的计算
    	 */
    	private void logic()
    	{
    		switch (mStatus)
    		{
    		case RUNNING:
    
    			mGrade = 0;
    			// 更新我们地板绘制的x坐标,地板移动
    			mFloor.setX(mFloor.getX() - mSpeed);
    
    			logicPipe();
    
    			// 默认下落,点击时瞬间上升
    			mTmpBirdDis += mAutoDownSpeed;
    			mBird.setY(mBird.getY() + mTmpBirdDis);
    
    			// 计算分数
    			mGrade += mRemovedPipe;
    			for (Pipe pipe : mPipes)
    			{
    				if (pipe.getX() + mPipeWidth < mBird.getX())
    				{
    					mGrade++;
    				}
    			}
    
    			checkGameOver();
    
    			break;
    
    		case STOP: // 鸟落下
    			
    		}
    
    	}

    可以看到,增加了几行计算分数的代码~我们的分数,每次都是置0,然后加上已经看不到的管道数量(都看不到了,肯定是通过的),然后再加上屏幕上在鸟左边的管道数量~

    这就是你获得的分数了~~

    所以记得移除管道的时候,通过下mRemovedPipe

    private int mRemovedPipe = 0;
    	private void logicPipe()
    	{
    		// 管道移动
    		for (Pipe pipe : mPipes)
    		{
    			if (pipe.getX() < -mPipeWidth)
    			{
    				mNeedRemovePipe.add(pipe);
    				mRemovedPipe++;
    				continue;
    			}
    			pipe.setX(pipe.getX() - mSpeed);
    		}
    且,在OVER以后,在initPos中将mRemovedPipe 置为 0 ;这样重新开始以后,又从0分开始了~~~

    好了,到此结束~~至于,管道间的距离,管道宽度,鸟下降速度各种常量,大家如果觉得不适,自行修改~~~

    请允许我再贴一次最终效果~:


    看着结果的同时,我们总结下:

    其实游戏总体来说不难,别看写了两篇这么长,你站远点看,其实还是我们最初的SurfaceViewTemplate,无非多了个重写onTouchEvent和logic()方法,onTouchEvent是交互必须要写的,其实没什么代码~logic方法进行一些重绘时需要的计算~~而draw就安心的draw进行了~~也就是说,这个游戏,其实和绘制个小鸟,点击上升,没什么区别~~~

    值得祝贺的是,我们的SurfaceView经过这三篇博客(还有个转盘),基本涵盖的知识点都覆盖了,说不定哪天,大家想出个虐心的简单的游戏就富了呢~~



    源码点击下载



    建了一个QQ群,方便大家交流。群号:423372824

    ----------------------------------------------------------------------------------------------------------

    博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

    视频目录地址:本人录制的视频教程










    版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 相关阅读:
    持续集成(Continuous Integration),
    python的几个有趣点
    C++语言发展历史 & 基础知识
    [C++] Windows下的socket编程(这是一个简单的TCP/IP例子)
    office app 代码简析之 task pane app
    佳言玩具
    数据的图形可视化[R语言结果GML引发出来的调查]
    收藏的一系列教程帖子,很适合有一定基础,想要进阶的同学
    各种排序算法总结
    三层架构+存储过程实现分页
  • 原文地址:https://www.cnblogs.com/dingxiaoyue/p/4924859.html
Copyright © 2020-2023  润新知