近期工作比較忙,所以更新的慢了一点,今天的主要内容是关于Android版2048的逻辑推断,经过本篇的解说,基本上完毕了这个游戏的主体部分。
首先还是看一下,我在实现2048时用到的一些存储的数据结构。
我在实现时,为了省事存储游戏过程中的变量主要用到的是List。
比方说:List<Integer> spaceList = new ArrayList<Integer>();这个spaceList主要用于保存。全部空白格的位置,也就是空白格在GridLayout中的位置(从0到15)
对于数字格,以及格子相应的数据。我写了一个类例如以下:
package com.example.t2048; import java.util.ArrayList; import java.util.List; import android.util.Log; /** * 用于保存数字格,已经数字格相应的数字 * @author Mr.Wang * */ public class NumberList { //这个list用于保存全部不为空的格子的坐标(在GridLayout中的位置从0到15) private List<Integer> stuffList = new ArrayList<Integer>(); //这个list用于保存全部不为空的格子相应的数字(以2为底数的指数) private List<Integer> numberList = new ArrayList<Integer>(); /** * 新增加的数字格 * @param index 数字格相应的位置 * @param number 相应数字的指数(以2为底数) */ public void add(int index, int number){ stuffList.add(index); numberList.add(number); } /** * 用于推断当前位置是否为数字格 * @param index 当前位置 * @return true表示是 */ public boolean contains(int index){ return stuffList.contains(index); } /** * 将当前的格子从数字列表中去掉 * @param index */ public void remove(int index){ int order = stuffList.indexOf(index); numberList.remove(order); stuffList.remove(order); } /** * 将当前格子相应的数字升级,指数加1 * @param index */ public void levelup(int index){ int order = stuffList.indexOf(index); numberList.set(order, numberList.get(order)+1); } /** * 将当前格子相应的位置置换为新的位置 * @param index 当前位置 * @param newIndex 新的位置 */ public void changeIndex(int index, int newIndex){ stuffList.set(stuffList.indexOf(index), newIndex); } /** * 通过格子相应的位置获取其相应的数字 * @param index 当前位置 * @return 格子相应数字的指数 */ public int getNumberByIndex(int index){ int order = stuffList.indexOf(index); return numberList.get(order) ; } public String toString(){ return stuffList.toString()+numberList.toString(); } public void printLog(){ Log.i("stuffList", stuffList.toString()); Log.i("numberList", numberList.toString()); } }
这个类主要是我对数字格、数字格相应数字的保存,和增删改等操作。
事实上就是两个list,我为了操作起来方便,所以把他们写在一个类里。
然后,我们来讲一下这个游戏的逻辑。
比方,我们在游戏过程中运行了一次向右滑动的操作,在这个操作中,我们要对全部能够移动和合并的格子进行推断和对应的操作:
1、数字格的右边假设是空白格。则数字格与空白格交换
2、数字格右边假设有多个空白格,则数字格与连续的最后一个空白格做交换
3、数字格的右边假设存在与之同样的数字格,则本格置空。右边的数字格升级(指数加一)
4、假设滑动方向连续存在多个同样的数字格,右的格子优先升级
5、在一次滑动中,每一个格子最多升级一次
当一个格子存在上述前四种中的随意一种时,则完毕了对它的操作
我们试着把上面的推断规则翻译成代码,首先,明白在GridLayout中的坐标位置。我在GridLayout中採用的是水平布局,所以每一个格子相应的位置例如以下
在这个基础上,我建立例如以下的坐标轴,以左上角为原点,x轴为横轴,y轴为竖轴:
当向右滑动的时候,从上面逻辑来看,为了方便,我们应当从右向左遍历格子
for(int y=0;y<4;y++){ for(int x=2;x>=0;x--){ int thisIdx = 4*y +x; Change(thisIdx,direction); } }
每遍历到一个新的格子,运行一次change()方法。事实上应该是每遍历到一个非空的格子。运行一次change()可是我为了省事,把非空推断加到了change的代码里。我们看一下change()这种方法的实现。这种方法主要是用来推断,一个格子是须要移动、合并,还是什么都不操作。:
/** * 该方法,为每一个符合条件的格子运行变动的操作。如置换,升级等 * @param thisIdx 当前格子的坐标 * @param direction 滑动方向 */ public void Change(int thisIdx,int direction){ if(numberList.contains(thisIdx)){ int nextIdx = getLast(thisIdx, direction); if(nextIdx == thisIdx){ //不能移动 return; }else if(spaceList.contains(nextIdx)){ //存在能够置换的空白格 replace(thisIdx,nextIdx); }else{ if(numberList.getNumberByIndex(thisIdx) == numberList.getNumberByIndex(nextIdx)){ //能够合并 levelup(thisIdx, nextIdx); }else{ int before = getBefore(nextIdx, direction); if(before != thisIdx){ //存在能够置换的空白格 replace(thisIdx,before); } } } } }
当中getLast()方法,用于获取当前格子在移动方向的能够移动或者合并的最后一个格子,假设返回值还是当前的格子,则表示不能移动。当中调用的getNext()方法是为了获取当前格子在移动方向的下个格子的位置。
/** * 用于获取移动方向上最后一个空白格之后的位置 * @param index 当前格子的坐标 * @param direction 移动方向 * @return */ public int getLast(int thisIdx, int direction){ int nextIdx = getNext(thisIdx, direction); if(nextIdx < 0) return thisIdx; else{ if(spaceList.contains(nextIdx)) return getLast(nextIdx, direction); else return nextIdx; } }然后是replace(int thisIdx, int nextIdx),这种方法是运行两个格子互换位置。内容主要是对两个格子中的view更换背景图片,然后操作空白格的list和数字格的list:
/** * 该方法用来交换当前格与目标空白格的位置 * @param thisIdx 当前格子的坐标 * @param nextIdx 目标空白格的坐标 */ public void replace(int thisIdx, int nextIdx){ moved = true; //获取当前格子的view,并将其置成空白格 View thisView = gridLayout.getChildAt(thisIdx); ImageView image = (ImageView) thisView.findViewById(R.id.image); image.setBackgroundResource(icons[0]); //获取空白格的view,并将其背景置成当前格的背景 View nextView = gridLayout.getChildAt(nextIdx); ImageView nextImage = (ImageView) nextView.findViewById(R.id.image); nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(thisIdx)]); //在空白格列表中。去掉目标格。加上当前格 spaceList.remove(spaceList.indexOf(nextIdx)); spaceList.add(thisIdx); //在数字格列表中,当前格的坐标置换成目标格的坐标 numberList.changeIndex(thisIdx, nextIdx); }
/** * 刚方法用于合并在移动方向上两个同样的格子 * @param thisIdx 当前格子的坐标 * @param nextIdx 目标格子的坐标 */ public void levelup(int thisIdx, int nextIdx){ //一次移动中,每一个格子最多仅仅能升级一次 if(!changeList.contains(nextIdx)){ moved = true; //获取当前格子的view,并将其置成空白格 View thisView = gridLayout.getChildAt(thisIdx); ImageView image = (ImageView) thisView.findViewById(R.id.image); image.setBackgroundResource(icons[0]); //获取目标格的view,并将其背景置成当前格升级后的背景 View nextView = gridLayout.getChildAt(nextIdx); ImageView nextImage = (ImageView) nextView.findViewById(R.id.image); nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(nextIdx)+1]); //在空白格列表中增加当前格 spaceList.add(thisIdx); //在数字列中删掉第一个格子 numberList.remove(thisIdx); //将数字列表相应的内容升级 numberList.levelup(nextIdx); changeList.add(nextIdx); } }
为解决这两个问题,我又加了两个变量
//用于保存每次操作时。已经升级过的格子 List<Integer> changeList = new ArrayList<Integer>(); //用于表示本次滑动是否有格子移动过 boolean moved = false;
还有个波尔型的moved变量,这个也是在每次滑动前置为false,假设在本次滑动中,有格子移动或者合并,就置为ture,在滑动的最后。通过这个变量推断是否要随机生产新的格子。
以下是完整的Activity中的代码:
package com.example.t2048; import java.util.ArrayList; import java.util.List; import java.util.Random; import android.app.Activity; import android.os.Bundle; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.GridLayout; import android.widget.ImageView; public class MainActivity extends Activity { final static int LEFT = -1; final static int RIGHT = 1; final static int UP = -4; final static int DOWN = 4; GridLayout gridLayout = null; //用于保存空格的位置 List<Integer> spaceList = new ArrayList<Integer>(); //全部非空的格子 NumberList numberList = new NumberList(); //用于保存每次操作时,已经升级过的格子 List<Integer> changeList = new ArrayList<Integer>(); //用于表示本次滑动是否有格子移动过 boolean moved = false; GestureDetector gd = null; /** * 图标数组 */ private final int[] icons = { R.drawable.but_empty, R.drawable.but2, R.drawable.but4, R.drawable.but8, R.drawable.but16, R.drawable.but32, R.drawable.but64, R.drawable.but128, R.drawable.but256, R.drawable.but512, R.drawable.but1024, R.drawable.but2048, R.drawable.but4096 }; protected void onCreate(Bundle savedInstanceState) { System.out.println("程序启动"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gridLayout = (GridLayout) findViewById(R.id.GridLayout1); init(); MygestureDetector mg = new MygestureDetector(); gd = new GestureDetector(mg); gridLayout.setOnTouchListener(mg); gridLayout.setLongClickable(true); } //初始化界面 public void init(){ System.out.println("初始化"); //首先在16个各种都填上空白的图片 for(int i=0;i<16;i++){ View view = View.inflate(this, R.layout.item, null); ImageView image = (ImageView) view.findViewById(R.id.image); image.setBackgroundResource(icons[0]); spaceList.add(i); gridLayout.addView(view); } //在界面中随机增加两个2或者4 addRandomItem(); addRandomItem(); } //从空格列表中随机获取位置 public int getRandomIndex(){ Random random = new Random(); if(spaceList.size()>0) return random.nextInt(spaceList.size()); else return -1; } //在空白格中随机增加数字2或4 public void addRandomItem(){ int index = getRandomIndex(); if(index!=-1){ System.out.println("随机生成数字 位置"+spaceList.get(index)); //获取相应坐标所相应的View View view = gridLayout.getChildAt(spaceList.get(index)); ImageView image = (ImageView) view.findViewById(R.id.image); //随机生成数字1或2 int i = (int) Math.round(Math.random()+1); //将当前格子的图片置换为2或者4 image.setBackgroundResource(icons[i]); //在numList中增加该格子的信息 numberList.add(spaceList.get(index), i); //在空白列表中去掉这个格子 spaceList.remove(index); } } public class MygestureDetector implements OnGestureListener,OnTouchListener{ @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub return gd.onTouchEvent(event); } @Override public boolean onDown(MotionEvent e) { // TODO Auto-generated method stub return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // 參数解释: // e1:第1个ACTION_DOWN MotionEvent // e2:最后一个ACTION_MOVE MotionEvent // velocityX:X轴上的移动速度,像素/秒 // velocityY:Y轴上的移动速度。像素/秒 // 触发条件 : // X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒 if(e1.getX()-e2.getX()>100){ System.out.println("向左"); move(LEFT); return true; }else if(e1.getX()-e2.getX()<-100){ System.out.println("向右"); move(RIGHT); return true; }else if(e1.getY()-e2.getY()>100){ System.out.println("向上"); move(UP); return true; }else if(e1.getY()-e2.getY()<-00){ System.out.println("向下"); move(DOWN); return true; } return false; } @Override public void onLongPress(MotionEvent e) { // TODO Auto-generated method stub } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // TODO Auto-generated method stub return false; } @Override public void onShowPress(MotionEvent e) { // TODO Auto-generated method stub } @Override public boolean onSingleTapUp(MotionEvent e) { // TODO Auto-generated method stub return false; } } /** * 用于获取移动方向上下一个格子的位置 * @param index 当前格子的位置 * @param direction 滑动方向 * @return 假设在边界在返回-1 */ public int getNext(int index,int direction){ int y = index/4; int x = index%4; if(x==3 && direction==RIGHT) return -1; if(x==0 && direction==LEFT) return -1; if(y==0 && direction==UP) return -1; if(y==3 && direction==DOWN) return -1; return index+direction; } /** * 用于获取移动方向上前一个格子的位置 * @param index 当前格子的位置 * @param direction 滑动方向 * @return 假设在边界在返回-1 */ public int getBefore(int index,int direction){ int y = index/4; int x = index%4; if(x==0 && direction==RIGHT) return -1; if(x==3 && direction==LEFT) return -1; if(y==3 && direction==UP) return -1; if(y==0 && direction==DOWN) return -1; return index-direction; } /** * 该方法用来交换当前格与目标空白格的位置 * @param thisIdx 当前格子的坐标 * @param nextIdx 目标空白格的坐标 */ public void replace(int thisIdx, int nextIdx){ moved = true; //获取当前格子的view,并将其置成空白格 View thisView = gridLayout.getChildAt(thisIdx); ImageView image = (ImageView) thisView.findViewById(R.id.image); image.setBackgroundResource(icons[0]); //获取空白格的view,并将其背景置成当前格的背景 View nextView = gridLayout.getChildAt(nextIdx); ImageView nextImage = (ImageView) nextView.findViewById(R.id.image); nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(thisIdx)]); //在空白格列表中,去掉目标格。加上当前格 spaceList.remove(spaceList.indexOf(nextIdx)); spaceList.add(thisIdx); //在数字格列表中,当前格的坐标置换成目标格的坐标 numberList.changeIndex(thisIdx, nextIdx); } /** * 刚方法用于合并在移动方向上两个同样的格子 * @param thisIdx 当前格子的坐标 * @param nextIdx 目标格子的坐标 */ public void levelup(int thisIdx, int nextIdx){ //一次移动中。每一个格子最多仅仅能升级一次 if(!changeList.contains(nextIdx)){ moved = true; //获取当前格子的view,并将其置成空白格 View thisView = gridLayout.getChildAt(thisIdx); ImageView image = (ImageView) thisView.findViewById(R.id.image); image.setBackgroundResource(icons[0]); //获取目标格的view,并将其背景置成当前格升级后的背景 View nextView = gridLayout.getChildAt(nextIdx); ImageView nextImage = (ImageView) nextView.findViewById(R.id.image); nextImage.setBackgroundResource(icons[numberList.getNumberByIndex(nextIdx)+1]); //在空白格列表中增加当前格 spaceList.add(thisIdx); //在数字列中删掉第一个格子 numberList.remove(thisIdx); //将数字列表相应的内容升级 numberList.levelup(nextIdx); changeList.add(nextIdx); } } /** * 该方法为不同的滑动方向。运行不同的遍历顺序 * @param direction 滑动方向 */ public void move(int direction){ moved = false; changeList.clear(); numberList.printLog(); switch(direction){ case RIGHT: for(int y=0;y<4;y++){ for(int x=2;x>=0;x--){ int thisIdx = 4*y +x; Change(thisIdx,direction); } } break; case LEFT: for(int y=0;y<4;y++){ for(int x=1;x<=3;x++){ int thisIdx = 4*y +x; Change(thisIdx,direction); } } break; case UP: for(int x=0;x<4;x++){ for(int y=1;y<=3;y++){ int thisIdx = 4*y +x; Change(thisIdx,direction); } } break; case DOWN: for(int x=0;x<4;x++){ for(int y=2;y>=0;y--){ int thisIdx = 4*y +x; Change(thisIdx,direction); } } break; } //假设本次滑动有格子移动过,则随机填充新的格子 if(moved) addRandomItem(); } /** * 该方法。为每一个符合条件的格子运行变动的操作,如置换,升级等 * @param thisIdx 当前格子的坐标 * @param direction 滑动方向 */ public void Change(int thisIdx,int direction){ if(numberList.contains(thisIdx)){ int nextIdx = getLast(thisIdx, direction); if(nextIdx == thisIdx){ //不能移动 return; }else if(spaceList.contains(nextIdx)){ //存在能够置换的空白格 replace(thisIdx,nextIdx); }else{ if(numberList.getNumberByIndex(thisIdx) == numberList.getNumberByIndex(nextIdx)){ //能够合并 levelup(thisIdx, nextIdx); }else{ int before = getBefore(nextIdx, direction); if(before != thisIdx){ //存在能够置换的空白格 replace(thisIdx,before); } } } } } /** * 用于获取移动方向上最后一个空白格之后的位置 * @param index 当前格子的坐标 * @param direction 移动方向 * @return */ public int getLast(int thisIdx, int direction){ int nextIdx = getNext(thisIdx, direction); if(nextIdx < 0) return thisIdx; else{ if(spaceList.contains(nextIdx)) return getLast(nextIdx, direction); else return nextIdx; } } public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
写到这里,做为我学习Android以来。第一个自己写的程序已经完毕一半了。
逻辑推断这部分写的时候,还是费了一点时间,由于总有一些情况没有考虑进来,到如今基本上已经实现了。可是也反应出来一个非常重要的问题。那就是自己在数据结构和算法方面还是非常薄弱。整个读一下自己写的代码,为了完毕对各种情况的推断。整个代码看起来十分冗余。并且效率之低就更不用说了。再看看别人写的代码。感觉自己在开发方面还是有非常长的路要走的。
接下来的时间,我会利用工作之余的时间不断去完好这个程序,并尽可能的去优化。
大家共勉吧!
代码写成这样,我也不藏拙了,我把代码打包上传了,须要的朋友能够下载,也希望大家多多指正