Android实训案例(八)——单机五子棋游戏,自定义棋盘,线条,棋子,游戏逻辑,游戏状态存储,再来一局


阿法狗让围棋突然就被热议了,鸿洋大神也顺势出了篇五子棋单机游戏的视频,我看到了就像膜拜膜拜,就学习了一下,写篇博客梳理一下自己的思路,加深一下印象

一.棋盘

我们一看就知道,我们必须自定义View,这里我们定义一个GameView来做游戏主类,第一步,先测量,我们这里不难知道,五子棋他的棋盘是一个正方形,所以我们需要去测量

 /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取高宽值
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int hightSize = MeasureSpec.getSize(heightMeasureSpec);
        int hightMode = MeasureSpec.getMode(heightMeasureSpec);

        //拿到宽和高的最小值,也就是宽
        int width = Math.min(widthSize, heightMeasureSpec);

        //根据测量模式细节处理
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            width = hightSize;
        } else if (hightMode == MeasureSpec.UNSPECIFIED) {
            width = widthSize;
        }

        //设置这样就是一个正方形了
        setMeasuredDimension(width, width);

    }

这里的逻辑还是十分简单的,我们拿到长和宽去比较一下,设置这个View的长宽Wie最小值,就是一个正方形了,所以我们的layout_main.xml是这样写的

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    android:background="@drawable/main_bg"
    >

    <com.lgl.fiverow.GameView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

这里我在构造方法中设置了一个半透明的红色背景,是在我们调试的时候可以更加清晰的看清楚GameView的大小,所以,运行的结果

二.线条

这个应该也算是棋盘的一部分吧,就是棋盘上的线条,我们应该怎么去画,首先,我们要去定义一些属性

    //线条数量
    private static final int MAX_LINE = 10;

    //线条的宽度
    private int mPanelWidth;

    //线条的高度
    private float mLineHeight;

然后,我们要去确定大小

/**
     * 测量大小
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //拿到宽
        mPanelWidth = w;
        //分割
        mLineHeight = mPanelWidth * 1.0f / MAX_LINE;
    }

不要着急,这些都只是一些准备的工作,我们画线条是必须要在onDraw(0方法里的,但是前期我们要准备一只画笔,对吧,所以我们要初始化画笔

     /**
     * 初始化画笔
     */
    private void initPaint() {
        //设置颜色
        mPaint.setColor(0x88000000);
        //抗锯齿
        mPaint.setAntiAlias(true);
        //设置防抖动
        mPaint.setDither(true);
        //设置Style
        mPaint.setStyle(Paint.Style.STROKE);
    }

现在我们可以去绘制了,我们在OnDraw(0方法里写一个drawLine方法来专门绘制线条

 /**
     * 绘制棋盘的方法
     *
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        //获取高宽
        int w = mPanelWidth;
        float lineHeight = mLineHeight;

        //遍历,绘制线条
        for (int i = 0; i < MAX_LINE; i++) {
            //横坐标
            int startX = (int) (lineHeight / 2);
            int endX = (int) (w - lineHeight / 2);

            //纵坐标
            int y = (int) ((0.5 + i) * lineHeight);

            //绘制横
            canvas.drawLine(startX, y, endX, y, mPaint);
            //绘制纵
            canvas.drawLine(y, startX, y, endX, mPaint);
        }

    }

我们运行一下

好的,这里,注意一下,我在activity_main.xml中定义了一个

android:gravity="center"

属性,所以让他居中,同样的,我们在initPaint中加上点代码让我们看的更加直观一点

//设置颜色
mPaint.setColor(Color.BLACK);
//设置线条宽度
mPaint.setStrokeWidth(3);

同样的,我们把构造法里的设置背景的测试代码注释掉

//测试代码
//setBackgroundColor(0x44ff0000);

这样,我们运行一下

得,我们现在有模有样了

三.棋子

棋子我们事先准备好了两张图片,但是这里我们要考虑他的大小的问题了,我们的思路是让他是行高的四分之三大小,所以先声明

 //黑棋子
 private Bitmap mBlack;

 //白棋子
 private Bitmap mWhite;

//比例,棋子的大小是高的四分之三
 private float rowSize = 3 * 1.0f / 4;

然后我们定义一个方法区初始化Bitmap

     /**
     * 初始化棋子
     */
    private void initBitmap() {
        //拿到图片资源
        mBlack = BitmapFactory.decodeResource(getResources(), R.drawable.stone_black);
        mWhite = BitmapFactory.decodeResource(getResources(), R.drawable.stone_white);
    }

拿到资源之后我们就可以设置大小了,我们在onSizeChanged()里面设置

 //棋子宽度
 int mWhiteWidth = (int) (mLineHeight * rowSize);

 //修改棋子大小
 mWhite = Bitmap.createScaledBitmap(mWhite, mWhiteWidth, mWhiteWidth, false);
 mBlack = Bitmap.createScaledBitmap(mBlack, mWhiteWidth, mWhiteWidth, false);

不过棋子可没我们想象的那么简单,我们要点击一下再去绘制一个棋子,这样的思路该怎么去实现呢?我们实现它的点击事件,这里先定义几个变量

    //存储用户点击的坐标
    private List<Point> mWhiteArray = new ArrayList<>();
    private List<Point> mBlackArray = new ArrayList<>();

    //标记,是执黑子还是白子 ,白棋先手
    private boolean mIsWhite = true;

这样才和触摸事件相得映彰

 /**
     * 触摸事件
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            //按下事件
            case MotionEvent.ACTION_UP:

                int x = (int) event.getX();
                int y = (int) event.getY();

                //封装成一个Point
                Point p = getValidPoint(x, y);

                //判断当前这个点是否有棋子了
                if(mWhiteArray.contains(p) || mBlackArray.contains(p)){

                    //点击不生效
                    return false;
                }

                //判断如果是白子就存白棋集合,反之则黑棋集合
                if (mIsWhite) {
                    mWhiteArray.add(p);
                } else {
                    mBlackArray.add(p);
                }

                //刷新
                invalidate();

                //改变值
                mIsWhite = !mIsWhite;

                break;
        }

        return true;
    }

这样,有几点是要说明一下的,首先我们new Point的时候为了避免重复绘制我们是实现了一个方法

/**
     * 不能重复点击
     *
     * @param x
     * @param y
     * @return
     */
    private Point getValidPoint(int x, int y) {

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

紧接着我们就判断,要是重复点击,返回false,而且我们在action选择也是选择了ACTION_UP,为什么?为什么不是ACTION_DOWN?因为这个毕竟是一个View,父View会拦截(某些场景),所以我们选在UP上才是合情合理的

好的,当我们点击之后就要绘制棋子了,这里我们也写一个方法

 /**
     * 绘制棋子的方法
     *
     * @param canvas
     */
    private void drawPieces(Canvas canvas) {
        for (int i = 0; i < mWhiteArray.size(); i++) {
            //获取白棋子的坐标
            Point whitePoint = mWhiteArray.get(i);
            canvas.drawBitmap(mBlack, (whitePoint.x + (1 - rowSize) / 2) * mLineHeight, (whitePoint.y + (1 - rowSize) / 2) * mLineHeight, null);
        }

        for (int i = 0; i < mBlackArray.size(); i++) {
            //获取黑棋子的坐标
            Point blackPoint = mBlackArray.get(i);
            canvas.drawBitmap(mWhite, (blackPoint.x + (1 - rowSize) / 2) * mLineHeight, (blackPoint.y + (1 - rowSize) / 2) * mLineHeight, null);
        }
    }

OK,我们实际运行一下

四.游戏逻辑

现在什么都有了,基本上都可用玩了,但是还少了重要的一点就是游戏结束,你到了五颗也需要判断是否胜利呀,对吧,我们写一个方法,在每次绘制完成之后就去判断是否有赢家

  /**
     * 判断是否胜利
     */
    private void checkWin() {

        //判断白棋是否有五个相同的棋子相连
        boolean mWhiteWin = checkFiveLine(mWhiteArray);
        //判断黑棋是否有五个相同的棋子相连
        boolean mBlackWin = checkFiveLine(mBlackArray);

        //只要有一个胜利,游戏就结束
        if (mWhiteWin || mBlackWin) {

            mIsGameOver = true;

            mIsWhiteWin = mWhiteWin;

            Toast.makeText(getContext(), mIsWhiteWin ? "白棋胜利" : "黑棋胜利", Toast.LENGTH_SHORT).show();

        }
    }

好的,我们重点逻辑就在checkFiveLine这个方法上了,这里,我们所知道的胜利有四种情况

我们先定义一个常量

    //胜利棋子数量
    private static final int MAX_COUNT_IN_LINE = 5;

OK,接下来我们可以实现以下胜利的逻辑了

 /**
     * //判断棋子是否有五个相同的棋子相连
     *
     * @param points
     * @return
     */
    private boolean checkFiveLine(List<Point> points) {

        //遍历棋子
        for (Point p : points) {
            //拿到棋盘上的位置
            int x = p.x;
            int y = p.y;

            /**
             * 四种情况胜利,横,竖,左斜,右斜
             */

            //横
            boolean win = checkHorizontal(x, y, points);
            if (win) return true;
            //竖
            win = checkVertical(x, y, points);
            if (win) return true;
            //左斜
            win = checkLeft(x, y, points);
            if (win) return true;
            //右斜
            win = checkRight(x, y, points);
            if (win) return true;

        }
        return false;
    }

我们不管哪个方向只要返回true就返回true,然后弹Toast,这里,四个方向的逻辑

 /**
     * 判断横向的棋子
     *
     * @param x
     * @param y
     * @param points
     */
    private boolean checkHorizontal(int x, int y, List<Point> points) {

        //棋子标记,记录是否有五个  =1是因为自身是一个
        int count = 1;

        //左
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x - i, y))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        //右
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x + i, y))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        return false;
    }

 /**
     * 判断纵向的棋子
     *
     * @param x
     * @param y
     * @param points
     */
    private boolean checkVertical(int x, int y, List<Point> points) {

        //棋子标记,记录是否有五个  =1是因为自身是一个
        int count = 1;

        //上
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x, y - i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        //下
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x, y + i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        return false;
    }
  • 左斜
 /**
     * 判断左斜向的棋子
     *
     * @param x
     * @param y
     * @param points
     */
    private boolean checkLeft(int x, int y, List<Point> points) {

        //棋子标记,记录是否有五个  =1是因为自身是一个
        int count = 1;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x - i, y + i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x + i, y - i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        return false;
    }
  • 右斜
 /**
     * 判断右斜向的棋子
     *
     * @param x
     * @param y
     * @param points
     */
    private boolean checkRight(int x, int y, List<Point> points) {

        //棋子标记,记录是否有五个  =1是因为自身是一个
        int count = 1;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x - i, y - i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            //如果有
            if (points.contains(new Point(x + i, y + i))) {
                count++;
            } else {
                break;
            }
        }

        //有五个就为true
        if (count == MAX_COUNT_IN_LINE) {

            return true;
        }

        return false;
    }

这样,我们运行一下

嘿嘿,好玩吧!

五.游戏状态存储

这个就是当我们游戏挂后台之后,再回来游戏就没了,我们应该存储他的状态,让他每一次进入的时候要是上一句没有下完接着下,那我们该怎么去实现呢?和Activity一样,我们View也有存储状态的方法

  /**
     * 存储状态
     *
     * @return
     */
    @Override
    protected Parcelable onSaveInstanceState() {

        Bundle bundle = new Bundle();
        bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
        bundle.putBoolean(INSTANCE_GAMEOVER, mIsGameOver);
        bundle.putParcelableArrayList(INSTANCE_WHITE_ARRAY, mWhiteArray);
        bundle.putParcelableArrayList(INSTANCE_BLACK_ARRAY, mBlackArray);
        return bundle;
    }

    /**
     * 重新运行
     *
     * @param state
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        //取值
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            mIsGameOver = bundle.getBoolean(INSTANCE_GAMEOVER);
            mWhiteArray = bundle.getParcelableArrayList(INSTANCE_WHITE_ARRAY);
            mBlackArray = bundle.getParcelableArrayList(INSTANCE_BLACK_ARRAY);

            //调用
            super.onRestoreInstanceState(bundle.getParcelable(INSTANCE));
            return;
        }
        super.onRestoreInstanceState(state);
    }

这样就可以了,但是,有一点要知道,不要忘记在布局文件上给控件加上ID,不然状态不会存储哦

 <com.lgl.fiverow.GameView
        android:id="@+id/mGameView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

六.再来一局

既然我们的游戏逻辑差不多了,那我们应该考虑一下当你胜利的时候,你是不是应该再来一局,所以我们还要实现这个逻辑,这个很简单

   /**
     * 再来一局
     */
    public void RestartGame() {
        mWhiteArray.clear();
        mBlackArray.clear();
        mIsGameOver = false;
        mIsWhiteWin = false;
        invalidate();
    }

这样,我们就可以直接调用了,我们来看看MainActivity

package com.lgl.fiverow;

import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

/**
 * 五子棋游戏
 */
public class MainActivity extends AppCompatActivity {

    //重来按钮
    private FloatingActionButton fab;
    //游戏
    private GameView game;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        game = (GameView) findViewById(R.id.mGameView);
        fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                game.RestartGame();
            }
        });
    }
}

OK,我们最终运行一下

OK,到这里,就算开发完成了

Demo下载:http://download.csdn.net/detail/qq_26787115/9521011

我的群:555974449欢迎你加入!

Android实训案例(八)——单机五子棋游戏,自定义棋盘,线条,棋子,游戏逻辑,游戏状态存储,再来一局的更多相关文章

  1. Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!

    Android实训案例(四)--关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程! 关于2048,我看到很多大神,比如医生,郭神,所以我也研究了一段时间,还好是研究 ...

  2. Android实训案例(九)——答题系统的思绪,自己设计一个题库的体验,一个思路清晰的答题软件制作过程

    Android实训案例(九)--答题系统的思绪,自己设计一个题库的体验,一个思路清晰的答题软件制作过程 项目也是偷师的,决心研究一下数据库.所以写的还是很详细的,各位看官,耐着性子看完,实现结果不重要 ...

  3. Android实训案例(六)——四大组件之一BroadcastReceiver的基本使用,拨号,短信,SD卡,开机,应用安装卸载监听

    Android实训案例(六)--四大组件之一BroadcastReceiver的基本使用,拨号,短信,SD卡,开机,应用安装卸载监听 Android中四大组件的使用时重中之重,我这个阶段也不奢望能把他 ...

  4. Android实训案例(五)——四大组件之一ContentProvider的使用,通讯录的实现以及ListView的优化

    Android实训案例(五)--四大组件之一ContentProvider的使用,通讯录的实现 Android四大组件是啥这里就不用多说了,看图吧,他们之间通过intent通讯 我们后续也会一一的为大 ...

  5. Android实训案例(三)——实现时间轴效果的ListView,加入本地存储,实现恋爱日记的效果!

    Android实训案例(三)--实现时间轴效果的ListView,加入本地存储,实现恋爱日记的效果! 感叹离春节将至,也同时感叹时间不等人,一年又一年,可是我依然是android道路上的小菜鸟,这篇讲 ...

  6. Android实训案例(二)——Android下的CMD命令之关机重启以及重启recovery

    Android实训案例(二)--Android下的CMD命令之关机重启以及重启recovery Android刚兴起的时候,着实让一些小众软件火了一把,切水果,Tom猫,吹裙子就是其中的代表,当然还有 ...

  7. Android实训案例(一)——计算器的运算逻辑

    Android实训案例(一)--计算器的运算逻辑 应一个朋友的邀请,叫我写一个计算器,开始觉得,就一个计算器嘛,很简单的,但是写着写着发现自己写出来的逻辑真不严谨,于是搜索了一下,看到mk(没有打广告 ...

  8. Android实训案例(七)——四大组件之一Service初步了解,实现通话录音功能,抽调接口

    Service Service的神奇之处,在于他不需要界面,一切的操作都在后台操作,所以很多全局性(手机助手,语音助手)之类的应用很长需要这个,我们今天也来玩玩 我们新建一个工程--ServiceDe ...

  9. Android实训案例(七)——四大组件之中的一个Service初步了解,实现通话录音功能,抽调接口

    Service Service的奇妙之处.在于他不须要界面,一切的操作都在后台操作,所以非常多全局性(手机助手,语音助手)之类的应用非常长须要这个.我们今天也来玩玩 我们新建一个project--Se ...

随机推荐

  1. Android的四大组件及应用安装安全问题(4)

    Android的四大组件及组件间通信 如果想对四大组件有深入的了解,那永远不要停留在一些条条干干的SDK API的认识,需要了解他的通讯,他的复用,他的边界问题,这样才会对四大组件有透明的认识. 四大 ...

  2. let内嵌lambda使用set!构成闭包

    查了半天没有找到scheme中判断数据类型的函数,索性自己写了个type?,发现闭包和递归有着微妙的联系. 本例中,自由变量是types,外层let初始化了types的值,内层let里的(set! t ...

  3. 20160214.CCPP体系详解(0024天)

    程序片段(01):CGI.c 内容概要:CGI-cloud #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main01(vo ...

  4. Dynamics CRM 打开数据加密报错及修改用户邮件保存报错的解决方法

    在项目里会碰到在修改用户的电子邮件时报错的问题 然后跑到数据管理里打开数据加密又是报错 解决上述问题只需要做下数据库的更改即可,把标志位置1即可,记得要重启下IIS才能生效 SELECT [Colum ...

  5. Effective Python 中文版

    如题,博主正在翻译一本Python相关的书. 图为Python作者. [美]Brett Slatkin的名作. Effective Python: 59 Specific Ways to Write ...

  6. Android自定义View(二、深入解析自定义属性)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51468648 本文出自:[openXu的博客] 目录: 为什么要自定义属性 怎样自定义属性 ...

  7. Java学习之栈和堆的区别

    在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配. 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配 ...

  8. 集合框架之Set接口

    一个不包含重复元素的 collection.更确切地讲,set 不包含满足e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null 元素. 在所有构造方法以及 add.equa ...

  9. MPI二维笛卡尔坐标划分【1】

    本文简单演示,如何对现有进程进行二维划分,如何获得进程的X和Y坐标. 只有一段程序: #include <mpi.h> #include <stdio.h> #include ...

  10. Cocos2D在新版Swift中常量枚举值引用代码的修改

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 我们知道在SpriteBuilder中是无法直接给一个CCB文 ...