自定义View实现2048
来源:互联网 发布:yy老虎机抽奖软件 编辑:程序博客网 时间:2024/06/06 14:16
一直觉得能写游戏的都是大神!因为学习方向以及时间的问题,很少动手开发游戏。在校的时候,记得写过当时很火的游戏“像素鸟”,哈哈,作为菜鸟来说还是挺有成就感的!进入正题,本文主要从自定义view,以及自定义layout来实现2048游戏。
思路:
1. 首先,当然是将游戏的所有格子画出来。这里,定义N,表示N行N列,即N*N个格子可以移动。每一个方块为一个自定义的GameItem。方块的长宽由layout决定。
2. 自定义Layout,用于绘制所有方块,以及相应滑动监听。这是最主要的一部分,涉及到具体的算法。
3. 上面的两个步骤实质上定义了view,当然需要主程序跑起来啰。设置游戏结束以及得分的监听接口。
自定义GameItem
每一个方块都是一个正方形,根据不同数字绘制方块的背景色,如果数字不为零,则绘制数字。比较简单,详见代码:
package com.example.huangzheng.game;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Rect;import android.support.annotation.Nullable;import android.util.AttributeSet;import android.util.Log;import android.view.View;/** * Created by huangzheng on 2017/11/20. */public class GameItem extends View { private int mNumber; private String mNumberVal; private Paint mPaint; private Rect mRect;//绘制文字区域 public GameItem(Context context) { this(context,null); } public GameItem(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public GameItem(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPaint = new Paint(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); String mBgColor = "#CCC0B3"; switch (mNumber) { case 0: mBgColor = "#CCC0B3"; break; case 2: mBgColor = "#EEE4DA"; break; case 4: mBgColor = "#EDE0C8"; break; case 8: mBgColor = "#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(Paint.Style.FILL); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);//宽高由layout决定 if (mNumber != 0){ drawText(canvas); } } private void drawText(Canvas mCanvas){ mPaint.setColor(Color.BLACK); float x = (getWidth() - mRect.width()) / 2; float y = (getHeight() + mRect.height()) / 2; //值得注意的是,y是text的下边际,x为起始位置 mCanvas.drawText(mNumberVal,x,y,mPaint); } public void setNumber(int number){ this.mNumber = number; mNumberVal = mNumber + ""; mPaint.setTextSize(30.0f); mRect = new Rect(); mPaint.getTextBounds(mNumberVal, 0, mNumberVal.length(), mRect); invalidate(); } public int getNumber(){ return mNumber; }}
自定义Layout
重要的部分来了!整体思路:
- 获取布局的长宽,在根据方块的行列数绘制所有初始方块,并随机将一个方块的值设为2;
- 按键监听用户的上向左右滑动事件,对每行每列的方块进行重新的排列并重新绘制
- 游戏结束的判断
先上代码,再庖丁解牛。
package com.example.huangzheng.game;import android.content.Context;import android.graphics.Canvas;import android.util.AttributeSet;import android.util.Log;import android.util.TypedValue;import android.view.GestureDetector;import android.view.MotionEvent;import android.widget.RelativeLayout;import java.util.ArrayList;import java.util.List;import java.util.Random;/** * Created by huangzheng on 2017/11/22. */public class GameLayout extends RelativeLayout { private final static String TAG = "GameLayout"; private int mN = 4; //n行n列 private int mMargin = 3;//item间隔 private int mItemSize;//方块边长 private int mWidth; private int mHeight; private int mPinding; private int mScore = 0; private GameItem[] mGameItem; private GestureDetector mGestureDetector; private CallBackInterface mCallBack; private boolean mIsFirst = true;//是否第一次启动 private boolean mIsMove = false;//是否发生了移动 private boolean mIsMarge = false;//是否发生了合并 /* * 动作枚举 */ private enum ACTION{ UP, RIGHT, DOWN, LEFT } private final static float MIX_DISTANCE = 10;//滑动的有效距离 public GameLayout(Context context) { this(context,null); } public GameLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public GameLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); /* * px、dp的相互转换, * type1:需要转换的是dp or px * type2:具体值 * type3:DisplayMetrics,屏幕信息类 */ mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMargin, getResources().getDisplayMetrics()); //获取边距 mPinding = Math.min(getPaddingLeft(), getPaddingTop()); //手势监听 mGestureDetector = new GestureDetector(new MyGestureDetector()); } //注册回调 public void setRegister(CallBackInterface callBackInterface){ this.mCallBack = callBackInterface; } //重新开始 public void reStart(){ //requestLayout();//执行onMeasure、onLayout、onDraw方法 //invalidate();//只会执行onDraw方法 for (GameItem item: mGameItem){ item.setNumber(0); } mScore = 0; if (mCallBack != null){ mCallBack.setScore(mScore); } getNewNumber(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { Log.d(TAG,"onLayout"); super.onLayout(changed, l, t, r, b); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.d(TAG,"onMeasure"); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); int lenght = Math.min(mWidth,mHeight); mItemSize = (lenght - mPinding * 2 - (mN - 1) * mMargin ) / mN; if (mIsFirst){ if (mGameItem == null){ mGameItem = new GameItem[mN * mN]; } for (int i = 0; i < mGameItem.length; i++){ GameItem item = new GameItem(getContext()); mGameItem[i] = item; item.setId(i + 1); RelativeLayout.LayoutParams lp = new LayoutParams(mItemSize,mItemSize); //非最后一列 if ((i + 1) % mN != 0){ lp.rightMargin = mMargin; } //非第一列 if (i % mN != 0){ lp.addRule(RelativeLayout.RIGHT_OF,mGameItem[i -1].getId()); } //非第一行 if ((i + 1) > mN){ lp.topMargin = mMargin; lp.addRule(RelativeLayout.BELOW,mGameItem[i - mN].getId()); } addView(item,lp); } getNewNumber(); } mIsFirst = false; setMeasuredDimension(lenght, lenght); } @Override protected void onDraw(Canvas canvas) { Log.d(TAG,"onDraw"); super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return true; } private class MyGestureDetector extends GestureDetector.SimpleOnGestureListener { //按下 @Override public boolean onDown(MotionEvent motionEvent) { return false; } //按下后没有松开或者拖动 @Override public void onShowPress(MotionEvent motionEvent) { } //轻触后松开 @Override public boolean onSingleTapUp(MotionEvent motionEvent) { return false; } //滑动 @Override public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) { return false; } //长按 @Override public void onLongPress(MotionEvent motionEvent) { } //快速移动(e1 滑动起点,e2 当前手势位置,Vx 每秒x轴移动像素,Vy每秒y轴方向移动像素) @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float Vx, float Vy) { float x = e2.getX() - e1.getX(); float y = e2.getY() - e1.getY(); if (x > MIX_DISTANCE && (Math.abs(Vx) > Math.abs(Vy))){ doAction(ACTION.RIGHT); } else if (x < -MIX_DISTANCE && (Math.abs(Vx) > Math.abs(Vy))){ doAction(ACTION.LEFT); } else if (y > MIX_DISTANCE && (Math.abs(Vx) < Math.abs(Vy))){ doAction(ACTION.DOWN); } else if (y < -MIX_DISTANCE && (Math.abs(Vx) < Math.abs(Vy))){ doAction(ACTION.UP); } return true; } } /* * 手指移动时,四个方向的所有行都需要移动 * 1、将每行的值取出并保存到数值 * 2、根据手势对数组进行移动和合并(判断是否移动、合并) * 3、将新的数组放置到每行中 */ private void doAction(ACTION action){ Log.d(TAG,"doAction:" + action); for (int i = 0;i < mN; i++){ List<GameItem> row = new ArrayList<GameItem>(); //1、将每行的值取出并保存到数值 for (int j = 0;j < mN; j++){ int index = getIndexByAction(action,i,j); GameItem item = mGameItem[index]; if (item.getNumber() != 0){ //Log.d(TAG,"number:" + item.getNumber()); row.add(item); } } //判断是否移动 for (int j = 0;j < row.size();j++){ int index = getIndexByAction(action,i,j); GameItem item = mGameItem[index]; if (item.getNumber() != row.get(j).getNumber()){ mIsMove = true; break; } } //2、根据手势对数组进行移动和合并 row = doMerageItem(row); //3、将新的数组放置到每行中 for (int j = 0; j < mN; j++){ int index = getIndexByAction(action, i, j); if (row.size() > j) { mGameItem[index].setNumber(row.get(j).getNumber()); } else { mGameItem[index].setNumber(0); } } } getNewNumber(); } private List<GameItem> doMerageItem(List<GameItem> row) { List<GameItem> backRow = new ArrayList<GameItem>(); if (row.size() < 2){ backRow = row; return backRow; } for (int j = 0;j < row.size() - 1;j++){ GameItem item1 = row.get(j); GameItem item2 = row.get(j + 1); if (item1.getNumber() == item2.getNumber()){ mIsMarge = true; int value = item1.getNumber() + item2.getNumber(); item1.setNumber(value); item2.setNumber(0); //回调显示分数 mScore += value; mCallBack.setScore(mScore); } } for (int j = 0;j < row.size();j++){ if (row.get(j).getNumber() != 0){ backRow.add(row.get(j)); } } return backRow; } //根据action获取对应下标,如果为down right则反向储存 private int getIndexByAction(ACTION action, int i, int j) { int index = 0; switch (action){ case UP: index = j*mN + i; break; case DOWN: index = (mN-j-1)*mN + i; break; case LEFT: index = i*mN + j; break; case RIGHT: index = i*mN + (mN-j-1); break; } return index; } //随机生成数字 private void getNewNumber(){ if (isGameOver()){ if (mCallBack != null){ mCallBack.setGameOver(); return; } } if (!isFull()){ if (mIsMarge || mIsMove || mIsFirst){ int n = mN * mN; Random random = new Random(); int next = random.nextInt(n); GameItem item = mGameItem[next]; while (item.getNumber() != 0){ next = random.nextInt(n); item = mGameItem[next]; } item.setNumber(2); mIsMarge = mIsMove = false; } } } //判断是否还有空格 private boolean isFull(){ boolean result = true; for (int i = 0;i < mN;i++){ for (int j = 0;j < mN;j++){ int index = i*mN + j; GameItem item = mGameItem[index]; if (item.getNumber() == 0){ return false; } } } return result; } //判断是否结束游戏(是否还有空格,如果无,是否相同数字) private boolean isGameOver(){ boolean result = true; if (!isFull()){ return false; } for (int i = 0;i < mN;i++){ for (int j = 0;j < mN;j++){ int index = i*mN + j; GameItem item = mGameItem[index]; //上 if (index - mN > -1){ if (item.getNumber() == mGameItem[index - mN].getNumber()){ return false; } } //下 if (index + mN < mN*mN){ if (item.getNumber() == mGameItem[index + mN].getNumber()){ return false; } } //左 if (index%mN !=0){ if (item.getNumber() == mGameItem[index -1].getNumber()){ return false; } } //右 if ((index + 1)%mN !=0){ if (item.getNumber() == mGameItem[index + 1].getNumber()){ return false; } } } } return result; }}
初始化所有方块
首先获取布局的长宽,从而计算出每个方块的边长;其次为每个方块设定位置约束规则;最后随机为某一方块赋值为“2”。
事件监听
1. 定义事件枚举,根据滑动前后的位置相应Up、Down、Left、Right事件。
2. 对每行每列进行排列重绘
通过两层循环,getIndexByAction(action,i,j)方法返回每一个方块的位置信息。
private int getIndexByAction(ACTION action, int i, int j) { int index = 0; switch (action){ case UP: index = j*mN + i; break; case DOWN: index = (mN-j-1)*mN + i; break; case LEFT: index = i*mN + j; break; case RIGHT: index = i*mN + (mN-j-1); break; } return index; }
如果为Up、Left,则顺序获取;如果为Down、Right则逆向获取。等等,就猜到你会问为什么!这样做的目的是为了方便我们后面的每行或没列的合并。举个例子:假如现在有第一行数据,2 2 4 4,如果此时相应Left事件,返回的位置id为0,1,2,3,数据合并后的值为4 8 0 0 ,则按位置信息放入行;如果响应的是Right事件,返回的位置id为3,2,1,0,因此返回的数据为4 4 2 2 ,合并后的数据为8 4 0 0 ,最后将合并后的值赋值给获取到的逆向id,为0 0 4 8。有点绕,拿张纸,画一画规律就好理解了!
关于数值的合并:将通过位置id获取的数值存储到列表row中,通过一层for循环对相同的数进行合并,第一个数设置数值为合并后的值,第二个数设置数值为0.
在合并的过程中,如果有合并,则mIsMarge为true;如果没有合并,但是有移动,mIsMove为true。合并或者移动结束后,如果mIsMove或者mIsMarge为true,则随机为某一空白方块赋值2。
游戏结束判断
条件:1、没有数值为0的方块;2、没有相连方块的数值相同。同时满足两个条件,则游戏结束。
应用View
有了上面的准备工作,我们只需要将GameLayout当做类似TextView的组件使用就可以了。采用回调机制,更新分数以及响应游戏结束。
回调接口:
public interface CallBackInterface { void setScore(int score); void setGameOver();}
MainActivity:
package com.example.huangzheng.game;import android.content.DialogInterface;import android.support.v7.app.AlertDialog;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;public class MainActivity extends AppCompatActivity implements CallBackInterface{ private TextView mScore; private GameLayout mGameLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mScore = (TextView) findViewById(R.id.sorce); mGameLayout = (GameLayout) findViewById(R.id.gameLayout); mGameLayout.setRegister(this); } @Override public void setScore(int score) { mScore.setText("Score: " + score); } @Override public void setGameOver() { new AlertDialog.Builder(this) .setTitle("GAME OVER") .setMessage("Do you want to try again?") .setPositiveButton("Yes", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { mGameLayout.reStart(); } }) .setNegativeButton("No", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { finish(); } }).show(); }}
哈哈,写游戏还是挺有成就感的!到这里,我的2048就可以跑起来了。因为代码中注释的都比较清楚,所以具体的细节就没有写出来,只是从总体思路进行了阐述。最重要的还是着手去写,遇到问题,解决问题,最后都不是问题。
效果图: