作者 吴亚峰

2.5 2D动画的开发
Android 3D游戏开发技术宝典——OpenGL ES 2.0
虽然本书是着重介绍3D的开发技术,但在大部分的3D应用中也需要有不少的2D界面,如菜单、帮助等。本节将介绍一般用于开发游戏中2D界面的SurfaceView类的使用。其继承自View类,但与View的不同之处在于,View更新画面必须是在UI线程中(也可以理解为主线程中),而SurfaceView更新画面可以在自定义线程中进行,大大方便了开发。

提示 关于Android下的多线程问题,读者可以参考笔者在人民邮电出版社出版的《Android应用开发完全自学手册——核心技术、传感器、2D/3D、多媒体与典型案例》一书中第8章的相关内容,那里有比较详细的介绍。

2.5.1 SurfaceView用法简介
实际开发中,一般采用继承SurfaceView进行自定义的方法来开发2D动画效果。开发时不但要继承SurfaceView类,一般还要实现SurfaceHolder.Callback接口。SurfaceView类中绘制界面的方法为onDraw,其具体签名为“protected void onDraw(Canvas canvas)”,每调用一次该方法,就会重新绘制一帧画面。

SurfaceHolder.Callback接口中主要包含了2D界面的3个生命周期相关的回调方法,其方法签名和说明如表2-3所列。




android 3d 游戏设计 pdf android 3d游戏开发技术宝典源码_ui



以上3个生命周期回调方法都有其各自的用途,具体情况如下所列。

每次创建界面时需要初始化图片、线程等资源,这些代码一般写在surfaceCreated方法中。
当SurfaceView变化时,如果需要改变一些值,这些代码应该放在surfaceChanged方法中。
SurfaceView被销毁时,有些与界面相关的资源应该被释放掉,这些代码应写在surfaceDestroyed方法中。
2.5.2 使用SurfaceView实现2D动画
上一小节介绍了SurfaceView的基本用法,本小节将通过一个2D动画的简单案例来具体说明SurfaceView在开发中的应用。本案例中的动画实现了这样的场景:一枚炮弹从屏幕的左下角发射,以抛物线的轨迹划过天空,并在一定的位置爆炸,其效果分别如图3-24、图3-25和图3-26所示。


android 3d 游戏设计 pdf android 3d游戏开发技术宝典源码_初始化_02




android 3d 游戏设计 pdf android 3d游戏开发技术宝典源码_移动开发_03



说明 图3-24、图3-25和图3-26从左到右分别为炮弹刚刚发射、炮弹将近飞到最高点和炮弹爆炸时的效果图。
介绍完本案例的运行效果之后,下面将详细讲解案例中各个类代码的具体实现,具体步骤如下。

(1)首先将开发本案例的主控制类——Sample2_8_Activity,该类在程序开始时执行。其主要功能为设置应用程序为全屏及横屏模式,并跳转到动画呈现对应的SurfaceView,具体代码如下。

1   package com/bn/pp8;          //声明包
2   import android.app.Activity;        //引入相关类
3   ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4   import android.view.WindowManager;      //引入相关类
5   public class Sample2_8_Activity extends Activity {
6  MySurfaceView gameView;        //游戏界面
7     @Override
8     public void onCreate(Bundle savedInstanceState) {   //重写onCreate方法
9         super.onCreate(savedInstanceState);
10   requestWindowFeature(Window.FEATURE_NO_TITLE);  //设置为全屏模式
11   getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN ,  
12                 WindowManager.LayoutParams.FLAG_FULLSCREEN);
13              //设置为横屏模式
14         this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
15         gameView = new MySurfaceView(this);    //创建2D动画界面对象
16         this.setContentView(gameView);     //跳转到2D动画界面
17 }}

第10-14行将Activity设置为全屏,并设置为横屏模式。
第15-16行创建了用于实现2D动画绘制呈现的MySurfaceView类的对象,并跳转到2D动画界面。这样,程序一启动成功用户看到的将是2D动画界面了。
(2)开发完本案例的主控制类后,接下来将对本案例中用于实现2D动画绘制呈现的MySurfaceView类进行开发。该类继承自SurfaceView,同时实现了SurfaceHolder.Callback接口,具体代码如下。

1 package com/bn/pp8;          //声明包
2 import android.graphics.Bitmap;       //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.view.SurfaceView;       //引入相关类
5 public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
6  Sample2_8_Activity activity;       //activity的引用
7  Paint paint;           //画笔引用
8  DrawThread drawThread;        //绘制线程引用
9  Bitmap bgBmp;           //背景图片
10  Bitmap bulletBmp;         //炮弹位图
11  Bitmap[] explodeBmps;        //爆炸位图数组
12  Bullet bullet;         //炮弹对象引用
13  public MySurfaceView(Sample2_8_Activity activity) { //构造器
14   super(activity);
15   this.activity = activity;
16   this.requestFocus();       //获取焦点
17   this.setFocusableInTouchMode(true);   //设置为可触控
18   getHolder().addCallback(this);     //注册回调接口
19  }
20  @Override
21  protected void onDraw(Canvas canvas) {    //绘制界面的方法
22   super.onDraw(canvas);
23   canvas.drawBitmap(bgBmp, 0, 0, paint);   //绘制背景
24   bullet.drawSelf(canvas, paint);    //绘制炮弹
25  }
26  @Override
27  public void surfaceChanged(SurfaceHolder holder,int format,int width,int  
   height){ }
28  @Override
29  public void surfaceCreated(SurfaceHolder holder){
30   paint = new Paint();       // 创建画笔
31   paint.setAntiAlias(true);      // 打开抗锯齿
32              //加载图片资源
33   bulletBmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.  
    bullet);
34   bgBmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.bg);
35   explodeBmps=new Bitmap[]{
36     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode0),
37     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode1),
38     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode2),
39     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode3),
40     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode4),
41     BitmapFactory.decodeResource(this.getResources(), R.drawable.  
      explode5),
42   };
43   bullet = new Bullet(this, bulletBmp,explodeBmps,0,290,1.3f,-5.9f);
               //创建炮弹对象
44   drawThread = new DrawThread(this);    //创建绘制线程
45   drawThread.start();       //启动绘制线程
46  }
47  @Override
48  public void surfaceDestroyed(SurfaceHolder holder) { //界面销毁时调用的方法
49   drawThread.setFlag(false);      //停止绘制线程
50 }}

第6-12行声明了画笔、绘制线程、位图资源、炮弹等对象的引用。
第13-19行为MySurfaceView类的构造器,其主要功能为获得焦点、设置为可触控,并注册了生命周期回调接口。
第21-25行为重写的onDraw方法,该方法主要功能为呈现2D动画的每一帧。
第27行实现了surfaceChanged方法。由于本案例中当SurfaceView变化时,没有需要做的工作,因此该方法为空实现。
第29-46行实现了surfaceCreated方法。在该方法中创建或加载了必要的资源,如创建画笔、加载图片、创建炮弹类、创建并绘制启动线程等。
第48-50行为surfaceDestroyed方法的实现。由于SurfaceView被销毁时,绘制线程还未关闭,因此应在该方法中停止绘制线程。
(3)开发完本案例的主控制类以及2D动画呈现类之后,接下来将开发绘制线程类——DrawThread。该类主要功能为每隔一定的时间调用onDraw方法,绘制动画中的每一帧,具体代码如下。

1 package com/bn/pp8;          //声明包
2 import android.graphics.Canvas;       //引入相关类
3 import android.view.SurfaceHolder;       //引入相关类
4 public class DrawThread extends Thread {
5  private boolean flag = true;       //线程工作标志位
6  private int sleepSpan = 100;       //线程休眠时间
7  MySurfaceView gameView;        //父界面引用
8  SurfaceHolder surfaceHolder;       //surfaceHolder引用
9  public DrawThread(MySurfaceView gameView) {    //构造器
10   this.gameView = gameView;
11   this.surfaceHolder = gameView.getHolder();  //创建SurfaceHolder对象
12  }
13  public void run(){
14   Canvas c;         //声明画布
15   while (this.flag){       //循环执行刷帧任务
16    c = null;
17    try {
18     c = this.surfaceHolder.lockCanvas(null);//锁定画布
19     synchronized (this.surfaceHolder) { //锁定surfaceHolder
20      gameView.onDraw(c);    //绘制一帧画面
21    } }finally{
22     if (c != null){      //释放锁
23      this.surfaceHolder.unlockCanvasAndPost(c);
24    } }
25    try{
26     Thread.sleep(sleepSpan);    //线程睡眠指定毫秒数
27    }catch (Exception e){
28     e.printStackTrace();     //打印错误堆栈信息
29  }}}
30  public void setFlag(boolean flag) {    //设置工作标志位的方法
31   this.flag = flag;
32 }}

第5-8行为该线程类的成员变量,其中flag为线程是否继续工作的标志,sleepSpan为每两次绘制的时间间隔。
第9-12行为该类的构造器,在构造器中获得了gameView及其surfaceHolder对象。
第13-29行为实现该线程任务的run方法,其中用一个while循环不断调用gameView的onDraw方法进行刷帧。
第30-32行为设置线程工作标志位的setFlag方法。调用该方法并传递参数false,可以停止该线程的绘制工作。
(4)接下来将要开发的是炮弹类——Bullet,此类每个对象表示一枚炮弹,具体代码如下。

1 package com/bn/pp8;          //声明包
2 import android.graphics.Bitmap;       //引入相关类
3 import android.graphics.Canvas;       //引入相关类
4 import android.graphics.Paint;        //引入相关类
5 public class Bullet {
6  MySurfaceView gameView;
7  private Bitmap bitmap;        // 位图
8  private Bitmap[] bitmaps;        // 爆炸动画图组
9  float x;            // _x_轴位置
10  float y;           // _y_轴位置
11  float vx;          // _x_轴速度
12  float vy;          // _y_轴速度
13  private float t = 0;        // 生存时间
14  private float timeSpan = 0.5f;      // 时间间隔
15  int size;          // 炮弹尺寸
16  boolean explodeFlag = false;      // 是否绘制炮弹的标记
17  Explosion mExplosion;        // 爆炸对象引用
18  public Bullet(MySurfaceView gameView, Bitmap bitmap, Bitmap[] bitmaps,
19    float x, float y, float vx, float vy) {
20   this.gameView = gameView;      
21   this.bitmap = bitmap;       // 初始化炮弹的图片
22   this.bitmaps = bitmaps;      // 初始化爆炸动画图片数组
23   this.x = x;         // 初始化炮弹的位置
24   this.y = y;
25   this.vx = vx;         // 初始化炮弹的速度
26   this.vy = vy;
27   size = bitmap.getHeight();      // 获得图片的高度
28  }
29  public void drawSelf(Canvas canvas, Paint paint) { // 绘制炮弹的方法
30   if (explodeFlag && mExplosion != null) { // 如果已经爆炸,绘制爆炸动画
31    mExplosion.drawSelf(canvas, paint);
32   } else {
33    go();         // 炮弹前进
34    canvas.drawBitmap(bitmap, x, y, paint);  // 绘制炮弹
35  } }
36  public void go() {        // 炮弹前进的方法
37   x += vx * t;         // 水平方向匀速直线运动
38   y += vy * t + 0.5f * Constant.G * t * t;  // 竖直方向上抛运动
39   if (x >= Constant.EXPLOSION_X || y >= Constant.SCREEN_HEIGHT) {//特定位置爆炸
40    mExplosion = new Explosion(gameView, bitmaps, x, y);// 创建爆炸对象
41    explodeFlag = true;      // 不再绘制炮弹
42    return;
43   }
44   t += timeSpan;        // 更新生存时间
45 }}

第18-28行为该类的构造器,在构造器中进行成员变量的初始化工作。
第29-35行为绘制炮弹的方法。如果炮弹已经爆炸,则绘制爆炸动画;否则先调用go方法改变炮弹的位置,再根据位置参数的值绘制炮弹。
第36-45行定义了进行炮弹运动计算的go方法。每调用一次该方法,就根据物理公式更新一次炮弹的位置。炮弹位置的计算分为水平(x轴)和垂直(y轴)两个方向进行,水平方向为匀速直线运动,垂直方向为上抛运动。当炮弹运动到特定位置时,创建爆炸对象,并标记不再绘制炮弹。
(5)接着对本案例中的Explosion类进行开发,该类主要在炮弹爆炸时使用,实现炮弹爆炸动画中每一帧图片的切换,具体代码如下。

1 package com/bn/pp8;         //声明包
2 import android.graphics.Bitmap;      //引入相关类
3 import android.graphics.Canvas;      //引入相关类
4 import android.graphics.Paint;       //引入相关类
5 public class Explosion {
6  MySurfaceView gameView;
7  private Bitmap[] bitmaps;       // 位图
8  float x;           // _x_轴位置
9  float y;           // _y_轴位置
10  private int anmiIndex = 0;      // 爆炸动画帧索引 
11  public Explosion(MySurfaceView gameView, Bitmap[] bitmaps, float x, float y) {
12   this.gameView = gameView;     //初始化MySurfaceView对象
13   this.bitmaps = bitmaps;
14   this.x = x;        //初始化_x_位置
15   this.y = y;        //初始化_y_位置
16  }      
17  public void drawSelf(Canvas canvas, Paint paint) {  // 绘制背景的方法
18   if (anmiIndex >= bitmaps.length - 1) { // 如果动画播放完毕,不再绘制爆炸效果
19    return;
20   }
21   canvas.drawBitmap(bitmaps[anmiIndex], x, y, paint); // 绘制数组中某一幅图
22   anmiIndex++;       // 当前下标加1
23 }}

第11-16行为该类的构造器,在构造器中进行了各成员变量的初始化工作。
第17-23行为绘制爆炸动画的方法。根据anmiIndex的值,选择图片数组中的某一幅图片进行绘制,然后anmiIndex自加1。

提示 从上面的代码中读者可以看出,其实爆炸动画是由一帧一帧单独的画面组成的。绘制线程定时调用绘制方法绘制动画中的每一帧,动画就呈现在用户眼前。
(6)最后需要开发的是本案例中存放一些常量的类——Constant,其代码如下。

1 package com.bn.pp8;
2 public class Constant {      //用于统一管理常量的类
3  public static final int SCREEN_WIDTH=480; //屏幕宽度
4  public static final int SCREEN_HEIGHT=320; //屏幕高度
5  public static final int EXPLOSION_X=270; //爆炸X位置
6  public static final float G = 1.0f;  //重力加速度的值
7 }

提示 从上面的代码中可以看出,Constant中包含了一些程序运行过程中需要的常量。在实际项目的开发中,将常量独立到一个类中进行管理是非常好的习惯,这很有利于提高代码的可维护性,降低维护的成本。