`
dato0123
  • 浏览: 915631 次
文章分类
社区版块
存档分类
最新评论

Android实例剖析笔记(七)

 
阅读更多

上一篇文章分析了Snake的界面Layout实现,本文将关注游戏主界面这个View是如何实现的,并提出了我的一些困惑之处,希望有朋友能帮忙解惑。

Snake这个项目把主界面剖成界面UI和游戏逻辑两层,最基础的界面UI部分用父类TileView来表示,子类SnakeView是在TileViewUI基础上,加入相应的游戏控制逻辑,从而实现了两者的分离,这对于游戏的修改非常有用。

UI实现部分

首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。

TileView的数据成员如下:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->//方格的大小
protectedstaticintmTileSize;
//方格的行数和列数
protectedstaticintmXTileCount;
protectedstaticintmYTileCount;
//xy坐标系的偏移量
privatestaticintmXOffset;
privatestaticintmYOffset;
//存储三种方格的图标文件
privateBitmap[]mTileArray;
//二维方格地图
privateint[][]mTileGrid;

那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->@Override
protectedvoidonSizeChanged(intw,inth,intoldw,intoldh)
{
//计算屏幕中可放置的方格的行数和列数
mXTileCount=(int)Math.floor(w/mTileSize);
mYTileCount
=(int)Math.floor(h/mTileSize);
mXOffset
=((w-(mTileSize*mXTileCount))/2);
mYOffset
=((h-(mTileSize*mYTileCount))/2);
mTileGrid
=newint[mXTileCount][mYTileCount];
clearTiles();
}

注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。

第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->publicvoidonDraw(Canvascanvas)
{
super.onDraw(canvas);
for(intx=0;x<mXTileCount;x+=1)
{
for(inty=0;y<mYTileCount;y+=1)
{
if(mTileGrid[x][y]>0)
{
canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset
+x*mTileSize,
mYOffset
+y*mTileSize,
mPaint);
}
}
}
}

onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。

游戏逻辑部分

再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->privateArrayList<Coordinate>mSnakeTrail=newArrayList<Coordinate>();//组成贪食蛇的方格列表
privateArrayList<Coordinate>mAppleList=newArrayList<Coordinate>();//苹果方格列表

由于SnakeViewTileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->privatevoidinitNewGame()
{
//清空蛇和苹果占据的方格
mSnakeTrail.clear();
mAppleList.clear();
//目前组成蛇的方格式固定的,而且方向也固定朝北
mSnakeTrail.add(newCoordinate(7,7));
mSnakeTrail.add(
newCoordinate(6,7));
mSnakeTrail.add(
newCoordinate(5,7));
mSnakeTrail.add(
newCoordinate(4,7));
mSnakeTrail.add(
newCoordinate(3,7));
mSnakeTrail.add(
newCoordinate(2,7));
mNextDirection
=NORTH;

//随即加入苹果
for(inti=0;i<nApples;++i)
{
addRandomApple();
}
//初始化运动速率和玩家成绩
mMoveDelay=600;
mScore
=0;
}

想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。
这里要着重说明下private long mMoveDelay = 600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?

可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的拍照的效果。

Handler的使用

写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用TimerSnake中使用了前者

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->classRefreshHandlerextendsHandler
{
@Override
publicvoidhandleMessage(Messagemsg)
{
//“苏醒”后的处理
SnakeView.this.update();
SnakeView.
this.invalidate();
}
publicvoidsleep(longdelayMillis)
{
//休眠delayMillis毫秒
this.removeMessages(0);
sendMessageDelayed(obtainMessage(
0),delayMillis);
}
};

而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做“引擎“不为过。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->publicvoidupdate()
{
if(mMode==RUNNING)
{
longnow=System.currentTimeMillis();
if(now-mLastMove>mMoveDelay)
{
clearTiles();
updateWalls();
updateSnake();
updateApples();
mLastMove
=now;
}
mRedrawHandler.sleep(mMoveDelay);
}
}

既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为“运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。

一点困惑
可是个人认为Snake下面这段代码读起来有点怪,有点像一个先有鸡,还是先有蛋?的问题,导致我的思维逻辑上出现一个怪圈

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->publicvoidhandleMessage(Messagemsg)
{
SnakeView.
this.update();
SnakeView.
this.invalidate();
}

按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。

哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去呢),更何况在update中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢?也就是说invalidate根本没有执行???

按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的,不过关于这一个迷惑我的地方,希望有朋友能指点我一下!

记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?

此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。

作者:phinecos(洞庭散人)
出处:http://phinecos.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接

分享到:
评论

相关推荐

    Android实例剖析笔记

    Android实例剖析笔记 Android实例剖析笔记

    android开发资料大全

    Android实例教程 会员贡献索引贴 实用Android开发工具和资源精选 APK权限大全 - Android必懂知识 最无私的Android资料(书籍+代码)分享[总结] Android中文帮助教程(非常合适新手入门) android程序编写及调试...

    Android学习笔记之应用单元测试实例分析

    主要介绍了Android学习笔记之应用单元测试,结合实例形式较为详细的分析了Android单元测试的实现原理与具体步骤,具有一定参考借鉴价值,需要的朋友可以参考下

    android学习笔记

    1. 入门实例剖析1 2 2.在测试时,如何实现一个提示 8 3.可以使用AlertDialog.Builder 才产生一个提示框. 9 4. menu 的用法. 10 1. 简单的代码 10 2. menu实现的两种方法 10 5.Activity 的切换(含Bundle传值) 14 1. ...

    Android学习笔记之ActionBar Item用法分析

    主要介绍了Android学习笔记之ActionBar Item用法,结合实例形式分析了ActionBar Item的具体功能与相关使用技巧,需要的朋友可以参考下

    Android开发之电话拨号器实例详解

    主要介绍了Android开发之电话拨号器,结合实例形式详细分析了Android电话拨号器的实现步骤与具体代码,并附带了一个Android开放电话拨号器的学习笔记,需要的朋友可以参考下

    Android编程中全局变量问题分析

    本文实例讲述了Android编程中全局变量。分享给大家供大家参考,具体如下: 现在每天都在忙,而且一忙起来,就把写笔记的事情放在了后面,最近在写程序的时候,突然要使用全局变量,就按照以前的方式,写了一个类,...

    百度地图开发java源码-blog-backup:学习文章,也是我博客的备份

    中的实例,来分析 ViewGroup 的事件分发机制。 本章介绍 View(视图) 动画相关概念以及应用。 本篇介绍 Handler 和 Message 以及 Looper 的基本用法和工作原理。 本篇介绍 AsyncTask 的使用方法和工作原理 本篇介绍 ...

Global site tag (gtag.js) - Google Analytics