ArkTS + Canvas + Preferences:拆解鸿蒙冒险大转盘的本地状态设计

举报
xinxin9527 发表于 2026/11/14 09:17:37 2026/11/14
【摘要】 以公开仓库 master@1a133629 为依据,拆解鸿蒙元服务“冒险大转盘”的抽样、去重、2.5D 绘制、Preferences 持久化与 JSON/TXT 迁移设计,说明状态边界如何决定实现质量。

做一个本地小游戏,界面通常不是最难的。真正容易乱的是:随机结果由谁决定,动画播放完后怎样保证结果没变,哪些状态只属于这一局,哪些数据需要跨设备带走。

华为云开发者社区的鸿蒙实践案例“冒险大转盘”,用一个纯本地元服务把这些问题串了起来。它没有服务器,也不依赖游戏账号;用户先确认本局任务,再轮流转动转盘,已完成任务退出当前候选,自定义任务通过 Preferences 保存,也可以用 JSON 或 TXT 文件迁移。

这篇不讨论视觉素材怎么画,而是从公开仓库 master@1a133629 的设计与源码出发,拆解一条更适合复用的状态链路。

一、先分清三种任务数据

项目里没有用一个对象包打天下,而是按生命周期拆成了三层。

第一层是转盘运行时使用的 TaskItem,它负责本次抽签:

export interface TaskItem {
  id: string
  type: TaskType
  level: RiskLevel
  text: string
  tags: string[]
  isCustom: boolean
}

type 区分真心话和大冒险,level 控制挑战程度,id 则用于本局完成集合和去重。

第二层是本地持久化的 LocalTaskRecord。它比运行时对象多了 enabled、createdAt 和 updatedAt,因为任务库需要支持启停、编辑和排序这些长期操作。

第三层是跨设备迁移的 TransferTaskRecord,只保留 type、level、text 和 enabled。本地 ID、创建时间不会写进迁移文件;导入时可以重新生成本地 ID。

这个拆分有一个直接好处:页面、存储和文件格式不必互相妥协。运行时需要字段就加在运行时对象里,持久化需要元数据就留在本地记录里,迁移只承诺真正应该携带的业务信息。

二、两层随机,而不是一次 Math.random()

转盘看起来只有一次随机,代码里实际上分成了两层。

第一层发生在开局:TaskEngine.pickForWheel() 按玩法、挑战程度筛选内置任务,再从启用且符合条件的小任务中选出自定义任务。自定义任务最多占本局数量的一半,余下名额由内置任务补齐,最后用 Fisher-Yates 洗牌。

这一步解决的是“本局拿哪些任务”。

第二层发生在每次点击“开始”时。WheelRoundState.pickAvailableIndex() 先过滤掉当前已完成的任务,再从剩余索引中选一个目标:

static pickAvailableIndex(
  tasks: TaskItem[],
  completedTaskIds: string[],
  randomValue: number
): number {
  const indices = WheelRoundState.availableIndices(tasks, completedTaskIds);
  if (indices.length === 0) return -1;
  const normalized = Math.max(0, Math.min(randomValue, 0.999999));
  return indices[Math.floor(normalized * indices.length)];
}

这一步解决的是“这一转落到谁”。

两层随机分开后,规则就不必绑在 Canvas 动画上。页面可以单独测试候选池、启用状态、挑战程度和本局去重,动画只负责把已经确定的目标表现出来。

三、先定目标,再算停在哪里

很多转盘实现会先随机一个角度,等停下来后再根据指针反推结果。这样也能工作,但结果逻辑会依赖动画细节。

“冒险大转盘”的顺序相反:

  1. 从未完成任务中选出目标扇区。
  2. 根据目标扇区计算停止角度。
  3. 播放减速动画。
  4. 动画结束后用最终角度反算实际命中扇区,并展示结果卡片。

在 WheelCanvas 中,目标中心角、额外旋转圈数和少量抖动共同生成最终角度;抖动限制在扇区宽度的较小比例内,避免视觉落点越过扇区边界。

动画使用 easeOutQuart 缓动,先快后慢。旋转过程中按钮禁用;停稳后,指针先做一次轻微回弹,再显示命中扇区。这样“看起来停在哪里”和“逻辑认为选中谁”就不会成为两套互不相关的随机。

对交互类应用来说,这个顺序值得保留:业务结果先确定,表现层只描述结果如何出现。

四、Canvas 只画转盘,不承载长文本

案例的转盘主体由 ArkUI Canvas 绘制。扇区、文字、指针和中心按钮都在画布上完成,外层再通过:

.rotate({ x: 1, y: 0, z: 0, angle: 10, perspective: 650 })

获得轻微透视。侧壁用多层圆形叠加和阴影制造厚度,颜色按玩法生成。

已完成任务不是从数组中删掉,而是保留原扇区位置,改成灰色,并把标签改成“完成”。这有两个作用:用户能看见进度;转盘尺寸也不会因为每次完成而重排。

任务正文并不会塞进扇区。扇区只显示编号和“真心/冒险”,完整内容放到居中的结果卡片。这样任务变长时,转盘仍然保持稳定,结果页则有足够空间阅读和交接。

五、本局状态与长期数据要分开

WheelPage 维护 completedTaskIds。用户点击“交给下一位”后,当前任务 ID 才加入集合;跳过后交接也会退出本局候选。这个集合只属于当前一局,不写入 Preferences,新开一局会重新计算。

这是很关键的一点:完成集合代表临时游戏状态,不应该和自定义任务库混在一起。否则用户重开一局,还要额外清理上一局留下的完成标记。

需要长期保存的是另一类数据:

  • 自定义任务及其启停状态;
  • 上次选择的玩法、任务数量、挑战程度和音效设置;
  • 用户已经确认过的安全提示。

LocalTaskStore 与 PreferenceStore 分别使用不同的 Preferences 实例,并通过 flush() 提交写入。任务库的读写串行排队,避免同一时间发生两次读改写后互相覆盖。

数据边界清楚后,页面行为也更容易解释:本局变化只在页面上流转,需要留下的设置和任务才进入本地存储。

六、迁移文件只带可重建的数据

任务的本地 ID 适合当前设备,但不适合作为跨设备身份。迁移包因此采用独立格式:

{
  "format": "adventure-wheel-tasks",
  "version": 1,
  "tasks": [
    {
      "type": "truth",
      "level": "light",
      "text": "说出一个最近学会的小技能",
      "enabled": true
    }
  ]
}

导入时先校验格式、版本、字段类型和正文规则,再统计新增、重复和无效数量。重复判断使用“类型 + 规范化正文”,不覆盖现有任务,只合并有效的新条目。单文件限制为 1 MiB,单次最多 1000 条。

文件选择使用系统 DocumentViewPicker,读写都针对用户选择的 URI。文件句柄放在 finally 中关闭,避免读取异常后一直占用。

“分享我的任务”写入的是扩展名为 TXT、正文仍为同一 JSON 契约的文本文件,并通过只读 URI 权限交给系统。这里要区分两个结果:接收应用被成功拉起,不等于对方已经实际接收并保存文件。成功提示应该以后续可观察结果为准。

七、写需求时,把状态和契约说明白

如果把这套结构交给 AI 生成,只说“做一个好看的转盘”通常不够。更有效的需求会直接写清状态边界:

请实现一个本地聚会转盘:
1. 运行时任务、持久化任务、迁移任务使用不同数据模型;
2. 开局先筛选并生成不重复清单,确认后才进入转盘;
3. 每次从本局未完成任务中选择目标,再计算停止角度;
4. 已完成任务保留扇区位置并显示“完成”,本局完成集合不持久化;
5. 自定义任务和设置存入 Preferences;
6. 迁移文件包含 format、version、tasks,只导入并合并有效新任务;
7. 构建成功后给出 HAP 实际路径,运行检查与构建检查分开汇报。

这比逐条描述动画曲线更有用。AI 可以决定具体组件怎么组织,但状态是否持久化、随机在哪一层发生、文件能否覆盖已有数据,必须由需求明确。

结语

“冒险大转盘”值得借鉴的不是某个动画参数,而是几条清晰的边界:运行时对象与持久化记录分开,本局状态与长期数据分开,业务结果与动画表现分开,迁移契约与本地存储分开。

这些边界建立后,Canvas 绘制、Preferences 保存和文件迁移都能独立演进。对鸿蒙元服务这类离线优先的小应用来说,这往往比把功能堆进一个页面更重要。

核验依据:公开仓库 harmony-practice-center/HarmonyOS-AtomSer-AdventureWheel 的 master@1a13362905ba1737beadcd35d8f685d4b2653a84,README 文档 v1.2 与 docs/设计说明.md。仓库地址:https://gitcode.com/harmony-practice-center/HarmonyOS-AtomSer-AdventureWheel

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。