Flutter for OpenHarmony游戏集合App实战之五子棋黑白棋子
本文介绍了五子棋游戏开发中棋子的实现方法。首先构建15×15的棋盘数据结构,使用二维数组存储棋子状态。棋子绘制采用圆形Container,添加边框和阴影增强视觉效果。通过GestureDetector实现点击响应,透明背景确保整个格子可点击。落子逻辑包括胜负判断(检查四个方向的五子连线)和玩家切换。代码展示了如何将棋子状态与UI同步,并处理游戏结束条件。整体实现了五子棋的核心游戏机制,为后续功能扩
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言

上一篇把棋盘画好了,这篇来放棋子。
五子棋的棋子就两种:黑子和白子。看起来简单,但要做好也有不少细节:棋子怎么画、怎么响应点击、黑白怎么交替、落子后怎么判断输赢。
棋盘数据结构
在画棋子之前,先要有地方存储棋盘状态:
static const int boardSize = 15;
late List<List<int>> board; // 0: empty, 1: black, 2: white
int currentPlayer = 1; // 1: black, 2: white
board是一个二维数组,15×15,每个位置存一个数字:
- 0: 空,没有棋子
- 1: 黑棋
- 2: 白棋
currentPlayer记录当前该谁下,1是黑棋先手。
为什么用数字而不是枚举?其实都可以,数字更简洁,判断也方便。
初始化
void _initGame() {
board = List.generate(boardSize, (_) => List.filled(boardSize, 0));
currentPlayer = 1;
gameOver = false;
winner = null;
}
List.generate创建外层List,List.filled创建内层List,每个位置初始化为0。
这种写法比嵌套for循环简洁。_表示不使用的参数,是Dart的惯例。
棋子的绘制
棋子用_buildPiece方法绘制:
Widget _buildPiece(int value) {
if (value == 0) return const SizedBox();
return Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: value == 1 ? Colors.black : Colors.white,
首先判断value,如果是0(空位),返回一个空的SizedBox,不显示任何东西。
如果有棋子,返回一个Container:
- width/height: 20像素,棋子大小
- shape: BoxShape.circle,圆形
- color: 根据value决定,1是黑色,2是白色
边框
border: Border.all(color: Colors.black54, width: 1),
给棋子加一圈边框,半透明黑色,1像素宽。
黑棋加边框看不太出来,但白棋必须有边框,不然在浅色棋盘上看不清边界。统一都加边框,视觉上更一致。
阴影
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 2,
offset: const Offset(1, 1),
),
],
),
);
}
加一个小阴影,让棋子有立体感,像是放在棋盘上的。
- blurRadius: 2: 模糊半径小一点,阴影不会太散
- offset: (1, 1): 向右下偏移1像素
💡 阴影参数是调出来的。一开始用了blurRadius: 4,阴影太大,棋子看起来飘在空中。改成2就自然多了。
点击落子
棋子放在GridView里,每个格子都能响应点击:
itemBuilder: (context, index) {
int row = index ~/ boardSize;
int col = index % boardSize;
return GestureDetector(
onTap: () => _placePiece(row, col),
child: Container(
color: Colors.transparent,
child: Center(
child: _buildPiece(board[row][col]),
),
),
);
},
index是GridView的线性索引,需要转换成行列坐标:
- row: index除以boardSize取整,得到行号
- col: index对boardSize取余,得到列号
~/是Dart的整除运算符,结果是整数。普通的/结果是double。
GestureDetector
return GestureDetector(
onTap: () => _placePiece(row, col),
用GestureDetector包裹,检测点击事件。点击时调用_placePiece,传入行列坐标。
透明背景
child: Container(
color: Colors.transparent,
Container设置透明背景,这很重要。
如果不设置color,Container默认没有背景,点击空白区域不会响应。设置透明色后,整个格子区域都能响应点击,即使那里没有棋子。
这是Flutter的一个小坑,很多人踩过。
_placePiece落子逻辑
void _placePiece(int row, int col) {
if (gameOver || board[row][col] != 0) return;
setState(() {
board[row][col] = currentPlayer;
前置检查
if (gameOver || board[row][col] != 0) return;
两个条件:
- gameOver: 游戏已结束,不能再下
- board[row][col] != 0: 这个位置已经有棋子了
任一条件满足就直接返回,不执行落子。
放置棋子
setState(() {
board[row][col] = currentPlayer;
把当前玩家的标记(1或2)写入棋盘数组。
用setState包裹,确保UI更新。
判断胜负
if (_checkWin(row, col)) {
gameOver = true;
winner = currentPlayer;
_showWinDialog();
落子后立即检查是否获胜。如果赢了,设置gameOver和winner,弹出胜利对话框。
检查平局
} else if (_isBoardFull()) {
gameOver = true;
_showDrawDialog();
如果没赢,检查棋盘是否满了。满了就是平局。
五子棋平局很少见,15×15有225个位置,下满之前通常早就分出胜负了。但逻辑上要处理这种情况。
切换玩家
} else {
currentPlayer = currentPlayer == 1 ? 2 : 1;
}
});
}
既没赢也没平局,切换到另一个玩家。
三元运算符:如果当前是1(黑棋),切换到2(白棋);如果是2,切换到1。
胜负判断
五子棋的胜利条件是五子连珠,横、竖、斜都算。
bool _checkWin(int row, int col) {
int player = board[row][col];
// Check all 4 directions
return _checkDirection(row, col, 1, 0, player) || // horizontal
_checkDirection(row, col, 0, 1, player) || // vertical
_checkDirection(row, col, 1, 1, player) || // diagonal
_checkDirection(row, col, 1, -1, player); // anti-diagonal
}
检查四个方向:
- (1, 0): 水平方向
- (0, 1): 垂直方向
- (1, 1): 主对角线(左上到右下)
- (1, -1): 副对角线(右上到左下)
任一方向连成5个就赢了。
_checkDirection方法
bool _checkDirection(int row, int col, int dRow, int dCol, int player) {
int count = 1;
// Check positive direction
for (int i = 1; i < 5; i++) {
int r = row + i * dRow;
int c = col + i * dCol;
if (r < 0 || r >= boardSize || c < 0 || c >= boardSize) break;
if (board[r][c] != player) break;
count++;
}
从落子位置出发,沿着指定方向数连续的同色棋子。
dRow和dCol是方向向量。比如水平方向是(1, 0),每次row加1,col不变。
循环最多4次(加上起点共5个),遇到边界或不同颜色就停止。
反方向也要数
// Check negative direction
for (int i = 1; i < 5; i++) {
int r = row - i * dRow;
int c = col - i * dCol;
if (r < 0 || r >= boardSize || c < 0 || c >= boardSize) break;
if (board[r][c] != player) break;
count++;
}
return count >= 5;
}
同样的逻辑,但方向相反(减而不是加)。
两个方向的count加起来,如果大于等于5就赢了。
💡 为什么要数两个方向? 因为新落的子可能在连线的中间,不一定在端点。比如已经有4个黑子排成一排,中间空一个,补上那个空位就赢了。只数一个方向会漏掉这种情况。
当前玩家指示器
页面顶部显示当前该谁下:
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: currentPlayer == 1 ? Colors.black : Colors.white,
border: Border.all(color: Colors.black),
),
),
一个小圆圈,颜色跟随currentPlayer变化。黑棋回合显示黑色,白棋回合显示白色。
const SizedBox(width: 12),
Text(
gameOver
? '游戏结束'
: '当前: ${currentPlayer == 1 ? "黑棋" : "白棋"}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
),
旁边是文字说明。游戏进行中显示"当前: 黑棋"或"当前: 白棋",游戏结束显示"游戏结束"。
圆圈和文字放在Row里水平排列,mainAxisAlignment: MainAxisAlignment.center让它们居中。
胜利对话框
void _showWinDialog() {
String winnerName = winner == 1 ? '黑棋' : '白棋';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('游戏结束'),
content: Text('$winnerName 获胜!'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() => _initGame());
},
child: const Text('再来一局'),
),
],
),
);
}
用showDialog弹出一个AlertDialog。
- title: 标题,“游戏结束”
- content: 内容,显示谁赢了
- actions: 按钮,点击后关闭对话框并重新开始游戏
Navigator.pop(context)关闭对话框,_initGame()重置游戏状态。
重新开始按钮
AppBar右边有个刷新按钮:
appBar: AppBar(
title: const Text('五子棋'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => setState(() => _initGame()),
),
],
),
点击直接重置游戏,不用等游戏结束。想悔棋?重来吧。
棋盘满了的判断
bool _isBoardFull() {
for (int i = 0; i < boardSize; i++) {
for (int j = 0; j < boardSize; j++) {
if (board[i][j] == 0) return false;
}
}
return true;
}
遍历整个棋盘,只要有一个空位(值为0)就返回false。全部遍历完都没找到空位,返回true。
这个方法效率不高,每次落子都要遍历225个格子。但五子棋棋盘不大,完全可以接受。
如果追求性能,可以维护一个空位计数器,每落一子减1,减到0就是满了。但没必要过度优化。
GridView配置
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: boardSize,
),
itemCount: boardSize * boardSize,
NeverScrollableScrollPhysics
physics: const NeverScrollableScrollPhysics(),
禁止GridView滚动。棋盘是固定大小的,不需要滚动。如果不禁止,手指滑动时棋盘会跟着动,很奇怪。
SliverGridDelegateWithFixedCrossAxisCount
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: boardSize,
),
指定每行15个格子。GridView会自动计算每个格子的大小,保证正好放下15个。
itemCount
itemCount: boardSize * boardSize,
总共15×15=225个格子。
棋子大小的考量
棋子设置成20像素:
width: 20,
height: 20,
这个值要配合格子大小。格子大小是棋盘宽度除以15,在不同屏幕上不一样。
20像素在大多数手机上看起来合适。如果想更精确,可以根据格子大小动态计算:
// 假设cellSize是格子大小
double pieceSize = cellSize * 0.8; // 棋子占格子的80%
但固定20像素也够用了,简单直接。
小结
这篇讲了五子棋的棋子实现,核心知识点:
- 二维数组存储棋盘状态,0/1/2表示空/黑/白
- Container + BoxDecoration绘制圆形棋子,带边框和阴影
- GestureDetector检测点击,透明背景确保整个格子可点击
- 整除和取余把线性索引转换成行列坐标
- setState更新状态并刷新UI
- 方向向量检查四个方向的连珠
- 双向计数处理棋子在连线中间的情况
- AlertDialog显示游戏结果
五子棋的核心逻辑就这些。规则简单,实现也不复杂,但玩起来很有意思。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)