Android小游戏——简单易懂单机人人对战五子棋源码详解
最近有人来555974449刘某人的群里问五子棋源码,由于我也是Android菜鸡于是心血来潮就去研究了单机版的五子棋综合了一些大牛的博客,为了防止忘记就所以记录下来,有什么不好处欢迎指导,谢谢!
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/bg"
tools:context="com.gx.wei.mywuziqi.MainActivity">
<com.gx.wei.mywuziqi.WuziqiPanel
android:layout_centerInParent="true"
android:id="@+id/id_wuziqi"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
自定义view的工具类
package com.gx.wei.mywuziqi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
/**
* Created by caobotao on 16/3/31.
*/
public class WuziqiPanel extends View {
//画笔
private Paint mPaint = new Paint();
//(一)画棋盘所需的属性
//线条数量
private static final int MAX_LINE = 10;
//线条的宽度
private int mPanelWidth;
//线条的高度
private float mLineHeight;
//(二)画棋子所需的属性
//黑棋子
private Bitmap mBlackPiece;
//白棋子
private Bitmap mWhitePiece;
//比例,棋子的大小是高的四分之三
private float rowSize = 3 * 1.0f / 4;
//存储用户点击的坐标
private ArrayList<Point> mWhiteArray = new ArrayList<>();
private ArrayList<Point> mBlackArray = new ArrayList<>();
//标记,是执黑子还是白子 ,白棋先手
private boolean mIsWhite = true;
//(三)游戏逻辑所需属性
//胜利棋子数量
private static final int MAX_COUNT_IN_LINE = 5;
private boolean isGameOver = false; //判读是否游戏结束
private final int INIT_WIN = -1; //游戏开始时的状态
public static final int WHITE_WIN = 0; //白棋赢
public static final int BLACK_WIN = 1; //黑棋赢
public static final int NO_WIN = 2; //和棋
private int mGameWinResult = INIT_WIN; //初始化游戏结果
private OnGameStatusChangeListener listener;//游戏状态监听器
public WuziqiPanel(Context context) {
this(context, null);
}
public WuziqiPanel(Context context, AttributeSet attrs) {
super(context, attrs);
//0x44ff0000红色半透明,使得运行的时候可以具体的看到View所占据的大小
//setBackgroundColor(0x44ff0000);
initPaint();
}
//设置游戏状态监听器
public void setOnGameStatusChangeListener(OnGameStatusChangeListener listener) {
this.listener = listener;
}
//一.设置棋盘的大小(设置一个正方形)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取高宽值
/**
* mode分为:
* EXACTLY:EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
* AT_MOST:最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
* UNSPECIFIED:未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式
*/
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int hightSize = MeasureSpec.getSize(heightMeasureSpec);
int hightMode = MeasureSpec.getMode(heightMeasureSpec);
//想把网格棋盘绘制成正方形
//如果传入的是一个精确的值,就直接取值
//同时也考虑到获得的widthSize与heightSize是设置的同样的值(如固定的100dp),但也有可能是match_parent,所以在这里取最小值
//拿到宽和高的最小值,也就是宽
int width = Math.min(widthSize, heightMeasureSpec);
//根据测量模式细节处理(不懂的话百度下MeasureSpec的用法)
//注意这里为else if,限定heightMode==MeasureSpec.UNSPECIFIED时才进入,使得逻辑更加严谨
if (widthMode == MeasureSpec.UNSPECIFIED) {
width = hightSize;
} else if (hightMode == MeasureSpec.UNSPECIFIED) {
width = widthSize;
}
//将宽和高设置为同样的值
//在重写onMeasure方法时,必需要调用该方法存储测量好的宽高值
//设置这样就是一个正方形了
setMeasuredDimension(width, width);
}
//二、画棋盘里的线
//1.先设置线的长宽
@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;
//棋子宽度
int mWhiteWidth = (int) (mLineHeight * rowSize);
//修改棋子大小,根据实际的棋盘格子的宽度按照一定的比例缩小棋子
mWhitePiece=Bitmap.createScaledBitmap(mWhitePiece,mWhiteWidth,mWhiteWidth,false);
mBlackPiece=Bitmap.createScaledBitmap(mBlackPiece,mWhiteWidth,mWhiteWidth,false);
}
/**
* 2.初始化画笔
*/
private void initPaint() {
//设置颜色
mPaint.setColor(Color.BLACK);// 0x88000000(灰色可替换Color.BLACK)
//抗锯齿(图像边缘相对清晰一点,锯齿痕迹不那么明显,即让画出来的线平滑)
mPaint.setAntiAlias(true);
//设置防抖动(使图像更柔和一点)
mPaint.setDither(true);
//设置Style设置画笔的风格为空心
mPaint.setStyle(Paint.Style.STROKE);
//设置线条宽度,加粗线条
mPaint.setStrokeWidth(3);
//获取棋子的资源文件(画棋子时加)
initBitmap();
/*mWhitePiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_w2);
mBlackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_b1);*/
}
//3.开始画线,需要引入画板(onDraw方法)才能显示
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);
}
}
//4.设置画板(初始化游戏数据)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawLine(canvas);//引入画的棋盘线
drawPieces(canvas);//引入画的棋子
IsGameOverMethod();//判断是否胜利
}
//三、画棋子
/**
* 1.先在上面定义上面定义一些棋子的属性,上面写好了
/**
* 2.初始化棋子
*/
private void initBitmap() {
//拿到图片资源
mWhitePiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_w2);
mBlackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_b1);
}
//3.拿到资源之后我们就可以设置大小了,我们在上面onSizeChanged()里面设置将要显示的棋子的样式
//4.写棋盘上的触摸事件
@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;
}
//不能重复点击
private Point getValidPoint(int x, int y) {
return new Point((int) (x / mLineHeight), (int) (y / mLineHeight));
}
//5.开始画棋子
private void drawPieces(Canvas canvas) {
for (int i = 0; i < mWhiteArray.size(); i++) {
//获取白棋子的坐标
Point whitePoint = mWhiteArray.get(i);
canvas.drawBitmap(mWhitePiece, (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(mBlackPiece, (blackPoint.x + (1 - rowSize) / 2) * mLineHeight, (blackPoint.y + (1 - rowSize) / 2) * mLineHeight, null);
}
}
/**
* 四.游戏逻辑(成米字型的四种胜利的方法)
*/
//1.先写一个判断游戏胜利的方法
private void IsGameOverMethod() {
//判断白棋是否有五个相同的棋子相连
boolean mWhiteWin = checkFiveLine(mWhiteArray);
//判断黑棋是否有五个相同的棋子相连
boolean mBlackWin = checkFiveLine(mBlackArray);
//判读是否平局
boolean noWin = checkNoWin(mWhiteWin,mBlackWin);
if (mWhiteWin) {
mGameWinResult = WHITE_WIN;
} else if (mBlackWin) {
mGameWinResult = BLACK_WIN;
} else if(noWin){
mGameWinResult = NO_WIN;
}
if (mWhiteWin || mBlackWin || noWin) {
isGameOver = true;
//回调游戏状态接口
if (listener != null) {
listener.onGameOver(mGameWinResult);//让接口获得游戏结果
}
}
}
/**
* 2.判断棋子是否有五个相同的棋子相连
**/
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;
}
/**
* 判断横向的棋子
*/
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;
}
/**
* 判断纵向的棋子
*/
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;
}
/**
* 判断右斜向的棋子
**/
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;
}
//检查是否和棋
private boolean checkNoWin(boolean whiteWin, boolean blackWin) {
if (whiteWin || blackWin) {
return false;
}
int maxPieces = MAX_LINE * MAX_LINE;
//如果白棋和黑棋的总数等于棋盘格子数,说明和棋
if (mWhiteArray.size() + mBlackArray.size() == maxPieces) {
return true;
}
return false;
}
/**
* 五.游戏的暂时存储
* 如果遇到接电话、屏幕旋转等情况,当重新进入游戏的时候,会使得开始的棋子的布局都丢失
* 当View被销毁时需要保存游戏数据
*/
private static final String INSTANCE = "instance";
private static final String INSTANCE_GAME_OVER = "instance_game_over";
private static final String INSTANCE_WHITE_ARRAY = "instance_white_array";
private static final String INSTANCE_BLACK_ARRAY = "instance_black_array";
//保存游戏数据
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(INSTANCE,super.onSaveInstanceState());
bundle.putBoolean(INSTANCE_GAME_OVER, isGameOver);
bundle.putParcelableArrayList(INSTANCE_WHITE_ARRAY, mWhiteArray);
bundle.putParcelableArrayList(INSTANCE_BLACK_ARRAY, mBlackArray);
return bundle;
}
//恢复游戏数据
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
isGameOver = bundle.getBoolean(INSTANCE_GAME_OVER);
mWhiteArray = bundle.getParcelableArrayList(INSTANCE_WHITE_ARRAY);
mBlackArray = bundle.getParcelableArrayList(INSTANCE_BLACK_ARRAY);
super.onRestoreInstanceState(bundle.getParcelable(INSTANCE));
return;
}
super.onRestoreInstanceState(state);
}
//重新开始游戏
public void restartGame() {
mWhiteArray.clear();
mBlackArray.clear();
isGameOver = false;
mGameWinResult = INIT_WIN;
invalidate();
}
}
游戏监听接口
package com.gx.wei.mywuziqi;
public interface OnGameStatusChangeListener {
void onGameOver(int gameWinResult);//游戏结束
}
MainActivity
package com.gx.wei.gobang;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Button;
public class MainActivity extends AppCompatActivity{
private WuziqiPanel mGamePanel;
private AlertDialog.Builder alertBuilder;
private AlertDialog alertDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
//游戏结束时弹出对话框
alertBuilder = new AlertDialog.Builder(MainActivity.this);
alertBuilder.setPositiveButton("再来一局", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mGamePanel.restartGame();
}
});
alertBuilder.setNegativeButton("退出游戏", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
MainActivity.this.finish();
}
});
alertBuilder.setCancelable(false);
alertBuilder.setTitle("此局结束");
mGamePanel = (WuziqiPanel) findViewById(R.id.id_wuziqi);
mGamePanel.setOnGameStatusChangeListener(new OnGameStatusChangeListener() {
@Override
public void onGameOver(int gameWinResult) {
switch (gameWinResult) {
case WuziqiPanel.WHITE_WIN:
alertBuilder.setMessage("白棋胜利!");
break;
case WuziqiPanel.BLACK_WIN:
alertBuilder.setMessage("黑棋胜利!");
break;
case WuziqiPanel.NO_WIN:
alertBuilder.setMessage("和棋!");
break;
}
alertDialog = alertBuilder.create();
alertDialog.show();
}
});
}
}
最后打一波广告:源码已放上555974449群,“通往Android的神奇之旅”欢迎来讨论Android或非Android的学习问题!