• Android基于box2d开发弹弓类游戏[二]游戏界面的搭建&移动游戏场景


    前面一讲中,我们介绍了,游戏开发的前期准备与如何创建项目。 

    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);
        }

    这样就能保证屏幕不会超出游戏场景的范围。

    再次运行程序。可以发现,运行正常。

    下一章中,我们将要介绍整个游戏的核心部门---》创建游戏世界!

     

     

     

     

     

     

     

     

     

     

  • 相关阅读:
    PowerDesigner与Eclipse同步开发,PowerDesigner使用教程
    什么是POJO
    java中的<?><T><E>详解Jdk5.0新特性Generic Types (泛型)
    PowerDesigner 数据建模技术视频教程
    SQL如何在已有的一张表中插入一列类型为INTEGER数据 并赋初始值为0
    PowerDesigner中CDM数据类型和PDM数据类型间的mapping (对应关系)详解
    Java Web项目设计数据库时是否需要在表中加备用字段?
    PowerDesigner使用教程
    聚合与组合的区别理解(原)
    The requested list key 'map' could not be resolved as a collection/array/map/enumeration/iterator type. Example: people or peopl .
  • 原文地址:https://www.cnblogs.com/81du/p/2797964.html
Copyright © 2020-2023  润新知