· engineering· 约 23 分钟精读

端侧离线教育沙盒的即时反馈通道工程:系统音效标识、触觉发生器生命周期与参数提示卡的信息闸门

核心要点速览 · TL;DR 概要

一个不联网、不注册、包内没有音频文件的教学沙盒,怎么让学习者在拖动参数的瞬间知道自己做对了还是做偏了?本文以密旋科技 QubitLab 的真实源码为样本,做一次反馈调用普查,拆开声音、触觉、视觉三条通道的实现与失效条件,并说明那张参数提示卡为什么只报区间、不报距离。

端侧离线教育沙盒的即时反馈通道工程:系统音效标识、触觉发生器生命周期与参数提示卡的信息闸门

一、看不见概率的人,靠什么判断自己做对了

在手机上做一次微观物理实验,和在学校实验室里拧旋钮是两回事。真实现场里,旋钮有阻尼、示波器会抖动、器件会发烫,操作者靠身体感受就能建立因果关系;而在移动端沙盒里,设备与算法之间只有一块玻璃。学习者拖动一个数值,屏幕另一侧发生的是概率幅的变化——他看不见那个概率,只能看见读数、提示文字,以及按下按钮时手上与耳边的那一点动静。

密旋科技的 QubitLab 是一款面向 App Store 的移动端量子物理与 STEM 启蒙沙盒:96 关闯关、可交互的布洛赫球面、双比特逻辑门拼装,全部离线完成。本文不评价它的趣味,而是把”即时反馈”这件事拆开看:三条通道各自由什么构成、调用点分布在哪里、在什么条件下会失效,以及那张参数提示卡为什么故意不把话说满。

端侧即时反馈的三条通道与各自的降级路径① 离散动作按钮点击、重置、模式切换一次动作对应一次轻反馈② 判定闭包得到实验结论的同一处代码达标与未达标分开表达③ 关卡结算星级写入与通关同拍发生触觉与声音同时到达触觉通道轻 / 中 / 重三档冲击反馈成功 / 警示 / 失败三档通知反馈声音通道四枚系统音效标识,包内无音频文件不接管音频会话,不打断正在播放的音乐视觉通道参数名、当前值与提示文字写在同一张卡上外框颜色表示相对目标的位置三条通道都可能被系统关闭:静音开关、无振动马达、色彩不可用;卡上的数值与文字始终保留

二、先做一次反馈调用普查

要谈反馈通道,先看它在代码里长什么样。把整个工程扫一遍——62 个 Swift 源文件、约 3.2 万行代码——反馈调用集中在两个封装里:一类管触觉,一类管声音。按调用点逐项统计如下。

通道语义档位调用点数承担的场景
触觉轻档冲击40重置、切换、列表选择等可预期动作
触觉警示通知34结论未达标、参数越界
触觉确认冲击34参数确认、按钮提交
触觉庆祝通知28通关与关卡结算
触觉成功通知25子目标达成
触觉重档冲击18碰撞、坠落、失败事件
触觉中档冲击14一般确认
触觉失败通知1诱骗态校验未通过(全工程唯一一处)
声音点击音38离散动作
声音胜利音26通关
声音星级唤醒音13星级写入
声音星级达成音0已定义,无调用点

合计:触觉调用 194 处,声音调用 77 处;触觉散落在 28 个视图文件中,声音集中在 8 个文件里。这张表里真正有信息量的不是总数,而是三个比例。

第一,轻档冲击最多,有 40 处。这说明反馈的主体用途是”我收到了你的操作”,而不是”你做对了”。开关、重置、切换这类动作在物理上没有后果,在心理上却需要即时确认,否则用户会怀疑触摸是否被识别。

第二,警示通知(34 处)多于成功通知(25 处)。实验类任务里,未达标的次数天然多于达标;如果每次未达标都给强反馈,学习过程很快会变成挫败练习。实现上两者同属通知类触觉,差别只在语义,强度没有差别——这是一处刻意的收敛。

第三,失败档只有 1 处,留给”诱骗态平均光子数超出安全范围”这种系统性参数错误,而不是”这题没做对”。报错反馈被压到最少,是为了不把正常试错标注成失败。

还有一处可以直接指出的事实:星级达成音定义了却没有调用点。这类”已声明未接线”的接口在迭代里很常见,它不产生运行时开销,而下面要讲的另一处留白比它更明显。

三、声音通道:四枚系统标识,包内没有音频文件

QubitLab 的音效不来自任何随包分发的音频文件,而是调用系统提供的播放接口,传入四枚系统标识,分别对应点击、星级唤醒、星级达成与胜利。

这样做的工程好处可以直接说清。其一,应用包里没有音频资源文件,音效由系统播放,不需要文件读取、解码与缓存,也不占下载体积。其二,不需要配置音频会话:播放系统标识不要求应用声明会话类别,因此不会打断用户正在播放的音乐,也不会因为会话切换带来额外状态。其三,四枚标识就是四个调用,没有资源清单要维护,也就没有”某个音效文件漏打包”这类发布事故。

对比一下同一应用里的另一条音频路径,取舍会更清楚:课程视频播放器在开始播放前会把音频会话显式设为播放类别、影片播放模式,并选择降低其他音频的选项;停止播放时再把会话交还给其他应用。两条路径分开是刻意的——一次短促的操作反馈不该抢走音频会话,否则一次点击就会变成对用户音乐播放的干扰。

代价同样要写清楚:

  • 出不出声由设备侧决定。静音开关状态、系统音量与辅助功能设置共同决定结果,应用既无法查询”刚才那一声是否真的响了”,也无法强制发声。
  • 系统标识是平台内部编号,不是公开文档中的稳定契约,音色可能随系统版本变化;也无法停止、淡出或循环播放。
  • 因此它不能作为语义的唯一载体:产品里不存在”听声辨状态”的设计,自动化测试也无法断言音效真的响过。

四、触觉通道:一次调用一个发生器,与预热窗口的长度

触觉比声音多一层要求:必须在触发之前让振动硬件进入待命状态。工程里的做法是每次反馈新建一个发生器实例,先调用预热,再立刻触发,然后不保留这个实例。

按 UIKit 的语义,预热的作用是让振动硬件提前启动,缩短从调用到手上感到振动的延迟;而硬件待命只维持很短一段时间,之后系统会收回。由于实例没有被复用,跨页面的”续热”并不存在——预热只对紧接着的那一次触发起作用。这种写法换来的是无状态:没有共享实例,就没有状态污染与泄露风险,代价是每一次反馈都要重新承担那点启动延迟。

档位分两组:冲击类三档(轻、中、重)与通知类三档(成功、警示、失败)。从普查看,分档与场景的对应相当稳定:轻档留给重置与切换这类可预期动作,中档留给确认,重档留给碰撞与坠落;通知类承担”结论对不对”。在通关这一类关键节点上,触觉与声音会一起给出——26 处通关点同时触发庆祝触觉与胜利音效,另外 2 处只给触觉,构成两条通道之间的冗余。

边界有三条。没有振动硬件的设备上,这些调用不会产生任何效果(模拟器与部分平板机型即属此类);系统里关闭系统触觉后同样静默;因此触觉只能算增强通道,不能算承载通道——任何只靠触觉传达的信息,在上述条件下都会丢失,界面必须保留等价的可见表达。

五、视觉通道:一张提示卡,七个输入,四件被画出来的东西

三条通道里,只有视觉具备足够的表达带宽,所以参数提示卡是唯一被认真设计过的通道。它的输入有七个:参数名、公式、当前值、目标值、进度(0 到 1 的亲近度)、位置(低于目标区、在目标区、高于目标区)与提示文字。而它真正画出来的只有四件东西。

参数提示卡的输入字段与真正绘制出来的内容提示卡声明的七个输入parameterNamecurrentValueTexthintTextzoneformulatargetValueTextprogressFraction青色参与绘制,灰色只在签名里占位绘制界面上真正绘制的四件事参数名,取自 parameterName当前值胶囊,取自 currentValueText提示文字,取自 hintText外框颜色,取自 zone 的三态公式、目标值与进度都不绘制所以卡上看不到离目标还差多远低于 / 目标区 / 高于只由颜色区分进度值在初始化时被裁剪到 0 到 1 区间,却从未参与绘制;八处逐关反馈元组声明后没有被引用
输入字段是否绘制画面上的形态
参数名是卡片左上角,配一个灯泡图标
当前值是右上角胶囊内的等宽数字
提示文字是卡片中部的一行说明
位置是,仅用颜色外框描边:橙、绿、红三色之一
公式否签名中声明,界面不使用
目标值否签名中声明,界面不使用
进度否初始化时被裁剪,之后不再读取

第四行值得单独说:位置三态只通过外框描边颜色表达,描边取状态色的三成半透明度,底色保持固定的深色。也就是说,“我离目标多远”这件事被有意省略了——卡片告诉你身在哪个区,不告诉你离边界还有多少。进度的取值在初始化时会被夹到 0 与 1 之间,防止非法输入把布局撑坏:

pout=min⁡(1, max⁡(0, pin))p_{\mathrm{out}} = \min\bigl(1,\ \max(0,\ p_{\mathrm{in}})\bigr)

但它从未参与绘制。调用点的代码仍然逐处计算这个亲近度,有几处干脆直接填了常量,这恰好说明该字段目前只是签名占位。数值文本则用等宽数字呈现,避免逐帧变化时左右抖动;提示文字按内容换行而不做截断,宁可把卡片撑高,也不让一句话被切掉半截。

这种”话不说满”的设计有明确的教学意图:如果给出连续进度条,任务就会退化成滑块对表——学习者不再追问为什么这个取值能穿过势垒,而是把进度条推到底。代价也要承认:只报区间、不报距离,就缺少”你正在接近”的正向反馈,试错次数偏多的关卡里,学习者只能靠数值与文字自行判断是否在收敛。

另有两处接线情况必须写实。第一,八个闯关小游戏各自声明了一份逐关反馈元组(参数名、公式、当前值、目标值、进度、位置、提示),但扫描全部源码后,这八处声明没有任何引用;真正把提示卡挂到界面上的只有一处——康普顿散射的观测反馈,并且带了一个可访问性标识供自动化检查。放射性时钟对照那一关用的是自己的一套文字反馈块。共享组件与逐关反馈文案之间,存在一层没有接上的断口:这类声明是纯计算属性,不引用就不执行,不留运行时开销,但”可复用的反馈组件”这件事目前只完成了一半。

第二,三态标签自带图标与本地化标签两个属性,全库没有一处引用——颜色是当前唯一的三态区分手段。从无障碍角度看这并不够:仅凭颜色区分状态,属于用色条款要规避的做法。实际风险被摊薄是因为提示卡旁边通常紧邻数值与提示文字,状态可以间接读出;但严格地说,颜色仍是唯一直接通道。

六、时机:反馈落在离散动作与判定出口上

反馈的第二个工程维度是”什么时候给”。扫描调用点可以看到两条规律。

其一,它们几乎都落在离散动作里,而不是连续手势里。重置、切换、提交这类按钮的动作闭包内有反馈,参数拖动过程本身不触发触觉。好处是天然去抖,不会出现每帧一次振动、把反馈变成噪音的情况;代价是拖动过程中没有连续触觉提示——要找到共振点,只能靠数值与提示卡,而不是靠手感。

其二,结论类反馈与判定写在同一个出口处。通关时先把星级写入,再给出庆祝触觉与胜利音效,两者同拍发生,不会出现”声音响了但星级没记上”这种错位。这种”先落数据、再给反馈”的顺序,也让自动化走查有稳定的断言点:状态变化先发生,反馈只是它的伴随现象。

家长门也是这条通道的使用者:答对给成功触觉并放行,答错给警示触觉并重新出题。这里有一处能力边界值得写清:这道门用两位数的乘法题把随手点击的儿童挡在外面,出题范围有限、答错没有次数上限、走查参数与调试构建都能绕过它。它保护的是流程——避免未成年人误入付费与数据处置入口——而不是安全边界;把它当作身份验证使用,是超出它能力的用法。

七、离线、隐私与可核查性

三条通道全部在本机完成:系统音效由系统播放,触觉由本机硬件产生,提示卡由本机数据渲染,没有任何一步需要网络。这与应用的离线承诺一致——数据不出设备,也就不存在第三方遥测。代价是明确的:没有埋点就没有反馈使用数据,无法统计哪个档位最常被触发,也无法用界面实验量化反馈对学习效果的影响。要保留隐私,就必须放弃这类度量。

可核查性走的是另一条路。整个工程注册了 122 个唯一的可访问性标识,配合启动参数(走查模式会跳过家长门的算术校验,调试构建提供自动求解入口),自动化测试可以在没有网络、也没有真实硬件的条件下稳定驱动这些路径:进入关卡、把参数拖进目标区、触发结算、打开家长报告。这里同样要说明上限:触觉与音效在测试环境里不产生可观测信号,可断言的只有状态变化与可见文本,因此自动化覆盖的是”反馈被触发时的条件与结果”,覆盖不了”反馈本身是什么感觉”。

通道依赖可被系统关闭的情形可否自动断言关闭后的等价表达
触觉本机振动硬件无振动硬件的设备、系统触觉开关否数值与提示文字
声音系统音效库静音开关、音量与辅助功能设置否视觉提示卡
视觉本机渲染颜色不可用时须保证文本可读是数值与状态文字

八、这套反馈设计的能力边界

把前面几节收拢成一份清单。

能够做到的:三条通道互相冗余,任何一条失效都不会让学习者”不知道发生了什么”;反馈落在离散动作与判定出口上,天然避免了高频轰炸;提示卡把”身在哪个区”说明白,同时不把答案交出去;先落数据再给反馈,保证记录与感受不会错位。

做不到的至少有四条。第一,不能保证声音与触觉一定呈现,设备状态与系统设置说了算,应用无法查询结果。第二,不能给出”离目标多远”的连续提示,因为进度字段没有被使用,也没有定义过该用线性还是对数尺度来表达亲近度。第三,不具备反馈层面的数据回流,无法量化体验优化,也拿不出哪一档反馈更有效这类证据。第四,只用颜色区分三态,严格说未满足用色条款,只能依靠旁边的文字与数值兜底。

这四条不是缺陷清单,而是这台设备在没有服务端、没有随包音频资源、没有云端的条件下,对”即时反馈”能给出的答案:用最少的外部依赖做出可感知的响应,再把做不到的部分如实标注出来。

九、结语

离线优先的承诺是数据不出设备。顺着这条承诺往下走会发现,被留在设备里的不只是记录,还有”怎么让用户知道自己做对了”这件事:既然没有服务端下发素材、没有网络请求可以等待,反馈就必须由端侧自己合成——音效借系统的、触觉借硬件的、解释留给一张本地渲染的提示卡,而每一处借来的东西都有它自己会失效的条件。把这三条通道各自的边界写进工程说明,比声称”反馈体验”更有用:一个学习者手上那一点振动、耳边那一声轻响、卡上那一行文字,本来就不是锦上添花,而是他在看不见概率的世界里唯一的坐标系。

参考文献

[1] Apple Inc., UIFeedbackGenerator — Preparing the Generator. https://developer.apple.com/documentation/uikit/uifeedbackgenerator [2] Apple Inc., AudioServicesPlaySystemSound(:). https://developer.apple.com/documentation/audiotoolbox/1405202-audioservicesplaysystemsound [3] Apple Inc., AVAudioSession — setActive(:options:). https://developer.apple.com/documentation/avfaudio/avaudiosession/setactive(_:options:) [4] Apple Inc., Human Interface Guidelines — Playing audio. https://developer.apple.com/design/human-interface-guidelines/playing-audio [5] W3C, Web Content Accessibility Guidelines (WCAG) 2.2, Success Criterion 1.4.1 Use of Color. https://www.w3.org/TR/WCAG22/#use-of-color [6] Apple Inc., Audio Services — System Sound Services. https://developer.apple.com/documentation/audiotoolbox/audio-services

Share:
Back to Blog