HarmonyOS ArkTS 实战:实现一个掷骰子模拟器

HarmonyOS ArkTS 实战:实现一个掷骰子模拟器

项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的掷骰子模拟器。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现1-6个骰子选择、投掷动画、骰子点数显示、总和计算和投掷历史等功能。

最终运行效果如下:

页面主要包含以下内容:

  • 顶部应用标题;
  • 骰子显示区域,用圆点显示点数;
  • 投掷动画效果;
  • 骰子数量选择;
  • 大投掷按钮;
  • 点数总和显示;
  • 投掷历史记录;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个趣味小工具单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

前言

掷骰子是很多桌游和游戏中必不可少的环节,有时候手边没有骰子,手机上的骰子模拟器就很方便。一个好的骰子模拟器不仅能随机出点数,还要有投掷的动画效果,增加真实感和趣味性。

从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、动画效果、组件封装、随机数和列表渲染等内容。

本文基于 HarmonyOS 和 ArkTS 实现一个掷骰子模拟器。用户可以选择1-6个骰子,点击投掷按钮,骰子会有旋转动画,最后显示随机点数,计算总和,并记录投掷历史。

这个项目的核心不是简单的随机数,而是流畅的投掷动画和直观的骰子显示。每一次投掷,都有视觉反馈,让用户有真实掷骰子的感觉,这正是 ArkUI 动画的魅力所在。

一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 自定义骰子组件,用圆点显示点数;
  • 支持1-6个骰子选择;
  • 投掷时的旋转动画;
  • 随机生成1-6点数;
  • 自动计算点数总和;
  • 记录投掷历史;
  • 骰子点数用圆点图形表示;
  • 使用 @Builder 封装骰子面和历史项;
  • 完成一个可以运行的掷骰子模拟器页面。

这个项目虽然是单页面应用,但它实现了有趣的动画效果和完整的投掷功能,比普通静态页面更适合练习 ArkTS。

二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / Button / Column / Row / Flex / Circle
状态管理 @State
数据处理 随机数 / 动画效果 / 历史记录
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。

三、为什么选择掷骰子模拟器项目

掷骰子模拟器适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,趣味性强。掷骰子有随机性和动画效果,玩起来很有趣。

第二,视觉效果好。骰子用圆点表示点数,动画旋转,界面美观。

第三,适合练习自定义组件。骰子可以封装成独立组件,练习组件化思想。

第四,适合练习动画效果。投掷时的旋转动画是基础动画练习。

第五,适合练习随机数。随机生成1-6的点数,模拟真实骰子。

第六,扩展空间比较大。基础功能完成后,可以继续增加不同面数的骰子(4面/8面/10面/20面)、骰子皮肤、掷骰子声音、多轮游戏和统计分析。

在本项目中,骰子数量和每个骰子的点数是核心数据。所有显示和计算都基于这些数据。

四、功能规则说明

骰子数量:1-6个,默认2个。

点数范围:每个骰子1-6点,均匀随机。

动画效果:投掷时骰子旋转360度,持续500毫秒。

总和计算:自动计算所有骰子点数之和。

历史记录:最多记录最近20次投掷结果,显示每次的点数和总和。

骰子显示:白色圆角正方形,黑色圆点表示点数,标准骰子点数排列。

五、项目结构

本项目主要修改首页文件:

复制代码
entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和骰子逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

六、核心实现思路

本项目的核心流程如下:

  1. 使用 @State 保存骰子数量、每个骰子点数、是否正在动画;
  2. 用户选择骰子数量;
  3. 点击投掷按钮;
  4. 触发旋转动画;
  5. 动画期间快速切换点数制造滚动效果;
  6. 动画结束后确定最终点数;
  7. 用圆点图形显示每个骰子的点数;
  8. 计算点数总和;
  9. 将结果添加到历史记录;
  10. 点击历史可以查看详情。

项目中最重要的状态变量如下:

复制代码
@State diceCount: number = 2
@State diceValues: number[] = [1, 1]
@State isRolling: boolean = false
@State history: { values: number[], total: number }[] = []

其中:

状态变量 作用
diceCount 骰子数量
diceValues 每个骰子的点数
isRolling 是否正在投掷动画中
history 投掷历史记录

骰子点数数组是本项目中最重要的数据,所有显示都基于它。

七、Index.ets 完整代码

打开文件:

复制代码
entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

typescript 复制代码
@Entry
@Component
struct Index {
  @State diceCount: number = 2
  @State diceValues: number[] = [1, 1]
  @State isRolling: boolean = false
  @State history: { values: number[], total: number }[] = []
  @State rotation: number = 0

  private countOptions: number[] = [1, 2, 3, 4, 5, 6]

  private rollDice(): void {
    if (this.isRolling) return
    
    this.isRolling = true
    this.rotation = 0
    
    let rollInterval = setInterval(() => {
      this.diceValues = this.diceValues.map(() => {
        return Math.floor(Math.random() * 6) + 1
      })
    }, 80)

    animateTo({
      duration: 500,
      curve: Curve.EaseOut,
      onFinish: () => {
        clearInterval(rollInterval)
        this.isRolling = false
        let finalValues = this.diceValues.map(() => {
          return Math.floor(Math.random() * 6) + 1
        })
        this.diceValues = finalValues
        let total = finalValues.reduce((sum, v) => sum + v, 0)
        this.history = [{ values: [...finalValues], total }, ...this.history].slice(0, 20)
      }
    }, () => {
      this.rotation = 360
    })
  }

  private setDiceCount(count: number): void {
    if (this.isRolling) return
    this.diceCount = count
    this.diceValues = new Array(count).fill(1)
  }

  private getTotal(): number {
    return this.diceValues.reduce((sum, v) => sum + v, 0)
  }

  @Builder
  DiceFace(value: number) {
    Stack() {
      Column() {
        if (value >= 2) {
          Row() {
            Circle().width(12).height(12).fill('#1F2937')
            Blank()
            if (value >= 3) {
              Circle().width(12).height(12).fill('#1F2937')
            }
          }
          .width('100%')
          .layoutWeight(1)
        }
        if (value === 1 || value === 3 || value === 5) {
          Row() {
            Blank()
            Circle().width(12).height(12).fill('#1F2937')
            Blank()
          }
          .width('100%')
          .layoutWeight(1)
        }
        if (value >= 2) {
          Blank().layoutWeight(1)
          Row() {
            if (value >= 4) {
              Circle().width(12).height(12).fill('#1F2937')
            }
            Blank()
            Circle().width(12).height(12).fill('#1F2937')
          }
          .width('100%')
          .layoutWeight(1)
        }
      }
      .width('100%')
      .height('100%')
      .padding(16)
    }
    .width(80)
    .height(80)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({
      radius: 8,
      color: '#08000000',
      offsetX: 0,
      offsetY: 4
    })
    .rotate({ angle: this.rotation })
    .animation({ duration: 500, curve: Curve.EaseOut })
  }

  @Builder
  HistoryItem(item: { values: number[], total: number }, index: number) {
    Row() {
      Text(`#${this.history.length - index}`)
        .fontSize(14)
        .fontColor('#9CA3AF')
        .width(40)
      Text(item.values.join(' + '))
        .fontSize(15)
        .fontColor('#4B5563')
        .layoutWeight(1)
      Text(`= ${item.total}`)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0A59F7')
    }
    .width('100%')
    .padding({ top: 12, bottom: 12 })
    .border({ width: { bottom: 1 }, color: '#F3F4F6' })
  }

  build() {
    Column() {
      Text('掷骰子')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })
      Text('基于 HarmonyOS ArkTS 的骰子模拟器')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 20 })

      Column() {
        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
          ForEach(this.diceValues, (value: number, index: number) => {
            this.DiceFace(value)
              .margin({ right: 12, bottom: 12, left: index % 3 === 0 ? 12 : 0 })
          }, (v: number, i: number) => `${i}-${v}`)
        }
        .width('100%')
        .padding({ top: 24, bottom: 20 })

        Text(`点数总和:${this.getTotal()}`)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0A59F7')
          .margin({ bottom: 20 })

        Button(this.isRolling ? '投掷中...' : '🎲 投掷骰子')
          .width('100%')
          .height(56)
          .fontSize(18)
          .fontColor(Color.White)
          .backgroundColor(this.isRolling ? '#9CA3AF' : '#EF4444')
          .borderRadius(28)
          .enabled(!this.isRolling)
          .onClick(() => {
            this.rollDice()
          })
      }
      .width('100%')
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      Column() {
        Text('骰子数量')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 12 })
        
        Flex({ justifyContent: FlexAlign.SpaceBetween }) {
          ForEach(this.countOptions, (count: number) => {
            Button(`${count}个`)
              .width(48)
              .height(48)
              .fontSize(15)
              .fontColor(this.diceCount === count ? Color.White : '#4B5563')
              .backgroundColor(this.diceCount === count ? '#0A59F7' : '#F3F4F6')
              .borderRadius(24)
              .onClick(() => {
                this.setDiceCount(count)
              })
          }, (c: number) => c.toString())
        }
        .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(16)
      .margin({ bottom: 16 })
      .shadow({
        radius: 8,
        color: '#08000000',
        offsetX: 0,
        offsetY: 2
      })

      if (this.history.length > 0) {
        Column() {
          Text('投掷历史')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor('#182431')
            .width('100%')
            .margin({ bottom: 8 })
          
          ForEach(this.history, (item: { values: number[], total: number }, index: number) => {
            this.HistoryItem(item, index)
          }, (item: { values: number[], total: number }, i: number) => i.toString())
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(16)
        .shadow({
          radius: 8,
          color: '#08000000',
          offsetX: 0,
          offsetY: 2
        })
      }
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 骰子点数绘制

用标准骰子圆点布局:

  • 1点:中间一个点
  • 2点:左上和右下
  • 3点:左上、中间、右下
  • 4点:四个角
  • 5点:四个角加中间
  • 6点:左右各三个点
2. 投掷动画

使用animateTo实现360度旋转动画,同时用setInterval快速切换点数制造滚动效果,动画结束后确定最终点数。

3. 随机数生成

每个骰子独立生成1-6的随机数,模拟真实骰子的均匀分布。

4. 数量选择

支持1-6个骰子,选择后重置所有骰子为1点。

5. 总和计算

使用reduce计算所有骰子点数之和,大字体蓝色显示。

6. 历史记录

每次投掷结果保存到历史,显示点数组合和总和,最多保存20条。

九、运行项目

代码编写完成后,在DevEco Studio中运行项目。选择不同骰子数量,点击投掷按钮,观察骰子旋转动画,检查点数是否正确,总和计算是否准确,历史记录是否正常。

十、开发中遇到的问题

  • 动画流畅度:用快速切换点数+旋转动画模拟真实掷骰子效果
  • 骰子布局:正确绘制1-6点的标准圆点位置
  • 防重复点击:动画进行中禁止再次点击
  • 响应式布局:多个骰子时自动换行,适配不同数量

十一、总结

本文基于HarmonyOS和ArkTS实现了一个掷骰子模拟器。项目通过自定义骰子组件和动画效果实现有趣的投掷体验,支持多骰子、点数统计和历史记录。这个项目展示了ArkTS中组件封装、动画效果和随机数处理的基本方法,适合作为入门练习项目。

相关推荐
LEO111102 小时前
HarmonyOS应用《玄象》开发实战:RenderingContextSettings(true) 抗锯齿对 Canvas 渲染的影响
harmonyos·鸿蒙
爱写代码的阿森2 小时前
鸿蒙三方库 | harmony-utils之ArrayUtil集合操作与去重详解
华为·harmonyos·鸿蒙·huawei
qizayaoshuap3 小时前
# 评分器组件开发实战:HarmonyOS ArkTS 五星评分交互应用解析
华为·交互·harmonyos
b130538100494 小时前
HarmonyOS应用《玄象》开发实战:AiPhotoFengshuiPage AI 拍照风水:@ohos.multimedia.camera 相机调用
harmonyos·鸿蒙
爱写代码的阿森4 小时前
鸿蒙三方库 | harmony-utils之NumberUtil数值精确计算详解
华为·harmonyos·鸿蒙·huawei
b130538100496 小时前
HarmonyOS应用《玄象》开发实战:HomePage 顶部标题栏的 Row + Blank 三段式布局
harmonyos·鸿蒙
●VON9 小时前
鸿蒙 PC Markdown 编辑器内核:在 ArkWeb 中离线运行 CodeMirror 6
安全·华为·编辑器·harmonyos·鸿蒙
YM52e11 小时前
鸿蒙Flutter Padding内边距:EdgeInsets详解
android·学习·flutter·华为·harmonyos·鸿蒙
爱写代码的阿木12 小时前
基于鸿蒙OS开发附近社交游戏平台(二十二)-ChatPage 即时通讯 UI
游戏·华为·harmonyos