Flutter開發(fā)之對角棋游戲實現(xiàn)實例詳解
前沿
關于對角棋相信大家都不陌生,其憑借著規(guī)則簡單又靈活多變成為我們童年不可缺少的益智游戲。
今天我將用Flutter來實現(xiàn)一個對角棋游戲,即鞏固自己Flutter的繪制和手勢知識,也希望這篇文章對大家有所幫助。
演示效果
老規(guī)矩,我們先演示下實現(xiàn)的最終效果:
對角棋規(guī)則
首先我們還是回顧下對角棋游戲的規(guī)則,這里借用 百度百科 的規(guī)則說明:
棋盤:象棋棋盤中,將士所在的帶對角線的田字框。
棋子:雙方各持三子,顏色不同。
初始:如圖1所示,各自對立。
勝利條件:其中一方三子,占據(jù)一條對角線,或者對方?jīng)]有棋子可以移動。
玩法:沿著棋盤劃線,雙方交互移動棋子,一次一只能移動一步,不包括交叉。
實現(xiàn)思路
- 棋盤。繪制棋盤
- 棋子。繪制棋子
- 手勢。處理點擊棋子及移動位置手勢
- 規(guī)則。規(guī)則分為棋子移動規(guī)則、游戲勝利規(guī)則兩部分
具體實現(xiàn)
1. 繪制棋盤
說到繪制,我們需要先創(chuàng)建 CustomPaint 通過自定義 CustomPainter 來實現(xiàn)。
CustomPaint( size: Size(width, height), painter: DiagonalChessPainter(), )
考慮到我們要適配不同的手機尺寸,因此我們先通過 LayoutBuilder 測量整個 Widget 的尺寸,并計算棋盤在屏幕上位置。
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, ), ), ); }, )
我們這里定義棋盤的九個點,從屏幕左上角開始,代碼如下:
width = constraints.maxWidth; height = constraints.maxHeight; rWidth = width * 0.4; rHeight = width * 0.6; // 棋盤各個點 // 第一行,從左到右 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 中進行繪制,先繪制一個矩形,然后繪制四條對角線完成整個棋盤的繪制,代碼如下:
// 繪制矩形 canvas.drawRect( Rect.fromLTRB(-rWidth, -rHeight, rWidth, rHeight), _chessboardPaint); // 繪制對角線 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);
棋盤展示效果:
2. 繪制棋子
我們先定義6個棋子,并添加必要的繪制用到的屬性。代碼如下:
// 定義棋子位置、顏色、文案 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"));
關于棋子的繪制,這里為了簡化,繪制一個簡單的圓+序號文案即可。
/// 繪制單個棋子 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(); }
文案的繪制。通過TextPainter進行繪制,繪制時注意先textPainter.layout()測量后再計算偏移量。
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. 手勢處理
通常我們下棋時,首先點擊某個棋子,然后點擊需要移動到的位置。此時,棋子先變成選中狀態(tài),然后移動到選中的位置,完成棋子的移動。
對于手勢的處理,F(xiàn)lutter通過GestureDetector來實現(xiàn)。我們先定義GestuerDetecotr,將child設為CustomPiant,我們在onTapDown中處理用戶點擊。代碼如下:
GestureDetector( onTapDown: _onTapDown, child: CustomPaint( size: Size(width, height), painter: DiagonalChessPainter(), ), );
通過手勢點擊的位置和棋子的位置進行比較即可判斷當前是否點擊的是棋子。代碼如下:
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; } } /// 是否是當前點 bool _checkPoint(double dx1, double dy1, double dx2, double dy2) => (dx1 - dx2).abs() < 40 && (dy1 - dy2).abs() < 40;
若判斷當前不是點擊的棋子,則判斷是否點擊的棋盤中9個點的位置,若是則判斷是否已選中棋子,若選中則修改棋子的Offset重新繪制。代碼如下:
// 若點擊是棋盤 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; } }
實現(xiàn)效果如下:
4. 游戲規(guī)則
1. 棋子移動規(guī)則
我們下棋時,每一方只能走一步交替進行下棋,且棋子只能按照棋盤規(guī)則行走。代碼如下:
// 棋盤各個點可移動位置 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個點可移動位置 ];
我們分別在點擊棋子和棋盤位置時判斷是否當前一方的棋子走,若是當前棋子是否可以走到該棋盤位置。代碼如下:
// 更新棋子選中狀態(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ù)對角棋的勝利規(guī)則定義比賽勝利需要移動到的位置。代碼如下:
// 勝利的位置 final winPositions = [ [0, 4, 8], [2, 4, 6] ];
在棋子每次發(fā)生移動后來校驗當前棋子是否匹配勝利的位置,若匹配則彈窗提示勝利方。代碼如下:
/// 獲取勝利的狀態(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; } }
最后,當一方無法走下一步時,自動判斷另外一方勝利。代碼如下:
/// 獲取勝利的狀態(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; } // 判斷另外一方是否已無法走棋 if (isOtherNotMoveVisible( [index1, index2, index3], [otherIndex1, otherIndex2, otherIndex3])) { return i; } } return -1; } /// 另一方是否無法走下一步 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; }
至此,我們完成了整個游戲的實現(xiàn)!??ヽ(°▽°)ノ?
優(yōu)化
上面已經(jīng)把對角棋游戲的整個功能都實現(xiàn)了,但仔細思考還是有可以優(yōu)化的點。
1. 對手視角棋子調整
前面我們都是以自己的視角來實現(xiàn)棋子,但實際使用時對手應該對方的視角來觀察。因此,我們需要把對手的棋子順序和文案進行倒序處理。代碼如下:
// 對手棋子倒序顯示 piecesOffsetList .add(PiecesBean(boardOffsetList[0], Colors.greenAccent, "3")); piecesOffsetList .add(PiecesBean(boardOffsetList[1], Colors.greenAccent, "2")); piecesOffsetList .add(PiecesBean(boardOffsetList[2], Colors.greenAccent, "1")); /// 繪制單個棋子 void _drawChessPiece( Canvas canvas, PiecesBean bean, bool reverse, bool isSelected) { ... canvas.save(); canvas.translate(offset.dx, offset.dy); // 對手棋子旋轉180度,文案倒序顯示 if (reverse) canvas.rotate(pi); ... canvas.restore(); }
2. CustomPainter刷新機制優(yōu)化
正常我們使用setStatus進行Widget刷新,但考慮到我們只需要對 CustomPainter 進行刷新,我們可以使用 Listenable 對象來控制畫布的刷新,這樣是最高效的方式。對于多個 Listenable 對象使用 Listenable.merge 來合并。代碼如下:
// 選擇棋子序號 ValueNotifier<int> piecesIndex = ValueNotifier<int>(-1); // 點擊棋盤位置 ValueNotifier<int> boardIndex = ValueNotifier<int>(-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; }
總結
雖然對角棋看起來非常簡單,但我們完全實現(xiàn)卻沒有那么容易。中間用到了 Canvas 的 translate 、rotate 、save/restore 、矩形 線段 文本 的繪制、CustomPainter 的 Listenable 對象刷新、手勢的處理等知識,算是對 Canvas 的繪制有一個大概的回顧。
實踐出真知!看十遍相關資料不如敲一遍代碼。后續(xù)我也會繼續(xù)出相關系列文章,如果大家喜歡的話,請關注一下吧!
最后附上 項目源碼地址
以上就是Flutter開發(fā)之對角棋游戲實現(xiàn)實例詳解的詳細內容,更多關于Flutter 對角棋游戲的資料請關注腳本之家其它相關文章!
相關文章
Android編程實現(xiàn)創(chuàng)建,刪除,判斷快捷方式的方法
這篇文章主要介紹了Android編程實現(xiàn)創(chuàng)建,刪除,判斷快捷方式的方法,結合實例形式分析了Android編程針對快捷方式的常用操作技巧,需要的朋友可以參考下2017-02-02解析android創(chuàng)建快捷方式會啟動兩個應用的問題
本篇文章是對關于android創(chuàng)建快捷方式會啟動兩個應用的問題進行了詳細的分析介紹,需要的朋友參考下2013-06-06Android如何自定義EditText光標與下劃線顏色詳解
在android開發(fā)中 EditTextText是我們經(jīng)常用到的,我們使用時會有一些小問題,下面這篇文章主要給大家介紹了關于利用Android如何自定義EditText光標與下劃線顏色的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-08-08Android 添加系統(tǒng)設置屬性的實現(xiàn)及步驟
這篇文章主要介紹了Android 添加系統(tǒng)設置屬性的實現(xiàn)及步驟的相關資料,需要的朋友可以參考下2017-07-07