WWDC Quick Look 💓 By SwiftGGTeam
Add accessibility to your Unity games

Add accessibility to your Unity games

观看原视频

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 调整来实现适配。例如降低透明度开启时,把半透明背景改为不透明;增强对比度开启时,使用更高对比度的颜色方案。


核心启发

  1. 给游戏交互对象挂上 AccessibilityNode:找出游戏中对玩法有关键影响的对象(卡牌、按钮、角色、道具),逐个挂载 AccessibilityNode 组件并实现 accessibilityValueDelegate。为什么值得做:VoiceOver 用户完全依赖屏幕阅读来了解游戏状态,没有 AccessibilityNode 的 GameObject 对他们来说就是空气。怎么开始:在 Unity 中打开你的游戏场景,选出 5 个最关键的交互对象,给每个挂上 AccessibilityNode 并写一个返回当前状态描述的 delegate。

  2. 监听 Dynamic Type 事件,同步缩放文字和图形:订阅 onPreferredTextSizesChanged,用 PreferredContentSizeMultiplier 缩放所有 Text 组件的字号,并为关键图形准备大尺寸版本的材质。为什么值得做:低视力用户会把系统字号调得很大,如果游戏文字不跟着放大,他们就完全看不清。怎么开始:创建一个 DynamicTextSize 脚本挂到所有 Text 组件上,再为关键图形(如卡牌、图标)准备 LargeMaterial 变体。

  3. 用 Reduce Motion 开关控制动画播放:在动画播放前检查 IsReduceMotionEnabled,开启时跳过动画直接呈现结果。为什么值得做:运动敏感用户在观看快速翻转、弹跳、旋转等动画时可能产生眩晕或不适,直接跳过动画是对他们的基本尊重。怎么开始:在游戏里找出所有播放动画的地方(翻牌、过场、特效),在每个入口加一个 if (AccessibilitySettings.IsReduceMotionEnabled) return; 检查。


关联 Session

评论

GitHub Issues · utterances