Highlight
Apple 在 WWDC 2022 发布了开源 Unity 无障碍插件(Apple Unity Plug-Ins),让 Unity 游戏开发者可以像原生应用一样接入 VoiceOver、Switch Control、Dynamic Type 等辅助功能,而不需要从零学习 Apple 的无障碍 API。
核心内容
Unity 游戏在 Apple 平台上的无障碍支持一直是个痛点。原生开发者可以用 UIAccessibility 协议、SwiftUI 的 accessibilityLabel 修饰符来接入 VoiceOver,但 Unity 引擎里这些 API 不可用。结果是大量 Unity 游戏完全没有无障碍支持,视障用户、运动障碍用户无法使用。
这场 session 介绍的 Apple Unity Plug-Ins 解决了这个问题。它在 Unity 侧提供了 AccessibilityNode 组件和 AccessibilitySettings API,底层通过原生桥接层对接 iOS/macOS 的无障碍系统。开发者不需要写一行原生代码,在 Unity Inspector 里配置 AccessibilityNode 就能让 VoiceOver 正确朗读游戏对象。
Session 用一个记忆配对卡牌游戏(Memory Match)作为演示案例。游戏的核心交互是翻开卡牌、匹配图案,但原始的卡牌对 VoiceOver 是完全不可见的——它们是 Unity GameObject,不是 UIKit 的 UIView。通过给每张卡牌挂上 AccessibilityNode 组件并实现 accessibilityValueDelegate,VoiceOver 用户就能听到”黑桃 A”、“覆盖的”这样的描述,可以正常参与游戏。
除了 VoiceOver,插件还支持 Dynamic Type——当用户在系统设置里调大字号时,游戏内的文字和卡牌图案会自动缩放;支持 Reduce Motion——当用户开启了减弱动态效果,卡牌翻转动画会被跳过,直接显示结果;以及 Reduce Transparency、Increase Contrast 等界面适配选项。
详细内容
接入 Apple Unity Accessibility 插件
插件通过 Unity Package Manager 安装,源码托管在 GitHub。安装后,每个需要被 VoiceOver 读取的 GameObject 都需要挂上 AccessibilityNode 组件。
首先是定义一个卡牌枚举类型(07:43):
public enum PlayingCard
{
AceOfSpade,
AceOfClubs,
AceOfDiamonds
}
然后在卡牌的 MonoBehaviour 中实现 AccessibleCard 类(07:53):
using Apple.Accessibility;
public class AccessibleCard : MonoBehaviour
{
public PlayingCard cardType;
public bool isCovered;
void Start()
{
var accessibilityNode = GetComponent<AccessibilityNode>();
accessibilityNode.accessibilityValueDelegate = () => {
if (isCovered) {
return "covered";
}
if (cardType == PlayingCard.AceOfSpades) {
return "Ace of Spades";
}
}
}
}
关键点:
GetComponent<AccessibilityNode>()获取挂载在同一个 GameObject 上的无障碍节点accessibilityValueDelegate是一个返回字符串的委托,VoiceOver 读取时会调用它- 根据
isCovered状态返回不同描述,让用户知道卡牌是盖着还是翻开 - 如果卡牌翻开且是黑桃 A,返回”Ace of Spades”,这会被 VoiceOver 朗读出来
支持 Dynamic Type
Dynamic Type 让文字和图形随系统字号设置自动缩放。插件通过 AccessibilitySettings 类提供字号信息(10:35):
public class DynamicCardFaces : MonoBehaviour
{
public Material RegularMaterial;
public Material LargeMaterial;
void OnEnable()
{
AccessibilitySettings.onPreferredTextSizesChanged += _settingsChanged;
}
void _settingsChanged()
{
var shouldUseLarge = AccessibilitySettings.PreferredContentSizeCategory >=
ContentSizeCategory.AccessibilityMedium;
GetComponent<Renderer>().material = shouldUseLarge ? RegularMaterial :
LargeMaterial;
}
}
关键点:
- 订阅
AccessibilitySettings.onPreferredTextSizesChanged事件,系统字号变化时触发 PreferredContentSizeCategory返回当前系统字号类别- 当字号大于等于
AccessibilityMedium时,切换到更大的卡牌材质(LargeMaterial),确保图案在放大文字时也能清晰辨认
文字本身也需要缩放(10:36):
using UnityEngine.UI;
public class DynamicTextSize : MonoBehaviour
{
int originalSize;
void Start()
{
originalSize = GetComponent<Text>().textSize;
}
void OnEnable()
{
AccessibilitySettings.onPreferredTextSizesChanged += _settingsChanged;
}
void _settingsChanged()
{
GetComponent<Text>().textSize = (int)(originalSize *
AccessibilitySettings.PreferredContentSizeMultiplier);
}
}
关键点:
- 保存文字的原始大小
originalSize - 用
PreferredContentSizeMultiplier乘上原始大小得到新字号 PreferredContentSizeMultiplier是系统根据用户字号设置计算的缩放倍数,不需要自己查表
支持 Reduce Motion
减弱动态效果是运动敏感用户的重要设置。在记忆配对游戏中,卡牌翻转动画可能引发不适,需要在检测到 Reduce Motion 开启时跳过动画(14:54):
using Apple.Accessibility;
public class CardController : MonoBehaviour
{
public void Flip()
{
var reduceMotionOn = !AccessibilitySettings.IsReduceMotionEnabled;
if (!reduceMotionOn)
{
StartCoroutine(Animate());
}
else
{
transform.rotation = Quaternion.identity;
}
}
IEnumerator Animate()
{
}
}
关键点:
AccessibilitySettings.IsReduceMotionEnabled返回用户是否在系统设置中开启了减弱动态效果- 如果开启,跳过动画协程,直接将卡牌旋转重置为
Quaternion.identity(正面朝上),用户瞬间看到结果 - 如果未开启,正常播放
Animate()协程中的翻转动画 - 这个逻辑放在
Flip()方法中,每次翻牌时都会检查
界面适配选项
除了上面三个主要功能,插件还暴露了以下设置:
AccessibilitySettings.IsReduceTransparencyEnabled:检查是否开启了降低透明度AccessibilitySettings.IsIncreaseContrastEnabled:检查是否开启了增强对比度AccessibilitySettings.IsBoldTextEnabled:检查是否开启了粗体文本
这些都可以在 Start() 或 OnEnable() 中读取,配合材质切换或 UI 调整来实现适配。例如降低透明度开启时,把半透明背景改为不透明;增强对比度开启时,使用更高对比度的颜色方案。
核心启发
-
给游戏交互对象挂上 AccessibilityNode:找出游戏中对玩法有关键影响的对象(卡牌、按钮、角色、道具),逐个挂载
AccessibilityNode组件并实现accessibilityValueDelegate。为什么值得做:VoiceOver 用户完全依赖屏幕阅读来了解游戏状态,没有 AccessibilityNode 的 GameObject 对他们来说就是空气。怎么开始:在 Unity 中打开你的游戏场景,选出 5 个最关键的交互对象,给每个挂上AccessibilityNode并写一个返回当前状态描述的 delegate。 -
监听 Dynamic Type 事件,同步缩放文字和图形:订阅
onPreferredTextSizesChanged,用PreferredContentSizeMultiplier缩放所有 Text 组件的字号,并为关键图形准备大尺寸版本的材质。为什么值得做:低视力用户会把系统字号调得很大,如果游戏文字不跟着放大,他们就完全看不清。怎么开始:创建一个DynamicTextSize脚本挂到所有 Text 组件上,再为关键图形(如卡牌、图标)准备LargeMaterial变体。 -
用 Reduce Motion 开关控制动画播放:在动画播放前检查
IsReduceMotionEnabled,开启时跳过动画直接呈现结果。为什么值得做:运动敏感用户在观看快速翻转、弹跳、旋转等动画时可能产生眩晕或不适,直接跳过动画是对他们的基本尊重。怎么开始:在游戏里找出所有播放动画的地方(翻牌、过场、特效),在每个入口加一个if (AccessibilitySettings.IsReduceMotionEnabled) return;检查。
关联 Session
- What’s new in accessibility — Apple 平台最新无障碍功能和 API 总览,了解 VoiceOver、Switch Control 等系统级辅助功能的变化
- Explore navigation design for iOS — 导航设计中的无障碍考量,包括 VoiceOver 焦点顺序和可达性
- Create accessible Single App Mode experiences — 单应用模式下的无障碍体验设计,与游戏全屏沉浸场景相关
- What’s new in AppKit — macOS 原生开发中的无障碍增强,了解底层桥接所依赖的平台能力
评论
GitHub Issues · utterances