一、应用概述

骰子(Dice Roller) 是一款经典的休闲娱乐应用,模拟了真实掷骰子的过程。应用投掷两个骰子(六面标准骰),使用 Unicode 骰面符号直观展示每个骰子的点数,并伴有快速滚动的动画效果。每次掷骰后,应用会自动计算点数总和,并根据特殊的点数组合(如蛇眼、Lucky 7、双六)给出趣味描述。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Unicode 特殊符号渲染、animation 动画属性、setInterval 定时器、以及 get 计算属性等核心技术的综合运用。

对于鸿蒙开发者而言,骰子应用是一个展示符号渲染与动画技巧的绝佳案例——它让你在约85行代码中,同时掌握Unicode 符号映射定时器驱动动画条件描述逻辑统计追踪四大核心能力,这些能力广泛适用于游戏、抽奖、模拟器等场景。

1.1 功能特性

  • 双骰子投掷:模拟两个六面骰子的随机投掷
  • Unicode 骰面显示:使用 ⚀⚁⚂⚃⚄⚅ 符号直观展示点数
  • 滚动动画:60ms 间隔快速变化,模拟真实骰子滚动
  • 点数总和计算:自动计算两个骰子点数和
  • 趣味描述:特殊组合(2点/3点/7点/12点)触发趣味文案
  • 投掷统计:记录总共投掷次数
  • 动画锁定:动画进行中防止重复点击

1.2 适用场景

  • 桌游电子化辅助工具
  • 聚会活动抽奖环节
  • 概率统计教学演示
  • 儿童数学启蒙教育

二、技术架构

2.1 整体架构概览

┌─────────────────────────────────────────┐
│           数据层                          │
│  dice1 / dice2 (1~6 整数)                │
│  total (点数总和)                         │
│  isRolling (动画状态)                     │
│  rollCount (投掷统计)                     │
├─────────────────────────────────────────┤
│           状态管理层                       │
│  @State 驱动 UI 重新渲染                  │
├─────────────────────────────────────────┤
│           业务逻辑层                       │
│  roll()      —— 掷骰子 + 动画             │
│  diceFace()  —— 数字转 Unicode 符号       │
│  get d1/d2   —— 动画期间的实时随机值       │
│  get description —— 特殊组合描述           │
├─────────────────────────────────────────┤
│           UI 表示层                      │
│  两个骰子显示 / 点数总和 / 趣味描述 /      │
│  投掷统计 / 掷骰按钮                      │
└─────────────────────────────────────────┘

2.2 动画状态机

静止状态 (isRolling=false)
    │
    │  点击"掷骰子"
    ▼
动画状态 (isRolling=true)
    │
    │  setInterval(60ms) × 12次
    │  每次更新 dice1/dice2 为随机值
    │  UI 显示实时随机值
    ▼
判定状态 (count >= 12)
    │
    │  clearInterval
    │  isRolling = false
    │  计算 total / description
    │  rollCount++
    ▼
静止状态 (等待下次投掷)

三、核心代码深度分析

3.1 完整的页面源码

import { router } from '@kit.ArkUI';

@Entry
@Component
struct index25 {
  @State dice1: number = 1;
  @State dice2: number = 6;
  @State total: number = 7;
  @State isRolling: boolean = false;
  @State rollCount: number = 0;

  // 动画期间的实时随机值
  get d1(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice1; }
  get d2(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice2; }

  roll() {
    if (this.isRolling) return;
    this.isRolling = true;
    let count = 0;
    const intervalId = setInterval(() => {
      this.dice1 = Math.floor(Math.random() * 6) + 1;
      this.dice2 = Math.floor(Math.random() * 6) + 1;
      count++;
      if (count >= 12) {
        clearInterval(intervalId);
        this.isRolling = false;
        this.total = this.dice1 + this.dice2;
        this.rollCount++;
      }
    }, 60);
  }

  // 数字转 Unicode 骰面符号
  diceFace(val: number): string {
    const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
    return faces[val - 1];
  }

  // 趣味描述
  get description(): string {
    const t = this.total;
    if (t === 2) return '🐍 蛇眼!';
    if (t === 7) return '🎯 Lucky 7!';
    if (t === 12) return '🎉 双六!';
    if (t === 3) return '😅 霉运';
    return '';
  }

  build() {
    Column() {
      Row() {
        Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
        Text('🎲 骰子').fontSize(18).fontWeight(FontWeight.Bold)
        Blank()
      }.width('92%').padding({ top: 16, bottom: 8 })

      Column() {
        // 双骰子展示
        Row({ space: 20 }) {
          Column() {
            Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))
              .fontSize(80).animation({ duration: 60 })
            Text('骰子 1').fontSize(14).fontColor('#888')
          }
          Column() {
            Text(this.diceFace(this.isRolling ? this.d2 : this.dice2))
              .fontSize(80).animation({ duration: 60 })
            Text('骰子 2').fontSize(14).fontColor('#888')
          }
        }.margin({ bottom: 16 })

        // 点数显示
        Text(`点数: ${this.isRolling ? '...' : this.total}`)
          .fontSize(36).fontWeight(FontWeight.Bold).fontColor('#F44336')

        // 趣味描述
        Text(this.description).fontSize(18).fontColor('#FF9800').margin({ top: 4 })

        // 投掷统计
        Text(`已掷 ${this.rollCount} 次`).fontSize(14).fontColor('#888').margin({ top: 8 })

        // 掷骰按钮
        Button('🎲 掷骰子').width(200).height(56).borderRadius(28)
          .backgroundColor('#4CAF50').fontSize(22).fontWeight(FontWeight.Bold)
          .margin({ top: 16 }).onClick(() => this.roll())
      }
      .layoutWeight(1).justifyContent(FlexAlign.Center)
    }
    .width('100%').height('100%').backgroundColor('#f8f8f8')
  }
}

3.2 Unicode 骰面符号映射

diceFace(val: number): string {
  const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
  return faces[val - 1];
}

这是本应用最独特的设计——使用 Unicode 的弥生骰子符号(Miscellaneous Symbols)来展示骰面:

数字 Unicode 字符 描述
1 U+2680 白色骰子1
2 U+2681 白色骰子2
3 U+2682 白色骰子3
4 U+2683 白色骰子4
5 U+2684 白色骰子5
6 U+2685 白色骰子6

这些 Unicode 字符在支持它们的字体中会渲染为带点阵的骰面图案,无需使用图片资源即可获得良好的视觉效果。这种纯文本符号的方案比图片方案具有以下优势:

  • 无资源依赖:不需要额外加载图片,减小应用体积
  • 矢量缩放:任意放大倍数下保持清晰,无锯齿
  • 主题适配:颜色可以通过 CSS 属性自由控制

3.3 动画期间实时随机值

get d1(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice1; }
get d2(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice2; }

这是一个精妙的设计——在动画期间,每次 UI 渲染时都实时生成新的随机值,而非等待 setInterval 更新 @State 变量。这样做的好处:

  1. 更高的刷新率:脱离 60ms 定时器的限制,随 UI 渲染帧率同步更新
  2. 更流畅的动画:渲染引擎每帧都看到新值,视觉上更加流畅
  3. 资源节约:无需额外的定时器管理

动画结束后的判定逻辑:

if (count >= 12) {
  clearInterval(intervalId);
  this.isRolling = false;  // 停止实时随机
  this.total = this.dice1 + this.dice2;
  this.rollCount++;
}

isRolling 设为 false 后,get d1() 返回固定的 this.dice1 值(即 setInterval 最后一次设置的值),骰子显示定格。

3.4 animation 动画属性

Text(this.diceFace(...))
  .fontSize(80).animation({ duration: 60 })

animation() 是 HarmonyOS 中用于为组件添加过渡动画的属性方法。它的参数是一个 AnimationOptions 对象,包含以下常用属性:

  • duration:动画持续时间(毫秒),默认1000
  • curve:动画曲线(Curve.EaseInOut 等),默认 Curve.Ease
  • delay:动画延迟开始的时间(毫秒)
  • iterations:动画重复次数,-1表示无限循环
  • playMode:播放模式(PlayMode.Normal / PlayMode.Reverse / …)

在本应用中,animation({ duration: 60 }) 设置为60ms的过渡动画。当骰子符号从 ⚀ 变为 ⚅ 时,这个过渡动画让变化过程更加平滑,而非生硬的瞬间切换。

需要注意的是,这里的 animation 属性作用于组件属性变化时的过渡效果。当 diceFace() 的返回值变化时(文字内容变化),动画会尝试在变化前后之间进行过渡。但对于文字内容变化,过渡效果可能不如数值属性(如 widthopacityrotate 等)明显。

3.5 趣味描述逻辑

get description(): string {
  const t = this.total;
  if (t === 2) return '🐍 蛇眼!';    // 两个1点
  if (t === 7) return '🎯 Lucky 7!';  // 最常见的点数
  if (t === 12) return '🎉 双六!';    // 两个6点
  if (t === 3) return '😅 霉运';      // 1+2 最小非蛇眼组合
  return '';                            // 其他组合不显示描述
}

四种特殊组合的判定规则:

  • 蛇眼(Snake Eyes):两个骰子都是1点,总和2 —— 骰子中最小的组合,名字源于两个1点看起来像蛇的眼睛
  • Lucky 7:总和7 —— 两个骰子点数和为7的概率最高(1/6),被视为幸运数字
  • 双六(Boxcars):两个骰子都是6点,总和12 —— 最大的组合
  • 霉运:总和3(1+2)—— 仅次于蛇眼的"坏运气"组合

其他的组合(4、5、6、8、9、10、11)不显示趣味描述,保持界面简洁。


四、HarmonyOS 特性深度解析

4.1 get 计算属性的实时求值机制

本应用中使用了三个 get 计算属性(d1d2description),它们在每次 UI 渲染时被重新求值。这种机制的关键理解是:

// 在模板中引用 get 属性
Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))

ArkTS 的渲染引擎在每次渲染周期中:

  1. 检测到 @State 变量变化(如 isRollingfalse 变为 true
  2. 触发重新渲染
  3. 重新求值所有模板表达式
  4. 计算 this.d1 → 调用 get d1() → 如果是动画状态则生成随机值
  5. 计算 this.diceFace(随机值) → 映射为 Unicode 符号
  6. 更新 UI 显示

这个链条中,get d1() 在动画期间每次渲染都生成不同的随机值,产生了持续的视觉变化。

4.2 setInterval 的正确清理时机

roll() {
  if (this.isRolling) return;
  this.isRolling = true;
  let count = 0;
  const intervalId = setInterval(() => {
    // ...
    if (count >= 12) {
      clearInterval(intervalId);  // 在回调内部清理
      this.isRolling = false;
    }
  }, 60);
}

定时器的清理发生在回调函数内部(通过 count 计数达到阈值时)。这种自清理模式的好处:

  1. 确定性清理:清理时机明确且可控
  2. 无需外部干预:不需要在 aboutToDisappear 中额外清理
  3. 避免内存泄漏:定时器在完成任务后立即释放

相比而言,如果定时器是持续运行的(如时钟应用),则必须在 aboutToDisappear 中清理。

4.3 条件渲染的不同模式

本应用在三个地方使用了条件渲染,展示了三种不同的场景:

// 模式1:三元表达式 - 用于文字内容
Text(`点数: ${this.isRolling ? '...' : this.total}`)

// 模式2:三元表达式 - 用于 get 属性选择
Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))

// 模式3:get 属性条件返回 - 用于描述文字
get description(): string {
  if (...) return '...';
  return '';
}

这三种模式分别适用于:

  1. 内联条件:简单的文字内容切换
  2. 值选择器:根据状态选择不同的数据源
  3. 计算属性:复杂的多分支逻辑

4.4 绿色主题的按钮设计

Button('🎲 掷骰子').width(200).height(56).borderRadius(28)
  .backgroundColor('#4CAF50').fontSize(22).fontWeight(FontWeight.Bold)

按钮使用了 Material Design 的绿色(#4CAF50),在心理学上绿色代表"开始"和"行动"。大尺寸(200×56)、大圆角(28px)和大字体(22px)的设计让按钮成为页面中最重要的视觉焦点,吸引用户点击。


五、UI/UX 设计模式

5.1 页面布局结构

┌─────────────────────────────────┐
│  Row: 导航栏                      │
├─────────────────────────────────┤
│  Column: 内容区 (居中)            │
│    ├── Row: 两个骰子              │
│    │   ├── 骰子1 (⚀~⚅)          │
│    │   └── 骰子2 (⚀~⚅)          │
│    ├── Text: 点数总和 (大号红色)   │
│    ├── Text: 趣味描述 (橙色)      │
│    ├── Text: 投掷次数 (灰色)      │
│    └── Button: "🎲 掷骰子"       │
└─────────────────────────────────┘

5.2 视觉层次与色彩搭配

元素 颜色 作用
骰子符号 默认(黑色) 核心视觉元素,80px超大字号
点数总和 #F44336 红色 最重要的结果信息
趣味描述 #FF9800 橙色 次要的趣味性信息
掷骰按钮 #4CAF50 绿色 行动召唤(CTA)
统计次数 #888 灰色 辅助信息

5.3 信息层级设计

信息按照重要性从上到下排列:

  1. 骰子符号(最大,最醒目)—— 核心视觉
  2. 点数总和(36px,红色)—— 核心数据
  3. 趣味描述(18px,橙色)—— 情感化反馈
  4. 投掷次数(14px,灰色)—— 辅助统计

每个层级之间通过 margin、字号和颜色差异形成清晰的区分。


六、最佳实践与经验总结

6.1 随机数的正确生成

Math.floor(Math.random() * 6) + 1

这是生成 1~6 范围内随机整数的标准写法。分解如下:

  • Math.random():生成 [0, 1) 区间的浮点数(含0,不含1)
  • * 6:扩展到 [0, 6) 区间
  • Math.floor():向下取整到 [0, 5] 整数
  • + 1:偏移到 [1, 6]

重要提示:在鸿蒙开发中,Math.random() 是伪随机数生成器,对于游戏类应用足够了。

6.2 动画期间的防重入

roll() {
  if (this.isRolling) return;
  this.isRolling = true;
  // ...
}

isRolling 标志位实现了按钮防重入。在动画进行中,即使用户多次点击"掷骰子"按钮,roll() 方法也会在入口处直接返回,不会启动新的动画循环。这是交互设计中防止意外操作的关键机制。

6.3 Unicode 符号的兼容性

Unicode 骰面符号(U+2680~U+2685)属于 Unicode 3.0 标准,在现代设备和操作系统中都有良好的支持。但在某些老旧设备或不支持这些符号的字体中,可能会显示为空白或方框。在生产环境中,建议:

  1. 提供降级方案:检测符号渲染情况,如果无法正常显示则使用数字替代
  2. 使用自定义字体:打包包含这些符号的字体文件
  3. 图片备选:在极端情况下回退到图片显示

6.4 代码组织建议

对于更复杂的骰子应用,可以考虑以下优化方向:

  1. 多种骰子类型:支持4面(D4)、8面(D8)、10面(D10)、12面(D12)、20面(D20)等角色扮演游戏常用骰子
  2. 投掷历史记录:保存最近几次投掷的结果
  3. 统计图表:展示点数分布的概率统计
  4. shake 传感器:利用设备加速度传感器,通过摇动手机触发掷骰
  5. 联机对战:通过分布式能力实现多人掷骰比大小

七、总结

骰子应用在约85行代码中,精彩展示了 ArkTS 声明式 UI 开发的多个核心概念:

  • Unicode 符号映射:使用 ⚀⚁⚂⚃⚄⚅ 纯文本方式渲染骰面
  • get 实时随机值:动画期间每次渲染生成新随机数
  • setInterval 定时器动画:60ms 间隔的快速滚动效果
  • animation 过渡动画:属性变化的平滑过渡
  • 防重入机制:isRolling 标志位的按钮锁定
  • 条件描述逻辑:特殊点数组合的趣味反馈

这个应用以"掷骰子"这一简单的游戏需求为切入点,展示了纯文本符号渲染和定时器动画的核心技术。它不仅是桌游爱好者的实用工具,更是学习 ArkTS 动画编程和符号处理的优秀教学案例。简洁的代码背后,蕴含着丰富的交互设计智慧和性能优化技巧。

Logo

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

更多推荐