• Android示例程序剖析之LunarLander游戏


      前面有几篇文章写的是对Android示例程序贪吃蛇Snake程序的剖析,本文继续分析Android自带的另一个小游戏LunarLander的程序。在贪吃蛇Snake程序中采用了“定时器+系统调用onDraw”的架构,而LunarLander程序采用的是“多线程+强制自行绘制”的架构思路,比前者更为实用。

           与贪吃蛇Snake程序的对比

           就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来实现,这里是LunarView。读过贪吃蛇程序剖析文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联产承包制”时代。

           此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不断发生的事件在进行就可以,例如Snake中使用的定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产生这种“差异源”的。

      SurfaceView初探

           如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是在Canvas(画布)中自行绘制出来的(背景,人物等)。

      SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的:The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. 使用的SurfaceView的时候,一般情况下还要对其进行创建、销毁、改变时的情况进行监视,这就要用到SurfaceHolder.Callback。

    Java代码
    1. class LunarView extends SurfaceView implements SurfaceHolder.Callback   
    2. {   
    3.     public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}   
    4. //在surface的大小发生改变时激发   
    5.     public void surfaceCreated(SurfaceHolder holder){}   
    6. //在创建时激发,一般在这里调用画图的线程。   
    7.     public void surfaceDestroyed(SurfaceHolder holder) {}   
    8. //销毁时激发,一般在这里将画图的线程停止、释放。   
    9. }  

           surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程。

    Java代码
    1. private LunarThread thread; // 实际工作线程   
    2. thread = new LunarThread(holder, context, new Handler() {   
    3.      @Override  
    4.      public void handleMessage(Message m)    
    5.      {   
    6.           mStatusText.setVisibility(m.getData().getInt("viz"));   
    7.           mStatusText.setText(m.getData().getString("text"));   
    8.      }   
    9. });   

      这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。

    Java代码
    1. public void surfaceChanged(SurfaceHolder holder, int format, int width,int height){   
    2.      thread.setSurfaceSize(width, height);   
    3. }   
    4. public void surfaceCreated(SurfaceHolder holder)   
    5. {//启动工作线程结束   
    6.       thread.setRunning(true);   
    7.      thread.start();   
    8. }   
    9. public void surfaceDestroyed(SurfaceHolder holder)   
    10. {   
    11.      boolean retry = true;   
    12.      thread.setRunning(false);   
    13.      while (retry)    
    14.      {   
    15.          try  
    16.          {//等待工作线程结束,主线程才结束   
    17.                thread.join();   
    18.              retry = false;   
    19.          }    
    20.          catch (InterruptedException e)    
    21.          {   
    22.          }   
    23.      }   
    24. }  

      工作线程LunarThread

      由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。

           再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾Android示例程序剖析之Snake贪吃蛇(三:界面UI、游戏逻辑和Handler)中SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。

    Java代码
    1. public void setState(int mode, CharSequence message)    
    2. {   
    3.      synchronized (mSurfaceHolder)   
    4.      {   
    5.           mMode = mode;   
    6.           if (mMode == STATE_RUNNING)   
    7.           {//运行中,隐藏界面文字信息   
    8.                Message msg = mHandler.obtainMessage();   
    9.                Bundle b = new Bundle();   
    10.                b.putString("text""");   
    11.                b.putInt("viz", View.INVISIBLE);   
    12.                msg.setData(b);   
    13.                mHandler.sendMessage(msg);   
    14.           }    
    15.           else    
    16.           {//根据当前状态设置文字信息   
    17.                mRotating = 0;   
    18.                mEngineFiring = false;   
    19.                Resources res = mContext.getResources();   
    20.                CharSequence str = "";   
    21.                if (mMode == STATE_READY)   
    22.                   str = res.getText(R.string.mode_ready);   
    23.                else if (mMode == STATE_PAUSE)   
    24.                   str = res.getText(R.string.mode_pause);   
    25.                else if (mMode == STATE_LOSE)   
    26.                   str = res.getText(R.string.mode_lose);   
    27.                else if (mMode == STATE_WIN)   
    28.                   str = res.getString(R.string.mode_win_prefix)   
    29.                                 + mWinsInARow + " "  
    30.                                 + res.getString(R.string.mode_win_suffix);   
    31.                if (message != null) {   
    32.                    str = message + "\n" + str;   
    33.                }   
    34.                if (mMode == STATE_LOSE)    
    35.                     mWinsInARow = 0;   
    36.                Message msg = mHandler.obtainMessage();   
    37.                Bundle b = new Bundle();   
    38.                b.putString("text", str.toString());   
    39.                b.putInt("viz", View.VISIBLE);   
    40.                msg.setData(b);   
    41.                mHandler.sendMessage(msg);   
    42.           }   
    43.      }   
    44. }  

        下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。

    Java代码
    1. public void run()    
    2. {   
    3.       while (mRun)    
    4.       {   
    5.            Canvas c = null;   
    6.            try    
    7.            {   
    8.                //锁定待绘制区域   
    9.                c = mSurfaceHolder.lockCanvas(null);   
    10.                synchronized (mSurfaceHolder)   
    11.                {   
    12.                     if (mMode == STATE_RUNNING)    
    13.                         updatePhysics();//更新底层数据,判断游戏状态   
    14.                         doDraw(c);//强制重绘制   
    15.                   }   
    16.            }    
    17.            finally    
    18.            {   
    19.                 if (c != null) {   
    20.                     mSurfaceHolder.unlockCanvasAndPost(c);   
    21.                 }   
    22.            }   
    23.      }   
    24. }  

           这里要注意的是最后要调用unlockCanvasAndPost来结束锁定画图,并提交改变。

      强制自行绘制

           doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:

    Java代码
    1. canvas.save();   
    2. canvas.rotate((float) mHeading, (float) mX, mCanvasHeight   
    3.                     - (float) mY);   
    4. if (mMode == STATE_LOSE) {   
    5.       mCrashedImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    6.                         + mLanderHeight);   
    7.       mCrashedImage.draw(canvas);   
    8. else if (mEngineFiring) {   
    9.       mFiringImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    10.                         + mLanderHeight);   
    11.       mFiringImage.draw(canvas);   
    12. else {   
    13.       mLanderImage.setBounds(xLeft, yTop, xLeft + mLanderWidth, yTop   
    14.                         + mLanderHeight);   
    15.       mLanderImage.draw(canvas);   
    16. }   
    17. canvas.restore();   

           在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。

      暂停/继续机制

           LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。

    Java代码
    1. public void pause()   
    2. {//暂停   
    3.       synchronized (mSurfaceHolder)    
    4.      {   
    5.           if (mMode == STATE_RUNNING)   
    6.               setState(STATE_PAUSE);   
    7.      }   
    8. }   
    9. public void unpause()   
    10. {// 继续   
    11.       // Move the real time clock up to now   
    12.      synchronized (mSurfaceHolder)   
    13.      {   
    14.            mLastTime = System.currentTimeMillis() + 100;   
    15.      }   
    16.      setState(STATE_RUNNING);   
    17. }  

           这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句:

    Java代码
    1. if (mLastTime > now) return;   
    2. double elapsed = (now - mLastTime) / 1000.0;   

           至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。

    本文转自:http://www.jizhuomi.com/android/example/196.html

  • 相关阅读:
    Java的一些命名规范
    Java利用泛型实现堆栈
    Java 将二进制打印成十六进制
    String对象的一些基本方法
    Java异常使用指南
    JAVAEE期末项目------文章发布系统
    java14周
    java第11周
    java第八周作业
    java第七周----json
  • 原文地址:https://www.cnblogs.com/daichangya/p/12959947.html
Copyright © 2020-2023  润新知