---------------------------------------------------------------------------------------------------------------------------------
该篇博文參加了CSDN博客大赛,投票地址:http://vote.blog.csdn.net/article/details?articleid=37863693
---------------------------------------------------------------------------------------------------------------------------------
前言:
在“阳光小强”的实战系列博文《是男人就下100层》的上一层我们一起从零開始完毕了我们自己的贪吃蛇游戏——CrazySnake,可能非常多朋友还只是瘾,那么我们今天就来玩一玩近期一直比較火的2048游戏,让大家再过一把瘾。由于“阳光小强”眼下并没有从事Android的游戏开发工作,所以这些游戏的实现并不须要非常专业的游戏开发知识,假设你有Android的基础就能够一起来參与进来共同完毕这个游戏。有些朋友可能就会说“这些小游戏,会不会有点简单。整天搞这些对自己没有帮助“,我认为经典就是对我们有非常大非常长远指导意义的东西,这些游戏尽管小(100行能够写出贪吃蛇)。可是里面蕴含的编程技巧和算法对刚開始学习的人是非常实用的。孔子说过”温故而知新“。学习的过程就分为”学“和”习“。研究前人的代码就是一种学,思考加改良后就是习。
假若有一天你能触类旁通的说这些小游戏我都能非常快的实现而且和有所变化和改进,你就会发现你已经不知不觉的提高了很多。在前面三篇CrazySnake中”阳光小强“用自己的方式和风格完毕了经典的贪吃蛇游戏,这一篇同样我们也要用自己的方式来一步步思考并实现我们不一样的2048.
一、游戏介绍
《2048》是一款单人在线和移动端游戏。由19岁的意大利人Gabriele Cirulli于2014年3月开发。游戏任务是在一个网格上滑动小方块来进行组合。直到形成一个带有有数字2048的方块。
为什么会出现这款游戏呢?作者开发这个游戏是为了測试自己是否有能力从零開始创造一款游戏。但游戏飙升的人气(不到1周内有400万訪客)全然出乎他的预料。如今2048被称为网络上“最上瘾的东西”。由于该游戏为开源软件,所以如今市场上有非常多改进版本号和变种。
游戏使用方向键让方块上下左右移动。假设两个带有同样数字的方块在移动中碰撞。则它们会合并为一个方块,且所带数字变为两者之和。每次移动时,会有一个值为2或者4的新方块出现。
当值为2048的方块出现时。游戏即胜利。游戏因此叫做2048。
二、终于效果展示
经过几天的摸索这款游戏终于能够告一段落了,终于的结果例如以下:
1、实现了2048游戏的所有功能。
2、新增了更换皮肤功能(有三款皮肤任你选择)。
3、声音开关(让你上班时也能偷着玩)。
4、新增了游戏介绍模块(各个皮肤的顺序一目了然)。
5、不可缺少的分享功能,和你的朋友一块来摇滚吧。
6、加入了有米广告(这个是阳光小强第一次这样搞。事实上对app中插广告我也是蛮愤慨的,这一次大家就忍耐一下。这样能够从头到尾的来演示一遍怎样将一款app公布到Android市场上,还有一方面则全然是由于第一次的好奇心)。
7、加入了版本号自己主动更新功能,保证新版本号和功能及时更新到你的手机。
8、随时随地不受阻挠的换肤和最酷的双杀到五杀的音效(lol游戏中的音效)。
9、最后再混淆了代码公布到市场上
所有源码请看下一篇博文(博文篇幅过长,分成两篇了):http://blog.csdn.net/dawanganban/article/details/38280945
三、实现游戏布局
游戏布局我就仿照了市面上一款2048游戏的界面,布局例如以下图:
具体的布局结构例如以下,外面的包裹均为LinearLayout.里面的显示部分使用TextView,这仅仅是一种布局方式,事实上还能够使用RelativeLayout布局(这样创建的对象更少),这里为了速度就使用LinearLayout包裹实现了。以下的自己定义View主要实现onDraw(用来绘制)和onTouchEvent(主要用来控制)方法。
布局文件activity_main.xml例如以下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:background="#ffffff" android:padding="20dip"> <LinearLayout android:layout_width="match_parent" android:layout_height="100dip" android:orientation="horizontal"> <TextView android:layout_width="0dip" android:layout_height="match_parent" android:layout_weight="1" android:background="@drawable/text_yellow_bg" android:textColor="#ffffff" android:gravity="center" android:text="2048" android:textStyle="bold" android:textSize="30dip"/> <LinearLayout android:layout_width="0dip" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:layout_marginLeft="15dip"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="3" android:orientation="vertical" android:background="@drawable/text_grey_bg" android:gravity="center_vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="#E3D4C1" android:textSize="16sp" android:text="分数"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="#ffffff" android:textSize="20dip" android:text="400"/> </LinearLayout> <TextView android:layout_marginTop="15dip" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="2" android:background="@drawable/text_red_bg" android:textColor="#ffffff" android:textSize="20dip" android:gravity="center" android:text="设置"/> </LinearLayout> <LinearLayout android:layout_width="0dip" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical" android:layout_marginLeft="15dip"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="3" android:orientation="vertical" android:background="@drawable/text_grey_bg" android:gravity="center_vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="#E3D4C1" android:textSize="16sp" android:text="最高分数"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textColor="#ffffff" android:textSize="20dip" android:text="400"/> </LinearLayout> <TextView android:layout_marginTop="15dip" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="2" android:background="@drawable/text_red_bg" android:textColor="#ffffff" android:textSize="20dip" android:gravity="center" android:text="分享"/> </LinearLayout> </LinearLayout> <com.example.my2048.My2048View android:layout_marginTop="20dip" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/my2048view_bg"/> </LinearLayout>
四、自己定义视图My2048View
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); this.mViewWidth = w; this.mViewHeight = h; cellSpace = ((float)mViewWidth - (TOTAL_COL + 1) * SPACE) / TOTAL_COL; textPaint.setTextSize(cellSpace / 3); }在View的onSizeChanged的方法中获取View的宽度和高度。并依据宽度计算出每一个小格子的长度(宽度和高度),当中TOTAL_COL=4表示四列。SPACE表示间隔宽度。
最后一行的textPaint.setTextSize(cellSpace / 3)是设置文字画笔的字体大小(如今不明确没关系。一会就会明确)。相关定义例如以下:
private static final int TOTAL_ROW = 4; //行 private static final int TOTAL_COL = 4; //列 private static final int SPACE = 15; //行和列之间的间隙 private int mViewWidth; //View的宽度 private int mViewHeight; //View的高度 private float cellSpace; //每一个格子的大小以下就開始在onDraw方法中绘制小方格
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String showNum; for(int i=0; i<TOTAL_ROW; i++){ for(int j=0; j<TOTAL_COL; j++){ pointX = SPACE * (j + 1) + j * cellSpace; pointY = SPACE * (i + 1) + i * cellSpace; //绘制背景 rectf.set(pointX, pointY, pointX + cellSpace, pointY + cellSpace); paint.setColor(Color.rgb(204, 192, 178)); canvas.drawRect(rectf, paint); } } }pintX和pointY是计算绘制方块的起始位置,例如以下图红点位置分别为方块1、2、3的起始位置。
绘制好方块接下来的目标是绘制上面的数字和每一个数字所相应颜色的方块,我们先来观察一下这个游戏中的数字有什么规律。2、4、8......2048能够这样表示2^1、2^2、2^3.......2^11.同一时候我们能够将没有数字的方块看成数字为1的方块也就是2^0.所以我们的数据范围就是从2^0到2^11.我们定义12中颜色
private int[] colors = { Color.rgb(204, 192, 178), //1 Color.rgb(253, 235, 213), //2 Color.rgb(252, 224, 174), //4 Color.rgb(255, 95, 95), //8 Color.rgb(255, 68, 68), //16 Color.rgb(248, 58, 58), //32 Color.rgb(240, 49, 49), //64 Color.rgb(233, 39, 39), //128 Color.rgb(226, 29, 29), //256 Color.rgb(219, 19, 19), //562 Color.rgb(211, 10, 10), //1024 Color.rgb(204, 0, 0) //2048 };找这么多颜色还真不easy,我就索性将Android的设计规范中红色的后9个颜色取到了这里(这里大家能够自己变成喜欢的颜色)
http://www.apkbus.com/design/style/color.html
通过上面对数据范围的分析,事实上我们能够用0到11这12个连续数字来表示1、2、4、8、16.......2048这些数字,最后通过Math的pow函数计算出来就可以。
我们先来模拟一些数据来绘制出来看看效果。
/** * 模拟測试数据 */ private void initData(){ for(int i=0; i<TOTAL_ROW; i++){ for(int j=0; j<TOTAL_COL; j++){ int a = (i+1) * (j+1); if(a < 12){ datas[i][j] = a; }else{ datas[i][j] = 0; } } } }上面的datas就是我们模拟的整型数据数组,我们再改写onDraw方法将这些数据绘制出来。
private float pointX; private float pointY; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String showNum; for(int i=0; i<TOTAL_ROW; i++){ for(int j=0; j<TOTAL_COL; j++){ pointX = SPACE * (j + 1) + j * cellSpace; pointY = SPACE * (i + 1) + i * cellSpace; //绘制背景 rectf.set(pointX, pointY, pointX + cellSpace, pointY + cellSpace); paint.setColor(colors[datas[i][j]]); canvas.drawRect(rectf, paint); if(datas[i][j] != 0){ //绘制数字 if(datas[i][j] == 1 || datas[i][j] == 2){ textPaint.setColor(Color.rgb(0, 0, 0)); }else{ textPaint.setColor(Color.rgb(255, 255, 255)); } showNum = (int)Math.pow(2, datas[i][j]) + ""; canvas.drawText(showNum, pointX + (cellSpace - textPaint.measureText(showNum)) / 2, pointY + (cellSpace + textPaint.measureText(showNum, 0, 1)) / 2, textPaint); } } } }上面对随机数字2、4的字体颜色和其它的字体颜色进行了区分,执行效果例如以下:
看来没有什么太大问题,我们接下来实现随机产生一个数字2或4来绘制到网格中,要随机产生一个数字2或4比較简单,使用(random.nextInt(2) + 1) * 2 就可以实现,事实上和上面的分析同样。这里我们仅仅须要随机产生1和2。所以就是random.nextInt (2) + 1.先看随机产生代码:
/** * 随机的产生1或者2 */ private void randomOneOrTwo(){ int row = random.nextInt(TOTAL_ROW); int col = random.nextInt(TOTAL_COL); //推断在该位置是否已存在数据 if(datas[row][col] != 0){ randomOneOrTwo(); }else{ datas[row][col] = random.nextInt(2) + 1; } }上面代码用到了递归,为什么要使用递归呢?这是由于我们在放置这个随机产生的数字的时候须要随机产生一个x和y方向的坐标值,假设(x,y)坐标处已经存在数据则须要又一次获取。直到该(x,y)处没有数据。
接下来我们来实现让这个小方块能够随着我们的手势上下左右移动到边缘。重写onTouchEvent方法例如以下。
private enum Directory{ LEFT, RIGHT, BOTTOM, TOP }
private float mDownX; private float mDownY; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = event.getX(); mDownY = event.getY(); return true; case MotionEvent.ACTION_MOVE: float disX = event.getX() - mDownX; float disY = event.getY() - mDownY; if(Math.abs(disX) > touchSlop || Math.abs(disY) > touchSlop){ System.out.println("isMove"); isMoved = true; if(Math.abs(disX) > Math.abs(disY)){ if(disX > 0){ currentDirectory = Directory.RIGHT; }else{ currentDirectory = Directory.LEFT; } }else{ if(disY > 0){ currentDirectory = Directory.BOTTOM; }else{ currentDirectory = Directory.TOP; } } } return true; case MotionEvent.ACTION_UP: if(isMoved == true){ changeState(); randomOneOrTwo(); invalidate(); isMoved = false; } } return super.onTouchEvent(event); }
private void changeState(){ switch (currentDirectory) { case TOP: toTop(); break; case BOTTOM: toBottom(); break; case LEFT: toLeft(); break; case RIGHT: toRight(); break; } }
首先我们来看toLeft()方法
private void toLeft(){ int temp; //向左移动 for(int i=0; i<TOTAL_ROW; i++){ for(int j=0; j<TOTAL_COL; j++){ for(int k=0; k<TOTAL_COL - j -1; k++){ if(datas[i][k] == 0){ temp = datas[i][k]; datas[i][k] = datas[i][k+1]; datas[i][k+1] = temp; } } } } //合并数字 for(int i=0; i<TOTAL_ROW; i++){ for(int j=0; j<TOTAL_COL; j++){ for(int k=0; k<TOTAL_COL - j -1; k++){ if(datas[i][k] !=0 && datas[i][k] == datas[i][k+1]){ datas[i][k] = datas[i][k] + 1; datas[i][k+1] = 0; } } } } }我在这种方法中共进行了两大步操作,一个是总体向左移动,并将值为0的方格向右移动,第二个操作是合并相邻的同样数字。
上面我们用的是冒泡法,每次循环推断当前位置是否为零,假设为零则左右交换,如此交换,直到将该零移动到最右边。最后将移动排列后的数组再进行合并,并实现相邻元素比較合并。有的朋友能够就会有疑问了,上面为什么要反复的写两个三层循环呢?为什么不把移动和合并操作放置到同一个循环结构内呢?事实上我也试图这样去优化代码。降低代码的复杂程度。可是这两个之间有一个先后问题。也就是说将上面的两个三层循环(合并和移动)换个位置,则就达不到我们的目的。这个合并是在移动整理的条件下进行的,所以就会使用两个三层循环。
这样我们就基本实现了我们自己的2048游戏,游戏中的规则和消除以及移动方式大家能够自己去设置,说不定某天就能创造出自己风格的2048或者4092,游戏中还有得分、最高纪录、设置等功能,感兴趣的朋友能够自己实现并改进后放到Android市场上。别急~~我们先试玩一下,保证没有问题了再放到Android市场上。不然被怕是要被别人骂的哦。
五、试玩并改动
通过我的试玩发现了例如以下几个问题:
1、当格子放满后就会奔溃自己主动退出,这个问题的解决办法非常easy,我们没有做游戏结束推断。
/** * 随机的产生1或者2 */ private void randomOneOrTwo() { if(count >= TOTAL_COL * TOTAL_ROW){ currentState = State.FAILL; return; } int row = random.nextInt(TOTAL_ROW); int col = random.nextInt(TOTAL_COL); // 推断在该位置是否已存在数据 if (datas[row][col] != 0) { randomOneOrTwo(); } else { datas[row][col] = random.nextInt(2) + 1; count++; } }将randomOneOrTwo函数改动如上,定义了一个记录当前格子使用情况的变量count,没当添加一个格子后就会添加1.同样在消除格子的时候就要降低1.
// 合并数字 for (int i = 0; i < TOTAL_ROW; i++) { for (int j = 0; j < TOTAL_COL; j++) { for (int k = 0; k < TOTAL_COL - j - 1; k++) { if (datas[i][k] != 0 && datas[i][k] == datas[i][k + 1]) { datas[i][k] = datas[i][k] + 1; datas[i][k + 1] = 0; count--; } } } }而且在状态改变的时候绘制“游戏结束”文字进行提醒
if(currentState == State.FAILL){ textPaint.setColor(Color.rgb(255, 255, 255)); canvas.drawText("游戏结束", (mViewWidth - textPaint.measureText("游戏结束")) / 2, mViewHeight / 2, textPaint); }
这样做貌似还不够,那么我们又怎样又一次開始呢?在onTouchEvent方法中进行例如以下推断,并改动结束时的显示(在底部加入一个又一次開始button)
case MotionEvent.ACTION_DOWN: mDownX = event.getX(); mDownY = event.getY(); if(currentState == State.FAILL){ if(mDownY < mViewHeight && mDownY > mViewHeight - cellSpace){ currentState = State.RUNNING; initData(); invalidate(); } } return true;
if(currentState == State.FAILL){ rectf.set(0 , mViewHeight - cellSpace, mViewWidth, mViewHeight); paint.setColor(colors[5]); canvas.drawRect(rectf, paint); textPaint.setColor(Color.rgb(255, 255, 255)); canvas.drawText("游戏结束", (mViewWidth - textPaint.measureText("游戏结束")) / 2, mViewHeight / 2, textPaint); canvas.drawText("又一次開始", (mViewWidth - textPaint.measureText("游戏结束")) / 2, mViewHeight - textPaint.measureText("游戏结束", 0, 1), textPaint); }
// 合并数字 for (int i = 0; i < TOTAL_ROW; i++) { for (int j = 0; j < TOTAL_COL; j++) { for (int k = 0; k < TOTAL_COL - j - 1; k++) { if (datas[i][k] != 0 && datas[i][k] == datas[i][k + 1]) { datas[i][k] = datas[i][k] + 1; datas[i][k + 1] = 0; score = score + (int)Math.pow(2, datas[i][k]); count--; } } } }
/** * 随机的产生1或者2 */ private void randomOneOrTwo() { if(count >= TOTAL_COL * TOTAL_ROW){ int maxScore = sharedPreference.getInt("maxScore", 0); if(score > maxScore){ Editor edit = sharedPreference.edit(); edit.putInt("maxScore", score); edit.commit(); } gameChangeListener.onChangedGameOver(score, maxScore); currentState = State.FAILL; return; } int row = random.nextInt(TOTAL_ROW); int col = random.nextInt(TOTAL_COL); // 推断在该位置是否已存在数据 if (datas[row][col] != 0) { randomOneOrTwo(); } else { datas[row][col] = random.nextInt(2) + 1; count++; } }上面将最高记录保持在了SharedPreference中,每次结束时和当前分数进行推断,假设游戏过程中突然退出我们也要考虑到记录当前最高记录值。代码例如以下:
@Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if(visibility != View.VISIBLE){ int maxScore = sharedPreference.getInt("maxScore", 0); if(score > maxScore){ Editor edit = sharedPreference.edit(); edit.putInt("maxScore", score); edit.commit(); } } }那么如今我们怎样将这些结果显示到MainActivity的TextView上面呢?我们在My2048View中定义一个接口例如以下:
public interface GameChangeListener{ public void onChangedGameOver(int score, int maxScore); public void onChangedScore(int score); }并提供接口的注冊方法:
public void setOnGameChangeListener(GameChangeListener gameChangeListener){ this.gameChangeListener = gameChangeListener; gameChangeListener.onChangedGameOver(score, sharedPreference.getInt("maxScore", 0)); gameChangeListener.onChangedScore(score); }在我们结束游戏或者消除方块加分的时候进行回调显示,MainActivity中的代码例如以下:
my2048View = (My2048View) findViewById(R.id.my2048view); my2048View.setOnGameChangeListener(new GameChangeListener() { @Override public void onChangedScore(int score) { scoreText.setText(score + ""); } @Override public void onChangedGameOver(int score, int maxScore) { scoreText.setText(score + ""); maxScoreText.setText(maxScore + ""); } });
除过上面的两个不足外,还存在消除时没有动画(会突然消掉),这个到影响不大,后面再完好吧(时间不早了)。
六、源码下载及说明
该项目的代码我托管到了CSDN的CODE上面。下载地址:git@code.csdn.net:lxq_xsyu/my2048.git(请使用Git工具下载)
说明:这是阳光小强熬夜赶出来的代码,假设上面的思路或者实现过程有什么问题或者疑问。请在博客以下回复,我们来共同完好。
另外阳光小强的这篇博客參加了CSDN博客大赛的决赛。假设你认为对你有所帮助请投出您宝贵的一票。投票地址:http://vote.blog.csdn.net/article/details?
articleid=37863693
-------------------------------------------------------------------------------------------------------------------------------------
7月17日改动:
改动说明:就在阳光小强发出博客的当天就有非常多朋友对代码提出了自己意见,阳光小强在这里非常感谢,这些朋友提出的问题我已经抽时间改动。而且加入了消除过程中的动画效果,假设兴许还存在什么问题请予以指出。我会及时改动,现将执行效果和代码贴出:
最新apk文件下载:(此版本号apk已更新。请向下看。下载最新apk)
-------------------------------------------------------------------------------------------------------------------------------------
7月18日凌晨改动
改动说明:昨天下午下班后走在路上阳光小强就正式的体验了一把咱们的2048游戏,玩了之后感觉不爽,回来后又在安卓市场上下载了别人做的2048游戏,结果一对照发现了新的问题。比方消除的方向,有时候如2024这样排列向右滑动会消除两次变为0008,在某个方向没有可消除的后还能继续产生新的数字(2或4)等问题。改动后的apk文件已经达到游戏的要求。不防下载下来玩一下,我将最新apk文件和代码贴出到以下。通过两次改动我们的游戏越来越健壮了,阳光小强也从中也收获了不少的知识,你有没有一些收获呢?假设有收获请记得给阳光小强投票哦~(投票地址:http://vote.blog.csdn.net/article/details?articleid=37863693)
最新apk文件下载地址:(此版本号已经更新。请再向下看。下载最新apk文件)
改动后的源码查看链接:https://code.csdn.net/lxq_xsyu/my2048/tree/85625badc0f3753c93678fd1242d06c993d8610e/src/com/example/my2048/My2048View.java
-------------------------------------------------------------------------------------------------------------------------------------
7月18日下午改动
今天中午吃饭的时候有一局玩的挺好,结果数据没有保存下来。顿时我就火了,于是回来就对对记录保存bug进行了改动,新增了切屏保存数据等内容,经过初步測试没有什么问题,玩了玩之后还是不够过瘾。阳光小强自从来到北京这边,住的地方网速老差了,好久没玩LOL了。何不把LOL的元素集成进来,这样就能够每天在手机上看到自己喜欢的英雄了,呵呵。有想法就干。
最新apk文件下载地址:(终于版已经出炉,终于版在文章第二个大标题以下可看到下载链接)
/** * 保存数据和状态 * @return */ public Bundle saveDataAndState(){ Bundle map = new Bundle(); map.putIntArray("row1", datas[0]); map.putIntArray("row2", datas[1]); map.putIntArray("row3", datas[2]); map.putIntArray("row4", datas[3]); map.putSerializable("currentDirectory", currentDirectory); return map; } /** * 取出保存的数据和状态 * @param bundle */ public void restoreDataAndState(Bundle bundle){ datas[0] = bundle.getIntArray("row1"); datas[1] = bundle.getIntArray("row2"); datas[2] = bundle.getIntArray("row3"); datas[3] = bundle.getIntArray("row4"); currentDirectory = (Directory) bundle.getSerializable("currentDirectory"); }在MainActivity中加入了状态保存代码,例如以下:(MainActivity.java)
package com.example.my2048; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; import android.widget.Toast; import com.example.my2048.My2048View.GameChangeListener; public class MainActivity extends Activity { private TextView scoreText; private TextView maxScoreText; private My2048View my2048View; private static final String DATA_NAME = "my2048Data"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); scoreText = (TextView) findViewById(R.id.score); maxScoreText = (TextView) findViewById(R.id.maxScore); my2048View = (My2048View) findViewById(R.id.my2048view); my2048View.setOnGameChangeListener(new GameChangeListener() { @Override public void onChangedScore(int score) { scoreText.setText(score + ""); } @Override public void onChangedGameOver(int score, int maxScore) { scoreText.setText(score + ""); maxScoreText.setText(maxScore + ""); } }); if(savedInstanceState != null){ Toast.makeText(this, "saveInstanceNotNull", 2000).show(); Bundle map = savedInstanceState.getBundle(DATA_NAME); if(map != null){ Toast.makeText(this, "mapNotNull", 2000).show(); my2048View.restoreDataAndState(map); } } } @Override protected void onPause() { super.onPause(); my2048View.saveMaxScore(); } @Override protected void onSaveInstanceState(Bundle outState) { outState.putBundle(DATA_NAME, my2048View.saveDataAndState()); } }最新源码查看地址:https://code.csdn.net/lxq_xsyu/my2048/tree/12f4fee0d72819c4421d20ac847ec94ecd687165/src/com/example/my2048/My2048View.java