Flutter 框架跨平台鸿蒙开发 - 虚拟骰子应用开发教程

Flutter虚拟骰子应用开发教程

项目简介

这是一款功能完整的虚拟骰子应用,为用户提供逼真的骰子投掷体验。应用采用Material Design 3设计风格,支持多种骰子类型、投掷动画、历史记录、统计分析等功能,界面精美生动,操作简单有趣。
运行效果图



核心特性

  • 多种骰子类型:支持D4、D6、D8、D10、D12、D20、D100等7种骰子
  • 多骰子投掷:支持同时投掷1-6个骰子
  • 逼真动画:摇摆和投掷动画效果,增强真实感
  • 投掷历史:记录所有投掷结果,支持查看和清除
  • 统计分析:详细的投掷统计和频率分析
  • 个性化设置:自定义骰子颜色、数量、反馈方式
  • 触觉反馈:震动和声音反馈增强体验
  • 投掷模式:支持摇一摇和点击两种投掷方式
  • 精美界面:渐变设计和流畅动画

技术栈

  • Flutter 3.x
  • Material Design 3
  • 动画控制器(AnimationController)
  • 自定义绘制(CustomPainter)
  • 手势识别
  • 触觉反馈(HapticFeedback)

项目架构

DiceHomePage
DicePage
HistoryPage
StatisticsPage
SettingsPage
DiceDisplay
RollButton
QuickActions
CurrentResult
SingleDice
DiceFace
D6Face
HistoryList
HistoryItem
OverallStats
FrequencyChart
DiceTypeStats
DiceSettings
AppearanceSettings
FeedbackSettings
RollModeSettings
DiceResult
DiceSettings
AnimationController

数据模型设计

DiceResult(投掷结果模型)

dart 复制代码
class DiceResult {
  final int value;                   // 投掷结果值
  final DateTime timestamp;          // 投掷时间
  final String diceType;             // 骰子类型(D4、D6等)
  final int diceCount;               // 骰子数量
}

设计要点

  • value存储投掷总和
  • timestamp用于历史记录排序
  • diceType和diceCount用于统计分析

DiceSettings(骰子设置模型)

dart 复制代码
class DiceSettings {
  final int diceCount;               // 骰子数量(1-6个)
  final String diceType;             // 骰子类型
  final bool soundEnabled;           // 声音开关
  final bool vibrationEnabled;       // 震动开关
  final bool animationEnabled;       // 动画开关
  final Color diceColor;             // 骰子颜色
  final String rollMode;             // 投掷模式(shake/tap)
}

骰子类型配置

类型 面数 用途 特点
D4 4面 简单随机 四面体,结果1-4
D6 6面 标准骰子 立方体,经典点数显示
D8 8面 桌游 八面体,结果1-8
D10 10面 百分比 十面体,结果1-10
D12 12面 角色扮演 十二面体,结果1-12
D20 20面 RPG游戏 二十面体,结果1-20
D100 100面 百分比 虚拟百面,结果1-100

核心功能实现

1. 骰子显示与动画

实现逼真的骰子显示和投掷动画效果。

dart 复制代码
Widget _buildDiceDisplay() {
  return AnimatedBuilder(
    animation: _rollAnimation,
    builder: (context, child) {
      return AnimatedBuilder(
        animation: _shakeAnimation,
        builder: (context, child) {
          return Transform.translate(
            offset: Offset(_shakeAnimation.value, 0),
            child: Transform.scale(
              scale: 1.0 + (_rollAnimation.value * 0.2),
              child: Wrap(
                alignment: WrapAlignment.center,
                spacing: 20,
                runSpacing: 20,
                children: _currentValues.asMap().entries.map((entry) {
                  final index = entry.key;
                  final value = entry.value;
                  return _buildSingleDice(value, index);
                }).toList(),
              ),
            ),
          );
        },
      );
    },
  );
}

动画设计

  • 摇摆动画:水平摇摆模拟真实摇骰子
  • 缩放动画:投掷时骰子放大效果
  • 弹性曲线:使用elasticOut曲线增加真实感

2. 单个骰子绘制

根据骰子类型绘制不同的骰子面。

dart 复制代码
Widget _buildSingleDice(int value, int index) {
  return Container(
    width: 100,
    height: 100,
    decoration: BoxDecoration(
      color: _settings.diceColor,
      borderRadius: BorderRadius.circular(16),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.3),
          blurRadius: 8,
          offset: const Offset(0, 4),
        ),
      ],
      gradient: LinearGradient(
        colors: [
          _settings.diceColor,
          _settings.diceColor.withValues(alpha: 0.8),
        ],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: _isRolling
        ? const Center(
            child: Icon(Icons.refresh, color: Colors.white, size: 40),
          )
        : _buildDiceFace(value),
  );
}

3. D6骰子点数绘制

为标准六面骰子绘制传统点数。

dart 复制代码
Widget _buildD6Face(int value) {
  final dotPositions = _getDotPositions(value);
  
  return Stack(
    children: dotPositions.map((position) {
      return Positioned(
        left: position.dx,
        top: position.dy,
        child: Container(
          width: 12,
          height: 12,
          decoration: const BoxDecoration(
            color: Colors.white,
            shape: BoxShape.circle,
          ),
        ),
      );
    }).toList(),
  );
}

List<Offset> _getDotPositions(int value) {
  const double size = 100;
  const double dotSize = 12;
  const double margin = 20;
  
  switch (value) {
    case 1:
      return [Offset((size - dotSize) / 2, (size - dotSize) / 2)];
    case 2:
      return [
        Offset(margin, margin),
        Offset(size - margin - dotSize, size - margin - dotSize),
      ];
    case 3:
      return [
        Offset(margin, margin),
        Offset((size - dotSize) / 2, (size - dotSize) / 2),
        Offset(size - margin - dotSize, size - margin - dotSize),
      ];
    case 4:
      return [
        Offset(margin, margin),
        Offset(size - margin - dotSize, margin),
        Offset(margin, size - margin - dotSize),
        Offset(size - margin - dotSize, size - margin - dotSize),
      ];
    case 5:
      return [
        Offset(margin, margin),
        Offset(size - margin - dotSize, margin),
        Offset((size - dotSize) / 2, (size - dotSize) / 2),
        Offset(margin, size - margin - dotSize),
        Offset(size - margin - dotSize, size - margin - dotSize),
      ];
    case 6:
      return [
        Offset(margin, margin),
        Offset(size - margin - dotSize, margin),
        Offset(margin, (size - dotSize) / 2),
        Offset(size - margin - dotSize, (size - dotSize) / 2),
        Offset(margin, size - margin - dotSize),
        Offset(size - margin - dotSize, size - margin - dotSize),
      ];
    default:
      return [];
  }
}

4. 投掷逻辑实现

核心的骰子投掷功能,包含动画和反馈。

dart 复制代码
Future<void> _rollDice() async {
  if (_isRolling) return;
  
  setState(() {
    _isRolling = true;
  });
  
  // 播放动画
  if (_settings.animationEnabled) {
    _shakeController.forward().then((_) => _shakeController.reverse());
    await _rollController.forward();
  }
  
  // 生成随机结果
  final random = Random();
  final maxValue = _diceTypes[_settings.diceType]!;
  
  setState(() {
    _currentValues = List.generate(_settings.diceCount, (index) {
      return random.nextInt(maxValue) + 1;
    });
  });
  
  // 添加到历史记录
  final total = _currentValues.reduce((a, b) => a + b);
  final result = DiceResult(
    value: total,
    timestamp: DateTime.now(),
    diceType: _settings.diceType,
    diceCount: _settings.diceCount,
  );
  
  setState(() {
    _rollHistory.add(result);
    _totalRolls++;
    _isRolling = false;
  });
  
  _updateFrequency();
  
  // 触觉反馈
  if (_settings.vibrationEnabled) {
    HapticFeedback.mediumImpact();
  }
  
  // 重置动画
  if (_settings.animationEnabled) {
    _rollController.reset();
  }
}

投掷流程

  1. 检查是否正在投掷
  2. 播放摇摆和缩放动画
  3. 生成随机数结果
  4. 更新界面显示
  5. 保存历史记录
  6. 提供触觉反馈
  7. 重置动画状态

5. 动画控制器初始化

设置投掷和摇摆动画的控制器。

dart 复制代码
@override
void initState() {
  super.initState();
  
  // 初始化动画控制器
  _rollController = AnimationController(
    duration: const Duration(milliseconds: 1500),
    vsync: this,
  );
  
  _shakeController = AnimationController(
    duration: const Duration(milliseconds: 500),
    vsync: this,
  );
  
  _rollAnimation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(parent: _rollController, curve: Curves.elasticOut),
  );
  
  _shakeAnimation = Tween<double>(begin: -10, end: 10).animate(
    CurvedAnimation(parent: _shakeController, curve: Curves.elasticInOut),
  );
  
  _initializeDice();
}

6. 投掷结果显示

实时显示当前投掷的统计信息。

dart 复制代码
Widget _buildCurrentResult() {
  if (_currentValues.isEmpty) return const SizedBox.shrink();
  
  final total = _currentValues.reduce((a, b) => a + b);
  final average = total / _currentValues.length;
  
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.grey.shade50,
      border: Border(top: BorderSide(color: Colors.grey.shade300)),
    ),
    child: Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            _buildResultItem('总和', '$total', Icons.add, Colors.blue),
            _buildResultItem('平均', average.toStringAsFixed(1), Icons.trending_up, Colors.green),
            _buildResultItem('最大', '${_currentValues.reduce(max)}', Icons.keyboard_arrow_up, Colors.red),
            _buildResultItem('最小', '${_currentValues.reduce(min)}', Icons.keyboard_arrow_down, Colors.orange),
          ],
        ),
        if (_currentValues.length > 1) ...[
          const SizedBox(height: 12),
          Text(
            '各骰子结果: ${_currentValues.join(', ')}',
            style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
          ),
        ],
      ],
    ),
  );
}

7. 历史记录管理

保存和显示所有投掷历史。

dart 复制代码
Widget _buildHistoryItem(DiceResult result, int index) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: Colors.red.shade100,
              borderRadius: BorderRadius.circular(25),
            ),
            child: Center(
              child: Text(
                '${result.value}',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.red.shade700,
                ),
              ),
            ),
          ),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    Text(
                      '${result.diceType} × ${result.diceCount}',
                      style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ),
                    const Spacer(),
                    Text(
                      _formatTime(result.timestamp),
                      style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
                    ),
                  ],
                ),
                const SizedBox(height: 4),
                Row(
                  children: [
                    _buildHistoryTag('结果', '${result.value}', Icons.casino, Colors.red),
                    const SizedBox(width: 8),
                    _buildHistoryTag('类型', result.diceType, Icons.category, Colors.blue),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    ),
  );
}

8. 统计分析功能

提供详细的投掷数据分析。

dart 复制代码
Widget _buildOverallStats() {
  if (_rollHistory.isEmpty) return const SizedBox.shrink();
  
  final allValues = _rollHistory.map((r) => r.value).toList();
  final total = allValues.reduce((a, b) => a + b);
  final average = total / allValues.length;
  final maxValue = allValues.reduce(max);
  final minValue = allValues.reduce(min);
  
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.assessment, color: Colors.blue.shade600),
              const SizedBox(width: 8),
              const Text('总体统计', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 16),
          Row(
            children: [
              Expanded(child: _buildStatCard('总投掷', '$_totalRolls', '次', Icons.refresh, Colors.blue)),
              const SizedBox(width: 12),
              Expanded(child: _buildStatCard('平均值', average.toStringAsFixed(1), '', Icons.trending_up, Colors.green)),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(child: _buildStatCard('最大值', '$maxValue', '', Icons.keyboard_arrow_up, Colors.red)),
              const SizedBox(width: 12),
              Expanded(child: _buildStatCard('最小值', '$minValue', '', Icons.keyboard_arrow_down, Colors.orange)),
            ],
          ),
        ],
      ),
    ),
  );
}

9. 频率分析图表

显示各个结果的出现频率。

dart 复制代码
Widget _buildFrequencyChart() {
  if (_valueFrequency.isEmpty) return const SizedBox.shrink();
  
  final maxFreq = _valueFrequency.values.reduce(max);
  final sortedEntries = _valueFrequency.entries.toList()
    ..sort((a, b) => a.key.compareTo(b.key));
  
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Icon(Icons.bar_chart, color: Colors.green.shade600),
              const SizedBox(width: 8),
              const Text('结果频率', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            ],
          ),
          const SizedBox(height: 16),
          ...sortedEntries.map((entry) {
            final percentage = entry.value / maxFreq;
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 4),
              child: Column(
                children: [
                  Row(
                    children: [
                      SizedBox(
                        width: 30,
                        child: Text('${entry.key}', style: const TextStyle(fontWeight: FontWeight.bold)),
                      ),
                      Expanded(
                        child: LinearProgressIndicator(
                          value: percentage,
                          backgroundColor: Colors.grey.shade200,
                          valueColor: AlwaysStoppedAnimation(Colors.green.shade400),
                        ),
                      ),
                      const SizedBox(width: 8),
                      Text('${entry.value}次', style: const TextStyle(fontSize: 12)),
                    ],
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    ),
  );
}

10. 设置页面实现

提供丰富的个性化设置选项。

dart 复制代码
Widget _buildSettingsPage() {
  return Column(
    children: [
      _buildSettingsHeader(),
      Expanded(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            // 骰子设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.casino, color: Colors.red.shade600),
                        const SizedBox(width: 8),
                        const Text('骰子设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    const Text('骰子类型', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: _diceTypes.keys.map((type) {
                        final isSelected = type == _settings.diceType;
                        return FilterChip(
                          label: Text('$type (${_diceTypes[type]}面)'),
                          selected: isSelected,
                          onSelected: (selected) {
                            if (selected) {
                              setState(() {
                                _settings = _settings.copyWith(diceType: type);
                                _initializeDice();
                              });
                            }
                          },
                        );
                      }).toList(),
                    ),
                    const SizedBox(height: 16),
                    Text('骰子数量: ${_settings.diceCount}'),
                    Slider(
                      value: _settings.diceCount.toDouble(),
                      min: 1, max: 6, divisions: 5,
                      label: '${_settings.diceCount}个',
                      onChanged: (value) {
                        setState(() {
                          _settings = _settings.copyWith(diceCount: value.toInt());
                          _initializeDice();
                        });
                      },
                    ),
                  ],
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            // 外观设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.palette, color: Colors.green.shade600),
                        const SizedBox(width: 8),
                        const Text('外观设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    const Text('骰子颜色', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      children: [Colors.red, Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal, Colors.indigo, Colors.brown].map((color) {
                        final isSelected = color.value == _settings.diceColor.value;
                        return GestureDetector(
                          onTap: () { setState(() { _settings = _settings.copyWith(diceColor: color); }); },
                          child: Container(
                            width: 40, height: 40,
                            decoration: BoxDecoration(
                              color: color, shape: BoxShape.circle,
                              border: isSelected ? Border.all(color: Colors.black, width: 3) : null,
                            ),
                            child: isSelected ? const Icon(Icons.check, color: Colors.white) : null,
                          ),
                        );
                      }).toList(),
                    ),
                  ],
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            // 反馈设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.feedback, color: Colors.orange.shade600),
                        const SizedBox(width: 8),
                        const Text('反馈设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    SwitchListTile(
                      title: const Text('声音效果'),
                      subtitle: const Text('投掷时播放声音'),
                      value: _settings.soundEnabled,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(soundEnabled: value); }); },
                    ),
                    SwitchListTile(
                      title: const Text('震动反馈'),
                      subtitle: const Text('投掷时提供触觉反馈'),
                      value: _settings.vibrationEnabled,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(vibrationEnabled: value); }); },
                    ),
                    SwitchListTile(
                      title: const Text('动画效果'),
                      subtitle: const Text('启用投掷动画'),
                      value: _settings.animationEnabled,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(animationEnabled: value); }); },
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    ],
  );
}

UI组件设计

1. 渐变头部组件

dart 复制代码
Widget _buildDiceHeader() {
  return Container(
    padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.red.shade600, Colors.red.shade400],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: Column(
      children: [
        Row(
          children: [
            const Icon(Icons.casino, color: Colors.white, size: 32),
            const SizedBox(width: 12),
            const Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('虚拟骰子', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white)),
                  Text('摇一摇或点击投掷骰子', style: TextStyle(fontSize: 14, color: Colors.white70)),
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
              child: Text(_settings.diceType, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
            ),
          ],
        ),
        const SizedBox(height: 16),
        Row(
          children: [
            Expanded(child: _buildHeaderCard('骰子数量', '${_settings.diceCount}个', Icons.casino)),
            const SizedBox(width: 12),
            Expanded(child: _buildHeaderCard('投掷次数', '$_totalRolls', Icons.refresh)),
          ],
        ),
      ],
    ),
  );
}

2. 投掷按钮

dart 复制代码
Widget _buildRollButton() {
  return GestureDetector(
    onTap: _isRolling ? null : _rollDice,
    child: AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      width: 120, height: 120,
      decoration: BoxDecoration(
        color: _isRolling ? Colors.grey : Colors.orange,
        shape: BoxShape.circle,
        boxShadow: [
          BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 12, offset: const Offset(0, 6)),
        ],
      ),
      child: Center(
        child: _isRolling
            ? const CircularProgressIndicator(color: Colors.white)
            : const Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.casino, color: Colors.white, size: 40),
                  SizedBox(height: 4),
                  Text('投掷', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
                ],
              ),
      ),
    ),
  );
}

3. 统计卡片

dart 复制代码
Widget _buildStatCard(String label, String value, String unit, IconData icon, Color color) {
  return Container(
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12)),
    child: Column(
      children: [
        Icon(icon, color: color, size: 24),
        const SizedBox(height: 8),
        Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color)),
        Text('$label$unit', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
      ],
    ),
  );
}
dart 复制代码
NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) { setState(() { _selectedIndex = index; }); },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.casino_outlined), selectedIcon: Icon(Icons.casino), label: '骰子'),
    NavigationDestination(icon: Icon(Icons.history_outlined), selectedIcon: Icon(Icons.history), label: '历史'),
    NavigationDestination(icon: Icon(Icons.analytics_outlined), selectedIcon: Icon(Icons.analytics), label: '统计'),
    NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: '设置'),
  ],
)

功能扩展建议

1. 3D骰子渲染

dart 复制代码
class Dice3DRenderer {
  // 使用Flutter的3D变换实现立体骰子
  Widget build3DDice(int value, Color color, double rotationX, double rotationY) {
    return Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // 透视效果
        ..rotateX(rotationX)
        ..rotateY(rotationY),
      child: Container(
        width: 100, height: 100,
        decoration: BoxDecoration(
          color: color,
          borderRadius: BorderRadius.circular(8),
          boxShadow: [
            BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 10, offset: const Offset(5, 5)),
          ],
        ),
        child: _buildDiceFace(value),
      ),
    );
  }
  
  // 3D旋转动画
  void animate3DRoll() {
    final random = Random();
    final rotationXController = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    final rotationYController = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    
    final rotationXAnimation = Tween<double>(
      begin: 0,
      end: random.nextDouble() * 4 * pi,
    ).animate(CurvedAnimation(parent: rotationXController, curve: Curves.elasticOut));
    
    final rotationYAnimation = Tween<double>(
      begin: 0,
      end: random.nextDouble() * 4 * pi,
    ).animate(CurvedAnimation(parent: rotationYController, curve: Curves.elasticOut));
    
    rotationXController.forward();
    rotationYController.forward();
  }
}

2. 物理引擎集成

dart 复制代码
class PhysicsDiceSimulation {
  // 使用物理引擎模拟真实骰子投掷
  void simulatePhysicsRoll() {
    final world = World(Vector2(0, 9.8)); // 重力
    
    // 创建骰子刚体
    final diceBody = Body()
      ..position = Vector2(0, -5)
      ..angularVelocity = (Random().nextDouble() - 0.5) * 10
      ..linearVelocity = Vector2((Random().nextDouble() - 0.5) * 5, -2);
    
    // 添加碰撞检测
    final shape = PolygonShape()..setAsBox(0.5, 0.5);
    final fixtureDef = FixtureDef(shape)
      ..density = 1.0
      ..friction = 0.3
      ..restitution = 0.6;
    
    diceBody.createFixture(fixtureDef);
    world.createBody(diceBody);
    
    // 模拟物理步进
    Timer.periodic(const Duration(milliseconds: 16), (timer) {
      world.step(1/60, 10, 10);
      
      // 更新骰子位置和旋转
      _updateDiceTransform(diceBody.position, diceBody.angle);
      
      // 检查是否静止
      if (diceBody.linearVelocity.length < 0.1 && diceBody.angularVelocity.abs() < 0.1) {
        timer.cancel();
        _finalizeDiceResult();
      }
    });
  }
}

3. 声音效果系统

dart 复制代码
class DiceSoundSystem {
  late AudioPlayer _audioPlayer;
  
  void initializeSounds() {
    _audioPlayer = AudioPlayer();
  }
  
  // 投掷声音
  Future<void> playRollSound() async {
    await _audioPlayer.play(AssetSource('sounds/dice_roll.mp3'));
  }
  
  // 碰撞声音
  Future<void> playBounceSound() async {
    await _audioPlayer.play(AssetSource('sounds/dice_bounce.mp3'));
  }
  
  // 结果声音
  Future<void> playResultSound(int value) async {
    if (value == 1) {
      await _audioPlayer.play(AssetSource('sounds/low_result.mp3'));
    } else if (value >= 6) {
      await _audioPlayer.play(AssetSource('sounds/high_result.mp3'));
    } else {
      await _audioPlayer.play(AssetSource('sounds/normal_result.mp3'));
    }
  }
  
  // 音效设置
  Widget buildSoundSettings() {
    return Card(
      child: Column(
        children: [
          SwitchListTile(
            title: const Text('投掷音效'),
            value: _rollSoundEnabled,
            onChanged: (value) => setState(() => _rollSoundEnabled = value),
          ),
          SwitchListTile(
            title: const Text('碰撞音效'),
            value: _bounceSoundEnabled,
            onChanged: (value) => setState(() => _bounceSoundEnabled = value),
          ),
          ListTile(
            title: const Text('音量'),
            subtitle: Slider(
              value: _soundVolume,
              onChanged: (value) {
                setState(() => _soundVolume = value);
                _audioPlayer.setVolume(value);
              },
            ),
          ),
        ],
      ),
    );
  }
}

4. 多人游戏模式

dart 复制代码
class MultiplayerDiceGame {
  List<Player> players = [];
  int currentPlayerIndex = 0;
  
  // 添加玩家
  void addPlayer(String name) {
    players.add(Player(name: name, id: players.length + 1));
  }
  
  // 轮流投掷
  Widget buildPlayerTurnIndicator() {
    final currentPlayer = players[currentPlayerIndex];
    
    return Card(
      color: Colors.blue.shade50,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              backgroundColor: Colors.blue,
              child: Text('${currentPlayer.id}', style: const TextStyle(color: Colors.white)),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('当前玩家', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
                  Text(currentPlayer.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                ],
              ),
            ),
            ElevatedButton(
              onPressed: _rollForCurrentPlayer,
              child: const Text('投掷'),
            ),
          ],
        ),
      ),
    );
  }
  
  // 玩家投掷
  void _rollForCurrentPlayer() async {
    final result = await _rollDice();
    players[currentPlayerIndex].addScore(result);
    
    // 切换到下一个玩家
    currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
    
    // 检查游戏结束条件
    _checkGameEnd();
  }
  
  // 排行榜
  Widget buildLeaderboard() {
    final sortedPlayers = List<Player>.from(players)
      ..sort((a, b) => b.totalScore.compareTo(a.totalScore));
    
    return Card(
      child: Column(
        children: [
          const Text('排行榜', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          ...sortedPlayers.asMap().entries.map((entry) {
            final rank = entry.key + 1;
            final player = entry.value;
            
            return ListTile(
              leading: CircleAvatar(
                backgroundColor: rank == 1 ? Colors.gold : (rank == 2 ? Colors.silver : Colors.bronze),
                child: Text('$rank'),
              ),
              title: Text(player.name),
              trailing: Text('${player.totalScore}分', style: const TextStyle(fontWeight: FontWeight.bold)),
            );
          }),
        ],
      ),
    );
  }
}

class Player {
  final String name;
  final int id;
  List<int> scores = [];
  
  Player({required this.name, required this.id});
  
  int get totalScore => scores.fold(0, (sum, score) => sum + score);
  double get averageScore => scores.isEmpty ? 0 : totalScore / scores.length;
  
  void addScore(int score) {
    scores.add(score);
  }
}

5. 自定义骰子设计器

dart 复制代码
class CustomDiceDesigner {
  // 自定义骰子面
  Widget buildDiceFaceEditor(int faceIndex) {
    return Card(
      child: Column(
        children: [
          Text('第${faceIndex + 1}面设计'),
          Container(
            width: 150, height: 150,
            decoration: BoxDecoration(
              border: Border.all(color: Colors.grey),
              borderRadius: BorderRadius.circular(8),
            ),
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
              itemCount: 9,
              itemBuilder: (context, index) {
                return GestureDetector(
                  onTap: () => _toggleDot(faceIndex, index),
                  child: Container(
                    margin: const EdgeInsets.all(2),
                    decoration: BoxDecoration(
                      color: _isDotActive(faceIndex, index) ? Colors.black : Colors.transparent,
                      shape: BoxShape.circle,
                    ),
                  ),
                );
              },
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(onPressed: () => _clearFace(faceIndex), child: const Text('清空')),
              ElevatedButton(onPressed: () => _randomizeFace(faceIndex), child: const Text('随机')),
            ],
          ),
        ],
      ),
    );
  }
  
  // 保存自定义骰子
  void saveCustomDice(String name, List<List<bool>> facePatterns) {
    final customDice = CustomDice(
      name: name,
      faces: facePatterns,
      createdAt: DateTime.now(),
    );
    
    // 保存到本地存储
    _saveToStorage(customDice);
  }
  
  // 自定义骰子库
  Widget buildCustomDiceLibrary() {
    return ListView.builder(
      itemCount: _customDiceList.length,
      itemBuilder: (context, index) {
        final dice = _customDiceList[index];
        return Card(
          child: ListTile(
            leading: _buildMiniDicePreview(dice),
            title: Text(dice.name),
            subtitle: Text('创建于 ${_formatDate(dice.createdAt)}'),
            trailing: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                IconButton(onPressed: () => _useDice(dice), icon: const Icon(Icons.play_arrow)),
                IconButton(onPressed: () => _editDice(dice), icon: const Icon(Icons.edit)),
                IconButton(onPressed: () => _deleteDice(dice), icon: const Icon(Icons.delete)),
              ],
            ),
          ),
        );
      },
    );
  }
}

6. 游戏模式扩展

dart 复制代码
class DiceGameModes {
  // 猜大小游戏
  Widget buildGuessGame() {
    return Card(
      child: Column(
        children: [
          const Text('猜大小', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: () => _makeGuess('small'),
                child: const Text('小 (1-3)'),
              ),
              ElevatedButton(
                onPressed: () => _makeGuess('big'),
                child: const Text('大 (4-6)'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Text('连胜: $_winStreak 次', style: const TextStyle(fontWeight: FontWeight.bold)),
          Text('胜率: ${(_winRate * 100).toStringAsFixed(1)}%'),
        ],
      ),
    );
  }
  
  // 点数累积游戏
  Widget buildAccumulationGame() {
    return Card(
      child: Column(
        children: [
          const Text('点数累积', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          const SizedBox(height: 16),
          Text('目标: $_targetScore 分', style: const TextStyle(fontSize: 16)),
          Text('当前: $_currentScore 分', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.blue)),
          const SizedBox(height: 16),
          LinearProgressIndicator(
            value: _currentScore / _targetScore,
            backgroundColor: Colors.grey.shade300,
            valueColor: const AlwaysStoppedAnimation(Colors.blue),
          ),
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(onPressed: _rollForAccumulation, child: const Text('投掷')),
              ElevatedButton(onPressed: _resetAccumulation, child: const Text('重置')),
            ],
          ),
        ],
      ),
    );
  }
  
  // 幸运数字游戏
  Widget buildLuckyNumberGame() {
    return Card(
      child: Column(
        children: [
          const Text('幸运数字', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          const SizedBox(height: 16),
          Text('今日幸运数字: $_luckyNumber', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.gold)),
          const SizedBox(height: 16),
          Text('投掷 $_luckyNumber 的次数: $_luckyHits'),
          Text('幸运指数: ${(_luckyHits / max(_totalRolls, 1) * 100).toStringAsFixed(1)}%'),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _generateNewLuckyNumber,
            child: const Text('生成新幸运数字'),
          ),
        ],
      ),
    );
  }
}

7. 数据导出与分享

dart 复制代码
class DiceDataManager {
  // 导出统计数据
  Future<void> exportStatistics() async {
    final data = {
      'totalRolls': _totalRolls,
      'rollHistory': _rollHistory.map((r) => r.toJson()).toList(),
      'valueFrequency': _valueFrequency,
      'settings': _settings.toJson(),
      'exportDate': DateTime.now().toIso8601String(),
    };
    
    final jsonString = jsonEncode(data);
    
    // 保存到文件
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/dice_statistics.json');
    await file.writeAsString(jsonString);
    
    // 分享文件
    await Share.shareFiles([file.path], text: '我的骰子统计数据');
  }
  
  // 生成统计报告
  Future<void> generateReport() async {
    final pdf = pw.Document();
    
    pdf.addPage(
      pw.Page(
        build: (pw.Context context) {
          return pw.Column(
            crossAxisAlignment: pw.CrossAxisAlignment.start,
            children: [
              pw.Text('骰子投掷统计报告', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
              pw.SizedBox(height: 20),
              pw.Text('生成时间: ${DateTime.now().toString()}'),
              pw.SizedBox(height: 20),
              
              // 基本统计
              pw.Text('基本统计', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
              pw.Text('总投掷次数: $_totalRolls'),
              pw.Text('平均值: ${_calculateAverage().toStringAsFixed(2)}'),
              pw.Text('最大值: ${_getMaxValue()}'),
              pw.Text('最小值: ${_getMinValue()}'),
              
              pw.SizedBox(height: 20),
              
              // 频率分析
              pw.Text('结果频率', style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
              ...._valueFrequency.entries.map((entry) {
                return pw.Text('${entry.key}: ${entry.value}次 (${(entry.value / _totalRolls * 100).toStringAsFixed(1)}%)');
              }),
            ],
          );
        },
      ),
    );
    
    final bytes = await pdf.save();
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/dice_report.pdf');
    await file.writeAsBytes(bytes);
    
    await Share.shareFiles([file.path], text: '骰子统计报告');
  }
  
  // 社交分享
  void shareResult(DiceResult result) {
    final text = '我刚刚投掷了${result.diceType}骰子,结果是${result.value}!'
        '\n\n使用虚拟骰子应用,体验真实的投掷乐趣!';
    
    Share.share(text);
  }
  
  // 成就系统
  Widget buildAchievements() {
    final achievements = [
      Achievement('初次投掷', '完成第一次投掷', _totalRolls >= 1),
      Achievement('投掷新手', '完成10次投掷', _totalRolls >= 10),
      Achievement('投掷专家', '完成100次投掷', _totalRolls >= 100),
      Achievement('投掷大师', '完成1000次投掷', _totalRolls >= 1000),
      Achievement('幸运儿', '连续投出3个6', _checkLuckyStreak()),
      Achievement('收集家', '使用过所有类型的骰子', _checkAllDiceTypes()),
    ];
    
    return ListView.builder(
      itemCount: achievements.length,
      itemBuilder: (context, index) {
        final achievement = achievements[index];
        return Card(
          child: ListTile(
            leading: Icon(
              achievement.isUnlocked ? Icons.star : Icons.star_border,
              color: achievement.isUnlocked ? Colors.gold : Colors.grey,
            ),
            title: Text(achievement.title),
            subtitle: Text(achievement.description),
            trailing: achievement.isUnlocked 
                ? const Icon(Icons.check, color: Colors.green)
                : null,
          ),
        );
      },
    );
  }
}

class Achievement {
  final String title;
  final String description;
  final bool isUnlocked;
  
  Achievement(this.title, this.description, this.isUnlocked);
}

8. AR增强现实模式

dart 复制代码
class ARDiceMode {
  // AR相机预览
  Widget buildARView() {
    return Stack(
      children: [
        CameraPreview(_cameraController),
        
        // AR骰子覆盖层
        Positioned.fill(
          child: CustomPaint(
            painter: ARDicePainter(_arDicePosition, _arDiceRotation),
          ),
        ),
        
        // AR控制界面
        Positioned(
          bottom: 50,
          left: 20,
          right: 20,
          child: _buildARControls(),
        ),
      ],
    );
  }
  
  // AR控制按钮
  Widget _buildARControls() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        FloatingActionButton(
          onPressed: _throwARDice,
          child: const Icon(Icons.casino),
        ),
        FloatingActionButton(
          onPressed: _resetARDice,
          child: const Icon(Icons.refresh),
        ),
        FloatingActionButton(
          onPressed: _captureARPhoto,
          child: const Icon(Icons.camera),
        ),
      ],
    );
  }
  
  // AR骰子投掷
  void _throwARDice() {
    // 使用ARCore/ARKit检测平面
    // 在检测到的平面上放置虚拟骰子
    // 应用物理引擎模拟投掷
  }
  
  // AR骰子绘制器
  class ARDicePainter extends CustomPainter {
    final Offset position;
    final double rotation;
    
    ARDicePainter(this.position, this.rotation);
    
    @override
    void paint(Canvas canvas, Size size) {
      // 绘制3D骰子在AR空间中
      final paint = Paint()
        ..color = Colors.red
        ..style = PaintingStyle.fill;
      
      // 应用3D变换
      canvas.save();
      canvas.translate(position.dx, position.dy);
      canvas.rotate(rotation);
      
      // 绘制骰子各个面
      _drawDiceFaces(canvas, paint);
      
      canvas.restore();
    }
    
    @override
    bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
  }
}

性能优化建议

1. 动画性能优化

dart 复制代码
class OptimizedAnimations {
  // 使用RepaintBoundary减少重绘
  Widget buildOptimizedDice() {
    return RepaintBoundary(
      child: AnimatedBuilder(
        animation: _rollAnimation,
        builder: (context, child) {
          return Transform.scale(
            scale: 1.0 + (_rollAnimation.value * 0.2),
            child: child,
          );
        },
        child: _buildStaticDiceContent(), // 静态内容不重复构建
      ),
    );
  }
  
  // 批量动画更新
  void batchAnimationUpdates() {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      // 批量更新所有动画状态
      setState(() {
        // 更新多个动画值
      });
    });
  }
}

2. 内存管理

dart 复制代码
class MemoryOptimizedDice {
  // 限制历史记录数量
  static const int maxHistorySize = 1000;
  
  void addToHistory(DiceResult result) {
    _rollHistory.add(result);
    
    if (_rollHistory.length > maxHistorySize) {
      _rollHistory.removeAt(0);
    }
  }
  
  @override
  void dispose() {
    _rollController.dispose();
    _shakeController.dispose();
    _rollHistory.clear();
    _valueFrequency.clear();
    super.dispose();
  }
}

测试建议

1. 单元测试

dart 复制代码
// test/dice_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:dice_app/models/dice_result.dart';

void main() {
  group('DiceResult Tests', () {
    test('should create dice result correctly', () {
      final result = DiceResult(
        value: 6,
        timestamp: DateTime.now(),
        diceType: 'D6',
        diceCount: 1,
      );
      
      expect(result.value, equals(6));
      expect(result.diceType, equals('D6'));
      expect(result.diceCount, equals(1));
    });
  });
  
  group('Dice Logic Tests', () {
    test('should generate valid random values', () {
      for (int i = 0; i < 100; i++) {
        final value = Random().nextInt(6) + 1;
        expect(value, greaterThanOrEqualTo(1));
        expect(value, lessThanOrEqualTo(6));
      }
    });
  });
}

2. Widget测试

dart 复制代码
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dice_app/main.dart';

void main() {
  group('Dice App Widget Tests', () {
    testWidgets('should display dice and roll button', (WidgetTester tester) async {
      await tester.pumpWidget(const DiceApp());
      
      expect(find.text('虚拟骰子'), findsOneWidget);
      expect(find.text('投掷'), findsOneWidget);
      expect(find.byType(NavigationBar), findsOneWidget);
    });
    
    testWidgets('should roll dice when button pressed', (WidgetTester tester) async {
      await tester.pumpWidget(const DiceApp());
      
      // 点击投掷按钮
      await tester.tap(find.text('投掷'));
      await tester.pumpAndSettle();
      
      // 验证结果显示
      expect(find.text('总和'), findsOneWidget);
    });
  });
}

部署指南

1. Android部署

bash 复制代码
# 构建APK
flutter build apk --release

# 构建App Bundle
flutter build appbundle --release

2. iOS部署

bash 复制代码
# 构建iOS应用
flutter build ios --release

3. 应用图标配置

yaml 复制代码
# pubspec.yaml
dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_icons:
  android: true
  ios: true
  image_path: "assets/icon/dice_icon.png"
  adaptive_icon_background: "#D32F2F"
  adaptive_icon_foreground: "assets/icon/dice_foreground.png"

项目总结

这个虚拟骰子应用展示了Flutter在游戏类应用开发中的强大能力。通过精美的动画效果、丰富的功能设置和详细的统计分析,为用户提供了完整的虚拟骰子体验。

技术亮点

  1. 流畅动画系统:摇摆和投掷动画增强真实感
  2. 多种骰子支持:从D4到D100的完整骰子类型
  3. 智能统计分析:频率分析和数据可视化
  4. 个性化设置:丰富的自定义选项
  5. 触觉反馈集成:震动和声音增强体验

学习价值

  • 动画控制器的高级应用
  • 自定义绘制技巧
  • 数据统计和可视化
  • 触觉反馈的使用
  • 游戏类应用的设计模式

这个项目为Flutter开发者提供了一个完整的游戏应用开发案例,涵盖了动画、交互、数据管理等多个方面,是学习Flutter游戏开发的优秀参考。


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

相关推荐
LawrenceLan2 小时前
Flutter 零基础入门(二十三):Icon、Image 与资源管理
开发语言·前端·flutter·dart
lbb 小魔仙2 小时前
【Harmonyos】开源鸿蒙跨平台训练营DAY2:多终端工程创建运行、代码提交至AtomGit平台自建公开仓库全流程(附带出现问题及解决方法)
windows·flutter·开源·harmonyos·鸿蒙·开源鸿蒙·鸿蒙开平台应用
AI_零食2 小时前
鸿蒙跨端框架Flutter学习day 2、常用UI组件-弹性布局进阶之道
学习·flutter·ui·华为·harmonyos·鸿蒙
AirDroid_cn2 小时前
鸿蒙NEXT:会议录音转文字,如何自动标记发言人?
华为·harmonyos
哈哈你是真的厉害2 小时前
基础入门 React Native 鸿蒙跨平台开发:实现AnimatedValueXY 双轴动画
react native·react.js·harmonyos
ujainu2 小时前
Flutter + HarmonyOS 前置知识:Dart语言详解(上)
flutter·华为·harmonyos·dart
前端不太难2 小时前
Flutter / RN / iOS 的状态策略,该如何取舍?
flutter·ios·状态模式
前端世界3 小时前
鸿蒙系统 IO 性能优化实战:从应用卡顿到 OTA 升级的完整解决方案
华为·性能优化·harmonyos
鸣弦artha3 小时前
Flutter框架跨平台鸿蒙开发——Icon组件基础
flutter