Android游戏开发之"抓住疯狂熊"(一)

??????? 刚接触android不久,自己根据网上的教程模仿了一个2014年的热门游戏“围住神经猫”,游戏方法非常简单,大家都玩过应该知道,只要将人物围住就可以获胜,若人物跑到地图边缘,则判定失败。

??????? 先上游戏界面:

?

????代码中有三个类:MainActivity、Playground、Dot

?

????Dot是格子的对象类,地图是由格子组成的,里面是基本的set、get方法。附代码:

public class Dot {
    int x,y;
    int status;
    public static final int STATUS_ON = 1;//设置障碍
    public static final int STATUS_OFF = 0;//设置为空
    public static final int STATUS_IN = 9;//神经猫的位置

    public Dot(int x, int y) {
        super();
        this.x = x;
        this.y = y;
        status = STATUS_OFF;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getStatus() {
        return status;
    }

    public void setX(int x) {

        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setXY(int x,int  y){
        this.x = x;
        this.y = y;
    }


}

?

?????? 这个游戏的主要实现方法在PlayGround类中,这个类继承了SurfaceView类。

?????? 下面我们先来了解一下surfaceView这个类:

????在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面。由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。

?????? 普通的Android控件,例如TextView、Button和CheckBox等,它们都是将自己的UI绘制在宿主窗口的绘图表面之上,这意味着它们的UI是在应用程序的主线程中进行绘制的。由于应用程序的主线程除了要绘制UI之外,还需要及时地响应用户输入,否则的话,系统就会认为应用程序没有响应了,因此就会弹出一个ANR对话框出来。对于一些游戏画面,或者摄像头预览、视频播放来说,它们的UI都比较复杂,而且要求能够进行高效的绘制,因此,它们的UI就不适合在应用程序的主线程中进行绘制。这时候就必须要给那些需要复杂而高效UI的视图生成一个独立的绘图表面,以及使用一个独立的线程来绘制这些视图的UI。

????SurfaceView及其宿主Activity窗口的绘图表面示意图

?

????? ?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来控制。

?

?

?????? 下面把Playground完整代码附上(已打注释):

public class Playground extends SurfaceView implements View.OnTouchListener {
    private static int WIDTH = 40;//定义格子的大小
    private static final int ROW = 9;//定义行数
    private static final int COL = 9;//定义列数
    private static final int BLOCK = 10;//定义路障的数量
    private int size = 0;//定义size 为格子与gif图片之间的间隔,又来调整gif的位置
    private Canvas c;//定义画布

    private long movieStart;//动图的开始
    private Dot matrix[][];//声明地图对象
    private Dot cat;//声明猫的对象

    //构造函数
    public Playground(Context context) {
        super(context);
        getHolder().addCallback(callback);//回调函数
        matrix = new Dot[ROW][COL];//初始化地图
        for (int i = 0;i < ROW; i++)
            for (int j = 0;j < COL; j++)
                matrix[i][j] = new Dot(j,i);
        setOnTouchListener(this);//添加监听器
        initGame();//初始化游戏数据
    }

    /*
    得到格子对象
     */
    private Dot getDot(int x,int y){
        return matrix[y][x];
    }

    //判断点是否在边界
    private boolean isAtEdge(Dot d){
        if (d.getX()*d.getY()==0||d.getX()+1==COL||d.getY()+1==ROW)
            return true;
        return false;
    }

    //得到对象one附近的对象
    private Dot getNeighbour(Dot one,int dir){
        /*
        dir为周围对象的下标,1为one点的左上点,2为one点的右上点,3为one点的正右点,4为one点的右下点,5为one点的左下点,6为one点的正左点
         */
        switch (dir) {
            case 1:
                return getDot(one.getX()-1, one.getY());
            case 2:
                if (one.getY()%2 == 0) {
                    return getDot(one.getX()-1, one.getY()-1);
                }else {
                    return getDot(one.getX(), one.getY()-1);
                }
            case 3:
                if (one.getY()%2 == 0) {
                    return getDot(one.getX(), one.getY()-1);
                }else {
                    return getDot(one.getX()+1, one.getY()-1);
                }
            case 4:
                return getDot(one.getX()+1, one.getY());
            case 5:
                if (one.getY()%2 == 0) {
                    return getDot(one.getX(), one.getY()+1);
                }else {
                    return getDot(one.getX()+1, one.getY()+1);
                }
            case 6:
                if (one.getY()%2 == 0) {
                    return getDot(one.getX()-1, one.getY()+1);
                }else {
                    return getDot(one.getX(), one.getY()+1);
                }
            default:
                break;
        }

        return null;
    }

    private void MoveTo(Dot one){
        one.setStatus(Dot.STATUS_IN);//设置猫的位置
        getDot(cat.getX(),cat.getY()).setStatus(Dot.STATUS_OFF);//将猫原来的位置设为空状态
        cat.setXY(one.getX(),one.getY());//设置猫的x,y坐标
    }

    //点击一个点后的移动判断事件
    private void move(){
        if(isAtEdge(cat)){//如果神经猫逃离到边界,则判断游戏失败
            lose();
            return;
        }
        Vector<Dot> avaliable=new Vector<>();//用vector记录某个点的各个方向路线
        Vector<Dot> positive=new Vector<>();//用vector记录某个点各个方向离边缘的距离
        HashMap<Dot,Integer> al=new HashMap<Dot,Integer>();
        for (int i=1;i<7;i++){//得到周围的所有点
            Dot n = getNeighbour(cat,i);
            if (n.getStatus()==Dot.STATUS_OFF) {
                avaliable.add(n);//如果n点为空,添加到avaliable中
                al.put(n, i);//将n和i记录到al中
                if (getDistance(n,i)>0){
                    positive.add(n);//将存在到边缘的路线添加到positive中
                }
            }
        }
        if(avaliable.size()==0)//one点周围没有空点
            win();
        else if (avaliable.size() == 1){
            MoveTo(avaliable.get(0));//猫的移动方法
        }
        else{
            Dot best=null;
            if (positive.size()!=0){//存在可以直接到达屏幕边缘的走向
                int min=999;
                for (int i=0;i<positive.size();i++){//遍历所有路线
                    int a = getDistance(positive.get(i),al.get(positive.get(i)));
                    if (a<min) {
                        min = a;
                        best = positive.get(i);//得到最优路线
                    }
                }

            }
            else{//所有方向都有路障
                int max = 1;
                for (int i=0;i<avaliable.size();i++){//遍历所有路线
                    int k = getDistance(avaliable.get(i),al.get(avaliable.get(i)));
                    if (k<max){
                        max=k;//选择离障碍最远的路线,这样才会有更多的路线选择打到边缘
                        best=avaliable.get(i);//最优路线
                    }
                }
            }
            MoveTo(best);//选择最优路线进行移动
        }
    }

    private int getDistance(Dot one, int dir) {
        int distance = 0;
        if (isAtEdge(one)) {//判断one点是否在格子边缘
            return 1;
        }
        Dot ori = one,next;
        while (true) {//在一条直线上
        // 如果下一个点遇到障碍点,distance返回值为负
        // 如果下一个点为空值,则distance+1,继续进行循环
        // 如果下一个点到了格子边缘,distance+1后返
            next = getNeighbour(ori,dir);
            if (next.getStatus() == Dot.STATUS_ON) {
                return distance*-1;
            }
            if (isAtEdge(next)) {//判断下一个点是否在格子边缘
                distance++;//距离增加
                return distance;
            }
            distance++;
            ori = next;
        }
    }

    /*
    失败方法
     */
    private void lose(){
        GameDialog gameDialog = new GameDialog(getContext(),this);//生成一个游戏结束时的自定义Dialog对象
        gameDialog.show();
//        Toast.makeText(getContext(),"Lose",Toast.LENGTH_SHORT).show();//弹出"Lose"消息
    }

    /*
    获胜方法
     */
    private void win(){
        Toast.makeText(getContext(),"Win",Toast.LENGTH_SHORT).show();//弹出"Win"消息
    }

    /*
    调整Bitmap的大小
     */
    public static Bitmap resizeBitmap(Bitmap bitmap, int w, int h) {
        if (bitmap != null) {
            int width = bitmap.getWidth();//得到图片的宽度
            int height = bitmap.getHeight();//得到图片的高度
            int newWidth = w;//你想调整的宽度
            int newHeight = h;//你想调整的高度
            float scaleWidth = ((float) newWidth) / width;//调整的宽度的比例
            float scaleHeight = ((float) newHeight) / height;//调整的高度的比例
            Matrix matrix = new Matrix();//实例化一个矩阵对象
            matrix.postScale(scaleWidth, scaleHeight);//缩放变换
            Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width,
                    height, matrix, true);//得到调整大小后的新Bitmap对象
            return resizedBitmap;//返回Bitmap对象
        } else {
            return null;//如果Bitmap不存在,返回空值
        }
    }

    public void gifDraw(Canvas canvas,float x, float y) {
        Movie movie = Movie.decodeStream(getResources().openRawResource(R.raw.p));//将p.gif资源读入Movie中
        long now = SystemClock.uptimeMillis();//从开机到现在的毫秒数(手机睡眠的时间不包括在内)
        if (movieStart == 0) { // first time
            movieStart = now;
        }
        if (movie != null) {

            int dur = movie.duration();//动画的持续时间
            if (dur == 0) {
                dur = 1000;
            }
            int relTime = (int) ((now - movieStart) % dur);//得到帧数
            movie.setTime(relTime);//设置movie的帧数
            movie.draw(canvas, x, y);//显示在canvas上
            invalidate();//界面刷新
        }
    }

    public void redraw(){
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg);
        Bitmap xrd = BitmapFactory.decodeResource(getResources(), R.raw.p);
        c = getHolder().lockCanvas();//获取canvas对象
//        c.drawColor(Color.LTGRAY);
        c.drawBitmap(resizeBitmap(bitmap,getWidth(),getHeight()),0,0,null);//设置背景图片

        size = (ROW / 3) * (getWidth() / 9 - WIDTH);//将格子向下平移

        Paint paint = new Paint();//实例化一个画笔对象
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);//消除锯齿
        for (int i = 0;i < ROW; i++) {
            int offset = 0;
            if(i%2 != 0){//偶数行向右平移半个格子的距离
                offset = WIDTH/2;
            }
            for (int j = 0; j < COL; j++) {
                Dot one = getDot(j, i);
                switch (one.getStatus()) {
                    case Dot.STATUS_OFF://格子为空状态
                        paint.setColor(0x7fC0C0C0);
//                        paint.setColor(0x7f040000);
                        c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint);
                        break;
                    case Dot.STATUS_IN://猫所在的格子
                        paint.setColor(0xFFFFAA00);
                        c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight() / 20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight() / 20), paint);
                        gifDraw(c, one.getX() * WIDTH + offset + size - 40, one.getY() * WIDTH + 7 * getHeight() / 20 - 140);
//                        c.drawBitmap(resizeBitmap(xrd,WIDTH * 3 / 2,WIDTH * 2),one.getX() * WIDTH + offset + size - WIDTH / 4, one.getY() * WIDTH + 7 * getHeight() / 20 - 3 * WIDTH / 2 , null);
                        break;
                    case Dot.STATUS_ON:
                        paint.setColor(0x7fFF0000);//障碍所在的格子
                        c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint);
                        break;
                    default:
                        break;
                }
//                c.drawOval(new RectF(one.getX() * WIDTH + offset + size, one.getY() * WIDTH + 7 * getHeight()/20, ((one.getX() + 1) * WIDTH) + offset + size, (one.getY() + 1) * WIDTH + 7 * getHeight()/20), paint);
            }
        }
        getHolder().unlockCanvasAndPost(c);//结束锁定画图,并提交改变,将图形显示
    }

    Callback callback = new Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            redraw();
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            WIDTH=width/(COL+1);
            redraw();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {

        }
    };

    //初始化游戏
    public void initGame(){
        for (int i = 0;i < ROW; i++)
            for (int j = 0;j < COL; j++)
                matrix[i][j].setStatus(Dot.STATUS_OFF);
        cat=new Dot(4,4);//初始神经猫的位置
        getDot(4,4).setStatus(Dot.STATUS_IN);
//        cat.setStatus(Dot.STATUS_IN);
        for (int i = 0;i<BLOCK;){//随机设置障碍的位置
            int x = (int) ((Math.random()*1000) % COL);//随机生成X坐标
            int y = (int) ((Math.random()*1000) % ROW);//随机生成Y坐标
            if (getDot(x,y).getStatus()==Dot.STATUS_OFF){//设置状态
                getDot(x,y).setStatus(Dot.STATUS_ON);
                i++;
            }
        }
    }

    /*
    屏幕的点击方法
     */
    public boolean onTouch(View v, MotionEvent e) {
        if(e.getAction()==MotionEvent.ACTION_UP){
            int x,y=0;
            if(e.getY()>(7 * getHeight()/20))//判断点击范围是否在格子范围高度内
                y= (int) ((e.getY()-7 * getHeight()/20)/WIDTH);
            else {//如若不是
                initGame();//重新开始游戏
                redraw();//重绘
                return true;
            }
            if (y%2==0){//判断是否为偶数行
                x= (int) ((e.getX() -size)/WIDTH);//得到x坐标
            }
            else{
                x= (int) ((e.getX()-WIDTH/2-size)/WIDTH);//同上
            }
            if (x+1 > COL ||y+1>ROW) {//判断坐标是否大于边界
                initGame();
            }
            else if(getDot(x, y).getStatus() == Dot.STATUS_OFF){
                getDot(x, y).setStatus(Dot.STATUS_ON);//设置障碍
                move();//进行移动方法
            }
            redraw();//重绘
        }
        return true;
    }
}

?

??????? 整个代码中最核心的部分是move(),人物移动的算法主要有两个:

?????? 1、人物的6个方向的直线上,只要有一条可以直通地图边缘的路线,则选择离边缘最短的那条走。

?????? 2、人物的6个方向的直线上,没有一条可以通往边缘的路线,即每个方向都有障碍物挡着,则选择离障碍物最远距离的那条路线走。

?

?

?????? 网上下载了一个图片想设为背景图片,可是没有一个函数可以调整图片的大小,找了一个自定义函数来实现:

public static Bitmap resizeBitmap(Bitmap bitmap, int w, int h) {
        if (bitmap != null) {
            int width = bitmap.getWidth();//得到图片的宽度
            int height = bitmap.getHeight();//得到图片的高度
            int newWidth = w;//你想调整的宽度
            int newHeight = h;//你想调整的高度
            float scaleWidth = ((float) newWidth) / width;//调整的宽度的比例
            float scaleHeight = ((float) newHeight) / height;//调整的高度的比例
            Matrix matrix = new Matrix();//实例化一个矩阵对象
            matrix.postScale(scaleWidth, scaleHeight);//缩放变换
            Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width,
                    height, matrix, true);//得到调整大小后的新Bitmap对象
            return resizedBitmap;//返回Bitmap对象
        } else {
            return null;//如果Bitmap不存在,返回空值
        }
    }

?

?

???????本来还想实现的是通过一个gif来实现人物的动画,android内部类没有实现gif的功能,搜索了一下资料,android实现gif有三种方式:

?????? 第一 :GifView支持android播放gif,效果是先加载第一帧,然后慢慢加载完其他的针,这样效果视觉很不好,是从模糊到清晰的过程;
?????? 第二:是流行的把gif图片通过工具分拆成n帧,然后使用逐帧动画播放,很麻烦;
?????? 第三 :使用Movie提供的Movie.decodeStream()方法解析gif,然后通过文件流的方式播放,效果特别好 ,和原图片没差。

?????? 我试了一下第三种的方法,依旧没有实现,每次点击屏幕一次,动画才会跳一帧,等询问大神后再做修改。

public void gifDraw(Canvas canvas,float x, float y) {
        Movie movie = Movie.decodeStream(getResources().openRawResource(R.raw.p));//将p.gif资源读入Movie中
        long now = SystemClock.uptimeMillis();//从开机到现在的毫秒数(手机睡眠的时间不包括在内)
        if (movieStart == 0) { // first time
            movieStart = now;
        }
        if (movie != null) {

            int dur = movie.duration();//动画的持续时间
            if (dur == 0) {
                dur = 1000;
            }
            int relTime = (int) ((now - movieStart) % dur);//得到帧数
            movie.setTime(relTime);//设置movie的帧数
            movie.draw(canvas, x, y);//显示在canvas上
            invalidate();//界面刷新
        }
    }

?

?????? 暂时实现了这个游戏的核心功能,开始界面和结束界面过两天实现。

?????????

?

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。