13.2 魔幻矩阵——2048" class="reference-link">13.2 魔幻矩阵——2048

本例要实现的是一个数字矩阵游戏——2048,相信这个游戏大家应该非常熟悉了,从Web版、PC版到手机版,各种各样的2048游戏着实火了一把。除了基础型的2048,后来各种衍生版的2048也层出不穷,例如六边形版、朝代版等。

13.2.1 2048概述" class="reference-link">13.2.1 2048概述

2048的游戏规则很简单,让人上手非常容易,但是游戏的过程却非常吸引人,让人很有游戏的动力。游戏界面也非常简单,一开始整体4×4一共16个小格子,随机在两个小格子中出现数字,数字随机为2或者4。玩家每次用手指进行上、下、左、右滑动,所有数字就会向滑动方向靠拢,遇到相同的数字则相加、合并。移动成功后,会在剩余的空格中随机出现一个数字,2或者4,直到数字合并到2048,则挑战成功。

在网上最多的版本就是基于Cocos2d开发的版本。的确,这款游戏引擎用来开发2D游戏是非常方便的。但作为Android开发者,我们希望能够使用原生Android来开发一款这样的游戏。正是这样一个念头,在2048火了之后,笔者就开始着手制作2048这款游戏。先来看一下这个游戏的游戏界面,对2048有一个大致的认识,如图13.10所示。

13.2 魔幻矩阵——2048 - 图1 图13.10 2048游戏界面

游戏界面其实非常简单,整个游戏的核心便是中间的游戏面板,最下面的三个控制菜单和最上面的信息显示区域,则是辅助游戏的功能。那么这样一个界面,该如何去实现呢?

13.2.2 2048游戏分析" class="reference-link">13.2.2 2048游戏分析

13.2.2.1 确定布局

  • 面板

游戏的面板,即4×4的格子面板。它是本实例实现的关键。要实现这个布局,方法有很多,例如自定义一个View,这个是万能方法,但是需要计算各个小方块的坐标,比较复杂。再比如用GridView,但是却不太好控制空格的小方块。因此,笔者最后选用了GridLayout布局,这个布局是Android 4.0新增的布局。该布局的引入,极大地方便了Grid类型的布局开发,不熟悉该布局的读者朋友可以在Android开发者网站上找到相关的开发资料。

  • 小方块

游戏中移动的小方块是2048最小的游戏对象。通过面向对象的设计方法,可以将这些小方块抽象成一个个对象。小方块的颜色、显示数字等属性都在对象中进行设置。对方块的合并、产生等操作,也是基于对象的操作,这样非常有利于程序逻辑的控制。

13.2.2.2 确定算法

2048虽然游戏过程非常简单,但是对判断小方块的合并、产生的算法也是有一定要求的,如果算法太过复杂,那么就会造成画面的卡顿。这对于一个游戏来说,是最不能让用户接受的。这里笔者提供了一种效率比较高的算法。当然,2048的算法是非常多的,读者朋友们可以重新设计自己的算法,笔者的算法简单描述如下:

玩家在进行上、下、左、右地滑动时,先去判断每行(列),使用0来代表空格,如果某一行(列)的数字为2 2 0 4,那么首先将这一行(列)的非0数字存入一个list,即224。接下来,根据游戏规则,将2和2进行合并,即44。并将其作为该行(列)的返回值,从滑动的方向开始放置list中的数字。这样将每行(列)处理完毕后,就完成了一次滑动。

13.2.3 2048初始化工作" class="reference-link">13.2.3 2048初始化工作

13.2.3.1 游戏主界面

游戏界面如图13.10所示,由于游戏面板部分我们准备重写GridLayout来实现,因此在布局的时候,只需要预留出面板的位置来实现其他部分的布局即可。剩下的布局,都是一些最基本的布局方式,唯一需要注意的是,在布局中,我们使用了自定义的Shape来美化系统控件,如图13.11所示。

13.2 魔幻矩阵——2048 - 图2 图13.11 Shape美化控件

这一方法在前面的实例中已经用的很多了,而且对于实现扁平化的UI风格,是非常重要的一个技巧。

13.2.3.2 配置界面

2048游戏的定制性非常强,如果仅仅实现一个4×4,目标为2048的游戏,那就显得太单调了。因此,可以设计一个配置界面,让用户来选择游戏的难度、增加趣味性,这里笔者给出了一个游戏配置界面供大家参考,如图13.12所示。

13.2 魔幻矩阵——2048 - 图3 图13.12 2048配置界面

在这个配置界面中,可以设置游戏的难度,即不是简单的4×4共16个小方块,还可以是5×5、6×6等类型,相信大家在实现了4×4的2048后,实现其他难度的2048,应该是非常简单的了。同时,还可以设定需要完成的目标,即目标2048或者4096等,读者朋友们可以根据自身技术的高低,选择不同的模板,增加游戏的可玩性。

界面的设计同样是非常简单的,在按钮的美化上,与之前一样,借助了Shape和Selector来进行美化。

13.2.4 小方块设计" class="reference-link">13.2.4 小方块设计

2048游戏中滑动的小方块是游戏最终操作的最小单元。游戏的面板使用的是GridLayout,在设计好小方块之后,只要将不同属性的小方块加入到GridLayout中即可。那么该如何实现这样的小方块呢?不光是简单地实现,更需要最大限度地提高效率。

基于效率的考虑,让小方块——GameItem继承自FrameLayout,FrameLayout是几大布局中最轻量级的布局,并给在GameItem中给小方块设置了要显示的数字,并根据要显示的数字来设置相应的颜色。这些都通过小方块GameItem的构造方法来实现,代码如下所示。

  1. public GameItem(Context context, int cardShowNum) {
  2. super(context);
  3. this.mCardShowNum = cardShowNum;
  4. //初始化Item
  5. initCardItem();
  6. }

根据传入的cardShowNum,确定该小方块应该显示的数字和颜色。在初始化Item方法中,首先将背景颜色都设置为灰色,从而将整个面板展现成一个大的灰色布局,再将其中用于显示Item的TextView设置对应数字的背景颜色和边距,来展现一个游戏小方块,代码如下所示。

  1. /**
  2. *初始化Item
  3. */
  4. private void initCardItem() {
  5. //设置面板背景色,是由Frame拼起来的
  6. setBackgroundColor(Color.GRAY);
  7. mTvNum = new TextView(getContext());
  8. setNum(mCardShowNum);
  9. //修改5×5时字体太大
  10. int gameLines = Config.mSp.getInt(Config.KEY_GAME_LINES, 4);
  11. if (gameLines == 4) {
  12. mTvNum.setTextSize(35);
  13. } else if (gameLines == 5) {
  14. mTvNum.setTextSize(25);
  15. } else {
  16. mTvNum.setTextSize(20);
  17. }
  18. TextPaint tp = mTvNum.getPaint();
  19. tp.setFakeBoldText(true);
  20. mTvNum.setGravity(Gravity.CENTER);
  21.  
  22. mParams = new LayoutParams(LayoutParams.MATCH_PARENT,
  23. LayoutParams.MATCH_PARENT);
  24. mParams.setMargins(5, 5, 5, 5);
  25. addView(mTvNum, mParams);
  26. }

其中setNum(mCardShowNum)方法如下所示。

  1. public void setNum(int num) {
  2. this.mCardShowNum = num;
  3. if (num == 0) {
  4. mTvNum.setText("");
  5. } else {
  6. mTvNum.setText("" + num);
  7. }
  8. //设置背景颜色
  9. switch (num) {
  10. case 0:
  11. mTvNum.setBackgroundColor(0x00000000);
  12. break;
  13. case 2:
  14. mTvNum.setBackgroundColor(0xffeee5db);
  15. break;
  16. case 4:
  17. mTvNum.setBackgroundColor(0xffeee0ca);
  18. break;
  19. case 8:
  20. mTvNum.setBackgroundColor(0xfff2c17a);
  21. break;
  22. case 16:
  23. mTvNum.setBackgroundColor(0xfff59667);
  24. break;
  25. case 32:
  26. mTvNum.setBackgroundColor(0xfff68c6f);
  27. break;
  28. case 64:
  29. mTvNum.setBackgroundColor(0xfff66e3c);
  30. break;
  31. case 128:
  32. mTvNum.setBackgroundColor(0xffedcf74);
  33. break;
  34. case 256:
  35. mTvNum.setBackgroundColor(0xffedcc64);
  36. break;
  37. case 512:
  38. mTvNum.setBackgroundColor(0xffedc854);
  39. break;
  40. case 1024:
  41. mTvNum.setBackgroundColor(0xffedc54f);
  42. break;
  43. case 2048:
  44. mTvNum.setBackgroundColor(0xffedc32e);
  45. break;
  46. default:
  47. mTvNum.setBackgroundColor(0xff3c4a34);
  48. break;
  49. }
  50. }

在setNum()方法中,给不同的数字设置不同的显示颜色,这里笔者提供了一套数字显示的颜色,读者可以根据自己的喜好来设置不同的颜色。而且,这里不仅仅可以设置颜色作为背景,同样可以设置图片作为背景,网上有很多2048的换肤版,用不同的图片来代替颜色,其实只要将这里的TextView换成ImageView,并将对应数字的图片设置给不同数字即可实现。

13.2.5 全局设置" class="reference-link">13.2.5 全局设置

由于游戏具有配置功能,所以需要使用一个数据持久化方法来保存这些配置,并在游戏开始时,Android提供了SharedPreferences数据持久化接口,它非常适合配置信息的保存,因此创建一个Application类——Config,并在Mainifest文件中设置程序的Application入口,代码如下所示。

  1. <application
  2. android:name=".config.Config"
  3. ……
  4. </application>

在Config类中,完成对游戏配置文件的读写,并给其增加默认的游戏配置值,代码如下所示。

  1. package com.imooc.game2048.config;
  2.  
  3. import android.app.Application;
  4. import android.content.SharedPreferences;
  5.  
  6. public class Config extends Application {
  7.  
  8. /**
  9. * SP对象
  10. */
  11. public static SharedPreferences mSp;
  12.  
  13. /**
  14. * Game Goal
  15. */
  16. public static int mGameGoal;
  17.  
  18. /**
  19. * GameView行列数
  20. */
  21. public static int mGameLines;
  22.  
  23. /**
  24. * Item宽高
  25.  
  26. */
  27. public static int mItemSize;
  28.  
  29. /**
  30. *记录分数
  31. */
  32. public static int SCROE = 0;
  33.  
  34. public static String SP_HIGH_SCROE = "SP_HIGHSCROE";
  35.  
  36. public static String KEY_HIGH_SCROE = "KEY_HighScore";
  37.  
  38. public static String KEY_GAME_LINES = "KEY_GAMELINES";
  39.  
  40. public static String KEY_GAME_GOAL = "KEY_GameGoal";
  41.  
  42. @Override
  43. public void onCreate() {
  44. super.onCreate();
  45. mSp = getSharedPreferences(SP_HIGH_SCROE, 0);
  46. mGameLines = mSp.getInt(KEY_GAME_LINES, 4);
  47. mGameGoal = mSp.getInt(KEY_GAME_GOAL, 2048);
  48. mItemSize = 0;
  49. }
  50. }

13.2.6 游戏面板设计" class="reference-link">13.2.6 游戏面板设计

13.2.6.1 初始化游戏矩阵

在初始化游戏矩阵时,首先需要移除之前游戏的所有布局,并通过从配置文件中读取出来的配置值来生成游戏的布局,无论是4×4还是其他布局,并由此来计算出每个小方块的具体宽度,代码如下所示。

  1. /**
  2. *初始化View
  3. */
  4. private void initGameMatrix() {
  5. //初始化矩阵
  6. removeAllViews();
  7. mScoreHistory = 0;
  8. Config.SCROE = 0;
  9. Config.mGameLines = Config.mSp.getInt(Config.KEY_GAME_LINES, 4);
  10. mGameLines = Config.mGameLines;
  11. mGameMatrix = new GameItem[mGameLines][mGameLines];
  12. mGameMatrixHistory = new int[mGameLines][mGameLines];
  13. mCalList = new ArrayList<Integer>();
  14. mBlanks = new ArrayList<Point>();
  15. mHighScore = Config.mSp.getInt(Config.KEY_HIGH_SCROE, 0);
  16. setColumnCount(mGameLines);
  17. setRowCount(mGameLines);
  18. setOnTouchListener(this);
  19. //初始化View参数
  20. DisplayMetrics metrics = new DisplayMetrics();
  21. WindowManager wm = (WindowManager) getContext().getSystemService(
  22. Context.WINDOW_SERVICE);
  23. Display display = wm.getDefaultDisplay();
  24. display.getMetrics(metrics);
  25. Config.mItemSize = metrics.widthPixels / Config.mGameLines;
  26. initGameView(Config.mItemSize);
  27. }

在完成面板的初始化之后,就要根据具体的游戏难度,对游戏矩阵进行初始化,即在如上所示代码中的initGameView(Config.mItemSize)方法。在该方法中,首先将所有小方块都设置为0,然后根据游戏规则,随机添加两个数字到面板中,代码如下所示。

  1. private void initGameView(int cardSize) {
  2. removeAllViews();
  3. GameItem card;
  4.  
  5. for (int i = 0; i < mGameLines; i++) {
  6. for (int j = 0; j < mGameLines; j++) {
  7. card = new GameItem(getContext(), 0);
  8. addView(card, cardSize, cardSize);
  9. //初始化GameMatrix全部为0空格List为所有
  10. mGameMatrix[i][j] = card;
  11. mBlanks.add(new Point(i, j));
  12. }
  13. }
  14. //添加随机数字
  15. addRandomNum();
  16. addRandomNum();
  17. }

对于添加随机数字,封装了addRandomNum()方法,并将所有的空格以Point(x, y)的形式保存在mBlankslist中,代码如下所示。

  1. /**
  2. *添加随机数字
  3. */
  4. private void addRandomNum() {
  5. getBlanks();
  6. if (mBlanks.size() > 0) {
  7. int randomNum = (int) (Math.random() * mBlanks.size());
  8. Point randomPoint = mBlanks.get(randomNum);
  9. mGameMatrix[randomPoint.x][randomPoint.y]
  10. .setNum(Math.random() > 0.2d ? 2 : 4);
  11. }
  12. }

在如上所示的代码中,通过Math.random() > 0.2d ? 2 : 4方法来指定2和4出现的比例,例如比例为4:1,大家可以根据自己的喜好来进行设置。

13.2.6.2 增加滑动事件

初始化完毕后,我们来封装游戏面板的滑动事件。在2048游戏中,需要对上、左、右、四滑个动方向进行判断。判断的方法非常简单,只需要在onTouch()事件中捕获ACTION_DOWN事件CTION_UP事件,并判断最终的起始点坐标即可,代码如下所示。

  1. @Override
  2. public boolean onTouch(View v, MotionEvent event) {
  3. switch (event.getAction()) {
  4. case MotionEvent.ACTION_DOWN:
  5. saveHistoryMatrix();
  6. mStartX = (int) event.getX();
  7. mStartY = (int) event.getY();
  8. break;
  9. case MotionEvent.ACTION_MOVE:
  10. break;
  11. case MotionEvent.ACTION_UP:
  12. mEndX = (int) event.getX();
  13. mEndY = (int) event.getY();
  14. judgeDirection(mEndX - mStartX, mEndY - mStartY);
  15. if (isMoved()) {
  16. addRandomNum();
  17. //修改显示分数
  18. Game.getGameActivity().setScore(Config.SCROE, 0);
  19. }
  20. checkCompleted();
  21. break;
  22. default:
  23. break;
  24. }
  25. return true;
  26. }

在获取起始点坐标后,通过judgeDirection()方法,就可以判断具体的移动方向,代码如下所示。

  1. /**
  2. *根据偏移量判断移动方向
  3.  
  4. *
  5. * @param offsetX offsetX
  6. * @param offsetY offsetY
  7. */
  8. private void judgeDirection(int offsetX, int offsetY) {
  9. int density = getDeviceDensity();
  10. int slideDis = 5 * density;
  11. int maxDis = 200 * density;
  12. boolean flagNormal =
  13. (Math.abs(offsetX) > slideDis |
  14. Math.abs(offsetY) > slideDis) &&
  15. (Math.abs(offsetX) < maxDis) &&
  16. (Math.abs(offsetY) < maxDis);
  17. boolean flagSuper = Math.abs(offsetX) > maxDis ||
  18. Math.abs(offsetY) > maxDis;
  19. if (flagNormal && !flagSuper) {
  20. if (Math.abs(offsetX) > Math.abs(offsetY)) {
  21. if (offsetX > slideDis) {
  22. swipeRight();
  23. } else {
  24. swipeLeft(); }
  25. } else {
  26. if (offsetY > slideDis) {
  27. swipeDown();
  28. } else {
  29. swipeUp();
  30. }
  31. }
  32. } else if (flagSuper) { //启动超级用户权限来添加自定义数字
  33. AlertDialog.Builder builder =
  34. new AlertDialog.Builder(getContext());
  35. final EditText et = new EditText(getContext());
  36. builder.setTitle("Back Door")
  37. .setView(et)
  38. .setPositiveButton("OK",
  39. new DialogInterface.OnClickListener() {
  40.  
  41. @Override
  42. public void onClick(DialogInterface arg0,
  43. int arg1) {
  44. if (!TextUtils.isEmpty(et.getText())) {
  45. addSuperNum(Integer.parseInt(et.getText().toString()));
  46. checkCompleted();
  47. }
  48. }
  49. })
  50. .setNegativeButton("ByeBye",
  51. new DialogInterface.OnClickListener() {
  52.  
  53. @Override
  54. public void onClick(DialogInterface arg0, int arg1) {
  55. arg0.dismiss();
  56. }
  57. }).create().show();
  58. }
  59. }

大家可以发现,这里添加了一个超级用户权限的后门,我们规定当手指滑动的距离超过maxDis时,系统弹出一个对话框让用户输入想要生成的数字!如图13.13所示。

13.2 魔幻矩阵——2048 - 图4 图13.13 超级用户权限</h4>

当然,这只是一个娱乐的功能。不过,这也体现了创造程序的乐趣,在自己的程序中,你自己就是创世者,你完全可以让程序按照你的意志来实现,这也算是一种开发程序的成就感吧。

在judgeDirection()方法中,当判断好具体滑动的方向之后,可以通过封装好的swipeRight()、swipeLeft()、swipeDown()、swipeUp()四个方法来执行小方块的合并操作。这就利用到了之前讲到的2048的游戏算法,这里以swipeLeft()方法为例,来看一下这个算法的实现,代码如下所示。

  1. /**
  2. *滑动事件:左
  3. */
  4. private void swipeLeft() {
  5. for (int i = 0; i < mGameLines; i++) {
  6. for (int j = 0; j < mGameLines; j++) {
  7. int currentNum = mGameMatrix[i][j].getNum();
  8. if (currentNum != 0) {
  9. if (mKeyItemNum == -1) {
  10. mKeyItemNum = currentNum;
  11. } else {
  12. if (mKeyItemNum == currentNum) {
  13. mCalList.add(mKeyItemNum * 2);
  14. Config.SCROE += mKeyItemNum * 2;
  15.  
  16. mKeyItemNum = -1;
  17. } else {
  18. mCalList.add(mKeyItemNum);
  19. mKeyItemNum = currentNum;
  20. }
  21. }
  22. } else {
  23. continue;
  24. }
  25. }
  26. if (mKeyItemNum != -1) {
  27. mCalList.add(mKeyItemNum);
  28. }
  29. //改变Item值
  30. for (int j = 0; j < mCalList.size(); j++) {
  31. mGameMatrix[i][j].setNum(mCalList.get(j));
  32. }
  33. for (int m = mCalList.size(); m < mGameLines; m++) {
  34. mGameMatrix[i][m].setNum(0);
  35. }
  36. //重置行参数
  37. mKeyItemNum = -1;
  38. mCalList.clear();
  39. }
  40. }

通过外层循环,遍历每一行,再通过内层循环遍历每一列,对相同的数字进行合并,并通过mKeyItemNum标识来区分是否已经进行过一次合并,例如2 2 0 4,2和2进行合并后,不会再和4进行合并。整个算法概括起来就是一句话——选取基准,挨个比较,重新排列。最后,将处理之后的数字矩阵重新设置给游戏面板的数字矩阵,从而实现滑动之后的数字矩阵变换。

13.2.6.3 记录历史矩阵

针对每次数字矩阵变换前的矩阵,我们可以使用一个二维数组保存起来。作为上一次变换的历史矩阵,这样做有两个好处。一个是可以通过对比两次数字矩阵的差异,判断是否发生了移动,另一个是可以实现“Revert”撤销功能。不过,由于“撤销”功能极大地降低了游戏的挑战性,因此笔者这里只记录了上次的历史数字矩阵,而没有记录所有的矩阵,如果读者需要实现所有步骤的“Revert”功能,可以将这些历史数字矩阵都保存到一个list中来实现。保存历史矩阵的代码如下所示。

  1. /**
  2. *保存历史记录
  3. */
  4. private void saveHistoryMatrix() {
  5. mScoreHistory = Config.SCROE;
  6. for (int i = 0; i < mGameLines; i++) {
  7. for (int j = 0; j < mGameLines; j++) {
  8. mGameMatrixHistory[i][j] = mGameMatrix[i][j].getNum();
  9. }
  10. }
  11. }

只需要在捕获到ACTION_DOWN事件时,进行矩阵的记录即可。有了移动前的历史矩阵,就可以通过它来实现判断是否移动过的功能和“Revert”功能,代码如下所示。

  1. /**
  2. *判断是否移动过(是否需要新增Item)
  3. *
  4. * @return是否移动
  5. */
  6. private boolean isMoved() {
  7. for (int i = 0; i < mGameLines; i++) {
  8. for (int j = 0; j < mGameLines; j++) {
  9. if (mGameMatrixHistory[i][j] != mGameMatrix[i][j].getNum()) {
  10. return true;
  11. }
  12. }
  13. }
  14. return false;
  15. }
  16.  
  17. /**
  18. *撤销上次移动
  19. */
  20. public void revertGame() {
  21. //第一次不能撤销
  22. int sum = 0;
  23. for (int[] element : mGameMatrixHistory) {
  24. for (int i : element) {
  25. sum += i;
  26. }
  27. }
  28. if (sum != 0) {
  29. Game.getGameActivity().setScore(mScoreHistory, 0);
  30. Config.SCROE = mScoreHistory;
  31. for (int i = 0; i < mGameLines; i++) {
  32. for (int j = 0; j < mGameLines; j++) {
  33. mGameMatrix[i][j].setNum(mGameMatrixHistory[i][j]);
  34. }
  35. }
  36. }
  37. }

在判断发生了移动之后,就需要重新生成一个新的数字填充到剩余的空格中。添加随机数字的方法,前面已经写好了,这里只需要简单地调用即可。那么只需要一个方法来获取整个面板中剩余空格的位置。方法非常简单,直接遍历每个小方块,如果值为0,即代表空格小方块,代码如下所示。

  1. /**
  2. *获取空格Item数组
  3. */
  4. private void getBlanks() {
  5.  
  6. mBlanks.clear();
  7. for (int i = 0; i < mGameLines; i++) {
  8. for (int j = 0; j < mGameLines; j++) {
  9. if (mGameMatrix[i][j].getNum() == 0) {
  10. mBlanks.add(new Point(i, j));
  11. }
  12. }
  13. }
  14. }

13.2.6.4 判断游戏结束

游戏面板所提供的最后一个逻辑功能就是判断游戏是否已经结束,即checkCompleted()方法。游戏成功意味着完成目标值2048或其他设定值,游戏失败意味着所有的小方块都无法再移动。在每次捕获ACTION_UP事件后,都需要调用CheckComplete()方法来进行游戏是否状态的判断。判断游戏是否成功非常简单,只需要遍历,看是否具有目标值即可。而判断游戏失败,则需要判断每个小方块的数值在其上、下、左、右所对应的方块中是否存在相同的小方块。如果存在,就可以继续游戏。判断代码如下所示。

  1. /**
  2. *判断是否结束
  3. * <p/>
  4. * 0:结束1:正常2:成功
  5. */
  6. private void checkCompleted() {
  7. int result = checkNums();
  8. if (result == 0) {
  9. ……
  10. } else if (result == 2) {
  11. ……
  12. }
  13. }
  14.  
  15. /**
  16.  
  17. *检测所有数字看是否有满足条件的
  18. *
  19. * @return 0:结束1:正常2:成功
  20. */
  21. private int checkNums() {
  22. getBlanks();
  23. if (mBlanks.size() == 0) {
  24. for (int i = 0; i < mGameLines; i++) {
  25. for (int j = 0; j < mGameLines; j++) {
  26. if (j < mGameLines - 1) {
  27. if (mGameMatrix[i][j].getNum() == mGameMatrix[i][j + 1]
  28. .getNum()) {
  29. return 1;
  30. }
  31. }
  32. if (i < mGameLines - 1) {
  33. if (mGameMatrix[i][j].getNum() == mGameMatrix[i + 1][j]
  34. .getNum()) {
  35. return 1;
  36. }
  37. }
  38. }
  39. }
  40. return 0;
  41. }
  42. for (int i = 0; i < mGameLines; i++) {
  43. for (int j = 0; j < mGameLines; j++) {
  44. if (mGameMatrix[i][j].getNum() == mTarget) {
  45. return 2;
  46. }
  47. }
  48. }
  49. return 1;
  50. }

当判断游戏结束后,可以弹出对话框,提示用户“Game Over”(游戏结束),重新开始游戏。游戏成功后,将用户目标值提升为下一个目标值,并提示用户继续游戏或者退出游戏,这些都是一些简单的对话框操作,这里就不再给出详细的代码了。到此为止,游戏面板的逻辑,就基本实现了。

13.2.7 主程序设计" class="reference-link">13.2.7 主程序设计

13.2.7.1 添加游戏面板和游戏信息

在程序的主Activity中,只需要将游戏面板添加到前面预留的布局中即可,整个游戏的逻辑,都封装在游戏面板这个类中,所以主程序Activity只需要负责一些信息的显示、辅助功能即可。例如最下方三个辅助按钮的点击事件、游戏目标、游戏记录和当前分数等显示。为了显示分数,在游戏面板的逻辑中,还需要加入统计分数的功能,相信看完前面游戏面板实现方法的读者应该对这个功能的添加信手拈来,只需要在合并数字的时候,对分数进行增加即可,这里不再详述。

13.2.7.2 动画效果

程序实现到这里,相信大家都已经能够完成基本的2048游戏功能了,不过,我们所开发的2048与网上流传的2048相比,好像总觉得少了点什么。由于每次都是根据重新生成的数字矩阵来对GridLayout进行重新布局,实现变换的效果,这就导致缺少了生成小方块的动画效果。下面我们就来给2048增加相应的动画效果,在addRandomNum()方法中,会生成一个新的小方块,在生成小方块的时候,只需要对新生成的小方块做一个简单的Scale变换即可,代码如下所示。

  1. /**
  2. *添加随机数字
  3. */
  4. private void addRandomNum() {
  5. getBlanks();
  6. if (mBlanks.size() > 0) {
  7. int randomNum = (int) (Math.random() * mBlanks.size());
  8. Point randomPoint = mBlanks.get(randomNum);
  9.  
  10. mGameMatrix[randomPoint.x][randomPoint.y]
  11. .setNum(Math.random() > 0.2d ? 2 : 4);
  12. animCreate(mGameMatrix[randomPoint.x][randomPoint.y]);
  13. }
  14. } /**
  15. *生成动画
  16. * @param target GameItem
  17. */
  18. private void animCreate(GameItem target) {
  19. ScaleAnimation sa = new ScaleAnimation(0.1f, 1, 0.1f, 1,
  20. Animation.RELATIVE_TO_SELF, 0.5f,
  21. Animation.RELATIVE_TO_SELF, 0.5f);
  22. sa.setDuration(100);
  23. target.setAnimation(null);
  24. target.getItemView().startAnimation(sa);
  25. }

13.2.8 功能进阶" class="reference-link">13.2.8 功能进阶

到此为止,2048就基本开发完毕了。与之前的拼图游戏一样,2048还有很多地方是可以进行优化的,例如前面所提到的换肤功能,其实就是改变一下小方块的显示背景而已。相信看加这样一个功能,应该是不在话下。另外,还有我们所实现的一个简单的后门,通过大距离的滑动来开启超级用户权限的作弊功能,这也是增加游戏娱乐性的一个方法。“我的程序听我的”,如果要对2048进行进一步的优化,还可以给2048增加SNS功能,毕竟游戏就是要大家一起玩才好玩。