欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter開(kāi)發(fā)之對(duì)角棋游戲?qū)崿F(xiàn)實(shí)例詳解

 更新時(shí)間:2022年11月28日 10:01:32   作者:Fitem  
這篇文章主要為大家介紹了Flutter開(kāi)發(fā)之對(duì)角棋游戲?qū)崿F(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前沿

關(guān)于對(duì)角棋相信大家都不陌生,其憑借著規(guī)則簡(jiǎn)單又靈活多變成為我們童年不可缺少的益智游戲。

今天我將用Flutter來(lái)實(shí)現(xiàn)一個(gè)對(duì)角棋游戲,即鞏固自己Flutter的繪制和手勢(shì)知識(shí),也希望這篇文章對(duì)大家有所幫助。

演示效果

老規(guī)矩,我們先演示下實(shí)現(xiàn)的最終效果:

對(duì)角棋規(guī)則

首先我們還是回顧下對(duì)角棋游戲的規(guī)則,這里借用 百度百科 的規(guī)則說(shuō)明:

棋盤(pán):象棋棋盤(pán)中,將士所在的帶對(duì)角線的田字框。

棋子:雙方各持三子,顏色不同。

初始:如圖1所示,各自對(duì)立。

勝利條件:其中一方三子,占據(jù)一條對(duì)角線,或者對(duì)方?jīng)]有棋子可以移動(dòng)。

玩法:沿著棋盤(pán)劃線,雙方交互移動(dòng)棋子,一次一只能移動(dòng)一步,不包括交叉。

實(shí)現(xiàn)思路

  • 棋盤(pán)。繪制棋盤(pán)
  • 棋子。繪制棋子
  • 手勢(shì)。處理點(diǎn)擊棋子及移動(dòng)位置手勢(shì)
  • 規(guī)則。規(guī)則分為棋子移動(dòng)規(guī)則、游戲勝利規(guī)則兩部分

具體實(shí)現(xiàn)

1. 繪制棋盤(pán)

說(shuō)到繪制,我們需要先創(chuàng)建 CustomPaint 通過(guò)自定義 CustomPainter 來(lái)實(shí)現(xiàn)。

CustomPaint(
    size: Size(width, height),
    painter: DiagonalChessPainter(),
)

考慮到我們要適配不同的手機(jī)尺寸,因此我們先通過(guò) LayoutBuilder 測(cè)量整個(gè) Widget 的尺寸,并計(jì)算棋盤(pán)在屏幕上位置。

LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          initPosition(constraints);
          return GestureDetector(
            onTapDown: _onTapDown,
            child: CustomPaint(
              size: Size(width, height),
              painter: DiagonalChessPainter(
                rWidth: rWidth,
                rHeight: rHeight,
                boardOffsetList: boardOffsetList,
              ),
            ),
          );
        },
      )

我們這里定義棋盤(pán)的九個(gè)點(diǎn),從屏幕左上角開(kāi)始,代碼如下:

    width = constraints.maxWidth;
    height = constraints.maxHeight;
    rWidth = width * 0.4;
    rHeight = width * 0.6;
    // 棋盤(pán)各個(gè)點(diǎn)
    // 第一行,從左到右
    boardOffsetList.add(Offset(-rWidth, -rHeight));
    boardOffsetList.add(Offset(0, -rHeight));
    boardOffsetList.add(Offset(rWidth, -rHeight));
    // 第二行,從左到右
    boardOffsetList.add(Offset(-rWidth, 0));
    boardOffsetList.add(Offset.zero);
    boardOffsetList.add(Offset(rWidth, 0));
    // 第二行,從左到右
    boardOffsetList.add(Offset(-rWidth, rHeight));
    boardOffsetList.add(Offset(0, rHeight));
    boardOffsetList.add(Offset(rWidth, rHeight));

在自定義的 DiagonalChessPainter 中進(jìn)行繪制,先繪制一個(gè)矩形,然后繪制四條對(duì)角線完成整個(gè)棋盤(pán)的繪制,代碼如下:

    // 繪制矩形
    canvas.drawRect(
        Rect.fromLTRB(-rWidth, -rHeight, rWidth, rHeight), _chessboardPaint);
    // 繪制對(duì)角線
    Path path = Path()
      // P1-P9
      ..moveTo(boardOffsetList[0].dx, boardOffsetList[0].dy)
      ..lineTo(boardOffsetList[8].dx, boardOffsetList[8].dy)
      // P2-P8
      ..moveTo(boardOffsetList[1].dx, boardOffsetList[1].dy)
      ..lineTo(boardOffsetList[7].dx, boardOffsetList[7].dy)
      // P3-P7
      ..moveTo(boardOffsetList[2].dx, boardOffsetList[2].dy)
      ..lineTo(boardOffsetList[6].dx, boardOffsetList[6].dy)
      // P4-P6
      ..moveTo(boardOffsetList[3].dx, boardOffsetList[3].dy)
      ..lineTo(boardOffsetList[5].dx, boardOffsetList[5].dy);
    canvas.drawPath(path, _chessboardPaint);

棋盤(pán)展示效果:

2. 繪制棋子

我們先定義6個(gè)棋子,并添加必要的繪制用到的屬性。代碼如下:

 // 定義棋子位置、顏色、文案
    piecesOffsetList.clear();
    piecesOffsetList
        .add(PiecesBean(boardOffsetList[0], Colors.greenAccent, "1"));
    piecesOffsetList
        .add(PiecesBean(boardOffsetList[1], Colors.greenAccent, "2"));
    piecesOffsetList
        .add(PiecesBean(boardOffsetList[2], Colors.greenAccent, "3"));
    piecesOffsetList.add(PiecesBean(boardOffsetList[6], Colors.redAccent, "1"));
    piecesOffsetList.add(PiecesBean(boardOffsetList[7], Colors.redAccent, "2"));
    piecesOffsetList.add(PiecesBean(boardOffsetList[8], Colors.redAccent, "3"));

關(guān)于棋子的繪制,這里為了簡(jiǎn)化,繪制一個(gè)簡(jiǎn)單的圓+序號(hào)文案即可。

  /// 繪制單個(gè)棋子
  void _drawChessPiece(
      Canvas canvas, PiecesBean bean, bool reverse, bool isSelected) {
    var offset = bean.offset;
    var color = bean.color;
    double radius = 25;
    canvas.save();
    canvas.translate(offset.dx, offset.dy);
    canvas.drawCircle(Offset.zero, radius, _chessPiecesPaint..color = color);
    _drawChessPieceText(canvas, bean, isSelected);
    canvas.restore();
  }

文案的繪制。通過(guò)TextPainter進(jìn)行繪制,繪制時(shí)注意先textPainter.layout()測(cè)量后再計(jì)算偏移量。

 var textPainter = TextPainter(
      text: TextSpan(
          text: bean.text,
          style: TextStyle(
            fontSize: isSelected ? 35 : 30,
            color: Colors.white,
            fontWeight: FontWeight.bold,
          )),
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();
    var textSize = textPainter.size;
    textPainter.paint(
        canvas, Offset(textSize.width * -0.5, textSize.height * -0.5));
  // 定義步數(shù),判斷那一方走下一步棋
  int step = 0;

棋子展示效果:

3. 手勢(shì)處理

通常我們下棋時(shí),首先點(diǎn)擊某個(gè)棋子,然后點(diǎn)擊需要移動(dòng)到的位置。此時(shí),棋子先變成選中狀態(tài),然后移動(dòng)到選中的位置,完成棋子的移動(dòng)。

對(duì)于手勢(shì)的處理,F(xiàn)lutter通過(guò)GestureDetector來(lái)實(shí)現(xiàn)。我們先定義GestuerDetecotr,將child設(shè)為CustomPiant,我們?cè)趏nTapDown中處理用戶點(diǎn)擊。代碼如下:

GestureDetector(
            onTapDown: _onTapDown,
            child: CustomPaint(
              size: Size(width, height),
              painter: DiagonalChessPainter(),
            ),
          );

通過(guò)手勢(shì)點(diǎn)擊的位置和棋子的位置進(jìn)行比較即可判斷當(dāng)前是否點(diǎn)擊的是棋子。代碼如下:

 var offset = details.globalPosition;
    var dx = offset.dx - width * 0.5;
    var dy = offset.dy - height * 0.5;
    for (MapEntry<int, PiecesBean> entry in piecesOffsetList.asMap().entries) {
      var bean = entry.value;
      var index = entry.key;
      var piecesOffset = bean.offset;
      if (_checkPoint(piecesOffset.dx, piecesOffset.dy, dx, dy)) {
        // 更新棋子選中狀態(tài)
        piecesIndex.value = index;
        // debugPrint("piecesIndex:$piecesIndex");
        return;
      }
    }
  /// 是否是當(dāng)前點(diǎn)
  bool _checkPoint(double dx1, double dy1, double dx2, double dy2) =>
      (dx1 - dx2).abs() < 40 && (dy1 - dy2).abs() < 40;

若判斷當(dāng)前不是點(diǎn)擊的棋子,則判斷是否點(diǎn)擊的棋盤(pán)中9個(gè)點(diǎn)的位置,若是則判斷是否已選中棋子,若選中則修改棋子的Offset重新繪制。代碼如下:

    // 若點(diǎn)擊是棋盤(pán)
    for (MapEntry<int, Offset> entry in boardOffsetList.asMap().entries) {
      var offset = entry.value;
      var index = entry.key;
      if (_checkPoint(offset.dx, offset.dy, dx, dy)) {
        if (piecesIndex.value > -1) {
          var bean = piecesOffsetList[piecesIndex.value];
          bean.offset = boardOffsetList[index];
        }
        // debugPrint("boardsIndex:$index");
        return;
      }
    }

實(shí)現(xiàn)效果如下:

4. 游戲規(guī)則

1. 棋子移動(dòng)規(guī)則

我們下棋時(shí),每一方只能走一步交替進(jìn)行下棋,且棋子只能按照棋盤(pán)規(guī)則行走。代碼如下:

 // 棋盤(pán)各個(gè)點(diǎn)可移動(dòng)位置
  final moveVisibleList = [
    [1, 3, 4],
    [0, 2, 4],
    [1, 4, 5],
    [0, 4, 6],
    [0, 1, 2, 3, 5, 6, 7, 8],
    [2, 4, 8],
    [3, 4, 7],
    [4, 6, 8],
    [4, 5, 7], //第9個(gè)點(diǎn)可移動(dòng)位置
  ];

我們分別在點(diǎn)擊棋子和棋盤(pán)位置時(shí)判斷是否當(dāng)前一方的棋子走,若是當(dāng)前棋子是否可以走到該棋盤(pán)位置。代碼如下:

 // 更新棋子選中狀態(tài)
        if (step % 2 == 1 && index < 3 || step % 2 == 0 && index >= 3) {
          piecesIndex.value = index;
        }
 // 判斷棋子是否可以走到該位置
        if (piecesIndex.value > -1 &&
            isMoveViable(piecesIndex.value, index) &&
            (step % 2 == 1 && piecesIndex.value < 3 ||
                step % 2 == 0 && piecesIndex.value >= 3)) {
          var bean = piecesOffsetList[piecesIndex.value];
          bean.offset = boardOffsetList[index];
          boardIndex.value = index;
          step++;
        }

2. 比賽勝利規(guī)則

我們首先根據(jù)對(duì)角棋的勝利規(guī)則定義比賽勝利需要移動(dòng)到的位置。代碼如下:

  // 勝利的位置
  final winPositions = [
    [0, 4, 8],
    [2, 4, 6]
  ];

在棋子每次發(fā)生移動(dòng)后來(lái)校驗(yàn)當(dāng)前棋子是否匹配勝利的位置,若匹配則彈窗提示勝利方。代碼如下:

/// 獲取勝利的狀態(tài)
  int getWinState() {
    for (int i = 0; i < piecesOffsetList.length / 3; i++) {
      var offset1 = piecesOffsetList[i * 3 + 0].offset;
      var offset2 = piecesOffsetList[i * 3 + 1].offset;
      var offset3 = piecesOffsetList[i * 3 + 2].offset;
      if (isWinPosition(offset1, offset2, offset3)) {
        return i;
      }
    }
    return -1;
  }
  /// 是否是符合勝利的位置
  bool isWinPosition(Offset offset1, Offset offset2, Offset offset3) {
    var position1 = boardOffsetList.indexOf(offset1);
    var position2 = boardOffsetList.indexOf(offset2);
    var position3 = boardOffsetList.indexOf(offset3);
    for (var positionList in winPositions) {
      if (positionList.contains(position1) &&
          positionList.contains(position2) &&
          positionList.contains(position3)) {
        return true;
      }
    }
    return false;
  }
  /// 判斷是否有一方勝利
  void checkWinState() {
    var winState = getWinState();
    switch (winState) {
      case 0: // 綠色方勝利
        _showDialogTip("綠色方勝利!");
        break;
      case 1: // 紅色放勝利
        _showDialogTip("紅色方勝利!");
        break;
      default:
        break;
    }
  }

最后,當(dāng)一方無(wú)法走下一步時(shí),自動(dòng)判斷另外一方勝利。代碼如下:

 /// 獲取勝利的狀態(tài)
  int getWinState() {
    for (int i = 0; i < piecesOffsetList.length / 3; i++) {
      var index1 = piecesOffsetList[i * 3 + 0].boardIndex;
      var index2 = piecesOffsetList[i * 3 + 1].boardIndex;
      var index3 = piecesOffsetList[i * 3 + 2].boardIndex;
      var lastIndex = piecesOffsetList.length - 1;
      var otherIndex1 = piecesOffsetList[lastIndex - (i * 3 + 0)].boardIndex;
      var otherIndex2 = piecesOffsetList[lastIndex - (i * 3 + 1)].boardIndex;
      var otherIndex3 = piecesOffsetList[lastIndex - (i * 3 + 2)].boardIndex;
      // 判斷一方是否已勝利
      if (isWinPosition(index1, index2, index3)) {
        return i;
      }
      // 判斷另外一方是否已無(wú)法走棋
      if (isOtherNotMoveVisible(
          [index1, index2, index3], [otherIndex1, otherIndex2, otherIndex3])) {
        return i;
      }
    }
    return -1;
  }
/// 另一方是否無(wú)法走下一步
bool isOtherNotMoveVisible(List<int> list1, List<int> list2) {
    List<int> list = [...list1, ...list2];
    for (var index in list2) {
      for (var moveIndex in moveVisibleList[index]) {
        if (!list.contains(moveIndex)) {
          return false;
        }
      }
    }
    return true;
  }

至此,我們完成了整個(gè)游戲的實(shí)現(xiàn)!??ヽ(°▽°)ノ?

優(yōu)化

上面已經(jīng)把對(duì)角棋游戲的整個(gè)功能都實(shí)現(xiàn)了,但仔細(xì)思考還是有可以優(yōu)化的點(diǎn)。

1. 對(duì)手視角棋子調(diào)整

前面我們都是以自己的視角來(lái)實(shí)現(xiàn)棋子,但實(shí)際使用時(shí)對(duì)手應(yīng)該對(duì)方的視角來(lái)觀察。因此,我們需要把對(duì)手的棋子順序和文案進(jìn)行倒序處理。代碼如下:

 // 對(duì)手棋子倒序顯示
 piecesOffsetList
        .add(PiecesBean(boardOffsetList[0], Colors.greenAccent, "3"));
    piecesOffsetList
        .add(PiecesBean(boardOffsetList[1], Colors.greenAccent, "2"));
    piecesOffsetList
        .add(PiecesBean(boardOffsetList[2], Colors.greenAccent, "1"));
 /// 繪制單個(gè)棋子
  void _drawChessPiece(
      Canvas canvas, PiecesBean bean, bool reverse, bool isSelected) {
    ...
    canvas.save();
    canvas.translate(offset.dx, offset.dy);
    // 對(duì)手棋子旋轉(zhuǎn)180度,文案倒序顯示
    if (reverse) canvas.rotate(pi);
    ...
    canvas.restore();
  } 

2. CustomPainter刷新機(jī)制優(yōu)化

正常我們使用setStatus進(jìn)行Widget刷新,但考慮到我們只需要對(duì) CustomPainter 進(jìn)行刷新,我們可以使用 Listenable 對(duì)象來(lái)控制畫(huà)布的刷新,這樣是最高效的方式。對(duì)于多個(gè) Listenable 對(duì)象使用 Listenable.merge 來(lái)合并。代碼如下:

// 選擇棋子序號(hào)
ValueNotifier&lt;int&gt; piecesIndex = ValueNotifier&lt;int&gt;(-1);
// 點(diǎn)擊棋盤(pán)位置
ValueNotifier&lt;int&gt; boardIndex = ValueNotifier&lt;int&gt;(-1);
CustomPaint(
  size: Size(width, height),
  painter: DiagonalChessPainter(
    ...
    piecesIndex: piecesIndex,
    boardIndex: boardIndex,
    repaint: Listenable.merge([piecesIndex, boardIndex]),
  ),
)
 @override
 bool shouldRepaint(covariant DiagonalChessPainter oldDelegate) {
   return oldDelegate.repaint != repaint;
 }

總結(jié)

雖然對(duì)角棋看起來(lái)非常簡(jiǎn)單,但我們完全實(shí)現(xiàn)卻沒(méi)有那么容易。中間用到了 Canvas 的 translate 、rotate 、save/restore 、矩形 線段 文本 的繪制、CustomPainter 的 Listenable 對(duì)象刷新、手勢(shì)的處理等知識(shí),算是對(duì) Canvas 的繪制有一個(gè)大概的回顧。

實(shí)踐出真知!看十遍相關(guān)資料不如敲一遍代碼。后續(xù)我也會(huì)繼續(xù)出相關(guān)系列文章,如果大家喜歡的話,請(qǐng)關(guān)注一下吧!

最后附上 項(xiàng)目源碼地址

以上就是Flutter開(kāi)發(fā)之對(duì)角棋游戲?qū)崿F(xiàn)實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter 對(duì)角棋游戲的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論