自定义View实现五子棋游戏

时间:2022-07-08 16:36:38

成功的路上一点也不拥挤,因为坚持的人太少了。

                                                         ---简书上看到的一句话

未来请假三天顺带加上十一回家结婚,不得不说真是太坑了,去年婚假还有10天,今年一下子缩水到了3天,只能赶着十一办事了。

最近还在看数据结构,打算用java实现一遍,所以没着急写读书笔记,不过前段时间看了一个简单的五子棋游戏,记录一下。

整体效果如下,整个功能在一个自定义View里面实现:

自定义View实现五子棋游戏

由于主activity比较简单直接列出来:

public class MainActivity extends Activity {

    private static String TAG ="MainActivity111";

    private FiveView fiveView;
    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext=this;
        fiveView = (FiveView) findViewById(R.id.five);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // TODO Auto-generated method stub
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if(item.getItemId()==R.id.action_settings){
            fiveView.setrestart();
        }
        return true;
    }

}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.lly.simple_five.FiveView
        android:id="@+id/five"
        android:layout_centerInParent="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </com.lly.simple_five.FiveView>

</RelativeLayout>

上面只有一点 android:layout_centerInParent=”true” 设置自定义view居中,这样看起来比较美观。

主要代码在FiveView中实现,

考虑五子棋游戏一共有几个步骤:

1、画棋盘

2、根据用户选择在指定位置画棋子

3、判定输赢

4、需要有个重新开始的菜单,当确定输赢后重新开始游戏。

1、画棋盘

棋盘需要手动画出来,所以这里自定义了一个view

public class FiveView extends View {
    public FiveView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(0x88000000);
            setBackgroundColor(0x44440000);
            mBlackList = new ArrayList<Point>();
            mWhileList = new ArrayList<Point>();

        }
    ...
}

然后重新他的两个参数的构造方法,为什么要写两个构造参数的方法呢,这是因为这里只需要用自定义view的布局,不需要自定义属性。

在构造方法里面我们初始化了画笔,棋盘的背景,并实例化保存棋子的list。

这里设置了这个view的背景颜色,以方便看出这个view的位置

小知识:

一般情况下view有三个构造方法,其中带一个参数的构造方法是在activity中new一个控件时调用的。如TextView tv = new TextView(mContext);

在xml中使用不带自定义属性的自定义控件时会调用两个参数的构造方法,如本例。

在xml中使用带自定义属性的自定义控件时,会调用带三个参数的构造方法。

这里想想,我们前面定义的view宽和高都是占满了整个屏幕,所以在手机上看到的就是一个长方行的布局,但是一般我们在现实中看到的棋盘都是正方形的,这也很好实现,

自定义view里面的测量方法能很好的解决这个问题:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Width = MeasureSpec.getSize(widthMeasureSpec);
        Hight = MeasureSpec.getSize(heightMeasureSpec);
        int WidthMode = MeasureSpec.getMode(widthMeasureSpec);

        Width = Math.min(Width, Hight);
        int measuredWidth = MeasureSpec.makeMeasureSpec(Width, WidthMode);
        setMeasuredDimension(measuredWidth, measuredWidth);

    }

如上代码 我们只需要取出测量的宽高,然后以宽高中较小的值作为棋盘的宽高,这样不管是横屏或竖屏都能得到一个正方形的棋盘了,

小知识

view中的测试模式有三种也就是上面getMode取出来的值,分别对应

match_parent

wrap_content

xxxxxdp

规范的操作在自定义测量方法里面要对这三种模式分别去出来,这里比较简单就不做出来了。

如上我们得到了棋盘的宽高。

在画棋盘之前还需要考虑下,我们棋盘应该怎么画 ,画多少条线,

这里我们在棋盘的大小发生改变时初始化一些初始化棋盘的操作

...
    private static final int MAX_LINE = 10;
...
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mBroad = Width;
        mLineWidth = mBroad * 1.0f / MAX_LINE;
        mBlackPiece = BitmapFactory.decodeResource(getResources(),
                R.drawable.stone_b1);
        mWhilePiece = BitmapFactory.decodeResource(getResources(),
                R.drawable.stone_w2);
        int dstpoint = (int)(mLineWidth*roation);
        mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, dstpoint, dstpoint, false);
        mWhilePiece = Bitmap.createScaledBitmap(mWhilePiece, dstpoint, dstpoint, false);
    }

上面代码 ,我们定义了棋盘的宽高为测量时的宽高,

定义了两条线之间的宽度,即10平分整个区域

另外初始化了黑白棋子这个在画棋子的时候具体说明。

接着就需要去画棋盘了


    protected void onDraw(Canvas canvas) {
        DrawBroad(canvas);
        DrawPiece(canvas);
        checkGameOver();
    }
    private void DrawBroad(Canvas canvas) {
        for (int i = 0; i < MAX_LINE; i++) {
            float startX = mLineWidth / 2;
            float stopX = mBroad - (mLineWidth / 2);
            float startY = (float) ((0.5 + i) * mLineWidth);
            float stopY = (float) ((0.5 + i) * mLineWidth);
            canvas.drawLine(startX, startY, stopX, stopY, mPaint);

            canvas.drawLine(startY, startX, stopY, stopX, mPaint);
        }
    }
可以发现在上面代码中,调用DrawBroad 去画棋盘,分别横竖画了十条线,组成了棋盘,这里发现在横竖开始的时候都是从0.5*mLineWidth开始的,这是为了留出上面一点空隙,可以使棋子可以显示完整,这里用来一个for循环就完成了横竖线,归功于这是一个正方形,x,y坐标互换一下就可以实现画横竖线了。
到这里棋盘就已经画好了

2、画棋子

这个比较复杂 我们在分几步实现;

1)、首先要实例化两种颜色的棋子
2)、想想在现实中下五子棋的时候每人手边有一个盒子装着自己的棋子,这里我们要有我们的盒子来保存我们的棋子,
3)、画棋子是根据客户手指按下的位置进行画的,所以要实现onTouchEvent方法
4)、画棋子

1)、实例和两种颜色的棋子

这个其实上在上面初始化变量的时候就已经做过了。

private static final float roation = 3*1.0f/4;
mBlackPiece = BitmapFactory.decodeResource(getResources(),
                R.drawable.stone_b1);
        mWhilePiece = BitmapFactory.decodeResource(getResources(),
int dstpoint = (int)(mLineWidth*roation);
        mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, dstpoint, dstpoint, false);
        mWhilePiece = Bitmap.createScaledBitmap(mWhilePiece, dstpoint, dstpoint, false);

这里的一个小技巧就是让棋子占每格的3/4,这样不管我们传多大棋子图片都能正确的显示出来,并且只占3/4的格子。

2)、实现黑白棋的棋盒

其实我们在构造函数里面已经实例化了两个盒子

        mBlackList = new ArrayList<Point>();
        mWhileList = new ArrayList<Point>();

只不过这里还没有棋子,这里我们的棋盒里面的棋子其实都是下在棋盘上的棋子。

3)、用户下棋也就是触发onTouchEvent方法

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if(IsGameOver) return false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        Point p = getPoint(x,y);

        if(event.getAction()==MotionEvent.ACTION_UP){
            if(mWhileList.contains(p)||mBlackList.contains(p)){
                return false;
            }
            if(isWhile){
                mWhileList.add(p);
            }else{
                mBlackList.add(p);
            }
            invalidate();
            isWhile=!isWhile;
        }
        return true;
    }

    private Point getPoint(int x, int y) {

        return new Point((int)(x/mLineWidth),(int)(y/mLineWidth));
    }

这里当用户下棋时,获取所下的位置的坐标,这里有一个小技巧

在保存所下棋子的x,y坐标时我们让x,y分别除每个格子的大小,这样所获得的位置一定在棋盘横竖的交界点上,这个可以去体会一下,

然后当用户抬起手指时我们去判断下这个位置是不是已经有棋子了,有的话直接return false;什么都不做,没有的话,看下当前该谁下了,把这个棋子添加到对应的棋盒里,并通知刷新棋盘

4)、画棋子

最后就是把现在棋盒里面的棋子画到棋盘中

private void DrawPiece(Canvas canvas) {
        for(int i=0,n=mWhileList.size();i<n;i++){
            Point WhilePiece = mWhileList.get(i);
            canvas.drawBitmap(mWhilePiece, (WhilePiece.x+(1-roation)/2)*mLineWidth,
                    (WhilePiece.y+(1-roation)/2)*mLineWidth, mPaint);

        }
        for(int i=0,n=mBlackList.size();i<n;i++){
            Point BlackPiece = mBlackList.get(i);
            canvas.drawBitmap(mBlackPiece, (BlackPiece.x+(1-roation)/2)*mLineWidth,
                    (BlackPiece.y+(1-roation)/2)*mLineWidth, null);
        }

    }

这步就很简单了 循环遍历棋盒里面的棋子并把它画出来,唯一要注意的就是棋子的位置,

 (WhilePiece.x+(1-roation)/2)*mLineWidth,
                    (WhilePiece.y+(1-roation)/2)*mLineWidth

看下图:

自定义View实现五子棋游戏

以第一个点的x坐标为例:

因为开始的时候设定的棋盘开始的x坐标离我们View的左边距是0.5*mLineWidth,然后在初始化棋子的时候棋子的大小是3/4*mLineWidth,现在我们要计算棋子的左边距到view左边距的距离

所以棋子的x位置就应该是(0.5-3/4/2)*mLineWidth,第二个点就是(1.5-3/4/2)*mLineWidht,其中0.5,1.5是我们下子的位置系数即WhilePiece.x,所以最后提取出来就是(WhilePiece.x+(1-roation)/2)*mLineWidth,,竖坐标也是同意道理,这个要好好理解一下

3、判定输赢

首先要确定在哪里判断是否游戏一方胜利,可以看出在画棋子的时候是最好的时机了 当游戏结束后就不再画棋子。


    private void checkGameOver() {
        boolean Whilewin = checkWhileFive(mWhileList);
        boolean Blackwin = checkWhileFive(mBlackList);
        if(Whilewin){
            IsGameOver=true;
            Toast.makeText(getContext(), "白旗胜利", Toast.LENGTH_SHORT).show();
        }
        if(Blackwin){
            IsGameOver=true;
            Toast.makeText(getContext(), "黑棋胜利", Toast.LENGTH_SHORT).show();
        }
    }

    private boolean checkWhileFive(List<Point> points) {
        for(Point p:points){
            int x = p.x;
            int y = p.y;

             boolean Horizewin =checkHorizefive(x,y,points);
             boolean verwin =checkverfive(x,y,points);
             boolean leftwin =checkleftfive(x,y,points);
             boolean reghitwin =checkreghitfive(x,y,points);
             if(Horizewin||verwin||leftwin||reghitwin){
                 return true;
             }

        }
        return false;
    }
    private boolean checkHorizefive(int x, int y, List<Point> points) {
        int count=1;
        for(int i=1;i<FIVE_WIN;i++){
            if(points.contains(new Point(x+i,y))){
                count++;
            }else{
                break;
            }
        }
        if(count ==FIVE_WIN) return true;
        for(int i=1;i<FIVE_WIN;i++){
            if(points.contains(new Point(x-i,y))){
                count++;
            }else{
                break;
            }
        }
        if(count ==FIVE_WIN) return true;
        return false;

    }

上面代码首先在ondraw中调用checkGameover方法检查游戏是否结束

检查的方法就是分别去判断白棋和黑球是否达到五子连珠的效果。

上面贴出来了横向五子连珠的判断,取出当前棋子循环检查他的左边是否有五个相同颜色的棋子,有就返回游戏结束,没有的话在去检查右边是否有五个相同颜色的棋子,有的话返回游戏结束,这样横竖,左斜,右斜都判断后就可以确定游戏是否结束,

结束的话在onTouchEvent方法中直接返回false表示我们不需要这个事件了。

到此这个简单的五子棋差不多就完成了。

4、添加重新开始菜单

为了使它更完善一点我们加入了 重新开始的菜单键,

这个应该没什么难度,主要就是按下重新开始的时候

修改一些变量的初始值

public void setrestart() {
        IsGameOver=false;
        mWhileList.clear();
        mBlackList.clear();
        invalidate();
    }

最后的最后当app异常退出的时候,发现下了半天的棋子没有保存,因此这里加入

    private String INSTANCE = "instaNce";
    private String INSTANCE_GEMEOVER = "instance_gameover";
    private String INSTANCE_WHILEARRAY = "instance_whilearray";
    private String INSTANCE_BLACKARRAY = "instance_blackarray";

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
        bundle.putBoolean(INSTANCE_GEMEOVER, IsGameOver);
        bundle.putParcelableArrayList(INSTANCE_WHILEARRAY, mWhileList);
        bundle.putParcelableArrayList(INSTANCE_BLACKARRAY, mBlackList);

        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if(state instanceof Bundle){
            Bundle bundle = (Bundle) state;
            IsGameOver = bundle.getBoolean(INSTANCE_GEMEOVER);
            mWhileList = bundle.getParcelableArrayList(INSTANCE_WHILEARRAY);
            mBlackList = bundle.getParcelableArrayList(INSTANCE_BLACKARRAY);
            super.onRestoreInstanceState(bundle.getBundle(INSTANCE));
            return;
        }
        super.onRestoreInstanceState(state);
    }

处理异常退出的情况。

GAME OVER。。。。

源码

github地址