Android实例剖析笔记(八)

上一篇文章分析了小游戏Snake的基本框架,本文将分析Android自带的另一个小游戏LunarLander,它与前者的定时器+系统调用onDraw”架构相比,由于采用了多线程+强制自行绘制的架构思路,因而更为实用。

Android实例剖析笔记(八)_第1张图片 

Snake的比较

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

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

 

SurfaceView初探

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

SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surfaceView及其子类(如TextView, Button

要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理viewsurface上的绘图操作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的: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.

 

  
  
  
  
  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. //销毁时激发,一般在这里将画图的线程停止、释放。  

     surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed

 下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程

  
  
  
  
  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的大部分工作都委托给后者去执行。

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

 

工作线程LunarThread

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

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

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

  
  
  
  
  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上进行绘制,具体的绘制就不解释了,主要就是用drawBitmapdrawRectdrawLine。值得注意的一段代码是下面这个:

  
  
  
  
  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就不再更新了,这样做事为了与当前时间同步起来。

  
  
  
  
  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函数里这两句

  
  
  
  
  1. if (mLastTime > now) return;  
  2. ble elapsed = (now - mLastTime) / 1000.0

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

你可能感兴趣的:(java,android,移动开发,职场,休闲)