Android SurfaceView仿“是男人就下一百层”

来源:互联网 发布:毕业生烧炭自杀知乎 编辑:程序博客网 时间:2024/04/29 21:28

本文感谢鸿洋先生的博客

http://blog.csdn.net/lmj623565791/article/details/42965779

本文基于以上博客,利用鸿洋先生的创意改编而成。
正题,本例子想做到的效果:

1.不断上升的地板条
2.小人,踩到地板会跟着上升,否则自然下坠
3.小人上升过屏幕顶或者下降到屏幕底部外则判定为死亡,地板停止,小人自然下坠

最终效果截图:
这里写图片描述

这里写图片描述
本例子所用素材来自游戏:涂鸦跳跃
好了,开工。

1.首先,回顾下SurfaceView的一般用法,直接上鸿洋的干货

package com.zhy.view;  import android.content.Context;  import android.graphics.Canvas;  import android.graphics.PixelFormat;  import android.util.AttributeSet;  import android.view.SurfaceHolder;  import android.view.SurfaceHolder.Callback;  import android.view.SurfaceView;  public class GameFlabbyBird extends SurfaceView implements Callback, Runnable  {      private SurfaceHolder mHolder;      /**      * 与SurfaceHolder绑定的Canvas      */      private Canvas mCanvas;      /**      * 用于绘制的线程      */      private Thread t;      /**      * 线程的控制开关      */      private boolean isRunning;      public GameFlabbyBird(Context context)      {          this(context, null);      }      public GameFlabbyBird(Context context, AttributeSet attrs)      {          super(context, attrs);          mHolder = getHolder();          mHolder.addCallback(this);          setZOrderOnTop(true);// 设置画布 背景透明          mHolder.setFormat(PixelFormat.TRANSLUCENT);          // 设置可获得焦点          setFocusable(true);          setFocusableInTouchMode(true);          // 设置常亮          this.setKeepScreenOn(true);      }      @Override      public void surfaceCreated(SurfaceHolder holder)      {          // 开启线程          isRunning = true;          t = new Thread(this);          t.start();      }      @Override      public void surfaceChanged(SurfaceHolder holder, int format, int width,              int height)      {          // TODO Auto-generated method stub      }      @Override      public void surfaceDestroyed(SurfaceHolder holder)      {          // 通知关闭线程          isRunning = false;      }      @Override      public void run()      {          while (isRunning)          {              long start = System.currentTimeMillis();              draw();              long end = System.currentTimeMillis();              try              {                  if (end - start < 50)                  {                      Thread.sleep(50 - (end - start));                  }              } catch (InterruptedException e)              {                  e.printStackTrace();              }          }      }      private void draw()      {          try          {              // 获得canvas              mCanvas = mHolder.lockCanvas();              if (mCanvas != null)              {                  // drawSomething..              }          } catch (Exception e)          {          } finally          {              if (mCanvas != null)                  mHolder.unlockCanvasAndPost(mCanvas);          }      }  }  

SurfaceView的优点就在此,给予开发者很大的便利,可以直接开普通线程更改UI,
上面的关键就在继承SurfaceView,并拿到holder,在主线程中开一个循环线程,循环绘制得到的画布

接下来干啥?当然要有背景

2.背景
这里我封装成了一个类,便于以后停止游戏等操作:

public class Background {    Bitmap bitmap;    RectF rectf;    int gameHeight;    int gameWidth;    public Background(Bitmap bitmap, RectF rectf) {        this.bitmap = bitmap;        this.rectf = rectf;    }    public void setGameHeight(int gameHeight) {        this.gameHeight = gameHeight;    }    public void setGameWidth(int gameWidth) {        this.gameWidth = gameWidth;    }    public void draw(Canvas canvas)    {        canvas.save(Canvas.MATRIX_SAVE_FLAG);        canvas.drawBitmap(bitmap, null, rectf, null);        canvas.restore();    }}

很简单,主要就是后面的draw,拿到目标画布后在上面绘制背景

3.地板
地板的横轴坐标是随机的,一开始出现了一个小麻烦,就是上一次绘制的地板没有去掉,导致屏幕上都是地板。。。后来动了点小手脚就搞定了

 Paint mPaint = new Paint();            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));            mCanvas.drawPaint(mPaint);

在每次绘制之前执行这段,清除掉Canvas原来的内容
我还试过一个方法,就是自己新建一个Canvas,在这上面画,再还给holders,这就不用清楚了吗,结果就是报错:)要用人家原来的画布。。

public class Floor {    int width;    int height;    int x;    int y;    Bitmap mBitmap;    public Floor(Context context, int gameHeight, int gameWidth, Bitmap bitmap)    {        y=gameHeight;        mBitmap=bitmap;        isWith=false;    }   //省略众多赋值取值语句    public  void draw(Canvas canvas,RectF rectf)    {        canvas.save(Canvas.MATRIX_SAVE_FLAG);        canvas.translate(x, y);        canvas.drawBitmap(mBitmap,null,rectf,null);        canvas.restore();    }}

从代码上看也很简单

3.地板的增加、删除、移动

public void caculate() {        for (Floor f : floors) {            if (f.getY() < 0 - floor_hight) {            //delete the floor out of window                floors_delete.add(f);                break;            }        }        floors.removeAll(floors_delete);        floors_delete.clear();        //caculate need add floor or not        if (mTmpMove > mTargetMove) {            int randx = (int) (Math.random() * (game_width - floor_width));            Floor floor = new Floor(getContext(), game_height, game_width, floor_bitmap);            floor.setX(randx);            floor.setY(game_height);            floor.setHeight(floor_hight);            floor.setWidth(floor_width);            floors.add(floor);            mTmpMove = 0;            mGrade++;        }    }

采用的还是鸿洋先生那个方法,遍历,看看当前是否需要删除地板,利用链表储存,然后再看看当前移动距离是否需要再增加地板

地板移动,相对简单

/**floors move up*/    private void floor_move() {        for (Floor f : floors) {            f.setY(f.getY() - mSpeed);        }        mTmpMove += mSpeed;    }

只是注意,这里我们是向上,因此坐标是要减的,(0,0)在左上角

4.小人man
必要的坐标啊,大小矩形啊是肯定的,我们做出来的小人肯定是要移动的,所以我希望向左的时候有一张向左的图,向右亦然

public class Man {   private int width;    private int height;    private int x;    private int y;    private  int gameWidth;    private  Bitmap mBitmap;    private RectF rectf;    private Bitmap bitmap_center;    private Bitmap bitmap_left;    private  Bitmap bitmap_right;    private  int ori;    private  boolean boom_left;    private  boolean boom_right;    private  int boom_time;    /**     * set the ori,-1left 0center 1right     */    public void setOri(int x) {        ori = x;        if (x == -1) {            mBitmap = bitmap_left;        } else if (x == 1) {            mBitmap = bitmap_right;        } else {            mBitmap = bitmap_center;        }    }    public Man(Context context, int gameHeight, int gameWidth, Bitmap bitmap) {        y = gameHeight / 2;        x = gameWidth/2;        mBitmap = bitmap;        this.gameWidth = gameWidth;        ori = 0;        boom_left = false;        boom_right=false;        boom_time=5;    }    public void trans_y(int x)    {        this.y+=x;    }   //省略一堆get\set    public void draw(Canvas canvas, RectF rectf) {        canvas.save(Canvas.MATRIX_SAVE_FLAG);        if (this.x>0&&this.x<gameWidth-width) {        }else if(this.x<=0)        {            this.x=0;        }else{            this.x=gameWidth-width;        }        canvas.translate(x,y);        canvas.drawBitmap(mBitmap, null, rectf, null);        canvas.restore();        this.rectf = rectf;    }   }

可以看到,首先我要求要有三张图,代表正面、向左、向右
而且在这里有限制的绘制条件,不允许水平超出屏幕
我开放了一个函数,可以根据调用改变当前显示图像
在相应的地方调用

/*set the man ori,-1 left ;0 center;1 right/
public void setManOri(int x) {
if (man != null)
this.man.setOri(x);
}

3.碰撞
这个例子有一个很重要的点就是判断man是否站在了地板上,如果是则跟着地板上升
首先,判断,不难

/**     * return is will click the floor     */    private boolean pre_calcute_pomb(Floor f) {        if (f.getX() <= man.getX() && f.getX() + floor_width >= man.getX() && (f.getY() - man_height <= man.getY() && f.getY() + floor_hight >= man.getY())) {            return true;        }        return false;    }

然后是绘制
一开始的想法是判断成功后先画地板,然后man的y坐标跟着移动
实践后发现这样卡顿效果明显,可以往上翻翻看,我们的绘制都涉及到画布的保存回收等

 canvas.save(Canvas.MATRIX_SAVE_FLAG);        canvas.translate(x, y);        canvas.restore();

也就是这样才看上去才卡顿

然后就是第二种想法,可不可以给地板一个Man对象,如果落到这个地板上,则地板绘制的时候连着小人一起画?

for (Floor f : floors) {                if (pre_calcute_pomb(f)) {//find the floor the man will on,if have                    f.setWith(man);                    f.draw(mCanvas, floor_rectf);                    floor_touch = f;                    isClick = true;                } else {                    f.draw(mCanvas, floor_rectf);                }            }            if (!isClick) {                man.trans_y(man_down_speed);                man.draw(mCanvas, man_Rectf);                if (floor_touch != null)                    floor_touch.setUnWith();                man_down_speed++;            } else {                man_down_speed = 0;            }

判断,如果落下了就把当前man绑定到这个floor中,然后还记得标记,否则等下原先的man也绘制,屏幕上老蹦出小人

 public void setWith(Man m)    {        man=m;        isWith=true;    }    public void setUnWith()    {        isWith=false;    }    public  void draw(Canvas canvas,RectF rectf)    {        canvas.save(Canvas.MATRIX_SAVE_FLAG);        canvas.translate(x, y);        canvas.drawBitmap(mBitmap,null,rectf,null);        canvas.restore();        if(isWith)        {            man.setY(this.y-man.getHeight()/2- this.height/2);            man.draw(canvas);        }    }

这样相对静止的效果看上去还是很舒服的;

4.碰撞到顶部的时候地板静止,小人掉落
这个时候就要说到我们程序的状态了,分为三个

private enum Game_State {        WAITTING, RUNNING, STOP;    }

一开始是准备阶段WAITTING
点击后是RUNNING
死亡后是STOP
这个时候只要不执行地板移动以及碰撞检测,让小人死尸式掉落就行啦

private void draw_Stop() {        //let the man flow down        if (man.getY() <= game_height) {            long start = System.currentTimeMillis();            mCanvas = surfaceHolder.lockCanvas();            if (mCanvas != null) {                Paint mPaint = new Paint();                mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));//clear the canvas                mCanvas.drawPaint(mPaint);                background.draw(mCanvas);                for (Floor f : floors) {                        f.draw(mCanvas, floor_rectf);                }                man.trans_y(man_down_speed);                    man.draw(mCanvas, man_Rectf);                    if (floor_touch != null)                        floor_touch.setUnWith();                    man_down_speed++;                  drawGrades(mCanvas);                if (mCanvas != null) {                    surfaceHolder.unlockCanvasAndPost(mCanvas);                }                long end = System.currentTimeMillis();                if (end - start < 50) {                    try {                        Thread.sleep(50 - end + start);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }         }    }

一开始脑抽了下想保存当前画布,不清除的,直接画小人就行
结果。。。一排小人井喷= =

5.利用手机翻转控制小人左右移动
这个算是亮点了吧,记不记得上面那个提供出来的函数

/**set the man ori,-1 left ;0 center;1 right*/    public void setManOri(int x) {        if (man != null)            this.man.setOri(x);    }

这里就要用到它啦~当前我们是在完成一个View,不好监控手机倾斜等,就暴露出这个函数,具体的监控程序放在Activity中,主要是请求两个传感器

public class BeAManAct extends Activity {    GameBeAMan gameBeAMan;    SensorManager sensorManager;    /**     * use to get asset     */    Context mContext;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,                WindowManager.LayoutParams.FLAG_FULLSCREEN);        requestWindowFeature(Window.FEATURE_NO_TITLE);        gameBeAMan = new GameBeAMan(this);        setContentView(gameBeAMan);        /**Sensor*/        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);        Sensor magnetticsensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);//方向传感器        Sensor acclerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);        sensorManager.registerListener(listener, magnetticsensor, SensorManager.SENSOR_DELAY_GAME);        sensorManager.registerListener(listener, acclerSensor, SensorManager.SENSOR_DELAY_GAME); }  @Override    protected void onDestroy() {        super.onDestroy();        if (sensorManager != null) {            sensorManager.unregisterListener(listener);        }    }    private SensorEventListener listener = new SensorEventListener() {        float[] acceValues = new float[3];        float[] magnetValues = new float[3];        @Override        public void onSensorChanged(SensorEvent sensorEvent) {            if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {                acceValues = sensorEvent.values.clone();            } else if (sensorEvent.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {                magnetValues = sensorEvent.values.clone();            }            float[] R = new float[9];            float[] values = new float[3];            SensorManager.getRotationMatrix(R, null, acceValues, magnetValues);            SensorManager.getOrientation(R, values);            if (values[2] < -0.1) {                gameBeAMan.man_runleft();                gameBeAMan.setManOri(-1);            } else if (values[2] > 0.1) {                gameBeAMan.man_runright();                gameBeAMan.setManOri(1);            }else{                gameBeAMan.setManOri(0);            }        }        @Override        public void onAccuracyChanged(Sensorsensor, int i)          {        }    };}

看上去有点长,没关系,我们慢慢看:
想要请求系统服务,下面这步必不可少

sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);

然后从经理:)那里求两个传感器

 Sensor magnetticsensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);//方向传感器        Sensor acclerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);        //加速度传感器        //然后注册监听者(好吧不会起名字= =)        sensorManager.registerListener(listener, magnetticsensor, SensorManager.SENSOR_DELAY_GAME);        sensorManager.registerListener(listener, acclerSensor, SensorManager.SENSOR_DELAY_GAME);

然后就是监听器内部的代码了,这个东西理解起来还是可以的,要死记也行。。

private SensorEventListener listener = new SensorEventListener() {        float[] acceValues = new float[3];        float[] magnetValues = new float[3];        @Override        public void onSensorChanged(SensorEvent sensorEvent) {            if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {                acceValues = sensorEvent.values.clone();            } else if (sensorEvent.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {                magnetValues = sensorEvent.values.clone();            }            float[] R = new float[9];            float[] values = new float[3];            SensorManager.getRotationMatrix(R, null, acceValues, magnetValues);            SensorManager.getOrientation(R, values);            if (values[2] < -0.1) {                gameBeAMan.man_runleft();                gameBeAMan.setManOri(-1);            } else if (values[2] > 0.1) {                gameBeAMan.man_runright();                gameBeAMan.setManOri(1);            }else{                gameBeAMan.setManOri(0);            }        }        @Override        public void onAccuracyChanged(Sensor sensor, int i) {        }    };

我们直接跳步,前面的都是标配,没啥好讲

 SensorManager.getOrientation(R, values);

跟经理要方向信息,储存在三位数组中
这里写图片描述
盗图的,来自

http://www.cnblogs.com/mengdd/archive/2013/05/19/3086781.html
临时找个,这位先生写的也不错,推荐看看

继续
其中value[0]是x轴,values[1]z轴,values[2]y轴
咱们手机都是正面朝上放在手里的嘛,这里我们监控倾斜就要用到y轴
一开始闹了小笑话,我是看参考书搞这个的:(,人家说变化范围是-180到180,我就信了,监控的时候也这么写,结果死活不出效果,后来自己用Log监听才乐呵了,人家的数值范围是-1到1= =

 if (values[2] < -0.1) {                gameBeAMan.man_runleft();                gameBeAMan.setManOri(-1);            } else if (values[2] > 0.1) {                gameBeAMan.man_runright();                gameBeAMan.setManOri(1);            }else{                gameBeAMan.setManOri(0);            }

这里我们持续监听,用那个开放的函数,实时改变状态以及坐标值
那个Run函数只是改变x值而已

  /**change the man's x to left*/    public void man_runleft() {        if (man != null)            man.setX(man.getX() - x_moveSpeed);    }

好了,来填坑了。。
说实话,写这个跟用github我都是第一次,多有疏漏希望读者们谅解并提出来,大家共同进步~
源代码:

最终效果:
这里写图片描述

https://github.com/SGZoom/GZoomDaily

谢谢观看~

0 0
原创粉丝点击