Android 开发第四弹:围住神经猫(简单Demo)

前言

如下图所示,这篇文章要完成的就是这个简单的示例,后续会继续添加上动画和声音。这里主要包含了游戏的一些简单元素和逻辑。

在我的多次尝试后发现想赢它还是挺难的……毕竟它的走法不是简简单单的Random而已。

代码已经上传至Github,建议大家直接Fork而不是Download,毕竟开源的意义在于彼此分享代码,而且这个太简单了,后续肯定还会继续更新的,所以……

游戏背景元素的定义

由于代码还会继续更新,所以博客中只是简单的介绍一下,而且我都写好了注释。

如大家所见的,背景中有许多的点,这里就定义一个Dot类即可。

关于Dot类,它显然应该有坐标X、Y,还有一个状态,那么这个状态具体有哪些呢?

1,点击后开启路障,这里设为红色
2,未点击时路障为关闭状态,这里设为灰色
3,神经猫所在的位置自然也应该是一个状态,毕竟这个位置玩家不能再去点击了

然后设置相应的构造函数。

package myapplication.nomasp.com.catchcrazycat;

/**
 * Created by nomasp on 2015/09/25.
 */

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 void setXY(int x, int y){
        this.x=x;
        this.y=y;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

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

    public int getStatus() {
        return status;
    }

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

初始化游戏

游戏的背景等我们全都放在Playground类中。

在初始化游戏之前,我们首先需要定义一个Dot类型的二维数组用于保存这些游戏中的原点,我这里就称之为游戏元素好了。

然后还需要为神经猫实例化,它本质上也是Dot类型的。

    private Dot matrix[][];  // 声明数组matrix来保存游戏元素
    private Dot cat;    // 猫

当然了,行与列,还有路障,这些都可以设置为static且final的,如果你希望在游戏中设置不同的难度对应不同的行列和路障后续可以再修改。

    private static final int COL = 10;   // 每列有10个元素
    private static final int ROW = 10;   // 每行有10个元素
    private static final int BLOCKS = 15;   // 默认添加的路障数量

下面就是初始化游戏的方法,最后的输出是测试使用的,为了方便我这种初学者的学习也就没有删掉。

   private 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(ROW/2,COL/2);   // 初始化猫的位置
        getDot(ROW / 2, COL / 2).setStatus(Dot.STATUS_IN);  // 初始化猫所在位置的状态
        for(int i = 0; i < BLOCKS;){
            int x = (int)((Math.random()*1000)%COL);  // x为横坐标,数组中的列
            int y = (int)((Math.random()*1000)%ROW);  // y为纵坐标,数组中的行
            if(getDot(x,y).getStatus() == Dot.STATUS_OFF){   // 如果当前Dot的状态为OFF
                getDot(x,y).setStatus(Dot.STATUS_ON);        // 则将其打开,并让i自增
                i++;
                //System.out.println("BLOCKS+"+i);
            }
        }
    }

元素的宽度这里也可以一并声明好:

private static int WIDTH;

关于宽度的设置有两种方法:

方法一就是在MainActivity类中获取屏幕的分辨率。

public static int screenWidth = 0;
// 使用Playground类中提到的根据屏幕设置元素宽度的方法一
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenWidth = metrics.widthPixels;

然后在Playground类中进行设置。

private static final int WIDTH = (int)(MainActivity.screenWidth*0.8)/10;

方法二到下文中继续介绍。

重绘、回调以及触控

首先为Playground类扩展SurfaceView以及实现OnTouchListener。

下面就是重绘背景的方法了。记得为paint设置抗锯齿,后面根据不同状态的Dot设置不同的颜色,最后在drawOval方法类实例化一个RectF即可。

   private void redraw(){
        Canvas c = getHolder().lockCanvas();  // 先上锁
        c.drawColor(Color.LTGRAY);   // 为canvas设置为青色
        Paint paint = new Paint();  // 开始绘制到屏幕上
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);  // 抗锯齿
        for (int i = 0; i < ROW; i++){
            int offset = 0;     // 设置偏移量
            if(i%2 != 0){   // i为奇数表示是第2,4,6……行(索引为1,3,5……)
                offset = WIDTH/2;   // 偏移量为元素的宽度的一半
            }
            for (int j = 0; j < COL; j++){
                Dot one = getDot(j,i);
                switch (one.getStatus()){
                    case Dot.STATUS_OFF:
                        paint.setColor(0xFFEEEEEE);
                        break;
                    case Dot.STATUS_IN:
                        paint.setColor(0xFFFF0000);
                        break;
                    case Dot.STATUS_ON:
                        paint.setColor(0xFFFFAA00);
                        break;
                    default:
                        break;
                }
                c.drawOval(new RectF(one.getX()*WIDTH+offset,
                        one.getY()*WIDTH,
                        (one.getX()+1)*WIDTH+offset,
                        (one.getY()+1)*WIDTH),paint);    // 大小由屏幕宽度决定
            }
        }
        getHolder().unlockCanvasAndPost(c);   // 解锁
    }

然后就可以对它进行回调了。看,这里就是前文中所说的设置WIDTH的第二种方法,巧妙的利用了surfaceChanged中传入的width参数。

    Callback callback = new Callback() {    // 回调方法
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            redraw();  // 调用redraw进行重绘
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            WIDTH = width/(COL+1);
            redraw();   // 修改宽度后进行重绘
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {

        }
    };

以下就开始写触控个功能了,直接重载onTouch方法即可。

注释已经写的非常详细了,和上面一样,测试代码为了学习方面也保留了。

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_UP){
            //Toast.makeText(getContext(),event.getX()+":"+ event.getY(),
            //        Toast.LENGTH_SHORT).show();
            int x,y;    // 触摸的X、Y坐标
            y = (int)(event.getY()/WIDTH);
            if(y%2 == 0){   // 表示第1、3、5……行元素(索引为0、2、4……)
                x = (int)(event.getX()/WIDTH);
            }else{
                x = (int)((event.getX()-WIDTH/2)/WIDTH);    // 事件获取的X坐标减去左边空余的WIDTH/2部分,再除以WIDTH
            }
            // 以下代码对坐标进行保护,如果没有这些代码当点击位置超出坐标范围后也就是超出了数组索引
            if(x+1 > COL || y+1 > ROW){     // 超出边界,当前点击无效
                // 以下为测试代码
//                System.out.println("------------------------");
//                getNrighbour(cat,k).setStatus(Dot.STATUS_IN);
//                k++;
//                redraw();
                initGame();
                // 以下为测试代码
//                System.out.println("------------------------");
//                for(int i = 1;i < 7; i++){
//                    System.out.println(i+"@"+getDistance(cat,i));

            } else if(getDot(x,y).getStatus() == Dot.STATUS_OFF){  // 当这个点可用时
                getDot(x,y).setStatus(Dot.STATUS_ON);   // 点击有效,开启路障状态
                move();
            }
            redraw();   // 重绘
        }
        return true;
    }

噢对了,前面用到的getDot方法还没有说,这是由于上面的X、Y是触控事件的X、Y坐标,但对应到数组中X就成了列,Y则成了行,而二维数组是行在前、列在后的,所以就为了方便起见就特意设置了一个方法。

   private Dot getDot(int x, int y){  // 传入x,y返回matrix[y][x]
        return matrix[y][x];
    }

当然,你也可以直接写成matrix[y][x]。

下面这个其实是一开始就写的,只是其中用到的一些东西是逐个补充上去,所以这里放在了后面。

   public Playground(Context context) {
        super(context);
        getHolder().addCallback(callback);  // 为getHolder设置回调
        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);  // x表示行,y表示列,因此和数组的COL/ROW是错开的
            }
        }
        setOnTouchListener(this);  // 该类已经实现了OnTouchListener接口,因此只用传入this即可
        initGame();
    }

获取距离等一些功能性方法

首先,游戏需要判断神经猫等是否处于边缘部分,作用不言而喻。它自然需要的是一个Dot类型的参数。

    private boolean isAtEdge(Dot d){
        if(d.getX()*d.getY() == 0 || d.getX()+1 == COL || d.getY()+1 == ROW){   // 此时处于游戏边界
            return true;
        }
        return false;
    }

接下来这个就要好好理一理关系了,有了下面的图片示例就很好理解了,按顺时针计算就好。

    private Dot getNeighbor(Dot d, int dir){ // 从水平相连的左侧点开始为1,依次顺时针计数
        switch (dir){
            case 1:
                return getDot(d.getX()-1,d.getY());
            case 2:
                if(d.getY()%2 == 0){
                    return getDot(d.getX()-1,d.getY()-1);
                }else{
                    return getDot(d.getX(),d.getY()-1);
                }
            case 3:
                if(d.getY()%2 == 0){
                    return getDot(d.getX(),d.getY()-1);
                }else{
                    return getDot(d.getX()+1,d.getY()-1);
                }
            case 4:
                return getDot(d.getX()+1,d.getY());
            case 5:
                if(d.getY()%2 == 0){
                    return getDot(d.getX(),d.getY()+1);
                }else{
                    return getDot(d.getX()+1,d.getY()+1);
                }
            case 6:
                if(d.getY()%2 == 0){
                    return getDot(d.getX()-1, d.getY()+1);
                }else{
                    return getDot(d.getX(),d.getY()+1);
                }
            default:
                return null;
        }
    }

以下这个方法用于计算距离:

如果距离某个方向路障中间有2个灰色(OFF)则返回-2;
如果紧挨该方向的路障,则返回0;
如果该方向没有路障,则返回至边界中间的灰色元素个数,为正数。
    private int getDistance(Dot d, int dir){
        int distance = 0;
        if(isAtEdge(d)){    // 如果该点已经在屏幕边缘,那么就不必继续判断直接返回distance即可
            return 1;
        }
        Dot ori = d,next;
        while (true) {
            next = getNrighbour(ori,dir);     // 将当前点d的某方向的邻居赋值给next
            if(next.getStatus() == Dot.STATUS_ON){  // 碰到了路障,返回0或负数
                return distance*-1;
            }
            if(isAtEdge(next)){     // 抵达了场景边缘,返回正数
                distance++;     // 说明下一个点也是可用的
                return distance;
            }
            distance++;     // 距离自增
            ori = next;     // 将next设置为当前参考点
        }
    }

胜负已分

由于这只是一个简单的Demo,所以胜负就用Toast通知好了,具体的后续继续完善,也欢迎大家贡献代码。

    private void lose(){    // 游戏失败
        Toast.makeText(getContext(),"Lose",Toast.LENGTH_SHORT).show();
    }

    private void win(){     // 游戏成功
        Toast.makeText(getContext(),"Win",Toast.LENGTH_SHORT).show();
    }

负责神经猫移动的方法

好吧,我承认移动这一部分正在写代码的时候顺序不在最后的,这是这里为重点就放在最后介绍了。

按功能分为两步,移动以及移动到,当然了move中肯定是要调用MoveTo的,只不过后者侧重于神经猫的动作,前者侧重于神经猫的走法。

private void MoveTo(Dot d){     // 移动神经猫到某个点
   d.setStatus(Dot.STATUS_IN);
   getDot(cat.getX(),cat.getY()).setStatus(Dot.STATUS_OFF);
   cat.setXY(d.getX(),d.getY());
}

下面就来写move方法,为了方面学习,这里按步骤进行。

1,判断神经猫是否处于边界,是则游戏失败,不进行move,直接return。

if(isAtEdge(cat)){  // 判断猫是否在场景边界
    lose();     // 游戏失败
    return;
}

而后判断周围的6个元素是否可以走(简单的说就是:是否可用)。

for(int i = 1; i < 7; i++){
    Dot n = getNeighbor(cat, i);
    if(n.getStatus() == Dot.STATUS_OFF){    // 如果这邻居(点)可用
         avaliable.add(n);      // 将其添加到avaliable中
         length.put(n,i);    // i为方向
         if(getDistance(n,i) > 0){   // 正数表示可以到达边界
             positive.add(n);
         }
     }
}

上面实例化了一个n出来,当然你也可以直接写成:

if(getNeighbor(cat,i).getStatus() == Dot.STATUS_OFF)

如果它的邻居的状态是关闭的,那么也就意味着这一点是可用的,于是添加到avaliable中。在for循环之前就应该定义好它们:

Vector<Dot> avaliable = new Vector<>();     // 当前点周围6个点中的可用点
Vector<Dot> positive = new Vector<>();  // 当前点到边界的距离点
HashMap<Dot,Integer> length = new HashMap<Dot,Integer>();// 搭配positive,其用于记录方向

如果没有可用点,就意味着已经成功围住了神经猫;如果只有一个可用点,就直接移动过去就好了。

if(avaliable.size() == 0){      // 没有可用点
    win();      // 成功围住神经猫
}else if(avaliable.size() == 1) {     // 只有一个可用点
    MoveTo(avaliable.get(0));
}

那么最关键的部分来了,如果有多条路可以走怎么办?

else{      // 既不是第一次点击,且有多条路可走
   // 根据到边界edge的距离distance(包括路障等的计算)来决定走的方向
   Dot best = null;   // 最终决定要移动到的元素(点)
   if(positive.size() != 0){   // 含有可到达边界的方向
      System.out.println("向前进");
      int min = 999;  // 用于记录到达边界的最小值,初始值为一个较大值
      for(int i = 0; i < positive.size(); i++){
         int tempDis = getDistance(positive.get(i),length.get(positive.get(i)));
         if(tempDis < min) {
              min = tempDis;
              best = positive.get(i);
         }
      }
   }else{  // 所有方向都有路障
      System.out.println("躲路障");
      int max = 0;    // 所有方向都有路障时,距离要么为负数要么为0
      for(int i = 0; i < avaliable.size(); i++){
         int tempDis = getDistance(avaliable.get(i),length.get(avaliable.get(i)));
         if(tempDis < max) {   // 因为tempDis是负数,所以用小于号
            max = tempDis;
            best = avaliable.get(i);
         }
      }
  }

如果各个方向都没有路障,就选择路径最短的一个,如果有路障就要躲避这些路障。这里分别用min和max来记录最佳的路径,最后就将其赋值给best:

best = avaliable.get(i);

为了防止best为空时不知道怎么走,就为它设置一个随机路径好了。

if(best == null) {      // 当best为空时,随机一个路径
   int s = (int) ((Math.random() * 1000) % avaliable.size());
   MoveTo(avaliable.get(s));
}else{
   MoveTo(best);   // 移动到最合适的一点
}

欢迎大家pull request

我都会上传到Github上,且都会有注释,为了帮助到更多人,欢迎大家Fork和Star,更欢迎大家pull request,地址为:传送门

时间: 2024-10-30 20:13:37

Android 开发第四弹:围住神经猫(简单Demo)的相关文章

Android 开发第六弹:简易时钟(计时器)

接上篇Android 开发第五弹:简易时钟(闹钟) ,这次是一个时钟类应用,目前依旧是主要的功能,长得还是很挫.当然了,核心功能是有的-- 时钟 先把简单的时钟给列出来吧,这里都写的很简单,即便要用世界各个城市的话,也只是相应的加上或减去几个小时. 新建TimeView类,并扩展自LinearLayout,然后布局文件和上一篇中那么写就好了. <myapplication.nomasp.com.clock.TimeView android : id = "@+id/tabTime"

Android 开发第七弹:简易时钟(秒表)

本文承接,Android 开发第五弹:简易时钟(闹钟) 和 Android 开发第六弹:简易时钟(计时器),这一部分是关于秒表的. 布局 同样是新建一个类(StopWatchView)并扩展自LinearLayout,并将其用作布局. <myapplication.nomasp.com.clock.StopWatchView android : id = "@+id/tabStopWatch" android : layout_width = "match_parent

Android 开发第五弹:简易时钟(闹钟)

这次是一个时钟类应用,目前依旧是主要的功能,长得还是很挫.当然了,核心功能是有的-- 闹钟之前的准备 布局的话,不管是采用FrameLayout或者LinearLayout都可以. 我这里采用了FrameLayout,然后加上一个TabHost,之前在论坛看到有同学提问在WF中这种多个栏目的用什么控件,我的答案是在WF.WPF.Windows App.ASP.NET以及安卓上都是Tab开头的控件. <FrameLayout xmlns:android="http://schemas.and

Android 开发第三弹:自定义左右菜单(滑动动画+蒙版效果)

下面的截图--哎,因为1080P在Windows 10上虽然适配了,但大部分软件并没有跟上,比如某个录制GIF的软件,所以这里有一定的偏移导致画面不完整,但效果大概就是这么一个效果了. MainUI.java 首先需要这么一个类,在这里一些UI的滑动呀之类的都会定义.首先吧,定义好这些变量,当然了,实际开发过程中肯定需要哪一个就添加上哪一个的. private Context context; // 上下文 private FrameLayout leftMenu; // 左边部分 privat

单机搭建Android开发环境(四)

单机搭建安卓开发环境,前三篇主要是磨刀霍霍,这一篇将重点介绍JDK.REPO.GIT及编译工具的安装,下载项目代码并编译.特别说明,以下操作基于64位12.04 Server版Ubuntu.若采用其他版本系统,请参考Google官方操作步骤.访问不了的同学,请找大师(蓝灯),他能助你一臂之力. 1)安装OpenJDK 7 sudo apt-get install openjdk-7-jdk --force-yes –y   安装OpenJDK8        sudo add-apt-repos

Android开发环境安装和配置图文教程_Android

一.安装JDK.SDK.NDK 无论是用C#和VS2015开发Androd App还是用Java和Eclipse开发Androd App,都需要先安装JDK和Android SDK.把这些安装完毕后,再在开发工具中配置一下安装的路径就行了. 1.安装jdk 下面两个都要安装: jdk-6u45-windows-i586.exe jdk-7u79-windows-i586.exe 到网上找到对应的文件后下载运行即可,Oracle的官网上有,免费下载. 2.安装Android SDK 下载下面的An

Android开发环境安装和配置图文教程

一.安装JDK.SDK.NDK 无论是用C#和VS2015开发Androd App还是用Java和Eclipse开发Androd App,都需要先安装JDK和Android SDK.把这些安装完毕后,再在开发工具中配置一下安装的路径就行了. 1.安装jdk 下面两个都要安装: jdk-6u45-windows-i586.exe jdk-7u79-windows-i586.exe 到网上找到对应的文件后下载运行即可,Oracle的官网上有,免费下载. 2.安装Android SDK 下载下面的An

Android开发中使用SQLite 数据库

SQPte 一个非常流行的嵌入式数据库,它支持 SQL 语言,并且只利用很少的内存就有很好的性能.此外它还是开源的,任何人都可以使用它.许多开源项目((Mozilla, PHP, Python)都使用了 SQPte. SQPte 由以下几个组件组成:SQL 编译器.内核.后端以及附件.SQPte 通过利用虚拟机和虚拟数据库引擎(VDBE),使调试.修改和扩展 SQPte 的内核变得更加方便. 图 1. SQPte 内部结构 开发中使用SQLite 数据库-android sqlite数据库"&g

Android开发使用 SQLite 存储数据

一.创建SQLite数据库和表 我们可以通过SQLiteDatabase.openOrCreateDatabase()来创建一个数据库实例. SQLiteDatabase db = openOrCreateDatabase(dbName, MODE_PRIVATE, null);// openOrCreateDatabase(String name, int mode, CursorFactory factory)// 第一个参数为创建数据库的名称// 第二个参数为创建数据库的权限,其权限同内部