Android 带你玩转实现游戏2048 其实2048只是个普通的控件
原文出处: 【张鸿洋的博客】
1、概述
博主本想踏入游戏开放行业,无奈水太深,不会游泳;于是乎,只能继续开发应用,但是原生Android也能开发游戏么,2048、像素鸟、别踩什么来着;今天给大家带来一篇2048的开发篇,别怕不分上下文,或者1、2、3、4,一篇包你能玩happy~虽然我从来没有玩到过2048!!!其实大家也可以当作自定义控件来看~~~
特别说明一下,游戏2048里面的方块各种颜色来源于:http://download.csdn.net/detail/qq1121674367/7155467,这个2048的代码中,其他代码,太多,未参考;特此感谢分享;大家也可以下载下,对比学习下;
接下来贴个我们项目的效果图:
ok 看完效果图,我就准备带领大家征服这款游戏了~~~
2、实现分析
贴一张静态图,开始对我们游戏的设计:
可以看到,游戏其实就是一个容器,里面很多个方块,触摸容器,里面的方块的形态会发生变化。那么:
1、容器我们准备自定义ViewGroup ,叫做Game2048Layout ; 里面的块块自定义View ,叫做Game2048Item
接下来从简单的开始:
2、Game2048Item
Game2048Item是个View,并且需要哪些属性呢?
首先得有个number,显示数字嘛,然后绘制的时候根据number绘制背景色;还需要呢?嗯,需要正方形边长,再考虑下,这个边长应该Item自己控制么?显然不是的,Game2048Layout 是个n*n的面板,这个n是不确定的,所以Item的边长肯定是Game2048Layout 计算好传入的。这样必须的属性就这两个。
3、Game2048Layout
Game2048Layout是个容器,我们观察下,里面View是个 n*n的排列,我们准备让其继承RelativeLayout ; 这样可以通过设置Item的RIGHT_OF之类的属性进行定位;
我们在onMeasure里面得到Layout的宽和高,然后根据n*n,生成一定数目的Item,为其设置宽和高,放置到Layout中,这样整个游戏的布局就做好了;绘制的细节上:Item间有横向与纵向的间距,所以需要设置这个值,叫做mMargin。然后Item的边长 = ( Layout边长 – (n-1)*mMagin ) / n ;
剩下的就是onTouchEvent里面去判断用户手势了,然后就行各种逻辑操作了~
3、代码之旅
首先来看看我们的Game2048Item
1、Game2048Item
package com.zhy.game2048.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * 2048的每个Item * * @author zhy * */ public class Game2048Item extends View { /** * 该View上的数字 */ private int mNumber; private String mNumberVal; private Paint mPaint; /** * 绘制文字的区域 */ private Rect mBound; public Game2048Item(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mPaint = new Paint(); } public Game2048Item(Context context) { this(context, null); } public Game2048Item(Context context, AttributeSet attrs) { this(context, attrs, 0); } public void setNumber(int number) { mNumber = number; mNumberVal = mNumber + ""; mPaint.setTextSize(30.0f); mBound = new Rect(); mPaint.getTextBounds(mNumberVal, 0, mNumberVal.length(), mBound); invalidate(); } public int getNumber() { return mNumber; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String mBgColor = ""; switch (mNumber) { case 0: mBgColor = "#CCC0B3"; break; case 2: mBgColor = "#EEE4DA"; break; case 4: mBgColor = "#EDE0C8"; break; case 8: mBgColor = "#F2B179";// #F2B179 break; case 16: mBgColor = "#F49563"; break; case 32: mBgColor = "#F5794D"; break; case 64: mBgColor = "#F55D37"; break; case 128: mBgColor = "#EEE863"; break; case 256: mBgColor = "#EDB04D"; break; case 512: mBgColor = "#ECB04D"; break; case 1024: mBgColor = "#EB9437"; break; case 2048: mBgColor = "#EA7821"; break; default: mBgColor = "#EA7821"; break; } mPaint.setColor(Color.parseColor(mBgColor)); mPaint.setStyle(Style.FILL); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); if (mNumber != 0) drawText(canvas); } /** * 绘制文字 * * @param canvas */ private void drawText(Canvas canvas) { mPaint.setColor(Color.BLACK); float x = (getWidth() - mBound.width()) / 2; float y = getHeight() / 2 + mBound.height() / 2; canvas.drawText(mNumberVal, x, y, mPaint); } }
很简单,基本就一个onDraw通过number来绘制背景和数字;number通过调用setNumer进行设置;它的宽和高都是固定值,所以我们并不需要自己进行测量~~
2、Game2048Layout
1、成员变量
这就是我们最主要的一个类了,首先我们看看这个类的成员变量,先看看各个成员变量的作用:
/** * 设置Item的数量n*n;默认为4 */ private int mColumn = 4; /** * 存放所有的Item */ private Game2048Item[] mGame2048Items; /** * Item横向与纵向的边距 */ private int mMargin = 10; /** * 面板的padding */ private int mPadding; /** * 检测用户滑动的手势 */ private GestureDetector mGestureDetector; // 用于确认是否需要生成一个新的值 private boolean isMergeHappen = true; private boolean isMoveHappen = true; /** * 记录分数 */ private int mScore;
主要的成员变量就这些,直接看注释也比较容易理解~~
了解了成员变量,接下来我们需要在构造方法里面得到一些值和初始化一些变量
2、构造方法
public Game2048Layout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMargin, getResources().getDisplayMetrics()); // 设置Layout的内边距,四边一致,设置为四内边距中的最小值 mPadding = min(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); mGestureDetector = new GestureDetector(context , new MyGestureDetector()); }
我们在构造方法里面得到Item间的边距(margin)和我们容器的内边距(padding,),这个值应该四边一致,于是我们取四边的最小值;这两个属性可以抽取为自定义的属性;然后初始化了我们的mGestureDetector
有了margin和padding,我们就可以计算我们item的边长了。这个计算过程肯定在onMeasure里面,因为我们需要在onMeasure获取容器的宽和高
3、onMeasure
private boolean once; /** * 测量Layout的宽和高,以及设置Item的宽和高,这里忽略wrap_content 以宽、高之中的最小值绘制正方形 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 获得正方形的边长 int length = Math.min(getMeasuredHeight(), getMeasuredWidth()); // 获得Item的宽度 int childWidth = (length - mPadding * 2 - mMargin * (mColumn - 1)) / mColumn; if (!once) { if (mGame2048Items == null) { mGame2048Items = new Game2048Item[mColumn * mColumn]; } // 放置Item for (int i = 0; i < mGame2048Items.length; i++) { Game2048Item item = new Game2048Item(getContext()); mGame2048Items[i] = item; item.setId(i + 1); RelativeLayout.LayoutParams lp = new LayoutParams(childWidth, childWidth); // 设置横向边距,不是最后一列 if ((i + 1) % mColumn != 0) { lp.rightMargin = mMargin; } // 如果不是第一列 if (i % mColumn != 0) { lp.addRule(RelativeLayout.RIGHT_OF,// mGame2048Items[i - 1].getId()); } // 如果不是第一行,//设置纵向边距,非最后一行 if ((i + 1) > mColumn) { lp.topMargin = mMargin; lp.addRule(RelativeLayout.BELOW,// mGame2048Items[i - mColumn].getId()); } addView(item, lp); } generateNum(); } once = true; setMeasuredDimension(length, length); }
首先设置容器的边长为宽高中的最小值;然后(length – mPadding * 2 – mMargin * (mColumn – 1)) / mColumn ; 去计算Item的边长;
拿到以后,根据我们的mColumn初始化我们的Item数组,然后遍历生成Item,设置Item的LayoutParams以及Rule(RIGHT_OF , BELOW),最后添加到我们的容器中;
最后我们通过setMeasuredDimension(length, length);改变我们布局占据的空间;
到此,我们整个面板绘制完成了;
接下来,就是根据用户的手势,去进行游戏逻辑操作了,手势那么肯定是onTouchEvent了:
4、onTouchEvent
@Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return true; }
我们把触摸事件交给了mGestureDetector,我们去看看我们的mGestureDetector,在构造方法中有这么一句:
mGestureDetector = new GestureDetector(context , new MyGestureDetector());
so,我们需要去看看MyGestureDetector:
class MyGestureDetector extends GestureDetector.SimpleOnGestureListener { final int FLING_MIN_DISTANCE = 50; @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { float x = e2.getX() - e1.getX(); float y = e2.getY() - e1.getY(); if (x > FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) { action(ACTION.RIGHT); } else if (x < -FLING_MIN_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) { action(ACTION.LEFT); } else if (y > FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) { action(ACTION.DOWM); } else if (y < -FLING_MIN_DISTANCE && Math.abs(velocityX) < Math.abs(velocityY)) { action(ACTION.UP); } return true; } }
很简单,就是判读用户上、下、左、右滑动;然后去调用action(ACTION)方法;ACTION是个枚举:
/** * 运动方向的枚举 * * @author zhy * */ private enum ACTION { LEFT, RIGHT, UP, DOWM }
这么看,核心代码都在action方法里面了:
5、根据用户手势重绘Item
看代码前,先考虑下,用户从右向左滑动时,面板应该如何变化;取其中一行,可能性为:
0 0 0 2 -> 2 0 0 0
2 0 4 0 -> 2 4 0 0
2 2 4 0 -> 4 4 0 0
大概就这3中可能;
我们算法是这么做的:
拿2 2 4 0 来说:
1、首先把每行有数字的取出来,临时存储下来,即[ 2, 2, 4 ];
2、然后遍历合并第一个相遇的相同的,即[ 4, 4 ,0 ]
3、然后直接放置到原行,不足补0,即[ 4, 4, 0 ,0 ];
中间还有几个操作:
1、生成一个新的数字,游戏在每次用户滑动时,可能会生成一个数字;我们的生成策略:如果发生移动或者合并,则生成一个数字;
移动的判断,拿原数据,即【 2 ,2,4,0】和我们第一步临时存储的做比较,一一对比(遍历临时表),发现不同,则认为移动了;
合并的判断,在合并的时候会设置合并的标志位为true;
2、加分,如果发生合并,则加分,分值为合并得到的数字,比如 4,4 -> 8 ,即加8分 ; 也只需要在合并的时候进行相加就行了;
介绍完了,来看我们的代码:
/** * 根据用户运动,整体进行移动合并值等 */ private void action(ACTION action) { // 行|列 for (int i = 0; i < mColumn; i++) { List<Game2048Item> row = new ArrayList<Game2048Item>(); // 行|列 for (int j = 0; j < mColumn; j++) { // 得到下标 int index = getIndexByAction(action, i, j); Game2048Item item = mGame2048Items[index]; // 记录不为0的数字 if (item.getNumber() != 0) { row.add(item); } } for (int j = 0; j < mColumn && j < row.size(); j++) { int index = getIndexByAction(action, i, j); Game2048Item item = mGame2048Items[index]; if (item.getNumber() != row.get(j).getNumber()) { isMoveHappen = true; } } // 合并相同的 mergeItem(row); // 设置合并后的值 for (int j = 0; j < mColumn; j++) { int index = getIndexByAction(action, i, j); if (row.size() > j) { mGame2048Items[index].setNumber(row.get(j).getNumber()); } else { mGame2048Items[index].setNumber(0); } } } generateNum(); }
大体上是两层循环,外层循环代码循环次数,内层有3个for循环;
第一个for循环,对应上述:首先把每行有数字的取出来,临时存储下来,即[ 2, 2, 4 ];
第二个for循环,判断是否发生移动;
// 合并相同的
mergeItem(row); 是去进行合并操作,对应上述:然后遍历合并第一个相遇的相同的,即[ 4, 4 ,0 ];以及加分和设置合并标志位都在方法中;
第三个for循环:设置合并后的值,对应上述:然后直接放置到原行,不足补0,即[ 4, 4, 0 ,0 ];
最后生成数字,方法内部会进行判断游戏是否结束,是否需要生成数字;
那么先看mergeItem的代码:
/** * 合并相同的Item * * @param row */ private void mergeItem(List<Game2048Item> row) { if (row.size() < 2) return; for (int j = 0; j < row.size() - 1; j++) { Game2048Item item1 = row.get(j); Game2048Item item2 = row.get(j + 1); if (item1.getNumber() == item2.getNumber()) { isMergeHappen = true; int val = item1.getNumber() + item2.getNumber(); item1.setNumber(val); // 加分 mScore += val; if (mGame2048Listener != null) { mGame2048Listener.onScoreChange(mScore); } // 向前移动 for (int k = j + 1; k < row.size() - 1; k++) { row.get(k).setNumber(row.get(k + 1).getNumber()); } row.get(row.size() - 1).setNumber(0); return; } } }
也比较简单,循环查找相同的number,发现合并数字,加分;
加分我们设置了一个回调,把分数回调出去:
if (mGame2048Listener != null) { mGame2048Listener.onScoreChange(mScore); }
最后看我们生成数字的代码:
/** * 产生一个数字 */ public void generateNum() { if (checkOver()) { Log.e("TAG", "GAME OVER"); if (mGame2048Listener != null) { mGame2048Listener.onGameOver(); } return; } if (!isFull()) { if (isMoveHappen || isMergeHappen) { Random random = new Random(); int next = random.nextInt(16); Game2048Item item = mGame2048Items[next]; while (item.getNumber() != 0) { next = random.nextInt(16); item = mGame2048Items[next]; } item.setNumber(Math.random() > 0.75 ? 4 : 2); isMergeHappen = isMoveHappen = false; } } }
首先判断是否结束,如果结束了,依然是回调出去,得让玩的人知道结束了;
然后判断当然面板是有木有空的格子,如果没有,在判断需要生成新的数字么,需要则随机生成一个新的2或4;
那么如何判断是否结束呢?
首先肯定是没有空格了,然后四个方向上没有相同的数字就结束了:
/** * 检测当前所有的位置都有数字,且相邻的没有相同的数字 * * @return */ private boolean checkOver() { // 检测是否所有位置都有数字 if (!isFull()) { return false; } for (int i = 0; i < mColumn; i++) { for (int j = 0; j < mColumn; j++) { int index = i * mColumn + j; // 当前的Item Game2048Item item = mGame2048Items[index]; // 右边 if ((index + 1) % mColumn != 0) { Log.e("TAG", "RIGHT"); // 右边的Item Game2048Item itemRight = mGame2048Items[index + 1]; if (item.getNumber() == itemRight.getNumber()) return false; } // 下边 if ((index + mColumn) < mColumn * mColumn) { Log.e("TAG", "DOWN"); Game2048Item itemBottom = mGame2048Items[index + mColumn]; if (item.getNumber() == itemBottom.getNumber()) return false; } // 左边 if (index % mColumn != 0) { Log.e("TAG", "LEFT"); Game2048Item itemLeft = mGame2048Items[index - 1]; if (itemLeft.getNumber() == item.getNumber()) return false; } // 上边 if (index + 1 > mColumn) { Log.e("TAG", "UP"); Game2048Item itemTop = mGame2048Items[index - mColumn]; if (item.getNumber() == itemTop.getNumber()) return false; } } } return true; }
/** * 是否填满数字 * * @return */ private boolean isFull() { // 检测是否所有位置都有数字 for (int i = 0; i < mGame2048Items.length; i++) { if (mGame2048Items[i].getNumber() == 0) { return false; } } return true; }
到此,我们的代码介绍完毕~~~完成了我们的Game2048Layout ; 接下来看如何使用呢?
写游戏的过程很艰辛,但是用起来,看看什么叫so easy ; 当成普通的View即可:
4、实践
1、布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.zhy.game2048.view.Game2048Layout android:id="@+id/id_game2048" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_centerInParent="true" android:background="#ffffff" android:padding="10dp" > </com.zhy.game2048.view.Game2048Layout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/id_game2048" android:layout_centerHorizontal="true" android:layout_marginBottom="20dp" android:background="#EEE4DA" android:orientation="horizontal" > <TextView android:id="@+id/id_score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:text="Score: 0" android:textColor="#EA7821" android:textSize="30sp" android:textStyle="bold" /> </LinearLayout> </RelativeLayout>
2、MainActivity
package com.zhy.game2048; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.widget.TextView; import com.zhy.game2048.view.Game2048Layout; import com.zhy.game2048.view.Game2048Layout.OnGame2048Listener; public class MainActivity extends Activity implements OnGame2048Listener { private Game2048Layout mGame2048Layout; private TextView mScore; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mScore = (TextView) findViewById(R.id.id_score); mGame2048Layout = (Game2048Layout) findViewById(R.id.id_game2048); mGame2048Layout.setOnGame2048Listener(this); } @Override public void onScoreChange(int score) { mScore.setText("SCORE: " + score); } @Override public void onGameOver() { new AlertDialog.Builder(this).setTitle("GAME OVER") .setMessage("YOU HAVE GOT " + mScore.getText()) .setPositiveButton("RESTART", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mGame2048Layout.restart(); } }).setNegativeButton("EXIT", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }).show(); } }
很简单,代码主要就是设置个接口,当发生加分已经游戏结束时会交给Activity去处理~~~如果喜欢,你可以在一个界面放4个游戏~~~
当然了游戏Item的个数也可以动态设置~~~最后贴一个5*5游戏的截图~~
好了,2048到此结束,拿只笔开始设计,然后根据自定义View的经验去写,相信你可以学会不少东西~~~
并且我们的View是抽取出来的,其实换成图片也很简单~~
今天又看了war3十大经典战役,献上war3版,代码就不贴了,改动也就几行代码,贴个截图,纪念我们曾经的war3~~~:
额,咋都弄成5*5了~大家可以把mColumn改为4~~~