·  阅读 1406

1. 交换方块+动画
2. 消除方块+动画
3. 保证棋盘为可消除状态
4. 保证一次消除到底

# 目录：

1. 方块，墙壁和棋盘在代码中表示
2. 完成交换和动画
3. 设计消除算法，动画，并填充消除后的区域
4. 预测能否继续消除，并随机棋盘
5. 最后

# 一、方块，墙壁和棋盘在代码中的表示

### 1. 棋盘代码展示

board代码如下：

``````///墙壁flag
final kB = 0x100;
///未初始化的正常棋盘
final kN = 0;
///方块的正常样式
final kBlockType = 0xFF;
///方块的状态信息
final kBlockStateInfo = 0xFF00;

var board= [
[kB, kB, kB, kB, kB, kB, kB, kB, kB],
[kB, kB, kB, kB, kB, kB, kB, kB, kB],
[kB, kB, kB, kB, kN, kB, kB, kB, kB],
[kB, kB, kB, kN, kN, kN, kB, kB, kB],
[kB, kB, kN, kN, kN, kN, kN, kB, kB],
[kB, kN, kN, kN, kN, kN, kN, kN, kB],
[kB, kN, kN, kN, kN, kN, kN, kN, kB],
[kB, kN, kN, kN, kN, kN, kN, kN, kB],
[kB, kN, kN, kN, kN, kN, kN, kN, kB],
[kB, kN, kN, kN, kN, kN, kN, kN, kB],
[kB, kB, kB, kB, kB, kB, kB, kB, kB],
];

### 3. 生成棋盘

``````generateBlocks() {
print("随机棋盘");
for (var i = 0; i < board.length; i++) {
for (var j = 0; j < board[i].length; j++) {
///墙壁不必随机
if (board[i][j] & kB != 0) {
continue;
}
///随机只复制最后一个字节，避免清除状态（先拿到状态信息，然后再和随机数结合）
board[i][j] = board[i][j] & kBlockStateInfo | (Random().nextInt(6) + 1);
}
}
}

### 4. 映射资源

``````///棋盘布局  Stack[ 背景,  方块 ]
///方块布局  Column套Row
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: logic.board
.mapIndexed((i, row) => Row(
mainAxisSize: MainAxisSize.min,
children: row
.mapIndexed((j, e) => GameBlockWidget(
e,
Point(i, j),
))
.toList(),
))
.toList(),
);

`GameBlockWidget`就是我们定义的方块。

``````if (widget.type & kB == 0) {
final blockType = widget.type & kBlockType;
String asset;
if (blockType == 1) {
///水壶
asset = Res.shuihu;
} else if (blockType == 2) {
///花束
asset = Res.huashu;
} else if (blockType == 3) {
///沙发
asset = Res.shafa;
} else if (blockType == 4) {
///手机
asset = Res.shouji;
} else if (blockType == 5) {
///水晶球
asset = Res.shuijingqiu;
} else if (blockType == 6) {
///小熊
asset = Res.xiaoxiong;
} else {
asset = Res.shuihu;
}
child = Image.asset(asset);
}

# 二、完成交换和动画

### 1. 动画的实现

``````enum BlockAnimState {
no,
anim,
}

1. 在 BlockAnimState.no 状态下，展示方块本来的组件。
2. 在 BlockAnimState. ready 状态下，对于需要动画的方块，包裹上AnimatedSlide 组件，并将offset置为初始状态。
3. 在 BlockAnimState. ready 对于需要动画的方块的 AnimatedSlide 组件的offset 置为动画完成状态。
4. 期间注意在每次更新界面的时候，预留好动画执行的时间。 很简单就完成了动画。 `AnimatedSlide`的用法很简单，如下所示：
``````AnimatedSlide(
duration: const Duration(milliseconds: kBlockAnimDuration),
curve: Curves.bounceIn,
child: child);
}

### 2. 交换的细节

1. 按下一个方块，获取焦点
2. 按下另一个方块
3. 如果两个方块临近，就交换
4. 如果两个方块不临近，就更换焦点 流程简单明了，所以我们使用`GeatureDetector`进行按下手势捕获,并在focusBlock方法中判断交换，大致代码如下：
``````///点击事件
GestureDetector(
onTapDown: (detail)async  {
var controller = Get.find<GameController>();
///聚焦方块
bool swap=await controller.focusBlock(detail.localPosition);
},
child:child)

///返回是否交换
Future<bool> focusBlock(Offset localPosition) async {
///计算是第几个方块
final i = localPosition.dy ~/ kBlockSize;
final j = localPosition.dx ~/ kBlockSize;
if (isBorderWithXY(i, j)) return false;

///当选在之前选中块的边上，交换
///当距离太远，更新焦点方块
if (currentFocus != null) {
if ((i - currentFocus!.x).abs() == 1 &&
(j - currentFocus!.y).abs() == 0 ||
((i - currentFocus!.x).abs() == 0 &&
(j - currentFocus!.y).abs() == 1)) {
targetBlock = Point(i, j);

///交换
await swapWithAnim(currentFocus!, targetBlock!);

///消除代码
if(eliminateAll()){
}
///无法消除取消交换
else {
await swapBack();
return false;
}
}
}
currentFocus = Point(i, j);
downLocalPosition = localPosition;
update();
return false;
}

# 三、设计消除算法，动画，并填充消除后的区域

### 1. 设计思路

1. 遍历棋盘数组，判断每一个方块的横向往右或者竖向往下有没有三连同样类型的方块
2. 新建和棋盘大小一致的消除数组，记录可消除的方块信息到其中。
3. 消除，并填充方块（忽略墙壁）
1. 新建每列队列，从顶部开始遍历棋盘的每一列。
2. 对于无需消除的方块到队列尾部。对于需要消除的方块，随机一个新类型的方块添加到队列。
3. 将队列赋值给棋盘对应列 利用了一个棋盘大小的新空间，基本上是一次遍历完成了算法，消除，动画，随机填充一气呵成，效率还行。 对应的动画的实现方案和交换动画一样，只是将`AnimatedSlide`替换成了`AnimatedScale`(对应代码在源码中)。

### 2. 具体代码

``````///消除应该消除的并重新随机
///如果成功消除了  返回true，失败了 返账false
///是否用户触发的消除，用户触发的需要动画，并在后期计算分值
Future<bool> eliminateAll({bool byUser = false,VoidCallback? beforeEliminate}) async {
eliminateMarkResult = markNeedEliminate(board);
if (eliminateMarkResult == null) return false;
beforeEliminate?.call();
///等待消除动画
if (byUser) {
update();
eliminateAnim = BlockAnimState.anim;
update();
await delay(kBlockElimintateAnimDuration);
eliminateAnim = BlockAnimState.no;
}
Queue<int> list = Queue();
for (var j = 0; j < board[0].length; j++) {
///消除方格
for (var i = 0; i < board.length; i++) {
///如果是墙壁，不看了
if (board[i][j] & kB != 0) {
continue;
}

if (eliminateMarkResult![i][j] == 0) {
} else {
///插入应该填充的标志
}
}
for (var i = 0; i < board.length; i++) {
///如果是墙壁，不看了
if (board[i][j] & kB != 0) {
continue;
}
final node = list.removeFirst();
if (node == -1) {
board[i][j] =
board[i][j] & kBlockStateInfo | (Random().nextInt(6) + 1);
} else {
board[i][j] = node;
}
}
}
return true;
}

# 四、预测能否继续消除，并随机棋盘

### 2. 算法设计思路

1. 遍历每个方块
1. 如果横向二连了（如图2和3），检查前一个（1）和后一个（4）周边有没有同类方块
2. 如果横向检查当前和间隔方块类型一致（如图1和3），检查中间那个（2）能否交换出三连
3. 如果竖向二连了（如图2和3），检查前一个（1）和后一个（4）能否交换出三连。
4. 竖向间隔了，交换中间尝试

1. 如果遍历到的方块有解，可被消除，中断遍历，预测为可消除。

### 3. 具体代码

``````///检测当前棋盘是否可被消除(棋盘需要是消除过的状态，不能当前就可消除)
bool detectCanEliminate() {
///[fromDirect]消除来源方向 0 左，1 上，2 右，3下
bool checkSurround(int targetType, int i, int j,
int fromDirect) {
///边界外直接返回false
if (i < 0 || i >= board.length || j < 0 || j >= board.length) {
return false;
}

![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57d81436ab334df8b7f0b66aa18a9cd3~tplv-k3u1fbpfcp-watermark.image?)
if (fromDirect & CheckFromDirect.left == 0 &&
getTypeWithXY(i, j - 1) == targetType) {
return true;
}
if (fromDirect & CheckFromDirect.top == 0 &&
getTypeWithXY(i - 1, j) == targetType) {
return true;
}
if (fromDirect & CheckFromDirect.right == 0 &&
getTypeWithXY(i, j + 1) == targetType) {
return true;
}
if (fromDirect & CheckFromDirect.bottom == 0 &&
getTypeWithXY(i + 1, j) == targetType) {
return true;
}
return false;
}

///检测该格子可否被预测消除
for (var i = 0; i < board.length; i++) {
for (var j = 0; j < board[i].length; j++) {
///如果是墙壁，不看了
if (board[i][j] & kB != 0) {
continue;
}
final type = getTypeWithXY(i, j);

///如果不是正常的方块，下一步
if (type == 0) continue;

///如果横向二连了，检查首尾周围有无同类
if (getTypeWithXY(i, j + 1) == type) {
if (checkSurround(type, i, j - 1, CheckFromDirect.right)) {
return true;
}
if (checkSurround(type, i, j + 2, CheckFromDirect.left)) {
return true;
}
///如果横向间隔二连了，检查中间有无同类
} else if (getTypeWithXY(i, j + 2) == type) {
if (checkSurround(type, i, j + 1, CheckFromDirect.right)) {
return true;
}
}
///如果竖向二连了，检查首尾周围有无同类
if (getTypeWithXY(i + 1, j) == type) {
if (checkSurround(type, i - 1, j, CheckFromDirect.bottom)) {
return true;
}
if (checkSurround(type, i + 2, j, CheckFromDirect.top)) {
return true;
}
///如果竖向间隔二连了，检查中间有无同类
} else if (getTypeWithXY(i + 2, j) == type) {
if (checkSurround(type, i + 1, j, CheckFromDirect.bottom)) {
return true;
}
}
}
}
return false;
}

# 五、最后

1. 保证初始棋盘可用 -> 初始化的时候随机棋盘，利用消除算法保证棋盘不在消除中，利用预测算法保证棋盘可被消除
2. 在交换后利用消除和预测算法计分，多次消除 这样就完成了消消乐的一直可以玩了～～ 后面其实还可以优化，但我就先弄到这里啦～

## Todo

• 引入flame引擎，优化增强一些游戏效果
• 加入计分系统，和关卡目标