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

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.

class LunarView extends SurfaceView implements SurfaceHolder.Callback
{
public void surfaceChanged(SurfaceHolderholder, int format, int width, int height){}
// 在surface的大小发生改变时激发
public void surfaceCreated(SurfaceHolderholder){}
// 在创建时激发,一般在这里调用画图的线程。
public void surfaceDestroyed(SurfaceHolderholder){}
// 销毁时激发,一般在这里将画图的线程停止、释放。
}

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

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

private LunarThreadthread; // 实际工作线程
thread = new LunarThread(holder,context, new Handler(){
@Override
public void handleMessage(Messagem)
{
mStatusText.setVisibility(m.getData().getInt(
" viz " ));
mStatusText.setText(m.getData().getString(
" text " ));
}
});

这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。

由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。

public void surfaceChanged(SurfaceHolderholder, int format, int width, int height)
{
thread.setSurfaceSize(width,height);
}
public void surfaceCreated(SurfaceHolderholder)
{
// 启动工作线程结束
thread.setRunning( true );
thread.start();
}
public void surfaceDestroyed(SurfaceHolderholder)
{
boolean retry = true ;
thread.setRunning(
false );
while (retry)
{
try
{
// 等待工作线程结束,主线程才结束
thread.join();
retry
= false ;
}
catch (InterruptedExceptione)
{
}
}
}

工作线程LunarThread

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

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

public void setState( int mode,CharSequencemessage)
{
synchronized (mSurfaceHolder)
{
mMode
= mode;
if (mMode == STATE_RUNNING)
{
// 运行中,隐藏界面文字信息
Messagemsg = mHandler.obtainMessage();
Bundleb
= new Bundle();
b.putString(
" text " , "" );
b.putInt(
" viz " ,View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else
{
// 根据当前状态设置文字信息
mRotating = 0 ;
mEngineFiring
= false ;
Resourcesres
= mContext.getResources();
CharSequencestr
= "" ;
if (mMode == STATE_READY)
str
= res.getText(R.string.mode_ready);
else if (mMode == STATE_PAUSE)
str
= res.getText(R.string.mode_pause);
else if (mMode == STATE_LOSE)
str
= res.getText(R.string.mode_lose);
else if (mMode == STATE_WIN)
str
= res.getString(R.string.mode_win_prefix)
+ mWinsInARow + " "
+ res.getString(R.string.mode_win_suffix);
if (message != null ){
str
= message + " \n " + str;
}
if (mMode == STATE_LOSE)
mWinsInARow
= 0 ;
Messagemsg
= mHandler.obtainMessage();
Bundleb
= new Bundle();
b.putString(
" text " ,str.toString());
b.putInt(
" viz " ,View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}

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

public void run()
{
while (mRun)
{
Canvasc
= null ;
try
{
// 锁定待绘制区域
c = mSurfaceHolder.lockCanvas( null );
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
updatePhysics();
// 更新底层数据,判断游戏状态
doDraw(c); // 强制重绘制
}
}
finally
{
if (c != null ){
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}

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

强行自绘制

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

canvas.save();
canvas.rotate((
float )mHeading,( float )mX,mCanvasHeight
- ( float )mY);
if (mMode == STATE_LOSE){
mCrashedImage.setBounds(xLeft,yTop,xLeft
+ mLanderWidth,yTop
+ mLanderHeight);
mCrashedImage.draw(canvas);
}
else if (mEngineFiring){
mFiringImage.setBounds(xLeft,yTop,xLeft
+ mLanderWidth,yTop
+ mLanderHeight);
mFiringImage.draw(canvas);
}
else {
mLanderImage.setBounds(xLeft,yTop,xLeft
+ mLanderWidth,yTop
+ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();

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

暂停/继续机制

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

public void pause()
{
// 暂停
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
setState(STATE_PAUSE);
}
}
public void unpause()
{
// 继续
// Movetherealtimeclockuptonow
synchronized (mSurfaceHolder)
{
mLastTime
= System.currentTimeMillis() + 100 ;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造 延迟 的效果,都是因为 updatePhysics 函数里这两句 if (mLastTime > now) return ;
double elapsed = (now - mLastTime) / 1000.0 ;

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

更多相关文章

  1. 由浅入深研究Android(3)--浅谈线程的一些东西
  2. 第一行代码读书笔记 Kotlin Android
  3. 阅读《Android(安卓)从入门到精通》(3)——Activity 与界面
  4. Android中的AsyncTask
  5. Android的视频播放之VideoView与SurfaceView
  6. Android(安卓)最火快速开发框架androidannotation简介
  7. Android(安卓)按键消息path
  8. 强制保持Android(安卓)Activity状态
  9. Android线程管理之ThreadLocal理解及应用场景

随机推荐

  1. 【Android】win10操作系统下Android开发
  2. Android(安卓)Studio开发常见问题及解决
  3. Android App 结束运行后重启
  4. Androidx和Android(安卓)support库的冲突
  5. Android(安卓)可任意拖动的悬浮窗(类似悬
  6. 【Java】java和android网络编程 - 对byte
  7. Android进度条样式
  8. Android 之retrofit2 之 @body上传服务器
  9. 16个最热门的 Android Apps 推荐下载
  10. Android 之listview \gridview 属性设置