1. 项目背景与需求分析
在开发三国杀攻略App的过程中,我发现线下桌游玩家经常需要随机数工具来解决游戏中的各种判定场景。比如决斗时决定谁先出杀、拼点时随机生成点数,或者某些自创玩法需要随机分配角色。传统方式是使用实体骰子,但实际游戏中经常会遇到骰子丢失、滚动距离过远等问题。
基于这个实际需求,我决定开发一个数字骰子工具,主要解决以下几个痛点:
- 实体骰子容易丢失或损坏
- 多人游戏中骰子传递不便
- 需要记录历史投掷结果时缺乏有效手段
- 不同游戏场景需要不同数量的骰子
2. 技术选型与架构设计
2.1 为什么选择Flutter
作为跨平台框架,Flutter在这个项目中有几个显著优势:
- 高性能的动画支持:骰子滚动需要流畅的视觉效果
- 强大的自定义绘制能力:需要实现真实的骰子外观
- 热重载特性:方便快速迭代UI设计
- 单一代码库支持iOS和Android:降低维护成本
2.2 整体架构设计
采用经典的三层架构模式:
code复制数据层(DiceModel/DiceHistory)
↑↓
逻辑层(DiceController)
↑↓
UI层(DiceWidget/DiceToolPage)
这种分层设计使得各模块职责清晰:
- 数据层:纯粹的数据结构和存储
- 逻辑层:处理业务规则和状态变化
- UI层:只负责展示和用户交互
3. 核心功能实现详解
3.1 数据模型设计
DiceModel类解析
dart复制class DiceModel {
final int id; // 骰子唯一标识
int currentValue; // 当前点数(1-6)
bool isRolling; // 是否正在滚动
DiceModel({
required this.id,
this.currentValue = 1,
this.isRolling = false,
});
// 不可变模式支持
DiceModel copyWith({
int? id,
int? currentValue,
bool? isRolling,
}) {
return DiceModel(
id: id ?? this.id,
currentValue: currentValue ?? this.currentValue,
isRolling: isRolling ?? this.isRolling,
);
}
}
设计要点:
- 使用final修饰id确保唯一性
- currentValue默认值为1(骰子最小点数)
- copyWith模式便于状态管理
- 所有字段都有明确的初始值
DiceHistory类解析
dart复制class DiceHistory {
final DateTime timestamp; // 投掷时间
final List<int> results; // 各骰子结果
final int total; // 计算结果总和
DiceHistory({
required this.timestamp,
required this.results,
}) : total = results.fold(0, (sum, value) => sum + value);
// 时间格式化显示
String get formattedTime => '${timestamp.hour}:${timestamp.minute}:${timestamp.second}';
// 结果字符串表示
String get resultString {
return results.length == 1
? '结果: ${results[0]}'
: '结果: ${results.join(' + ')} = $total';
}
}
关键设计决策:
- 使用fold计算总和避免重复运算
- 通过getter实现格式化逻辑
- 根据骰子数量动态生成不同格式的结果字符串
3.2 状态管理实现
DiceController核心逻辑
dart复制class DiceController extends ChangeNotifier {
List<DiceModel> _dices = [DiceModel(id: 0)];
List<DiceHistory> _history = [];
final Random _random = Random();
bool _isRolling = false;
// 添加骰子
void addDice() {
if (_dices.length >= 6) return;
_dices.add(DiceModel(id: _dices.length));
notifyListeners();
}
// 投掷动画实现
Future<void> rollAllDices() async {
if (_isRolling) return;
_isRolling = true;
for (var dice in _dices) {
dice = dice.copyWith(isRolling: true);
}
notifyListeners();
// 动画效果:快速切换点数
for (int i = 0; i < 10; i++) {
await Future.delayed(Duration(milliseconds: 80));
for (var dice in _dices) {
dice = dice.copyWith(currentValue: _random.nextInt(6) + 1);
}
notifyListeners();
}
// 确定最终结果
final results = _dices.map((d) {
final value = _random.nextInt(6) + 1;
d = d.copyWith(currentValue: value, isRolling: false);
return value;
}).toList();
_history.insert(0, DiceHistory(
timestamp: DateTime.now(),
results: results,
));
_isRolling = false;
notifyListeners();
}
}
关键技术点:
- 使用ChangeNotifier实现轻量级状态管理
- _isRolling标志防止重复投掷
- 分阶段的动画实现:
- 准备阶段:设置isRolling状态
- 动画阶段:快速切换点数
- 收尾阶段:确定最终结果
- 历史记录自动维护
3.3 自定义骰子绘制
DiceWidget实现
dart复制class DiceWidget extends StatelessWidget {
final int value;
final bool isRolling;
final double size;
const DiceWidget({
required this.value,
this.isRolling = false,
this.size = 80,
});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Duration(milliseconds: 100),
width: size,
height: size,
decoration: BoxDecoration(
color: isRolling ? Colors.grey[300] : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isRolling ? Colors.grey[400]! : Colors.black87,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: Offset(2, 2),
),
],
),
child: CustomPaint(
painter: DicePainter(value: value, isRolling: isRolling),
),
);
}
}
设计亮点:
- 使用AnimatedContainer实现状态切换动画
- 动态调整颜色和边框表示滚动状态
- 阴影效果增强立体感
- 完全参数化设计(大小可配置)
DicePainter绘制逻辑
dart复制class DicePainter extends CustomPainter {
final int value;
final bool isRolling;
DicePainter({required this.value, required this.isRolling});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = isRolling ? Colors.grey[600]! : Colors.black87;
final dotRadius = size.width * 0.08;
final padding = size.width * 0.2;
final center = size.width / 2;
// 根据点数绘制对应图案
switch (value) {
case 1:
_drawDot(canvas, center, center, dotRadius, paint);
break;
case 2:
_drawDot(canvas, padding, padding, dotRadius, paint);
_drawDot(canvas, size.width-padding, size.height-padding, dotRadius, paint);
break;
// 其他点数情况...
}
}
void _drawDot(Canvas canvas, double x, double y, double radius, Paint paint) {
canvas.drawCircle(Offset(x, y), radius, paint);
}
@override
bool shouldRepaint(DicePainter oldDelegate) {
return oldDelegate.value != value || oldDelegate.isRolling != isRolling;
}
}
绘制要点:
- 根据标准骰子布局绘制点数
- 所有尺寸基于容器大小动态计算
- shouldRepaint优化性能
- 使用switch-case处理不同点数情况
4. 界面实现与交互设计
4.1 主界面结构
dart复制Scaffold(
appBar: AppBar(
title: Text('骰子工具'),
backgroundColor: Colors.red[700],
),
body: Column(
children: [
// 骰子展示区
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[700]!, Colors.red[500]!],
),
),
child: Column(
children: [
Wrap(
children: controller.dices.map((dice) =>
DiceWidget(
value: dice.currentValue,
isRolling: dice.isRolling,
)
).toList(),
),
ElevatedButton(
onPressed: controller.isRolling ? null : controller.rollAllDices,
child: Text(controller.isRolling ? '投掷中...' : '投掷骰子'),
),
],
),
),
// 控制面板
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: controller.diceCount > 1 ? controller.removeDice : null,
),
Text('${controller.diceCount}个骰子'),
IconButton(
icon: Icon(Icons.add),
onPressed: controller.diceCount < 6 ? controller.addDice : null,
),
],
),
),
// 历史记录区
Expanded(
child: ListView.builder(
itemCount: controller.history.length,
itemBuilder: (context, index) => ListTile(
leading: CircleAvatar(child: Text('${index+1}')),
title: Text(controller.history[index].resultString),
subtitle: Text(controller.history[index].formattedTime),
),
),
),
],
),
)
UI设计原则:
- 功能分区明确(展示区、控制区、历史区)
- 使用Wrap布局自动换行显示多个骰子
- 按钮状态与控制器状态同步
- 历史记录使用ListView优化性能
4.2 交互优化技巧
-
视觉反馈优化:
- 投掷按钮在动画期间禁用
- 骰子滚动时变为灰色
- 增减骰子按钮在边界情况下禁用
-
历史记录设计:
- 最新记录显示在最上方
- 每条记录包含序号、结果和时间
- 自动限制记录数量(最多20条)
-
空状态处理:
dart复制controller.history.isEmpty ? Center(child: Text('暂无历史记录')) : ListView.builder(...)
5. 高级功能扩展
5.1 震动反馈集成
-
添加依赖:
yaml复制dependencies: vibration: ^1.8.4 -
在投掷结束时触发:
dart复制import 'package:vibration/vibration.dart'; if (await Vibration.hasVibrator() ?? false) { Vibration.vibrate(duration: 100); }
5.2 音效支持
-
添加依赖:
yaml复制dependencies: audioplayers: ^5.2.1 -
音效管理类:
dart复制class SoundManager { static final AudioPlayer _player = AudioPlayer(); static Future<void> playRollSound() async { try { await _player.play(AssetSource('sounds/roll.mp3')); } catch (e) { debugPrint('播放音效失败: $e'); } } }
5.3 数据持久化
-
使用shared_preferences保存数据:
yaml复制dependencies: shared_preferences: ^2.2.2 -
扩展DiceController:
dart复制Future<void> saveData() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('dice_history', jsonEncode(_history.map((h) => h.toJson()).toList())); await prefs.setInt('dice_count', _dices.length); } Future<void> loadData() async { final prefs = await SharedPreferences.getInstance(); final count = prefs.getInt('dice_count') ?? 1; resetDices(count); final historyJson = prefs.getString('dice_history'); if (historyJson != null) { _history = (jsonDecode(historyJson) as List) .map((e) => DiceHistory.fromJson(e)) .toList(); } }
6. 性能优化与调试
6.1 关键性能指标
-
动画流畅度:
- 确保60fps的动画帧率
- 使用AnimatedContainer代替手动动画
- 限制同时进行的动画数量
-
内存使用:
- 及时释放不用的资源
- 限制历史记录数量
- 使用const构造函数
6.2 常见问题排查
-
动画卡顿:
- 检查是否在主线程执行耗时操作
- 减少动画期间的UI重建
- 使用性能图层检查工具分析
-
状态不同步:
- 确保所有状态修改后调用notifyListeners()
- 使用不可变数据模式
- 添加调试打印语句
-
内存泄漏:
- 及时取消订阅和关闭控制器
- 使用DevTools的内存分析工具
- 实现dispose方法清理资源
7. 项目总结与扩展思路
7.1 技术要点回顾
- 状态管理:使用ChangeNotifier实现轻量级状态管理
- 自定义绘制:通过CustomPaint实现专业级骰子外观
- 动画系统:组合使用多种动画技术实现流畅效果
- 架构设计:清晰的三层架构确保可维护性
7.2 可能的扩展方向
-
多骰子类型:
- 支持不同面数的骰子(如4面、8面、20面)
- 自定义骰子颜色和样式
-
多人模式:
- 通过网络同步多设备间的骰子状态
- 添加回合制投掷功能
-
高级记录功能:
- 按时间筛选历史记录
- 结果统计分析(点数分布等)
- 导出记录为图片或文本
-
游戏集成:
- 直接与三国杀游戏逻辑对接
- 为特定游戏场景预设骰子配置
在实际开发过程中,我发现Flutter的自定义绘制能力特别适合这类需要高度定制UI的场景。通过合理的状态管理和架构设计,即使是一个看似简单的骰子工具,也能体现出专业级的开发水准。这个项目的代码已经过充分测试和优化,可以直接集成到各类需要随机数生成功能的App中。