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的状态为OFFgetDot(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() { // 回调方法@Overridepublic void surfaceCreated(SurfaceHolder holder) {redraw(); // 调用redraw进行重绘}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {WIDTH = width/(COL+1);redraw(); // 修改宽度后进行重绘}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}};
以下就开始写触控个功能了,直接重载onTouch方法即可。
注释已经写的非常详细了,和上面一样,测试代码为了学习方面也保留了。
@Overridepublic 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的某方向的邻居赋值给nextif(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; // 所有方向都有路障时,距离要么为负数要么为0for(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,地址为:传送门。
Android 开发第四弹:围住神经猫(简单Demo)相关推荐
- Android 开发第七弹:简易时钟(秒表)
本文承接,Android 开发第五弹:简易时钟(闹钟) 和 Android 开发第六弹:简易时钟(计时器),这一部分是关于秒表的. 布局 同样是新建一个类(StopWatchView)并扩展自Line ...
- 学习Mac开发第四弹 通过NSImageView加载图片
学习Mac开发第四弹 通过NSImageView加载图片 创建NSImageView 1 . 拖拽大法好 Autolayout 创建一个宽高160 *160 的NSImageView 创建是正方形的 ...
- android intent例程,Android开发(四)| 探究活动(详解Intent+大量实例)
在Android的开发项目中,Activity(活动)是最容易吸引到用户的地方,因为相比于算法.架构,它是实际可见的. Activity是一个可以包含用户界面的组件,主要用于和用户进行交互.一个用户程 ...
- JNI开发笔记(四)--实现一个简单的JNI工程并生成so库
实现一个简单的JNI工程并生成so库 引 前言 1. 编写C/h文件并添加到工程 2. 修改CmakeLists.txt文件 3. 编写native-lib.cpp文件 4. 在MainActivit ...
- Android开发(四):在标题栏右上角实现菜单(三个点)
很多APP的右上角都有如下图的三个点 (1)方法一: 实现起来也很简单,首先按照下图的指示新建一个menu文件夹.然后在menu文件夹下新建一个menu.xml的文件.名称必须为menu. 以下为xm ...
- 微信小程序开发第四弹
1.微信小程序的页面跳转 按照微信官方文档的说法,一共有三种跳转方式: (1)wx.navigateTo()是保留当前页面,跳转到某个页面,跳转页面后可以返回上一页. 实例:wx.navigate ...
- Android 开发基于Webview 自制一个简单的手机浏览器
初衷: 虽然现在市场上浏览器很多,比如chrome,夸克,没有广告,新闻等乱七八糟的东西,页面简单清晰,但是自己的浏览记录还是存放在别人的服务器,就算删除了,开隐身模式了,但是具体后台的操作谁又知道呢 ...
- android开发 问卷调查案例_安卓 问卷调查Demo 原生代码
[实例简介] android 问卷调查Demo,android原生代码,不是webView加载H5,比较容易理解,大家可以改改 [实例截图] [核心代码] QuestionDemo └── Quest ...
- android计算器弹窗,【Android 开发-kotlin】计算器+分享页面demo
运行效果 计算(同一应用页面跳转) 分享(不同应用页面跳转) 实现横屏 内容简概 一.搭建界面 二.实现界面之间跳转和传值 三.实现隐式跳转并回调数据 四.Activity生命周期--解决横屏数据丢失 ...
最新文章
- 为什么在CSDN中同时打开两个Markdown编辑窗口会相互干扰?
- python flask 如何修改默认端口号
- LBE平行空间的技术实现
- Visual Studio 竖向选择功能
- JQuery Tree 树形结构插件 zTree
- 前端学习(2063):vue的生命周期
- 【Elasticsearch】ELASTICSEARCH集群节点的扩容(移除与添加)
- linux串口tx缓冲区大小,[Linux C]自己写的串口缓冲区
- react+socket搭建五子棋平台
- 程序员遇到什么样的工作状态应该果断跳槽走人?
- python绘制散点图的函数_python绘制散点图
- Illustrator最新教程,如何在 Illustrator 中微调图标?
- C语言中用二进制输出一个数字
- Python和C语言运行速度对比(累加1亿次)
- 工程点点app爬虫和sign算法破解
- HDU - 3237 Help Bubu (好题)
- android 解析rss,在Android中解析RSS源
- 输入法linux debin,Debian下安装搜狗拼音输入法
- 基础入门学习Python爬取微信公众号文章、标题、文章地址
- 【搬运及个人处理】联想电脑(Lenovo XiaoXinAir-14IIL 2020)散热模式Fn+Q 失效解决方案