自定义View实现2048

一直觉得能写游戏的都是大神!因为学习方向以及时间的问题,很少动手开发游戏。在校的时候,记得写过当时很火的游戏“像素鸟”,哈哈,作为菜鸟来说还是挺有成就感的!进入正题,本文主要从自定义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就可以跑起来了。因为代码中注释的都比较清楚,所以具体的细节就没有写出来,只是从总体思路进行了阐述。最重要的还是着手去写,遇到问题,解决问题,最后都不是问题。

效果图:

这里写图片描述

猜你喜欢

转载自blog.csdn.net/Mr_azheng/article/details/78897291