# Android围住神经猫的实现

*2016年3月2日 会当凌绝顶，一览众山小。*

为期三天的围住神经猫极简版已经出炉，虽说github或者其他博客已经吧这个简单的小项目写的很详细了，但我还是忍不住再扯一篇。一来为了加深理解，二来也给自己留个学习回忆，毕竟这是我的第一个完整的小游戏项目嘛。

下面我就按照我忍为比较好理解的思路来把整个项目重新整理一遍，期待可以和大家一起学习讨论。

### 功能预览

原版游戏项目预览：

![围住神经猫原版游戏预览](/files/8SQoqk9nWkxXJ98wjTkp) 本人的精简版：

![我的精简项目预览](/files/jCZZRcA97CyMQ5vfpcSq)

游戏玩法： 点击灰色的点设置红色的路障，每点击一次神经猫（橙色）都会移动一次，如果神经猫到达边界游戏失败，若围住了神经猫，则游戏胜利。

### 元素定义

由图中可以看出的，游戏的元素大致为10行10列的点阵，我们首先建立Dot类，然后建立Dot数组完成这个点阵。 Dot属性分析： 游戏中需要在背景图片上画出这些元素，所以他们仅仅通过数组下标进行区分是不合理的，为了找到他们在画板上的位置，我们就要给他们添上x,y坐标 其次，每个Dot都应该有着三种状态： 神经猫可走的状态 STATE\_ON 神经猫不可走的路障状态 STATE\_OFF 以及神经猫所处的状态 STATE\_IN 我们设置对应的颜色为 ON\_GRAY 灰色 OFF\_RED红色 IN\_ORANGE橙色 最后给对应的属性配置set和get方法。

Dot 类：

```
public class Dot {

    private int x, y;//点位置坐标
    private int status;//点此时的状态
    //点的三种状态
    public static final int STATUS_ON = 0;//神经猫可以进入
    public static final int ON_GRAY = Color.GRAY; //这个状态呈现的颜色
    public static final int STATUS_IN = 9;//神经猫所在的点
    public static final int IN_ORANGE = 0xFFFF7F00;
    public static final int STATUS_OFF = 1;//神经猫不可以走的点
    public static final int OFF_RED = Color.RED;


    public Dot(int x, int y) {  //初始点的位置
        this.x = x;
        this.y = y;
        status = STATUS_ON;
    }

    //为x,y,status创建get和set方法
    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;
    }
}
```

建立好这些点，我们要对游戏背景进行初始化，我们不能在通过activity\_main.xml进行设置。 这时我首先想到view，但是很不幸，只有UI线程才能修改UI，如果使用view，我们将频繁的在线程间通信，这无疑加大了代码的复杂度和线程阻塞的可能性。 这时我们就需要游戏开发的常客：SurfaceView

### SurfaceView

View必须在UI线程更新画面，而surfaceView是在一个新起的单独线程中重新绘制画面。这是两者的根本区别。 **要想实现surfaceview 首先要继承surfaceview类，并实现SurfaceView\.CallBack接口。** SurfaceVIew详解：<http://www.360doc.com/content/13/0103/14/7724936\\_257842268.shtml>

## 创建视图类

创建PlayGround类并继承SurfaceView。 该类主要实现游戏界面的绘制，游戏逻辑的实现。

```
public class PlayGround extends SurfaceView{
	private SurfaceHolder holder = null
	public PlayGround(Context context) {
        super(context);
		holder.addCallback(callback);//为holder指定callback
    }
	//建立CallBack内部类
    SurfaceHolder.Callback callback = new SurfaceHolder.Callback(){

        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
        }

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

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        }
    };
}
```

### Callback的三个方法：

surfaceCreated（） Surface第一次创建后会立即调用该函数。 程序可以在该函数中做些和绘制界面相关的初始化工作， 一般情况下都是在另外的线程来绘制界面，所以不要在这个函数中绘制Surface。 surfaceChanged（） 当Surface的状态（大小和格式）发生变化的时候会调用该函数，在surfaceCreated调用后该函数至少会被调用一次。 surfaceDestroyed（） 当Surface被摧毁前会调用该函数，该函数被调用后就不能继续使用Surface了，一般在该函数中来清理使用的资源。

为了方便检查程序的执行顺序，我习惯在每个方法上加上Log。

建立好后，我们为Activity设置

```
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Playground(this));
    }
}

```

### 初始化游戏

在初始化数组时，让我们首先看一张数组下标和点坐标关系的图

![蓝色为数组下标 黑色为坐标](/files/ZMml6B9kdSBuID602L6o) 由图中可知，点的坐标和数组下标刚好相反 例如dots\[0]\[1]坐标为(1,0)。

我有一个想把什么代码都放入方法的毛病，不知是好是坏，这里为了方便后面代码的理解，我就把数组的初始化单独的放在initDots方法中了。

定义数组的长度final：

```
    private final int ROW = 10;//数组行
    private final int COL = 10;//数组列
```

初始化数组

```
	private void initDots() { //初始化点数组
        Log.i("INFO---", "调用了initDots");
        dots = new Dot[ROW][COL];
        for (int i = 0; i < ROW; i++) {
            for (int j = 0; j < COL; j++) {
                dots[i][j] = new Dot(j, i);
            }
        }
    }
```

为了方便赋值state，我们可以先将所有元素都设置为灰色，然后指定橙色神经猫的位置，最后在已经变成灰色的点上随机出15个红色路障。 为这个数组的所有元素赋予属性STATE\_ON：

```
	//初始化所有元素为ON
    private void initStateOn() {
        Log.i("INFO---", "调用了initStateOn");
        for (int i = 0; i < ROW; i++) {
            for (int j = 0; j < COL; j++) {
                dots[i][j].setStatus(Dot.STATUS_ON);
            }
        }
    }
```

定义路障数量：

```
private int OFFNUM = 15;//设置15个路障
```

随机路障：

```
    //初始化路障
    private void initStateOFF() {
        Log.i("INFO---", "调用了initStateOFF");
        for (int i = 0; i < OFFNUM; ) {  //设置OFFNUM个路障
            int randomI = (int) (Math.random() * 1000 % ROW);//随机一个行标
            int randomJ = (int) (Math.random() * 1000 % COL);//随机一个列标
             //如果随机出的这个点可以设置路障（即 不是路障或猫）
            if (dots[randomI][randomJ].getStatus() == Dot.STATUS_ON) { 
                //就设置路障
                dots[randomI][randomJ].setStatus(Dot.STATUS_OFF);
                i++;
            }
        }
    }
```

再初始化神经猫之前，为了方便使用坐标引用对应的数组，建立方法

```
	//根据坐标获取对应的数组元素
    private Dot getDot(int x, int y) {
        return dots[y][x];
    }
```

初始化神经猫：

```
	private void initCat() {
        Log.i("INFO---", "调用了initCat");
        cat = getDot(4,5);
        cat.setStatus(Dot.STATUS_IN);
    }
```

初始化游戏： 每次重新开始游戏，都要调用该方法，重新初始化时，没必要重新初始化数组，只需改变数组中每个元素的STATE就好了，所以我们将initDots放在playground的构造函数时，而把其他三个初始化函数放入initGame中。 记住要按顺序调用函数。

```
	 private void initGame() {
        initStateOn();
        initCat();
        initStateOFF();
    }
```

```
	public Playground(Context context) {
        super(context);
        initDots();
        initGame();
    }
```

数组元素的属性都配置好了，让我们测试一下！

```
    private void testDots() {
        //测试初始化是否成功
        Log.i("INFO---", "test");
        for (int i = 0; i < ROW; i++) {
            for (int j = 0; j < COL; j++) {
                Log.i("INFO---", dots[i][j].getStatus() + "");
            }
        }
    }
```

在initGame后调用这个方法，检查一下初始化是否正确！

接下来就可以使用canves和paint进行绘制了！

### 元素的绘制

首先我们要根据屏幕的尺寸来确定点的半径。 共有两种实现方案。 1> 在MainActivity中获取屏幕尺寸

```
		public static int screenWidth;
```

```
		DisplayMetrics displayMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        screenWidth = displayMetrics.widthPixels;
```

```
	private final int DOT_WIDTH = MainActivity.screenWidth / 11;
    //或者是
    //private final int DOT_WIDTH = (int) (MainActivity.screenWidth * 0.9 / 10);
```

2> 第二种方法在callback的surfaceChanged的方法的第二个参数width（屏幕宽度）巧妙地获取点的直径

```
SurfaceHolder.Callback callback = new SurfaceHolder.Callback(){

        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
        }

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

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

        }
    };
```

有了点的直径，我们就可以进行绘制了！

### redraw方法

思考： 我们要根据点的属性的不同二给他们绘制不同的颜色，所以，优先考虑使用switch语句。 在游戏中，奇数行的起始位置显然比偶数行的起始位置大一个半径，我们可以使用偏移值来解决这个问题，当为偶数行时，偏移值为0，不骗移，如果为偶数时，偏移值为半径。

```
 private void redraw() {
        Log.i("INFO---", "调用了redraw进行重绘");

        Canvas canvas = holder.lockCanvas();//获取SurfaceView中的画板
        Paint paint = new Paint();//建立一个新的画笔
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);//设置画笔为抗锯齿
        canvas.drawColor(Color.WHITE);//绘制背景

        for (int i = 0; i < ROW; i++) {
            int offset = 0;
            if (ROW % 2 != 0) {  //当为奇数行时
                offset = DOT_WIDTH / 2;
            }
            for (int j = 0; j < COL; j++) {
                Dot dot = getDot(i,j);//获取这个dot对象
                switch (dot.getStatus()) {
                    case Dot.STATUS_ON:
                        paint.setColor(Dot.ON_GRAY);
                        break;
                    case Dot.STATUS_IN:
                        paint.setColor(Dot.IN_ORANGE);
                        break;
                    case Dot.STATUS_OFF:
                        paint.setColor(Dot.OFF_RED);
                        break;
                    default:
                        break;
                }
                //因为绘制圆时要提供圆心，较为麻烦，所以使用椭圆
                //参数为椭圆的外接矩形，RectF的参数为左上顶点和右下定点的坐标
                canvas.drawOval(new RectF(dot.getX() * DOT_WIDTH +offset,
                        dot.getY() * DOT_WIDTH,
                        (dot.getX()+1) * DOT_WIDTH + offset,
                        (dot.getY()+1) * DOT_WIDTH),paint);
            }
        }
    //绘制结束后释放绘图 提交绘制的图形
        holder.unlockCanvasAndPost(canvas);
    }
```

切记，不能在initGame后直接调用redraw方法，这样会使canvas对象为null 因为，如果surfaceCreated没有被调用就lockCanves，就会返回空，需要先创建surface才能使用canves绘制，如果没有surface，就永远不会有canves。

然后在callback方法中调用该方法，初始化游戏界面

```
SurfaceHolder.Callback callback = new SurfaceHolder.Callback(){//建立CallBack内部类

        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
            redraw();
        }

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

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

        }
    };
```

### 响应用户的点击

当用户点击灰色的点时，灰色变为红色的路障，神经猫移动一次。 用户点击点阵以外的背景，进行游戏初始化。 用户点击其他点不做处理。 通过判断用户点击的坐标所落得位置，来做相应的颜色处理。 要想响应用户的点击，首先要实现OnTouchListener 并 重写OnTouch方法。

判断用户点击了哪一个元素，并作出相应的处理：

```
public boolean onTouch(View view, MotionEvent motionEvent) {
        Log.i("INFO---", "调用了onTouch方法  触发了监听");
            if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
                int x,y;
                y = (int) (motionEvent.getY()/DOT_WIDTH );
                if (y % 2 == 0) { 
                    x = (int) (motionEvent.getX()/DOT_WIDTH );
                }else {
                    x = (int) ((motionEvent.getX()-DOT_WIDTH /2)/DOT_WIDTH );
                }

                if (x >= COL || y >= ROW) {  //用户点击点阵以外的背景，进行游戏初始化
                    initGame();
                } else {
                    if (getDot(x, y).getStatus() == Dot.STATUS_ON) {
                        getDot(x, y).setStatus(Dot.STATUS_OFF);
                        move(); //符合条件，神经猫移动一次
                    }
                }
                reDraw();
            }
            return true;
        }
```

### move()方法

```
给神经猫一个最优的行走路径
```

我们通过返回cat到边缘的距离或障碍物的距离，来选出最优路径

![getDistance](/files/2irytOSQDkEux2k3BbA6)

由图知，神经猫具有6个行走方向，我给他们设置了1-6编号

![神经猫的行走方向](/files/xxLuV8oXglW0FQzomNeC)

如果该方向处于边缘，返回1 如果该方向上没有障碍物，返回正数 如果有返回负数

移动cat到dot点：

```
	private void moveTo(Dot dot) {
        //注意：数组中的对象是不能改变的
        dot.setStatus(Dot.STATUS_IN);
        getDot(cat.getX(), cat.getY()).setStatus(Dot.STATUS_ON);//把猫的位置变为ON
        cat.setXY(dot.getX(), dot.getY());
    }
```

判断一点是否在边界：

```
	private boolean isAtEdge(Dot dot) {
        if (dot.getX() * dot.getY() == 0 || dot.getX() == COL-1 || dot.getY() == ROW-1) {
            return true;
        }else
            return false;
    }
```

根据方向获取cat某一方向上的相邻点：

```
	private Dot getNeighbor(Dot dot,int dir) {
        switch (dir) {
            case 1:
                return this.getDot(dot.getX() - 1, dot.getY());
            case 2:
                if (dot.getY() % 2 == 0) {  //这里不用判断周围的点是否存在 因为除了边界的点，其他的点都存在六个相邻点
                    return this.getDot(dot.getX() - 1, dot.getY() - 1);
                } else {
                    return this.getDot(dot.getX(), dot.getY() - 1);
                }
            case 3:
                if (dot.getY() % 2 == 0) {  //这里不用判断周围的点是否存在 因为除了边界的点，其他的点都存在六个相邻点
                    return this.getDot(dot.getX(), dot.getY() - 1);
                } else {
                    return this.getDot(dot.getX() + 1, dot.getY() - 1);
                }
            case 4:
                return this.getDot(dot.getX()+1, dot.getY());
            case 5:
                if (dot.getY() % 2 == 0) {  //这里不用判断周围的点是否存在 因为除了边界的点，其他的点都存在六个相邻点
                    return this.getDot(dot.getX(), dot.getY() + 1);
                } else {
                    return this.getDot(dot.getX() + 1, dot.getY() + 1);
                }
            case 6:
                if (dot.getY() % 2 == 0) {  //这里不用判断周围的点是否存在 因为除了边界的点，其他的点都存在六个相邻点
                        return this.getDot(dot.getX() - 1, dot.getY() + 1);
                } else {
                    return this.getDot(dot.getX(), dot.getY() + 1);
                }
            default:return null;
        }
    }
```

getDistance 获取该方向上的距离

```
	private int getDistance(Dot dot, int dir){
        int distance = 0;
        if(isAtEdge(dot)){    // 如果该点在屏幕边缘，直接返回distance即可
            return 1;
        }
        Dot ori = dot,next;
        while (true) {
            next = getNeighbor(ori, dir);     // 将当前点d的某方向的邻居赋值给next
            if(next.getStatus() == Dot.STATUS_OFF){  // 碰到了路障，返回0或负数
                return distance*-1;
            }
            if(isAtEdge(next)){     // 抵达了场景边缘，返回正数
                distance++;     
                return distance;
            }
            distance++;     // 距离自增
            ori = next;     //向该方向移动一次
        }
    }
```

判断玩家输赢：

```
	private void win() {
        Toast.makeText(getContext(), "你赢了", Toast.LENGTH_LONG).show();
    }
    private void lose() {
        Toast.makeText(getContext(), "你输了", Toast.LENGTH_LONG).show();
    }
```

move 根据距离选择最佳路径：

```
private void move() {
    if (isAtEdge(cat)) {
        lose();return;
    }
    Vector<Dot> avaliable = new Vector<Dot>();//cat可走的neighbor点
    Vector<Dot> positive = new Vector<Dot>(); //返回为正数的neighbor点 即无路障的点
    HashMap<Dot, Integer> al = new HashMap<Dot, Integer>(); 
    for (int i = 1; i < 7; i++) {
        Dot n = getNeighbor(cat, i);
        if (n.getStatus() == Dot.STATUS_ON) {
            avaliable.add(n);   
            al.put(n, i);
            if (getDistance(n, i) > 0) {
                positive.add(n);
            }
        }
    }
    if (avaliable.size() == 0) {
        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 = 0;
            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);
    }
}
```

源码网上一大堆，我就不上传了，欢迎大家指出错误或提出建议.........


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yangsx95.gitbook.io/notes/front-end/android/android-wei-zhu-shen-jing-mao-de-shi-xian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
