自定义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就可以跑起来了。因为代码中注释的都比较清楚,所以具体的细节就没有写出来,只是从总体思路进行了阐述。最重要的还是着手去写,遇到问题,解决问题,最后都不是问题。

效果图:

这里写图片描述

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 军人保障卡丢失怎么办 想当兵眼睛近视怎么办 父母被欺负了怎么办 上级老针对你怎么办 进了监狱以后怎么办 监狱老幼病残人怎么办 久站小腿粗怎么办 小腿骨头不直怎么办 腿站久了变粗怎么办 对生活没兴趣怎么办 话费权益受到侵犯怎么办 领土主权受到侵犯怎么办 感觉自己受到侵犯怎么办 内招兵退伍怎么办户口 导师名额已满怎么办 快手作品被屏蔽怎么办 体检没带照片怎么办 征兵年龄超了怎么办 剪了刘海不适合怎么办 头发太厚了怎么办 23岁了一事无成怎么办 该怎么办呀28一事无成 30几岁还一事无成怎么办 宝宝不安运动缺乏怎么办 30岁干什么工作怎么办 34岁还一事无成怎么办 想当兵身高不够怎么办 征兵体能测试不合格怎么办 警察收押犯人贿赂怎么办 16岁想当兵怎么办 当兵变丑了怎么办 军人户口未注销怎么办 孩子当兵了户口怎么办 户口注销了档案怎么办 当兵没有迁户口怎么办 军人买房无户口怎么办 想自学考会计证怎么办 会计从业证过期怎么办 专业课挂科了怎么办 自考没考过怎么办 高一计算机会考怎么办