| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- // board.dart
- import 'dart:async';
- import 'dart:math';
- import 'dart:typed_data';
- import 'dart:ui' as ui;
- import 'package:flutter/material.dart';
- import 'package:image_puzzle/config/device.dart';
- import 'package:image_puzzle/play/piece.dart';
- import 'package:logging/logging.dart';
- import 'package:vector_math/vector_math.dart' as vmath;
- final Logger _log = Logger('board.dart');
- enum BoardStatus { loading, playing, success }
- class Board {
- // 原图
- final ui.Image image;
- // 所有拼图碎片
- final List<Piece> pieces = [];
- /// 拼图行数(3/4/5,对应9/16/25宫格)
- final int rows;
- /// 拼图列数(3/4/5)
- final int cols;
- /// 整个拼图在屏幕上的目标区域(最终完整显示的位置和大小)
- final Rect targetRect;
- // 碎片的逻辑宽高
- double get pieceLogicalWidth => targetRect.width / cols;
- double get pieceLogicalHeight => targetRect.height / rows;
- // 设备信息
- final Device device;
- // 静态背景绘图 Picture (只绘制一次)
- ui.Picture? _backgroundPicture;
- ui.Picture? get backgroundPicture => _backgroundPicture;
- ValueNotifier boardNotifier = ValueNotifier(1);
- // 用户是否真正可以开始动手开玩(所有加载初始化完成,并且进入的插屏广告播放完成)
- final Completer<bool> startPlayCompleter = Completer();
- BoardStatus _status = BoardStatus.loading;
- BoardStatus get status => _status;
- // 备份的 groups (PieceGroup 对象的列表)
- List<PieceGroup> backupGroups = [];
- void start() {
- _status = BoardStatus.playing;
- invalidate();
- }
- void success() {
- _status = BoardStatus.success;
- invalidate();
- }
- void invalidate() {
- boardNotifier.value++;
- }
- // 是否全部完成
- bool get isAllDone => pieces.every((p) => p.isOK);
- final TickerProviderStateMixin ticker;
- Board(this.ticker, this.image, this.cols, this.rows, this.targetRect, this.device, {Map<String, dynamic>? json}) {
- _recordBackground(); // 录制静态背景,提升性能
- _initPieces();
- rebuildAllGroups();
- backupAllGroups();
- }
- /// 初始化碎片
- _initPieces() {
- final double imagePixelWidth = image.width.toDouble(); // 图片像素宽度
- final double imagePixelHeight = image.height.toDouble(); // 图片像素高度
- final double pieceLogicalWidth = targetRect.width / cols; // 绘图区域逻辑宽度 / 列数 = 每个piece的逻辑宽度
- final double pieceLogicalHeight = targetRect.height / rows; // 绘图区域逻辑高度 / 行数 = 每个piece的逻辑高度
- final double scaleX = imagePixelWidth / targetRect.width; // 像素宽度与逻辑宽度的scale比例
- final double scaleY = imagePixelHeight / targetRect.height; // 像素高度与逻辑高度的scale比例
- final double piecePixelWidth = pieceLogicalWidth * scaleX; // 每个piece的像素宽度
- final double piecePixelHeight = pieceLogicalHeight * scaleY; // 每个piece的像素高度
- final List<({int col, int row})> slotCoords = [];
- for (int r = 0; r < rows; r++) {
- for (int c = 0; c < cols; c++) {
- slotCoords.add((col: c, row: r));
- }
- }
- slotCoords.shuffle(Random());
- int index = 0;
- for (int r = 0; r < rows; r++) {
- for (int c = 0; c < cols; c++) {
- final correctX = targetRect.left + c * pieceLogicalWidth; // piece的正确位置坐标 x
- final correctY = targetRect.top + r * pieceLogicalHeight; // piece的正确位置坐标 y
- final correctOffset = Offset(correctX, correctY);
- final initialSlot = slotCoords[index];
- final initialX = targetRect.left + initialSlot.col * pieceLogicalWidth; // 初始位置坐标
- final initialY = targetRect.top + initialSlot.row * pieceLogicalHeight;
- final initialTransform = vmath.Matrix4.translationValues(initialX, initialY, 0.0); // 记录初始位置坐标transform matrix
- final sourceRect = Rect.fromLTWH(c * piecePixelWidth, r * piecePixelHeight, piecePixelWidth, piecePixelHeight); // 对应图片的像素矩形
- pieces.add(
- Piece(
- board: this,
- index: index,
- row: r,
- col: c,
- rows: rows,
- cols: cols,
- correctOffset: correctOffset,
- curCol: initialSlot.col,
- curRow: initialSlot.row,
- sourceRect: sourceRect,
- transform: initialTransform,
- borders: [true, true, true, true],
- ),
- );
- index++;
- }
- }
- }
- // 根据坐标查找指定位置的碎片
- Piece? findPieceAt(Offset localPos) {
- for (var piece in pieces.reversed) {
- // 从上层开始找
- // 计算碎片在画布上的绝对位置board
- final transform = piece.transform;
- final posX = transform.storage[12];
- final posY = transform.storage[13];
- final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
- if (pieceRect.contains(localPos)) {
- return piece;
- }
- }
- return null;
- }
- // 查找某个坐标上的碎片,排除某个piece
- Piece? findPieceAtExclude(Offset localPos, Piece excludePiece) {
- for (var piece in pieces.reversed) {
- // 从上层开始找
- if (piece == excludePiece) continue;
- // 计算碎片在画布上的绝对位置
- final transform = piece.transform;
- final posX = transform.storage[12];
- final posY = transform.storage[13];
- final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
- if (pieceRect.contains(localPos)) {
- return piece;
- }
- }
- return null;
- }
- vmath.Matrix4 getTransformByCoordinate(int row, int col) {
- final x = targetRect.left + col * pieceLogicalWidth;
- final y = targetRect.top + row * pieceLogicalHeight;
- final transform = vmath.Matrix4.translationValues(x, y, 0.0);
- return transform;
- }
- // 根据坐标获取该位置上的piece
- Piece? getPieceByCoordinate(int row, int col) {
- return pieces.firstWhereOrNull((p) => p.curRow == row && p.curCol == col);
- }
- // 根据坐标获取该位置上的piece
- Piece? getPieceByIndex(int index) {
- return pieces.firstWhereOrNull((p) => p.index == index);
- }
- // 初始化检查合并分组 (改进版:固定点迭代合并)
- void rebuildAllGroups() {
- _log.info('rebuildAllGroups');
- // 1. 清除所有旧组 和 原path
- for (var p in pieces) {
- p.group = null;
- p.path = null;
- p.outLinePath = null;
- p.innerLinePath = null;
- }
- bool mergedInPass;
- // 2. 迭代合并,直到一轮循环中没有发生任何合并
- do {
- mergedInPass = false;
- // 3. 遍历所有碎片对 (i, j)
- for (int i = 0; i < pieces.length; i++) {
- for (int j = i + 1; j < pieces.length; j++) {
- final piece = pieces[i];
- final otherPiece = pieces[j];
- // 4. 检查是否可以合并,并且它们不属于同一个组
- if (piece.canMerge(otherPiece)) {
- if (!piece.isSameGroup(otherPiece)) {
- // 5. 合并组
- // piece.groupWith(otherPiece) 会创建一个新的 PieceGroup,
- // 并将 piece 和 otherPiece (以及它们可能已有的组员) 的 group 引用全部指向新组。
- piece.groupWith(otherPiece);
- mergedInPass = true;
- }
- }
- }
- }
- // 如果 mergedInPass 为 true,说明本轮循环发生了合并,需要重新开始下一轮遍历
- // 因为新的合并可能促使其他碎片或碎片组也得以连接
- } while (mergedInPass);
- }
- // 备份所有group, 方便进行比较, 发现新合成的group,以便呈现动画特效
- void backupAllGroups() {
- backupGroups.clear();
- for (var p in pieces) {
- if (p.group != null && !backupGroups.contains(p.group)) {
- backupGroups.add(p.group!);
- }
- }
- _log.info('backupAllGroups: ${backupGroups.length}');
- }
- // 当前group与之前备份的group进行比较,返回新合并的group列表,这些group需要展示动画特效
- List<PieceGroup> compareAllGroups() {
- _log.info('compareAllGroups');
- List<PieceGroup> newGroups = [];
- List<PieceGroup> currentGroups = [];
- for (var p in pieces) {
- if (p.group != null && !currentGroups.contains(p.group)) {
- currentGroups.add(p.group!);
- }
- }
- if (backupGroups.isEmpty) {
- // 之前都不存在群组,那么当前新合成的所有group都是新group
- newGroups.addAll(currentGroups);
- } else {
- for (var g in currentGroups) {
- bool alreadyExists = false;
- for (var bakgroup in backupGroups) {
- if (bakgroup.containsGroup(g)) {
- alreadyExists = true;
- break;
- }
- }
- if (!alreadyExists) {
- newGroups.add(g);
- }
- }
- }
- if (newGroups.isNotEmpty) {
- for (var g in newGroups) {
- _log.info('发现新群组:');
- g.print();
- }
- }
- return newGroups;
- }
- // 检查游戏是否全部完成
- bool checkWinCondition() {
- return pieces.every((p) => p.isOK);
- }
- /// 退出释放资源
- dispose() {
- _backgroundPicture?.dispose();
- _backgroundPicture = null;
- image.dispose();
- boardNotifier.dispose();
- }
- // 录制背景,避免每次都重复绘制,提升性能
- void _recordBackground() {
- // 确保在录制前释放旧的 Picture 资源
- _backgroundPicture?.dispose();
- _backgroundPicture = null;
- final recorder = ui.PictureRecorder();
- // 录制器的边界设置为整个屏幕尺寸,因为我们在这里绘制的是 full screen background。
- final recordBounds = Rect.fromLTWH(0, 0, device.screenSize.width, device.screenSize.height);
- final canvas = Canvas(recorder, recordBounds);
- // --- 静态绘制配置 ---
- const double cornerRadius = 8.0;
- const double strokeWidth = 1.0; // 拼图槽位的线宽
- final double halfStroke = strokeWidth / 2.0;
- // 1. 绘制整个屏幕背景
- canvas.drawRect(
- recordBounds,
- Paint()
- ..color = Colors
- .lightGreen // 主背景色
- ..style = PaintingStyle.fill,
- );
- // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容)
- final slotFillPaint = Paint()
- ..color = Colors.green
- // .shade100 // 槽位填充色
- ..style = PaintingStyle.fill;
- final slotStrokePaint = Paint()
- ..color =
- Color(0xff26600c) // 槽位边框色
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth;
- for (int r = 0; r < rows; r++) {
- for (int c = 0; c < cols; c++) {
- // 计算当前槽位的边界 (Canvas坐标系)
- final left = targetRect.left + c * pieceLogicalWidth;
- final top = targetRect.top + r * pieceLogicalHeight;
- final right = left + pieceLogicalWidth;
- final bottom = top + pieceLogicalHeight;
- // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
- final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke);
- final slotRRect = RRect.fromRectAndRadius(slotRect, const Radius.circular(cornerRadius));
- // 绘制填充
- canvas.drawRRect(slotRRect, slotFillPaint);
- // 绘制描边
- canvas.drawRRect(slotRRect, slotStrokePaint);
- }
- }
- // --- 结束录制并存储 ---
- _backgroundPicture = recorder.endRecording();
- _log.info('Static background picture recorded. Size: ${recordBounds.size}');
- }
- }
- // 辅助扩展:解决 Dart 缺少 firstWhereOrNull 的问题
- extension IterableExtension<T> on Iterable<T> {
- T? firstWhereOrNull(bool Function(T element) test) {
- for (final element in this) {
- if (test(element)) {
- return element;
- }
- }
- return null;
- }
- }
|