helloPe的android项目实战之连连看—实现篇(二)
文接上回,之前介绍了项目的架构,进行了功能的分析,同时进行了BoardView类及时间控制类的开发及几个几口的介绍。这次我们将完整的实现游戏棋盘的绘制与touch事件的处理,以及游戏核心算法中连接算法、hint自动帮助算法与判断是否无解算法的实现。这些代码的处理都在继承自BoardView类的GameView类中。
首先在GameView类中添加实现本游戏主要算法的代码,即连接算法的代码(用于判断给定的两个位置的图标能够相连通):
/** * 本游戏的核心算法,判断两个连接点是否能够连接,这里传进来的就是我们点击的两个点转化成index的值 * @param p1 * @param p2 */ List<Point> p1Expand = new ArrayList<Point>(); List<Point> p2Expand = new ArrayList<Point>(); public boolean link(Point p1,Point p2){ if(p1.equals(p2)){ return false; } path.clear(); if(map[p1.x][p1.y] == map[p2.x][p2.y]){ if(linkDirect(p1,p2)){ path.add(p1); path.add(p2); return true; } /** * 一个拐点的判断 */ Point px = new Point(p1.x,p2.y); //假设第一种可能点 if(map[p1.x][p2.y] == 0 && linkDirect(p1,px) && linkDirect(px,p2)){ path.add(p1); path.add(px); path.add(p2); return true; } Point py = new Point(p2.x,p1.y); //假设第二种可能点 if(map[p2.x][p1.y] == 0 && linkDirect(p1,py) && linkDirect(py,p2)){//首先判断map[p2.x][p1.y]中介点是否有图标 path.add(p1); path.add(py); path.add(p2); return true; } /** * 两个折点(corner) */ expandX(p1,p1Expand); expandX(p2,p2Expand); for(int i = 0; i < p1Expand.size(); i++) for(int j = 0; j < p2Expand.size(); j++){ if(p1Expand.get(i).x == p2Expand.get(j).x){ if(linkDirect(p1Expand.get(i),p2Expand.get(j))){ path.add(p1); path.add(p1Expand.get(i)); path.add(p2Expand.get(j)); path.add(p2); return true; } } } expandY(p1,p1Expand); expandY(p2,p2Expand); for(Point exp1:p1Expand) for(Point exp2:p2Expand){ if(exp1.y == exp2.y){ if(linkDirect(exp1,exp2)){ path.add(p1); path.add(exp1); path.add(exp2); path.add(p2); return true; } } } return false; //最后三种方式都不能连通,还是要return false ,不然在两个同样的图标下却没有返回值! } return false; } /** * 判断直线链接,无拐角,传进来的点值是ScreenToIndex过的了,不过这里传进来的不一定就是我们点击的点,也可能是我们的拐角点(辅助点) * @param p1 * @param p2 */ public boolean linkDirect(Point p1,Point p2){ //if(map[p1.x][p1.y] == map[p2.x][p2.y]){ //纵向直线 if(p1.x == p2.x){ int y1 = Math.min(p1.y, p2.y); int y2 = Math.max(p1.y, p2.y); boolean flag = true; for(int y = y1 + 1; y < y2; y++){//这个循环里容易漏掉两个相邻的情况,所以才加上上面的flag样式 if(map[p1.x][y] != 0){ flag = false; break; } } if(flag){ return true; } } //横直线判断 if(p1.y == p2.y){ int x1 = Math.min(p1.x, p2.x); int x2 = Math.max(p1.x, p2.x); boolean flag = true; for(int x = x1 + 1; x < x2; x++){ if(map[x][p1.y] != 0){ flag = false; break; } } if(flag){ return true; } } //} return false; } /** * 向x方向扩展,传进来的点是index过的 * @param p * @param list */ public void expandX(Point p,List<Point> list){ list.clear(); for(int x = p.x + 1; x < xCount; x++){//注意此时可以等于xCount -1了 if(map[x][p.y] != 0) break; list.add(new Point(x,p.y)); } for(int x = p.x -1; x >= 0; x--){ if(map[x][p.y] != 0) break; list.add(new Point(x,p.y)); } } /** * 向Y方向扩展,传进来的点是index过的,而list是作为“返回值”需要保存的值 * @param p * @param list */ public void expandY(Point p,List<Point> list){ list.clear(); for(int y = p.y + 1; y < yCount; y ++){ if(map[p.x][y] != 0) break; list.add(new Point(p.x,y)); } for(int y = p.y -1 ; y >= 0; y--){ if(map[p.x][y] != 0) break; list.add(new Point(p.x,y)); } }
代码中尽量添加注释,此段代码中实现了第一篇文章中进行的算法分析,其中link(Point p1,Point p2)函数作为算法真正的完整实现者,算法的主逻辑有它实现,
linkDirect(Point p1,Point p2)函数作为一个工具函数,用于判断给定的两个位置(注意不是两个图标,因为给定的位置不一定含有图标,当我们在判断
”一折型“和“二折型”的情况的时候即使如此)。而expandX(Point p,List<Point> list)与 expandY(Point p,List<Point> list)两个方法的同样作为工具函数,
在判断“二折型”情况时候将会使用,也就是前面所说的“横向扫描”与“纵横扫描”。而对于link(Point p1,Point p2)函数中,我们的逻辑还是将大问题化为小问题处理。
最终还是分解到调用linkDirect(Point p1,Point p2)函数来进行“直线型”的处理。
以上即是程序的连接算法的实现,除了程序算法逻辑的理解之外,还需注意在判断的时候,若能够连通,我们已经将
private List<Point> path = new ArrayList<Point>();保存连通路径的path附上值,记得当link函数返回true时,path中即保存了一条相通的路径!完成了
连接算法,下一步我们将依赖于连接算法的实现,完成扫描是否当前地图已经出现无解的情况,因为程序的地图是随机生成的,难免有时候会出现无解的情况;
下面我们将实现判断是否处于无解状态,实现函数:
/** *用于判断是否当前已经无解 */ public boolean die(){ for(int y= 1; y < yCount; y++) //表示从此行中的一个元素开始扫描(起点) for(int x = 1; x < xCount; x++){ //表示此行中指定列,组成扫描起点 if(map[x][y] != 0){ for(int j = y; j < yCount; j++){//表示正在被扫描的行 if(j == y){//循环中的第一次扫描,为什么特殊?因为此时不一定从一行中的第一个元素开始扫描 for(int i = x + 1; i < xCount - 1; i++){ if(map[x][y] == map[i][j] && link(new Point(x,y),new Point(i,j))){ return false; } } }else{ for(int i = 1; i < xCount -1; i++){ if(map[x][y] == map[i][j] && link(new Point(x,y),new Point(i,j))) return false; } } } } } return true; }
代码中也有相应注释,每一次判断相当于一次遍历棋盘,同时注意,如果die()函数返回为false,这则证明link()函数返回了true!前面已经提醒过:当link返回true时,我们用于保存连通路径的path对象中已经保存了一条连通路径的点的集合,只不过在die()函数中运行得到的是按遍历顺序而来的,并不是我们所指定的两个始点与终点两个图标;所以在这儿,可以借die()的判断,完成我们算法实现的第三个功能,即hint的自动帮助!
/** * 当点击help按钮时候调用,会帮助玩家消除一对图标 */ public void autoHelp(){ if(help == 0){ //soundPlay.play(ID_SOUND_ERROR, 0); return ; }else{ //soundPlay.play(ID_SOUND_TIP, 0); help--; toolsChangedListener.onTipChanged(help); drawLine(path.toArray(new Point[] {})); refreshHandler.sendRefresh(500); } }
当然此处需要介绍一下最后一行代码的来历:
class RefreshHandler extends Handler{ @Override public void handleMessage(Message msg) { super.handleMessage(msg); if(msg.what == REFRESH_VIEW){ GameView.this.invalidate(); if(win()){ setMode(WIN); isStop = true; isContinue = false; }else if(die()){ //调用一次die方法!此时如果die返回为false,即还能够连通 change(); //由于die中使用link方法检测,所以此时path中的值又添加了进去, } //这对于我们使用autoHelp方法提供便利!!! } } /** * * @param delayTime */ public void sendRefresh(int delayTime){ Message msg = new Message(); this.removeMessages(0); msg.what = REFRESH_VIEW; this.sendMessageDelayed(msg, delayTime); } }
当然对于是否已经为赢了的判断win()函数比较简单,就是扫描棋盘,如果所有位置map值都为了0,即赢了,若不是,还未完成;这里就不贴代码了。
GameView类中还有一个职能就是初始化一张棋盘:
/** * 初始化地图 */ public void initMap(){ int x = 1; int y = 0; for(int i = 1; i < xCount -1; i++) for(int j =1; j < yCount -1; j++){ map[i][j] = x; if(y == 1){ x ++; y = 0; if(x == iconCounts){ x = 1; } }else{ y = 1; } } change(); GameView.this.invalidate(); }
我们初始化棋盘时,利用前面讲解的初始算法技术,遍历棋盘,先将棋盘填满,但是填满首先还有一个规则就是每一种图标的填入必须同时填入两张,是为每种图标都为偶数个而设定!介绍一下最后调用的change()函数,也是出自于第一篇的棋盘初始算法,用于随机将棋盘中的图标打乱:
/** * 随机将现有的布局打乱,重新布局,map中现有图标数量不变,相当于一次refresh */ public void change(){ Random random = new Random(); int tmp,xtmp,ytmp; for(int x = 1;x < xCount -1; x++){ for(int y = 1; y < yCount -1; y++){ xtmp = 1 + random.nextInt(xCount -2); ytmp = 1 + random.nextInt(yCount - 2); tmp = map[x][y]; map[x][y] = map[xtmp][ytmp]; map[xtmp][ytmp] = tmp; } } if(die()){ //如出现无解情况,即需要再次随机重新打乱 change(); } }
GameView类还是一个View,在此类中我们还要重写View的onTouchEvent方法:
/** * 对于选择的处理,如果是第一次按下,则将其加入到selected当中, * 若是第二次(selected.size()==1),则先判断能不能连通 */ @Override public boolean onTouchEvent(MotionEvent event) { int sx = (int)event.getX(); int sy = (int)event.getY(); Point p = screenToIndex(sx, sy); if(map[p.x][p.y] != 0){ if(selected.size() == 1){ if(link(selected.get(0),p)){ //能够连通,path中的数据是在link判断时如果返回真,方法内部就已经将数据添加进去 selected.add(p); drawLine(path.toArray(new Point[]{})); refreshHandler.sendRefresh(500); }else{ //不能够连通 selected.clear(); selected.add(p); GameView.this.invalidate(); //在这儿说一下refreshHanler.sendRefresh(int) 跟单纯调用GameView.this.invalidate()区别 //前者除了后者只拥有的刷新显示之外,还加了是否已经无解跟是否已经完成任务的判断的操作。 } }else{//此时的selected中的size只能等于0 selected.add(p); GameView.this.invalidate(); } } return super.onTouchEvent(event); }
方法中用到的selected是BoardView中的protected List<Point> selected = new ArrayList<Point>();代码中对于功能及实现有相应的注释。
到此我们可以提供接口startGame以供在程序的activity中调用:
public void startPlay(){ help = 3; refresh = 3; isContinue = true; isStop = false; toolsChangedListener.onRefreshChanged(refresh); toolsChangedListener.onTipChanged(help); leftTime = totalTime; initMap(); refreshTime = new RefreshTime(); Thread t = new Thread(refreshTime); //注意正确启动一个实现Runnable接口的线程类 t.start(); GameView.this.invalidate(); }
注意GameView中并没有实现相关的自定义的接口,而是我们将会在程序的activity中实现项目中涉及的三个接口,但是,我们可以在GameView中进行注册:
public void setOnTimerListener(OnTimerListener onTimerListener){ this.timerListener = onTimerListener; } public void setOnToolsChangedListener(OnToolsChangeListener toolsChangeListener){ this.toolsChangedListener = toolsChangeListener; } public void setOnStateChangeListener(OnStateListener stateListener){ this.stateListener = stateListener; }
然后在程序的activity中调用GameView的相关函数进行初始化注册。这样,根据多态性的原理,在GameView当中调用的相关接口中的函数,也就是activity中实现的接口中的函数。这也是android程序中interface实现与注册的一种方式。
以上已经基本描述了GameView的功能与最主要的实现。总结一下,实现了map的初始化,重写了touch时间的处理函数,完成了程序的连接算法,hint自动帮助算法,die的无解判断算法,还有用于更新显示的继承自Handler的内部类的实现。整个项目也已经基本成型了。
重申一下:之所以写本系列的文章,为了记录android小项目的经历,增加实战的能力,做个总结。并不是为了做出多么新颖的项目,当然也是向不少的网友学习了的!