前面一讲中,我们介绍了,游戏开发的前期准备与如何创建项目。
Android基于box2d开发弹弓类游戏[一]-------------前期准备&创建项目
在这一讲中,我们介绍如何搭建游戏界面,在游戏界面中加入静态如片,如何移动游戏场景。
呼呼呼!!那么,我们开始吧!
三.创建游戏界面
Android中用于显示游戏界面的视图,常用的有View和SurfaceView。SurfaceView是从View基类中派生出来的显示类SurfaceView和View最本质的区别在于,surfaceView是在一个新起的单独线程中可以重新绘制画面而View必须在UI的主线程中更新画面。
那么在UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。
当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要surfaceView中thread处理,一般就需要有一个event queue的设计来保存touch event,这会稍稍复杂一点,因为涉及到线程同步。所以基于以上,根据游戏特点,一般分成两类。
1 被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。
2 主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。
3.Android中的SurfaceView类就是双缓冲机制。因此,开发游戏时尽量使用SurfaceView而不要使用View,这样的话效率较高,而且SurfaceView的功能也更加完善。
考虑以上几点,所以我们选用SurfaceView 来进行游戏开发。
下面创建 基于SurfaceView的游戏界面类MainView.java:
package com.catapultdemo; import android.content.Context; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class MainView extends SurfaceView implements Callback,Runnable { public MainView(Context context) { super(context); } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder arg0) { // TODO Auto-generated method stub } @Override public void surfaceDestroyed(SurfaceHolder arg0) { // TODO Auto-generated method stub } @Override public void run() { // TODO Auto-generated method stub } }
只要继承SurfaceView类并实现SurfaceHolder.Callback接口和runnable接口就可以实现一个自定义的SurfaceView了。
SurfaceHolder.Callback在底层的Surface状态发生变化的时候通知View。
Runnable用来实现多线程
游戏界面已经搭建完成,下面要做的就是让项目启动之后,显示我们的游戏界面,也就是MainView。
自定义draw方法,用后之后话界面使用。为什么要实现这个方法,会在第四节进行说明。
public void draw() {}
现在可以运行一下项目,看一下运行结果。
可能与想象的有些差别。因为android项目创建完成之后,默认创建一个layout布局文件,用于初始布局。所以我们可以删除这个布局文件。
删除布局文件之后项目主文件MainActivity.java会报错。因为,默认情况下,向界面输出刚刚删除的布局文件,然后布局文件已经被我们删除了。
package com.catapultdemo; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
此时将setContentView(R.layout.activity_main);改成 setContentView(new MainView(this));
此行代码的意思是,让程序运行,向界面输出我们的游戏场景界面。接下来再一次运行程序。
我们删除了布局文件后,显示出了我们的游戏场景,中间的“hello world”也已经消失的,但是与我们的想象的结果还是有些差距。我们接下来需要去除头部的状态栏和应用程序的名称栏。还要试游戏屏幕变成横屏。
// 隐去电池等图标和一切修饰部分(状态栏部分) this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN); // 隐去标题栏(程序的名字) this.requestWindowFeature(Window.FEATURE_NO_TITLE); // 游戏界面横屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
此时屏幕显示界面已经全部变黑了。此时我们的游戏界面搭建完成了。
四.在游戏场景中加入静态图片
现在游戏界面还没有任何的东西。接下来,我们在游戏场景中加入背景图片,和一些静态的物体。由于这些背景和静态的物体不需要模拟物理场景,所以,之需要在游戏场景中画出图片即可。
此前已经创建了继承自SurfaceView的MainView.java游戏界面类。接下来对方法进行完善和介绍。
实现了CallBack接口,重写了一下方法。
public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//看其名知其义,在surface的大小发生改变时激发
public void surfaceCreated(SurfaceHolder holder){}
//同上,在创建时激发,一般在这里调用画图的线程。
public void surfaceDestroyed(SurfaceHolder holder) {}
//同上,销毁时激发,一般在这里将画图的线程停止、释放。
实现了Runnable接口,重写了run方法。下面介绍一下为什么要实现Runnable接口并且要重写run方法:surfaceView有onDraw方法,但是surfaceView不会自己去调用这个方法,所以我们要自己实现 draw方法,并放在run方法内。Runnable实现线程,run方法就是在开辟的线程中无限的去执行。所以我们自己完成的draw方法也可以不断的执行。这个就是刷屏。
在类中定义一些必要的变量。
private Resources res; private SurfaceHolder sfh; private Thread th; private Canvas canvas; private Paint paint; public MainView(Context context) { super(context); res = this.getResources(); sfh = this.getHolder(); sfh.addCallback(this); paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); this.setKeepScreenOn(true);// 保持屏幕常亮 }
Resources资源变量。可以通过this.getResources()获取项目中的资源。
SurfaceHolder: 它是一个用于控制surface的接口,它提供了控制surface 的大小,格式,上面的像素,即监视其改变的。SurfaceView的getHolder()函数可以获取SurfaceHolder对象,Surface 就在SurfaceHolder对象内。虽然Surface保存了当前窗口的像素数据,但是在使用过程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或则Canvas lockCanvas()函数来获取Canvas对象,通过在Canvas上绘制内容来修改Surface中的数据。如果Surface不可编辑或则尚未创建调用该函数会返回null,在 unlockCanvas() 和 lockCanvas()中Surface的内容是不缓存的,所以需要完全重绘Surface的内容,为了提高效率只重绘变化的部分则可以调用lockCanvas(Rect rect)函数来指定一个rect区域,这样该区域外的内容会缓存起来。在调用lockCanvas函数获取Canvas后,SurfaceView会获取Surface的一个同步锁直到调用unlockCanvasAndPost(Canvas canvas)函数才释放该锁,这里的同步机制保证在Surface绘制过程中不会被改变(被摧毁、修改)。
Thread:定义线程
Canvas: 定义游戏展示的平台,也就是一个画布。所有的游戏界面将会在画布上展示。
Paint: 定义画笔。 拥有了画布,我们需要一个画笔在画布上进行图画。
1. 场景中加入背景
首先要在资源文件中提取图片文件。
//背景图片 background_top = BitmapFactory.decodeResource(res, R.drawable.bg); background_bottom = BitmapFactory.decodeResource(res, R.drawable.fg); //两个松鼠图片 squirrel_1 = BitmapFactory.decodeResource(res, R.drawable.squirrel_1); squirrel_2 = BitmapFactory.decodeResource(res, R.drawable.squirrel_2); //发射器底座图片 catapult_base_1 = BitmapFactory.decodeResource(res, R.drawable.catapult_base_1); catapult_base_2=BitmapFactory.decodeResource(res,R.drawable.catapult_base_2); 加载了图片文件之后,定义一个常量FLOOR_HEIGHT。这个是地面的高度,为了能够更精确的摆放物体。这个高度就是手机屏幕下边缘到游戏中模拟的地图的高度。 private static final float FLOOR_HEIGHT =82f; 接下来还要定义屏幕的高和宽。这个高和宽指的是手机屏幕可见区域的高和宽,并不是游戏场景中的高和宽。请注意。 ScreenW = this.getWidth(); ScreenH = this.getHeight(); 接下来在canvas上画出这些图片。 public void surfaceCreated(SurfaceHolder arg0) { ScreenH = this.getHeight(); ScreenW = this.getWidth(); thread_flag = true; th = new Thread(this); // 创建线程 th.start(); //开启线程 } 此时需要注意。一定要把th.start()开启线程这段代码放到surfaceCreated最后,否则会出现启动自动退出的bug. private void draw() { try { canvas = sfh.lockCanvas(); // 得到一个canvas实例 if (canvas != null) { canvas.drawColor(Color.WHITE);// 刷屏 canvas.drawBitmap(background_top, 0-w/2, 0, paint); canvas.drawBitmap(catapult_base_2,260-w,ScreenH-FLOOR_HEIGHT-catapult_base_2.getHeight()-catapult_base_2.getHeight()/4,paint); canvas.drawBitmap(catapult_base_1,265-w,ScreenH-FLOOR_HEIGHT-catapult_base_1.getHeight()-catapult_base_1.getHeight()/4,paint); canvas.drawBitmap(squirrel_1, 50-w, ScreenH-FLOOR_HEIGHT-squirrel_1.getHeight(), paint); canvas.drawBitmap(squirrel_2, 350-w, ScreenH-FLOOR_HEIGHT-squirrel_2.getHeight(), paint); canvas.drawBitmap(background_bottom, 0-w, ScreenH-background_bottom.getHeight(), paint); } } catch (Exception ex) { } finally { if (canvas != null) sfh.unlockCanvasAndPost(canvas); // 将画好的画布提交 } }
此时运行程序。就可以看到我们的游戏场景了。但是现实的都是非物理模拟部分。
此时,是不是有些小小的兴奋。。。。但是不要怪我泼凉水。现在我们只是把一些图片拼凑在了一起。其他的什么都没有呢。手指滑动屏幕也没有任何反应。
接下来我们使游戏场景进行移动。
五.移动场景
现在的游戏运行之后,只能显示一半的场景,接下来实现用手指滑动屏幕移动场景。我们要在MainView.java主类中,复写View中的onTouchEvent方法。此方法用于检测触摸屏事件。
@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } 然后分别在onTouchEvent方法中,实现触屏 按下,抬起,移动事件。 public boolean onTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN) {} else if(event.getAction() == MotionEvent.ACTION_UP) {} else if(event.getAction() == MotionEvent.ACTION_MOVE) {} return super.onTouchEvent(event); }
还需要定义两个变量。position_X是当按下触摸屏时,触摸点在当前游戏场景中的x轴位置,move_X是移动触摸屏时,移动的偏移量。
private float position_X; private float move_X;
当按下触摸屏时计算当前的场景中的位置。event.getX()是获取触摸点的x轴坐标,这个是相对于屏幕的坐标,所以我们还需要加上move_X偏移量,这样就能获取当前触摸点在游戏场景中的位置。
if(event.getAction() == MotionEvent.ACTION_DOWN) { position_X =move_X+ event.getX(); } 接下来就完成当手指移动时,move_X偏移量的值。偏移量应该是按下手指时的位置与移动时当前位置的差。 else if(event.getAction() == MotionEvent.ACTION_MOVE) { move_X = position_X-event.getX(); }
得到了偏移量我之需要在draw方法中,画游戏界面时对每张图片的x轴位置进行改变。这样就能屏幕移动的效果。
例如话背景头部图片时,x轴的位置减去偏移量的位置就是移动之后的位置。其他图片方法一样。不明白的可以查看第二节,Android游戏坐标系一节。
canvas.drawBitmap(background_top, 0-move_X, 0, paint);
接下来我们可以运行程序查看一下效果。
此时的运行效果可能会很失望,移动触屏,游戏场景并没有进行移动。原因是我们只是实现了触屏方法,但是当前surfaceview不允许对触屏进行点击。所以我们需要在MainView构造方法中,添加以下代码。
setClickable(true);
此时再次运行程序。游戏界面可以进行移动了。
但是经过测试会发现,当移动场景的时候,可能会超出整个游戏场景。如下图。
这个bug是在有些尴尬啊!!
这是由于我们没有为偏移量进行限制的原因。
0<Move_X<游戏场景宽度 – 屏幕宽度
游戏场景的宽度也就是背景图片的宽度。定义一个变量 gameWidth,它的值为背景图片的宽度。
private float gameWidth; gameWidth = background_top.getWidth(); 在触屏移动时对move_X进行限制。 else if(event.getAction() == MotionEvent.ACTION_MOVE) { move_X = position_X-event.getX(); move_X = move_X<0?0:(move_X>gameWidth-ScreenW?gameWidth-ScreenW:move_X); }
这样就能保证屏幕不会超出游戏场景的范围。
再次运行程序。可以发现,运行正常。
下一章中,我们将要介绍整个游戏的核心部门---》创建游戏世界!