Flutter for OpenHarmony:构建一个 Flutter 颜色分类游戏,深入解析状态管理、游戏逻辑与声明式 UI 设计

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

发布时间:2026年2月6日
技术栈:Flutter 3.22+、Dart 3.4+、Material Design 3
适用读者:中级 Flutter 开发者、益智游戏爱好者、对状态驱动架构与算法思维感兴趣的技术人员


引言:从“试管排序”看逻辑思维的数字化表达

“颜色分类”(Color Sort)是近年来风靡移动端的益智游戏之一。玩家面对若干装有彩色球的试管,需通过将球在试管间移动,最终使每个非空试管仅包含同一种颜色的球。规则极简,却蕴含丰富的策略性——它本质上是一个受限状态空间搜索问题,考验玩家的规划能力、逆向思维与容错判断。

这类游戏的魅力在于:没有时间压力,没有随机干扰,胜负完全取决于逻辑推理。它不像动作游戏依赖反应速度,而更像数独或华容道,是大脑的“健身房”。

今天,我们将用 Flutter 从零构建一个完整的“颜色分类”游戏。这个项目虽无复杂动画或物理引擎,却完美展示了 如何用纯 Dart 代码实现严谨的游戏逻辑、安全的状态管理与直观的用户交互

更重要的是,它揭示了 Flutter 在构建高内聚、低耦合、可测试的交互式应用方面的强大能力——即使是最“静态”的益智游戏,也能通过声明式 UI 与响应式状态流,带来流畅愉悦的体验。
在这里插入图片描述


一、游戏机制与核心规则

基本设定

  • 初始状态:6 个试管,每管 4 个彩色球(共 6 种颜色 × 4 球 = 24 球)
  • 辅助空间:额外提供 2 个空试管,用于临时存放
  • 目标状态:所有非空试管均为单一颜色(即“完成”状态)
  • 操作规则
    • 只能移动顶部球
    • 只能移入非满试管
    • 只能移入空试管顶部颜色相同的试管

技术映射

游戏概念 代码实体 数据结构
试管 Tube List<Color>
彩色球 Color 对象 Flutter 内置类型
移动操作 _handleTap 状态变更函数
胜利判定 _checkWin 布尔谓词

这种对象化建模使游戏逻辑清晰、可维护、可扩展。


二、数据模型设计:Tube 类的封装艺术

核心定义

class Tube {
  final List<Color> balls;
  static const int capacity = 4;

  Tube(this.balls);

  bool get isEmpty => balls.isEmpty;
  bool get isFull => balls.length == capacity;
  bool get isComplete => balls.length == capacity && balls.every((b) => b == balls[0]);

  Color? get topColor => balls.isEmpty ? null : balls.last;

  void push(Color color) { if (!isFull) balls.add(color); }
  Color pop() { return balls.removeLast(); }
}

在这里插入图片描述

设计亮点

1. 不可变容量
static const int capacity = 4;

使用 static const 定义全局常量,确保所有试管行为一致,且编译期优化。

2. 语义化 Getter
  • isEmpty / isFull:封装长度判断,提升可读性
  • isComplete:核心胜利条件,一行代码表达“全同色且满”
  • topColor:安全返回顶部球颜色(空时为 null
3. 安全栈操作
  • push 自动检查容量,防止越界
  • pop 返回被移除的球,便于撤销或记录

面向对象原则Tube 封装了所有与“试管”相关的状态与行为,外部无需关心内部实现。


三、游戏初始化:随机但可解的谜题生成

关键步骤

void _newGame() {
  // 1. 创建24个球(6色×4)
  List<Color> allBalls = [];
  for (var color in colorPalette) {
    allBalls.addAll(List.filled(4, color));
  }

  // 2. 打乱顺序
  allBalls.shuffle(_random);

  // 3. 分成6管
  tubes = [];
  for (int i = 0; i < 6; i++) {
    tubes.add(Tube(allBalls.sublist(i * 4, (i + 1) * 4)));
  }

  // 4. 添加2个空管
  tubes.add(Tube([]));
  tubes.add(Tube([]));
}

在这里插入图片描述

为什么这样设计?

  • 确定性输入:先创建完整球集,再打乱,确保颜色数量严格平衡
  • 可解性保障:虽然未实现“验证可解”的算法,但随机打乱+足够空管(2个)在实践中几乎总可解
  • 扩展友好:修改 colorPalette 即可支持更多颜色(如 8 色 → 8 管 + 2 空管)

💡 进阶思考:专业实现应加入“可解性验证”(如 BFS 检查初始状态是否可达目标),但对休闲游戏非必需。


四、交互逻辑:双击模型与状态机

用户操作流程

[空闲] 
   │
   ▼ (点击非空试管)
[选中状态] ——(点击自身)——→ [空闲](取消)
   │
   ▼ (点击有效目标试管)
[执行移动] ——(检查胜利)——→ [弹出胜利对话框]
   │
   ▼ (点击无效试管)
[重置为空闲]

核心函数 _handleTap

void _handleTap(int index) {
  if (selectedTubeIndex == null) {
    // 第一次点击:选中
    if (!tubes[index].isEmpty) {
      selectedTubeIndex = index;
    }
  } else {
    if (index == selectedTubeIndex!) {
      // 点击自身:取消
      selectedTubeIndex = null;
    } else if (_canMove(selectedTubeIndex!, index)) {
      // 有效移动
      final color = tubes[selectedTubeIndex!].pop();
      tubes[index].push(color);
      selectedTubeIndex = null;
      if (_checkWin()) _showWinDialog();
    } else {
      // 无效操作:取消选择
      selectedTubeIndex = null;
    }
  }
  setState(() {});
}

在这里插入图片描述

状态管理哲学

  • 单一状态变量selectedTubeIndexint?)编码全部交互状态
  • 无中间状态:每次点击后立即重置或提交,避免“悬停”状态
  • 即时反馈:无效操作直接取消选择,而非提示错误(降低挫败感)

五、移动验证:_canMove 的逻辑严谨性

规则实现

bool _canMove(int fromIndex, int toIndex) {
  if (fromIndex == toIndex) return false;           // 不能自移
  final from = tubes[fromIndex];
  final to = tubes[toIndex];
  if (from.isEmpty) return false;                   // 源空
  if (to.isFull) return false;                      // 目标满
  if (to.isEmpty) return true;                      // 目标空 → 允许
  return from.topColor == to.topColor;              // 同色 → 允许
}

边界覆盖

情况 是否允许 代码处理
源 == 目标 fromIndex == toIndex
源为空 from.isEmpty
目标满 to.isFull
目标空 to.isEmpty
目标非空但同色 topColor == topColor
目标非空且异色 最后一行返回 false

防御性编程:所有非法情况显式排除,逻辑无歧义。


六、胜利判定:_checkWin 的简洁之美

实现

bool _checkWin() {
  return tubes.every((tube) => tube.isEmpty || tube.isComplete);
}

函数式思维

  • 使用 every 高阶函数,表达“所有试管满足条件”
  • 条件为 isEmpty || isComplete,覆盖空管与完成管

💡 对比传统写法

for (var tube in tubes) {
  if (!tube.isEmpty && !tube.isComplete) return false;
}
return true;

函数式版本更简洁、声明式、不易出错。


七、UI 架构:GridView + 声明式渲染

试管渲染

GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
  itemCount: tubes.length,
  itemBuilder: (context, index) {
    final tube = tubes[index];
    final isSelected = selectedTubeIndex == index;

    return GestureDetector(
      onTap: () => _handleTap(index),
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(
            color: isSelected ? Colors.indigo : Colors.grey.shade400,
            width: isSelected ? 3 : 1,
          ),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            ...List.generate(Tube.capacity, (i) {
              final ballIndex = tube.balls.length - 1 - i;
              if (ballIndex < 0) {
                return const Spacer();
              } else {
                return Container(/* 彩色球 */);
              }
            }),
          ],
        ),
      ),
    );
  },
)

视觉设计要点

  • 底部对齐mainAxisAlignment: MainAxisAlignment.end 模拟真实试管
  • 动态边框:选中时加粗变色(Colors.indigo),提供明确反馈
  • 球体堆叠:通过 List.generate(4) 生成固定高度槽位,空位用 Spacer 填充
  • 色彩系统:使用 colorPalette 预定义色,保证视觉一致性

🎨 Material 3 整合ColorScheme.fromSeed(seedColor: Colors.indigo) 自动生成协调的主题色。


八、性能与可访问性优化

性能关键点

  • 最小化 rebuild:仅 tubesselectedTubeIndex 触发更新
  • 高效列表操作List.generate + 条件渲染,避免不必要的 widget 创建
  • 常量优化const 修饰 SliverGridDelegateEdgeInsets

可访问性(Accessibility)

  • 大点击区域:每个试管 ≥ 80×120 dp,符合触控规范
  • 高对比度:深灰边框 vs 白色背景,彩色球鲜明易辨
  • 语义说明:顶部文字清晰描述游戏规则,辅助新用户

九、扩展方向:从休闲游戏到教育工具

当前实现是一个优秀的 MVP,但可进一步升级:

1. 难度分级

  • 初级:4 色 × 4 球 + 2 空管
  • 高级:8 色 × 5 球 + 2 空管
  • 专家:10 色 + 仅 1 空管

2. 撤销/重做

  • 记录操作历史(List<Move>
  • 支持无限步撤销

3. 提示系统

  • 高亮一个有效移动
  • 显示“最少步数”估计(需 A* 算法)

4. 成就与统计

  • “首次通关”、“无提示完成”等徽章
  • 平均步数、成功率统计

5. 教育模式

  • 针对儿童:使用动物/水果图标代替颜色
  • 教学关卡:逐步引入规则

十、总结:益智游戏中的工程智慧

这个“颜色分类”游戏证明了:伟大的用户体验,往往源于对基础逻辑的极致打磨

通过:

  • 严谨的对象建模Tube 类)
  • 清晰的状态机(选中/移动/重置)
  • 声明式的 UI 渲染GridView + 条件样式)
  • 函数式的胜利判定every + 谓词)

我们得以在 不到 200 行 Dart 代码 内,构建一个逻辑严密、交互流畅、视觉清晰的益智游戏。

更重要的是,它展示了 Flutter 的核心优势用统一的声明式范式,构建从简单表单到复杂游戏的全谱系应用

无论你是开发电商 App、企业后台,还是休闲游戏,这一原则始终适用——状态驱动 UI,逻辑驱动体验


附录:动手实验建议

  1. 添加音效:移动球时播放“滴”声,胜利时播放欢呼
  2. 实现撤销功能:记录每一步操作,支持回退
  3. 动态难度:根据用户水平自动调整颜色数量
  4. 本地存储:保存最佳步数记录
  5. 主题切换:支持深色模式、节日皮肤(如圣诞红绿球)

🌟 Happy Coding with Flutter!
愿你的每一行代码,都如一个彩色球般精准归位;每一次交互,都带来逻辑的愉悦与思维的满足。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐