和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.
class
LunarView
extends
SurfaceView
implements
SurfaceHolder.Callback
{
public
void
surfaceChanged(SurfaceHolder holder,
int
format,
int
width,
int
height){}
//
在surface的大小发生改变时激发
public
void
surfaceCreated(SurfaceHolder holder){}
//
在创建时激发,一般在这里调用画图的线程。
public
void
surfaceDestroyed(SurfaceHolder holder) {}
//
销毁时激发,一般在这里将画图的线程停止、释放。
}
surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程
private
LunarThread thread;
//
实际工作线程
thread
=
new
LunarThread(holder, context,
new
Handler() {
@Override
public
void
handleMessage(Message m)
{
mStatusText.setVisibility(m.getData().getInt(
"
viz
"
));
mStatusText.setText(m.getData().getString(
"
text
"
));
}
});
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
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做控制,当然没有这样的问题了。
public
void
setState(
int
mode, CharSequence message)
{
synchronized
(mSurfaceHolder)
{
mMode
=
mode;
if
(mMode
==
STATE_RUNNING)
{
//
运行中,隐藏界面文字信息
Message msg
=
mHandler.obtainMessage();
Bundle b
=
new
Bundle();
b.putString(
"
text
"
,
""
);
b.putInt(
"
viz
"
, View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else
{
//
根据当前状态设置文字信息
mRotating
=
0
;
mEngineFiring
=
false
;
Resources res
=
mContext.getResources();
CharSequence str
=
""
;
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
;
Message msg
=
mHandler.obtainMessage();
Bundle b
=
new
Bundle();
b.putString(
"
text
"
, str.toString());
b.putInt(
"
viz
"
, View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
public
void
run()
{
while
(mRun)
{
Canvas c
=
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上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
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()
{
//
继续
//
Move the real time clock up to now
synchronized
(mSurfaceHolder)
{
mLastTime
=
System.currentTimeMillis()
+
100
;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句
if
(mLastTime
>
now)
return
;
double
elapsed
=
(now
-
mLastTime)
/
1000.0
;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。