第九章 游戏程序的生命周期
在讲解游戏程序的生命周期之前,让我们先看看普通Android应用的生命周期。关于生命周期,SDK附带的文档上有详细的解释,让我们打开文档,找到andorid.app->Activity,我们会看到这样一张图片
图片将整个程序的生命周期描述的非常清楚,为了加深理解,我们创建一个程序实际看一下这个过程。
创建项目LifeCycle,sdk就选择1.6吧。在Activity中重载如下几个函数,并增加Log语句:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LogF();
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
LogF();
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
LogF();
}
@Override
protected void onRestart() {
// TODO Auto-generated method stub
super.onRestart();
LogF();
}
@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
LogF();
}
@Override
protected void onStart() {
// TODO Auto-generated method stub
super.onStart();
LogF();
}
@Override
protected void onStop() {
// TODO Auto-generated method stub
super.onStop();
LogF();
}
LogF()定义如下:
public static void LogF() {
Log.v(Thread.currentThread().getStackTrace()[3].getClassName(), Thread.currentThread().getStackTrace()[3].getMethodName());
}
除了onCreate之外,都需要手工添加,重载函数的方法前面有哦,一年过去了,大家没忘吧:)
让我们在模拟器中运行这个程序。同时在LogCat中查看输出。前面好像没有讲到LogCat,但是很多代码用到了Log,大家都已经找到了吧。
程序启动后,我们看到了3条自定义的Log信息:
让我们按下返回键结束程序,
这就是一个程序从创建到销毁的标准流程。但是作为手机应用,我们前面提到的被抢占屏幕的情况就必须要被考虑。让我们来测试一下:
重新运行LifeCycle,在DDMS中模拟一个电话呼入
日志中出现了
这次没有调用onDestory。
然后让我们把电话挂掉
同样,没有调用onCreate而调用了onRestart。
另外还有一种情况,就是当程序被放置到后台过久,系统在一定条件下会自动将程序销毁,让我们看一下这种情况下程序的生命周期会有什么变化。
运行LifeCycle,转到DDMS,模拟一个来电,然后在Devices找到LifeCycle并强行停止他
我们会发现日志中并没有任何输出。
这时,让我们挂掉电话,日志中出现了如下三行
可以看到,程序被重新创建了,调用了onCreate而不是onRestart,这与我们前面说的流程相悖,因为在这种情况下,我们应该继续程序的执行而不是重新初始化。那么如何解决这个问题呢?
方法如下,让我们重载下面这个函数:
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
Log.v(this.toString(), Thread.currentThread().getStackTrace()[2].getMethodName());
}
重新拨入电话,日志中出现了如下内容
可以看到,在onPause之前执行了onSaveInstanceState。那么执行了它有什么作用呢?是不是就已经把程序状态保存了呢?还没有,保存的过程需要我们自己来编码。我们拿这个函数与其他的onXXX对比会发现,它与onCreate一样,都有一个Bundle类型的参数,而缺省的名字似乎已经透露了玄机,onCreate的参数名叫savedInstanceState,意为被保存的状态,正与onSavedInstanceState对应,那么名为outState意为输出状态的参数,功能就不言自明了。把需要保存的值放到outState中,在onCreate中检查savedInstanceState是否为null,如果有值就取出来恢复现场。具体的用法,学习了游戏程序的生命周期之后会有实例讲解。
前面讲的是一个普通应用程序的生命周期,下面让我们进一步了解一个游戏程序的生命周期。我们的游戏同样基于SurfaceView。根据前面讲过的内容,我们知道,现在程序中增加了游戏循环,它是一个单独的线程,因此在程序的生命周期中就增加了对游戏线程的操作。
在LifeCycle中增加GameView,继承自SurfaceView,实现SurfaceHonder.Callback和Runnable接口(前面已经讲过哦)。重载函数,并在函数中添加Log。修改onCreate使其显示GameView。
让我们运行程序看看Log的输出:
启动程序
来电呼入
通话结束
应用结束
再追加两种前面没有讲到的情况,一是屏幕翻转(在模拟器中的快捷键是Ctrl+F11)
可以看到,如果程序没有设置固定的横屏或竖屏状态,每次翻转屏幕,就会将程序关闭并重新启动。
另一种情况是休眠。当我们没有强制应用不休眠时,或短暂按下电源键时,应用会进入暂停状态,屏幕上显示锁屏画面。让我们解锁屏幕,应用被唤醒,继续执行,让我们看一下此时的Log:
前面我们也讲过,我们在一个独立的线程中进行游戏循环,依照一般应用的生命周期,在适当的时候开始和结束游戏循环,保存游戏状态,就能够完美的控制游戏程序的生命周期了。
首先让我们用文字总结一下这个过程:
在onCreata中,初始化游戏状态或恢复游戏状态。注意,这里不是初始化图片,声音等数据(Data),只是游戏内状态,比如现在是开始菜单还是在游戏当中,玩家的位置,敌人的位置等等数值(Value);
在onRestart中恢复游戏状态;
在onResume中开始游戏循环(本应在onStart中,但是我们看到,各种情况下,onResume都会在onStart之后调用,所以简单的用onResume代替了onStart);
在onSaveInstanceState中保存游戏状态;
在onPause中结束游戏循环;
在onDestory中销毁游戏数据。
另外补充几句,游戏的保存和读取实际上是不属于应用程序生命周期的,而是一种游戏内操作。但是,你也可以根据程序的生命周期来决定是否应该保存和读取游戏,比如实现自动保存,以防止程序意外终止造成的损失。
最后,就让我们用一个程序来演示本章所讲的内容。我们依然使用计时器程序来演示,因为他很直观。程序的关键点在于当程序被隐藏时,停止计时,被重新显示时继续计时。
首先在游戏进程开始后进行计时,我们首先要取得开始的时间:
public void run() {
start = System.currentTimeMillis();
while(run) {
now = System.currentTimeMillis();
……
那么计时器显示的数值就是当前时间now减去开始时间start,为了便于观察,我们用秒作为单位,就是(now – start)/1000。
在程序被暂停后,我们需要停止游戏循环,可以终止线程也可以停止对数值的计算,本例中我们选择终止线程。我们为GameView增加函数pause
public void pause() {
run = false;
}
如果现在运行程序,我们会发现,程序被暂停再恢复就会重新计时,那么如何在程序恢复后继续计时呢?我们要在暂停时将当前的时间保存起来,恢复后就可以用now-start加上这个时间,就实现连续计时了。
我们设定变量last保存上次时间,那么计时器当前的值就是
millis = last + now - start;
而pause函数修改成
public void pause() {
run = false;
last = millis;
}
这样就解决了重新计时的问题。但是别忘了,前面我们还特别提到一种情况,就是程序会被系统销毁,并重新执行,那么这种方案就不适用了,因为程序重新执行时,last会被重新初始化,保存的值也就随之丢失了。当然,聪明的读者肯定记得前面我们说过的onSaveInstanceState,没错,下面就是它发挥作用的时候了。
首先我们在GameView中创建函数save
public void save(Bundle outState) {
outState.putLong("last", millis);
}
前面没有讲解Bundle,下面就让我们看看他的用法。
如果你了解Map,你会发现两者很相似,他们都可以用来存储key-value值对,读取方法也一样,只是Bundle的函数设定了变量类型而已。
有了保存就有读取,让我们增加load函数
public void load(Bundle savedInstanceState) {
if(savedInstanceState != null) {
last = savedInstanceState.getLong("last");
}
}
最后我们再增加一个resume函数,来启动线程,前面说过resume和start起着同样的作用
public void resume() {
run = true;
new Thread(this).start();
}
到此为止,我们已经有了控制游戏进程的所有函数,下一步就是在Activity中相应的回调函数中调用这些函数了,具体怎么调用,我想读者心中已经有数了吧。当然,你可以在本章的例程中找到完整的代码。
这个例子虽然简单,但即使是很复杂的游戏(使用SurfaceView)也可以通过这个方法来控制。但是细心的读者可能会发现,例子程序有一些误差,因为在surface还没有被创建时游戏循环就已经开始了,所以可能会直接看到屏幕显示3、4等,这当然不是致命的问题,因为大多数游戏不是在一开始就计时的。但是有一种情况却会有一些麻烦,就是我们用电源键让屏幕休眠,再开启屏幕,这时候手机应该处于锁屏状态,但onResume已经被调用,就是说,游戏循环已经开始了,而用户却无法看到。为了应对这种情况,我们可以重载onWindowFocusChanged函数,只要程序被其他界面遮挡或遮挡消失,就会调用这个函数。因为这个函数在两种情况都会调用,所以我们必须区分当前程序的状态时被遮挡还是被显示,读者不妨自己动手试试看(提示:函数hasWindowFocus会很有用)。