一直觉得能写游戏的都是大神!因为学习方向以及时间的问题,很少动手开发游戏。在校的时候,记得写过当时很火的游戏“像素鸟”,哈哈,作为菜鸟来说还是挺有成就感的!进入正题,本文主要从自定义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();}@Overrideprotected 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();}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {Log.d(TAG,"onLayout");super.onLayout(changed, l, t, r, b);}@Overrideprotected 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);}@Overrideprotected void onDraw(Canvas canvas) {Log.d(TAG,"onDraw");super.onDraw(canvas);}@Overridepublic boolean onTouchEvent(MotionEvent event) {mGestureDetector.onTouchEvent(event);return true;}private  class MyGestureDetector extends GestureDetector.SimpleOnGestureListener {//按下@Overridepublic boolean onDown(MotionEvent motionEvent) {return false;}//按下后没有松开或者拖动@Overridepublic void onShowPress(MotionEvent motionEvent) {}//轻触后松开@Overridepublic boolean onSingleTapUp(MotionEvent motionEvent) {return false;}//滑动@Overridepublic boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {return false;}//长按@Overridepublic void onLongPress(MotionEvent motionEvent) {}//快速移动(e1 滑动起点,e2 当前手势位置,Vx 每秒x轴移动像素,Vy每秒y轴方向移动像素)@Overridepublic 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;@Overrideprotected 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);}@Overridepublic void setScore(int score) {mScore.setText("Score: " + score);}@Overridepublic void setGameOver() {new AlertDialog.Builder(this).setTitle("GAME OVER").setMessage("Do you want to try again?").setPositiveButton("Yes", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {mGameLayout.reStart();}}).setNegativeButton("No", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialogInterface, int i) {finish();}}).show();}
}

哈哈,写游戏还是挺有成就感的!到这里,我的2048就可以跑起来了。因为代码中注释的都比较清楚,所以具体的细节就没有写出来,只是从总体思路进行了阐述。最重要的还是着手去写,遇到问题,解决问题,最后都不是问题。

效果图:

自定义View实现2048相关推荐

  1. Android开发自定义View实现数字与图片无缝切换的2048

    本博客地址:http://blog.csdn.net/talentclass_ctt/article/details/51952378 最近在学自定义View,无意中看到鸿洋大神以前写过的2048(附 ...

  2. Android自定义View基本步骤

    一.自定义属性 1.在res下的values下面新建attrs.xml 2.在布局中使用,声明命名空间 3.在自定义View构造方法中通过TypedArray获取属性 4.必须回收 array.rec ...

  3. Android自定义View —— TypedArray

    在上一篇中Android 自定义View Canvas -- Bitmap写到了TypedArray 这个属性 下面也简单的说一下TypedArray的使用 TypedArray 的作用: 用于从该结 ...

  4. Android 自定义View Canvas —— Bitmap

    Bitmap 绘制图片 常用的方法有一下几种 (1) drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint ...

  5. Android 自定义View —— Canvas

    上一篇在android 自定义view Paint 里面 说了几种常见的Point 属性 绘制图形的时候下面总有一个canvas ,Canvas 是是画布 上面可以绘制点,线,正方形,圆,等等,需要和 ...

  6. Android 自定义View —— Paint

    上一篇说了自定义view的坐标系以及view 的使用,下面说下自定义view Paint 的使用 Paint 相对于画笔 ,可以使用Paint 来决定画的内容的颜色,边距粗细,设置样式,字体大小 ,等 ...

  7. Android 自定义View (入门 篇) 的使用

    每次都是过了很久都需要温习一下,自己打算整理一下方便查阅, 自定义view 首选需要明白的就是它的坐标系了,以手机左上角为起始点(0.0),横向的为x轴,竖向的为y轴 为了更好的理解我画了一幅草图如下 ...

  8. 28自定义View 模仿联系人字母侧栏

    自定义View LetterView.java package com.qf.sxy.customview02;import android.content.Context; import andro ...

  9. android炫酷的自定义view,Android自定义View实现炫酷进度条

    本文实例为大家分享了Android实现炫酷进度条的具体代码,供大家参考,具体内容如下 下面我们来实现如下效果: 第一步:创建attrs文件夹,自定义属性: 第二步:自定义View: /** * Cre ...

  10. android 自定义音乐圆形进度条,Android自定义View实现音频播放圆形进度条

    本篇文章介绍自定义View配合属性动画来实现如下的效果 实现思路如下: 根据播放按钮的图片大小计算出圆形进度条的大小 根据音频的时间长度计算出圆形进度条绘制的弧度 通过Handler刷新界面来更新圆形 ...

最新文章

  1. 那些《西游记》中你不知道的野史,信不信由你
  2. 励志!86岁的他,申请获得国家自然科学基金!
  3. 压缩和解压文件:tar gzip bzip2 compress(转)
  4. 无线策略服务器,无线网络中的分布式资源管理策略研究
  5. 【Java】为什么java构造函数的构造器只能在第一行写this() 或者super() ?
  6. 文案一方面需要创意,但一方面不需要过分沉溺于创意
  7. 2017 Chinese Multi-University Training, BeihangU Contest
  8. windows2003——工作组和域控制器
  9. 【IMX6ULL笔记】--内核底层驱动初步探究
  10. 在SQL server 2008 R2进行数据查询操作时提示 “对象名无效”的问题
  11. Axure-产品交互设计师的利器
  12. 测试用例设计——微信发朋友圈(详细)
  13. Android 自带工具生成图标
  14. PHP header网页安全认证
  15. 『中级篇』什么是Container(15)
  16. 湖南省中职学业水平考试复习试题(数学)
  17. Ant Design vue 改变表格背景颜色
  18. MGRE结合OSPF
  19. 快速上手golang
  20. 小小Python爬虫一

热门文章

  1. 武汉大学计算机学院选考要求,武汉大学高考必选科目-考武汉大学需要选哪三科...
  2. Nginx日志管理——了解Nginx日志选项配置以及自定义日志格式使用
  3. 中国机床行业投资现状与十四五发展战略决策报告2022版
  4. openwrt环境下,使用externel commissioning组网openthread
  5. 监控网页内容,发现需要的内容后弹框和声音提醒
  6. LABjs分析 http://labjs.com/documentation.php#queuescript
  7. linux应用程序故障排查,为Linux应用程序排查故障的另类方法
  8. java课程设计通讯录_java课程设计(通讯录管理软件源代码)
  9. 任何共享软件作者都能挣到一年10万美金以上的收入,只要他想的话
  10. stm32h743单片机嵌入式学习笔记2-单片机获取电容触摸屏原理