从零开始Android游戏编程(第二版) 第九章 游戏程序的生命周期

第九章 游戏程序的生命周期

在讲解游戏程序的生命周期之前,让我们先看看普通Android应用的生命周期。关于生命周期,SDK附带的文档上有详细的解释,让我们打开文档,找到andorid.app->Activity,我们会看到这样一张图片

clip_image002

图片将整个程序的生命周期描述的非常清楚,为了加深理解,我们创建一个程序实际看一下这个过程。

创建项目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信息:

clip_image004

让我们按下返回键结束程序,

clip_image006

这就是一个程序从创建到销毁的标准流程。但是作为手机应用,我们前面提到的被抢占屏幕的情况就必须要被考虑。让我们来测试一下:

重新运行LifeCycle,在DDMS中模拟一个电话呼入

clip_image008

日志中出现了

clip_image010

这次没有调用onDestory。

然后让我们把电话挂掉

clip_image012

同样,没有调用onCreate而调用了onRestart。

另外还有一种情况,就是当程序被放置到后台过久,系统在一定条件下会自动将程序销毁,让我们看一下这种情况下程序的生命周期会有什么变化。

运行LifeCycle,转到DDMS,模拟一个来电,然后在Devices找到LifeCycle并强行停止他

clip_image014

我们会发现日志中并没有任何输出。

这时,让我们挂掉电话,日志中出现了如下三行

clip_image016

可以看到,程序被重新创建了,调用了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());

}

重新拨入电话,日志中出现了如下内容

clip_image018

可以看到,在onPause之前执行了onSaveInstanceState。那么执行了它有什么作用呢?是不是就已经把程序状态保存了呢?还没有,保存的过程需要我们自己来编码。我们拿这个函数与其他的onXXX对比会发现,它与onCreate一样,都有一个Bundle类型的参数,而缺省的名字似乎已经透露了玄机,onCreate的参数名叫savedInstanceState,意为被保存的状态,正与onSavedInstanceState对应,那么名为outState意为输出状态的参数,功能就不言自明了。把需要保存的值放到outState中,在onCreate中检查savedInstanceState是否为null,如果有值就取出来恢复现场。具体的用法,学习了游戏程序的生命周期之后会有实例讲解。

前面讲的是一个普通应用程序的生命周期,下面让我们进一步了解一个游戏程序的生命周期。我们的游戏同样基于SurfaceView。根据前面讲过的内容,我们知道,现在程序中增加了游戏循环,它是一个单独的线程,因此在程序的生命周期中就增加了对游戏线程的操作。

在LifeCycle中增加GameView,继承自SurfaceView,实现SurfaceHonder.Callback和Runnable接口(前面已经讲过哦)。重载函数,并在函数中添加Log。修改onCreate使其显示GameView。

让我们运行程序看看Log的输出:

启动程序

clip_image020

来电呼入

clip_image022

通话结束

clip_image024

应用结束

clip_image026

再追加两种前面没有讲到的情况,一是屏幕翻转(在模拟器中的快捷键是Ctrl+F11)

clip_image028

可以看到,如果程序没有设置固定的横屏或竖屏状态,每次翻转屏幕,就会将程序关闭并重新启动。

另一种情况是休眠。当我们没有强制应用不休眠时,或短暂按下电源键时,应用会进入暂停状态,屏幕上显示锁屏画面。让我们解锁屏幕,应用被唤醒,继续执行,让我们看一下此时的Log:

clip_image030

前面我们也讲过,我们在一个独立的线程中进行游戏循环,依照一般应用的生命周期,在适当的时候开始和结束游戏循环,保存游戏状态,就能够完美的控制游戏程序的生命周期了。

首先让我们用文字总结一下这个过程:

在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会很有用)。

本章示例程序http://u.115.com/file/f1ca1dfa00

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值