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(投掷结果模型)

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

设计要点

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

DiceSettings(骰子设置模型)

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. 骰子显示与动画

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

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. 单个骰子绘制

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

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骰子点数绘制

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

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. 投掷逻辑实现

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

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. 动画控制器初始化

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


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. 投掷结果显示

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

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. 历史记录管理

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

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. 统计分析功能

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

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. 频率分析图表

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

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. 设置页面实现

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

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. 渐变头部组件

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. 投掷按钮

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. 统计卡片

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)),
      ],
    ),
  );
}

4. NavigationBar底部导航

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骰子渲染

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. 物理引擎集成

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. 声音效果系统

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. 多人游戏模式

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. 自定义骰子设计器

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. 游戏模式扩展

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. 数据导出与分享

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增强现实模式

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);
    
    
    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();
    }
    
    
    bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
  }
}

性能优化建议

1. 动画性能优化

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. 内存管理

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

测试建议

1. 单元测试

// 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测试

// 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部署

# 构建APK
flutter build apk --release

# 构建App Bundle
flutter build appbundle --release

2. iOS部署

# 构建iOS应用
flutter build ios --release

3. 应用图标配置

# 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐