1. 程式人生 > 程式設計 >Android自定義View實現五子棋遊戲

Android自定義View實現五子棋遊戲

本文例項為大家分享了Android實現五子棋遊戲的具體程式碼,供大家參考,具體內容如下

直接上效果圖

原理

從棋盤到棋子,到開始下棋的各類點選事件,均在 ChessView 中實現,這個 View 沒有提供自定義屬性(因為我覺得沒有必要~~~)。

專案GitHub地址:Wuziqi

實現步驟

1.新建一個棋子類,這個類非常簡單,程式碼如下:

public class Chess {

 public enum Color {BLACK,WHITE,NONE}
 private Color color;

 public Chess(){
  this.color = Color.NONE;
 }
 public Color getColor() {
  return color;
 }
 public void setColor(Color color) {
  this.color = color;
 }
}

每個棋子類有三種狀態,即 WHITE,BLACK,NONE。這裡我們使用列舉來表示這三種狀態。

2. 自定義 ChessView 類,這個類就是核心類了,我們這個五子棋的所有邏輯都是在這個類裡面實現。構造方法初始化各個欄位,程式碼如下:

public ChessView(Context context,AttributeSet attrs,int defStyleAttr) {
  super(context,attrs,defStyleAttr);
  // 初始化欄位 mEveryPlay,悔棋會用到
  initEveryPlay();
  // 初始化每個棋子,設定屬性為 NONE
  initChess();
  // 初始化棋盤畫筆
  initBoardPaint();
  // 初始化棋子畫筆
  initChessPaint();
  // 初始化背景畫筆
  initBgPaint();
 }

各個方法的具體實現如下:

private void initEveryPlay() {
  // 初始化 List 大小,此方法不影響 list.size() 返回值
  mEveryPlay = new ArrayList<>(225);
 }

 private void initChess() {
  mChessArray = new Chess[15][15];
  for (int i = 0; i < mChessArray.length; i++) {
   for (int j = 0; j < mChessArray[i].length; j++) {
    mChessArray[i][j] = new Chess();
   }
  }
 }

 private void initChessPaint() {
  mChessPaint = new Paint();
  mChessPaint.setColor(android.graphics.Color.WHITE);
  mChessPaint.setAntiAlias(true);
 }

 private void initBoardPaint() {
  mBoardPaint = new Paint();
  mBoardPaint.setColor(android.graphics.Color.BLACK);
  mBoardPaint.setStrokeWidth(2);
 }

 private void initBgPaint() {
  mBgPaint = new Paint();
  mBgPaint.setColor(android.graphics.Color.GRAY);
  mBgPaint.setAntiAlias(true);
 }

3. 重寫 onMeasure() 方法,強制將 View 大小變為正方形,程式碼如下:

@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {

  int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  int min = widthSize < heightSize ? widthSize : heightSize;
  // 五子棋標準棋盤線條數目為 15 x 15,為了後面計算座標方便,我們將 View 的寬高處理為 16 的整數倍
  min = min / 16 * 16;

  setMeasuredDimension(min,min);
 }

之所以設定為 16 的整數倍而不是 15,是因為如果設定成 15,那麼棋盤的背景就會跟棋盤最邊界的線條重合,此時如果有棋子落在邊界,棋子將不能顯示完全。

4. 重點來了,重寫 onDraw() 方法,繪製出棋盤,程式碼如下:

@Override
protected void onDraw(Canvas canvas) {
  int height = getMeasuredHeight();
  int width = getMeasuredWidth();
  int avg = height / 16;

  canvas.drawRect(0,width,height,mBgPaint);
  for (int i = 1; i < 16; i++) {
   // 畫豎線
   canvas.drawLine(avg * i,avg,avg * i,height - avg,mBoardPaint);
   // 畫橫線
   canvas.drawLine(avg,width - avg,mBoardPaint);
  }
  for (int i = 1; i < 16; i++) {
   for (int j = 1; j < 16; j++) {
    switch (mChessArray[i - 1][j - 1].getColor()) {
     case BLACK:
      mChessPaint.setColor(android.graphics.Color.BLACK);
      break;
     case WHITE:
      mChessPaint.setColor(android.graphics.Color.WHITE);
      break;
     case NONE:
      continue;
    }
    canvas.drawCircle(avg * i,avg * j,avg / 2 - 0.5f,mChessPaint);
   }
  }
 }

這樣我們就將整個棋盤畫出來了,之後我們只需要改變陣列 mChessArray[][] 裡面物件的 Color 屬性,再呼叫 invalidate() 方法便可以重新整理棋盤了。

5. 接下來,我們便要處理點選事件,實現對弈的邏輯了,重寫 onTouchEvent() 方法,程式碼如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
   case MotionEvent.ACTION_DOWN:
    // 如果棋盤被鎖定(即勝負已分,返回檢視棋局的時候)
    // 此時只允許檢視,不允許落子了
    if (isLocked) {
     return true;
    }
    float x = event.getX();
    float y = event.getY();
    // 以點選的位置為中心,新建一個小矩形
    Rect rect = getLittleRect(x,y);
    // 獲得上述矩形包含的棋盤上的點
    Point point = getContainPoint(rect);
    if (point != null) {
     // 若點不為空,則重新整理對應位置棋子的屬性
     setChessState(point);
     // 記錄下每步操作,方便悔棋操作
     mEveryPlay.add(point);
     if (gameIsOver(point.x,point.y)) {
      // 遊戲結束彈窗提示
      showDialog();
     }
     // 更改遊戲玩家
     isBlackPlay = !isBlackPlay;
    }
    break;
   case MotionEvent.ACTION_MOVE:
    break;
   case MotionEvent.ACTION_UP:
    break;
  }
  return super.onTouchEvent(event);
 }

下面分別來說說呼叫到的各個方法的實現思路:

getLittleRect()

/**
  * 以傳入點為中心,獲得一個矩形
  *
  * @param x 傳入點 x 座標
  * @param y 傳入點 y 座標
  * @return 所得矩形
  */
 private Rect getLittleRect(float x,float y) {
  int side = getMeasuredHeight() / 16;
  int left = (int) (x - side / 2);
  int top = (int) (y - side / 2);
  int right = (int) (x + side / 2);
  int bottom = (int) (y + side / 2);
  return new Rect(left,top,right,bottom);
 }

getContainPoint()

 /**
  * 獲取包含在 rect 中並且是能夠下棋的位置的點
  *
  * @param rect 矩形
  * @return 返回包含的點,若沒有包含任何點或者包含點已有棋子返回 null
  */
 private Point getContainPoint(Rect rect) {
  int avg = getMeasuredHeight() / 16;
  for (int i = 1; i < 16; i++) {
   for (int j = 1; j < 16; j++) {
    if (rect.contains(avg * i,avg * j)) {
     Point point = new Point(i - 1,j - 1);
     // 包含點沒有棋子才返回 point
     if (mChessArray[point.x][point.y].getColor() == Chess.Color.NONE) {
      return point;
     }
     break;
    }
   }
  }
  return null;
 }

showDialog()

順便一提,這個方法用的是 v7 包裡面的對話方塊,因為這樣可以在版本較低的安卓平臺下也可以得到不錯的顯示效果,效果如下:

/**
  * 遊戲結束,顯示對話方塊
  */
 private void showDialog() {
  AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
  builder.setTitle("遊戲結束");
  if (isBlackPlay) {
   builder.setMessage("黑方獲勝!!!");
  } else {
   builder.setMessage("白方獲勝!!!");
  }
  builder.setCancelable(false);
  builder.setPositiveButton("重新開始",new DialogInterface.OnClickListener() {
   @Override
   public void onClick(DialogInterface dialog,int which) {
    resetChessBoard();
    dialog.dismiss();
   }
  });
  builder.setNegativeButton("返回檢視",int which) {
    isLocked = true;
    dialog.dismiss();
   }
  });
  builder.show();
 }

setChessState()

/**
  * 重新設定使用者所點位置的棋子狀態
  *
  * @param point 棋子的位置
  */
 private void setChessState(Point point) {
  if (isBlackPlay) {
   mChessArray[point.x][point.y].setColor(Chess.Color.BLACK);
  } else {
   mChessArray[point.x][point.y].setColor(Chess.Color.WHITE);
  }
  invalidate();
 }

以上幾個方法都較為簡單不多說了,接下來重點講一下判斷遊戲結束的邏輯。

- gameIsOver ()

 /**
  * 判斷遊戲是否結束,遊戲結束標誌:當前落子位置與其他同色棋子連成 5 個
  *
  * @param x 落子位置 x 座標
  * @param y 落子位置 y 座標
  * @return 若連成 5 個,遊戲結束,返回 true,負責返回 false
  */
 private boolean gameIsOver(int x,int y) {
  Chess.Color color = mChessArray[x][y].getColor();
  return isOverA(x,y,color) || isOverB(x,color) || isOverC(x,color) || isOverD(x,color);
 }

這個方法用來判斷遊戲是否結束,思路便是以當前落子位置為基準,去尋找豎直、水平、左上至右下、左下至右上四個方向是否連成 5 子,分別對應 isOverA(),isOverB(),isOverC(),isOverD() 四個方法,這四個方法的實現如下:

private boolean isOverA(int x,int y,Chess.Color color) {
  int amount = 0;
  for (int i = y; i >= 0; i--) {
   if (mChessArray[x][i].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  for (int i = y; i < mChessArray[x].length; i++) {
   if (mChessArray[x][i].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  // 迴圈執行完成後,當前落子位置算了兩次,故條件應是大於 5
  return amount > 5;
 }

 private boolean isOverB(int x,Chess.Color color) {
  int amount = 0;
  for (int i = x; i >= 0; i--) {
   if (mChessArray[i][y].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  for (int i = x; i < mChessArray.length; i++) {
   if (mChessArray[i][y].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  // 迴圈執行完成後,當前落子位置算了兩次,故條件應是大於 5
  return amount > 5;
 }

 private boolean isOverC(int x,Chess.Color color) {
  int amount = 0;
  for (int i = x,j = y; i >= 0 && j >= 0; i--,j--) {
   if (mChessArray[i][j].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  for (int i = x,j = y; i < mChessArray.length && j < mChessArray[i].length; i++,j++) {
   if (mChessArray[i][j].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  // 迴圈執行完成後,當前落子位置算了兩次,故條件應是大於 5
  return amount > 5;
 }
 private boolean isOverD(int x,j = y; i < mChessArray.length && j >= 0; i++,j = y; i >= 0 && j < mChessArray[i].length; i--,j++) {
   if (mChessArray[i][j].getColor() == color) {
    amount++;
   } else {
    break;
   }
  }
  // 迴圈執行完成後,當前落子位置算了兩次,故條件應是大於 5
  return amount > 5;
 }

6. 最後定義兩個公有方法,方便 Activity 呼叫,用來執行悔棋和重置棋盤操作。兩個方法程式碼如下:

/**
  * 悔棋,實現思路為:記錄每一步走棋的座標,若點選了悔棋,
  * 則拿出最後記錄的座標,對 mChessArray 裡面對應座標的
  * 棋子進行處理(設定顏色為 NONE),並移除集合裡面最後
  * 一個元素
  */
 public void retract() {
  if (mEveryPlay.isEmpty()) {
   return;
  }
  Point point = mEveryPlay.get(mEveryPlay.size() - 1);
  mChessArray[point.x][point.y].setColor(Chess.Color.NONE);
  mEveryPlay.remove(mEveryPlay.size() - 1);
  isLocked = false;
  isBlackPlay = !isBlackPlay;
  invalidate();
 }

 /**
  * 重置棋盤
  */
 public void resetChessBoard() {
  for (Chess[] chessRow : mChessArray) {
   for (Chess chess : chessRow) {
    chess.setColor(Chess.Color.NONE);
   }
  }
  mEveryPlay.clear();
  isBlackPlay = true;
  isLocked = false;
  invalidate();
 }

到此, ChessView 已經寫完了,接下來只要在佈局檔案裡面宣告即可。

7. 在 activity_main 佈局檔案如下,非常簡單,我相信不用多說都能看懂:

<?xml version="1.0" encoding="utf-8"?>
<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"
 android:paddingBottom="@dimen/activity_vertical_margin"
 android:paddingLeft="@dimen/activity_horizontal_margin"
 android:paddingRight="@dimen/activity_horizontal_margin"
 android:paddingTop="@dimen/activity_vertical_margin"
 tools:context=".MainActivity">

 <com.yangqi.wuziqi.ChessView
  android:id="@+id/chessView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_centerHorizontal="true"/>

 <LinearLayout
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:layout_below="@id/chessView"
  android:layout_margin="20dp"
  android:gravity="center_horizontal"
  android:orientation="horizontal">

  <Button
   android:id="@+id/bt_retract"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="悔棋" />

  <Button
   android:id="@+id/bt_reset"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="重新開始"/>
 </LinearLayout>
</RelativeLayout>

8. 最後一步了,只需要在 MainActivity 裡面拿到 ChessView 物件和兩個 Button 按鈕,即可實現悔棋與重新開始:

package com.yangqi.wuziqi;

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


public class MainActivity extends AppCompatActivity {

 private Button bt_reset;
 private Button bt_retract;
 private ChessView chessView;

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

  initUI();
  initListener();
 }

 private void initListener() {
  bt_reset.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    chessView.resetChessBoard();
   }
  });
  bt_retract.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    chessView.retract();
   }
  });
 }

 private void initUI() {

  bt_reset = (Button) findViewById(R.id.bt_reset);
  bt_retract = (Button) findViewById(R.id.bt_retract);
  chessView = (ChessView) findViewById(R.id.chessView);
 }
}

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。