Flutter開(kāi)發(fā)之對(duì)角棋游戲?qū)崿F(xiàn)實(shí)例詳解
前沿
關(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<int> piecesIndex = ValueNotifier<int>(-1); // 點(diǎn)擊棋盤(pán)位置 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; }
總結(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)文章
Android編程實(shí)現(xiàn)創(chuàng)建,刪除,判斷快捷方式的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)創(chuàng)建,刪除,判斷快捷方式的方法,結(jié)合實(shí)例形式分析了Android編程針對(duì)快捷方式的常用操作技巧,需要的朋友可以參考下2017-02-02ubuntu用wifi連接android調(diào)試程序的步驟
這篇文章主要介紹了ubuntu用wifi連接android調(diào)試程序的步驟,需要的朋友可以參考下2014-02-02Android自定義頂部導(dǎo)航欄控件實(shí)例代碼
這篇文章主要介紹了Android自定義頂部導(dǎo)航欄控件實(shí)例代碼,需要的朋友可以參考下2017-12-12解析android創(chuàng)建快捷方式會(huì)啟動(dòng)兩個(gè)應(yīng)用的問(wèn)題
本篇文章是對(duì)關(guān)于android創(chuàng)建快捷方式會(huì)啟動(dòng)兩個(gè)應(yīng)用的問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06Android如何自定義EditText光標(biāo)與下劃線顏色詳解
在android開(kāi)發(fā)中 EditTextText是我們經(jīng)常用到的,我們使用時(shí)會(huì)有一些小問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于利用Android如何自定義EditText光標(biāo)與下劃線顏色的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08Android實(shí)現(xiàn)熱門(mén)標(biāo)簽的流式布局
這篇文章主要介紹了Android實(shí)現(xiàn)熱門(mén)標(biāo)簽的流式布局的詳細(xì)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2015-12-12Android編程設(shè)計(jì)模式之責(zé)任鏈模式詳解
這篇文章主要介紹了Android編程設(shè)計(jì)模式之責(zé)任鏈模式,詳細(xì)分析了Android設(shè)計(jì)模式中責(zé)任鏈模式的概念、原理、應(yīng)用場(chǎng)景、使用方法及相關(guān)操作技巧,需要的朋友可以參考下2017-12-12Android 添加系統(tǒng)設(shè)置屬性的實(shí)現(xiàn)及步驟
這篇文章主要介紹了Android 添加系統(tǒng)設(shè)置屬性的實(shí)現(xiàn)及步驟的相關(guān)資料,需要的朋友可以參考下2017-07-07