Android实例剖析笔记(七)

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

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

UI实现部分

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

 

// 方格的大小
protected   static   int  mTileSize;    
// 方格的行数和列数
protected   static   int  mXTileCount;
protected   static   int  mYTileCount;
// xy坐标系的偏移量
private   static   int  mXOffset;
private   static   int  mYOffset;
// 存储三种方格的图标文件
private  Bitmap[] mTileArray; 
// 二维方格地图
private   int [][] mTileGrid; 

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

@Override
protected   void  onSizeChanged( int  w,  int  h,  int  oldw,  int  oldh) 
{
        
// 计算屏幕中可放置的方格的行数和列数
        mXTileCount  =  ( int ) Math.floor(w  /  mTileSize);
        mYTileCount 
=  ( int ) Math.floor(h  /  mTileSize);
        mXOffset 
=  ((w  -  (mTileSize  *  mXTileCount))  /   2 );
        mYOffset 
=  ((h  -  (mTileSize  *  mYTileCount))  /   2 );
        mTileGrid 
=   new   int [mXTileCount][mYTileCount];
        clearTiles();
}

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

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

     public   void  onDraw(Canvas canvas) 
    {
        
super .onDraw(canvas);
        
for  ( int  x  =   0 ; x  <  mXTileCount; x  +=   1 )
        {
            
for  ( int  y  =   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这个程序的。

     private  ArrayList < Coordinate >  mSnakeTrail  =   new  ArrayList < Coordinate > (); // 组成贪食蛇的方格列表
     private  ArrayList < Coordinate >  mAppleList  =   new  ArrayList < Coordinate > (); // 苹果方格列表

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

     private   void  initNewGame()
    {
        
// 清空蛇和苹果占据的方格
        mSnakeTrail.clear();
        mAppleList.clear();
        
// 目前组成蛇的方格式固定的,而且方向也固定朝北
        mSnakeTrail.add( new  Coordinate( 7 7 ));
        mSnakeTrail.add(
new  Coordinate( 6 7 ));
        mSnakeTrail.add(
new  Coordinate( 5 7 ));
        mSnakeTrail.add(
new  Coordinate( 4 7 ));
        mSnakeTrail.add(
new  Coordinate( 3 7 ));
        mSnakeTrail.add(
new  Coordinate( 2 7 ));
        mNextDirection 
=  NORTH;

        
// 随即加入苹果
         for  ( int  i  =   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方法,另外一个是应用Timer。Snake中使用了前者

  class  RefreshHandler  extends  Handler 
    {
        @Override
        
public   void  handleMessage(Message msg) 
        {
// “苏醒”后的处理
           SnakeView. this .update();
           SnakeView.
this .invalidate();
        }
        
public   void  sleep( long  delayMillis) 
        {
// 休眠delayMillis毫秒
             this .removeMessages( 0 );
            sendMessageDelayed(obtainMessage(
0 ), delayMillis);
        }
};

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

    public   void  update()
    {
        
if  (mMode  ==  RUNNING)
        {
            
long  now  =  System.currentTimeMillis();
            
if  (now  -  mLastMove  >  mMoveDelay) 
            {
                clearTiles();
                updateWalls();
                updateSnake();
                updateApples();
                mLastMove 
=  now;
            }
            mRedrawHandler.sleep(mMoveDelay);
        }
    }

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

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

     public   void  handleMessage(Message msg) 
    {
            SnakeView.
this .update();
            SnakeView.
this .invalidate();
     }

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

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

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

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

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

目录
相关文章
|
1月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
60 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
22天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
73 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
6月前
|
Android开发
Android应用实例(一)之---有道辞典VZ.0
Android应用实例(一)之---有道辞典VZ.0
42 2
|
3月前
|
JavaScript 前端开发 Java
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
IT寒冬使APP开发门槛提升,安卓程序员需转型。选项包括:深化Android开发,跟进Google新技术如Kotlin、Jetpack、Flutter及Compose;研究Android底层框架,掌握AOSP;转型Java后端开发,学习Spring Boot等框架;拓展大前端技能,掌握JavaScript、Node.js、Vue.js及特定框架如微信小程序、HarmonyOS;或转向C/C++底层开发,通过音视频项目如FFmpeg积累经验。每条路径都有相应的书籍和技术栈推荐,助你顺利过渡。
59 3
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
|
3月前
|
编解码 安全 Ubuntu
Android Selinux 问题处理笔记
这篇文章是关于处理Android系统中SELinux权限问题的笔记,介绍了如何通过分析SELinux拒绝的日志、修改SELinux策略文件,并重新编译部署来解决权限问题,同时提供了一些SELinux的背景知识和实用工具。
60 0
|
4月前
|
API Android开发
Android 监听Notification 被清除实例代码
Android 监听Notification 被清除实例代码
|
5月前
|
安全 Java Android开发
使用Unidbg进行安卓逆向实例讲解
使用Unidbg进行安卓逆向实例讲解
128 2
|
6月前
|
安全 Linux Android开发
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
该文介绍了如何在Linux服务器上交叉编译Android的FFmpeg库以支持HTTPS视频播放。首先,从GitHub下载openssl源码,解压后通过编译脚本`build_openssl.sh`生成64位静态库。接着,更新环境变量加载openssl,并编辑FFmpeg配置脚本`config_ffmpeg_openssl.sh`启用openssl支持。然后,编译安装FFmpeg。最后,将编译好的库文件导入App工程的相应目录,修改视频链接为HTTPS,App即可播放HTTPS在线视频。
107 3
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
|
5月前
|
Java API Android开发
技术经验分享:Android源码笔记——Camera系统架构
技术经验分享:Android源码笔记——Camera系统架构
54 0
|
6月前
|
Java 测试技术 开发工具
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
下一篇
无影云桌面