Tap into virtual and physical game controllers
Graphics & Games 进阶 22m

深入虚拟与物理游戏控制器

Tap into virtual and physical game controllers

2021年6月10日

在 Apple 官方观看视频

一句话判断

虚拟控制器(Virtual Controller)API 终于让非游戏 app 也能获得手柄级别的精确输入,但真正重要的是按钮自定义能力和触觉反馈的整合 —— 移动端手游操作体验被抬到了一个新高度。

这场 Session 讲了什么

这集全面讲解了 iOS/tvOS/macOS 上游戏控制器的更新。苹果在 Game Controller 框架中增强了虚拟控制器的支持,允许开发者在触摸屏上创建自定义的虚拟按钮、摇杆和方向键布局。

物理控制器方面,Session 介绍了对新版 Xbox 和 PlayStation DualSense 控制器的支持增强,包括触觉反馈(Haptic Feedback)、自适应扳机(Adaptive Triggers,仅 DualSense)和按钮映射的自定义。

还讨论了控制器输入的事件处理模型,包括如何同时响应物理和虚拟控制器的输入、如何在游戏中切换控制方案、以及如何处理控制器连接/断开事件。

值得深挖的点

Virtual Controller 的布局自定义GCVirtualController 允许你通过配置字典定义虚拟按钮的位置、大小和类型。你可以创建包含摇杆、按钮组和方向键的复杂布局,位置是相对于安全区域的百分比坐标。但布局编辑器是用户可调的 —— 玩家可以拖动按钮位置,你的配置只是默认值。这是一个很好的设计决策,但意味着你不能假设按钮在精确位置。

DualSense 自适应扳机的实现GCControllerextendedGamepad 属性新增了 adaptiveTriggers 支持。你可以设置扳机的阻力曲线(Resistance Curve),让扣扳机时有不同阶段的反馈(比如半程轻、后半程突然变重模拟拉弓)。这在射击和赛车游戏中能提供非常沉浸的体验,但需要开发者主动适配。

代码片段

创建自定义虚拟控制器

import GameController

func setupVirtualController() {
    // 定义虚拟控制器布局
    let config = [
        // 左摇杆
        GCControllerButtonInput.leftThumbstick: [
            "position": [0.15, 0.35],  // 相对安全区域的位置
            "size": [120, 120]
        ],
        // 右侧按钮组(A/B/X/Y)
        "ButtonA": ["position": [0.85, 0.30]],
        "ButtonB": ["position": [0.90, 0.40]],
        "ButtonX": ["position": [0.80, 0.40]],
        "ButtonY": ["position": [0.85, 0.50]],
    ]
    let virtualController = GCVirtualController(configuration: config)
    virtualController.connect { controller, error in
        guard controller != nil else {
            print("虚拟控制器连接失败: \(error?.localizedDescription ?? "")")
            return
        }
        print("虚拟控制器已连接")
    }
}

处理物理控制器输入

import GameController

func monitorControllerInput() {
    // 监听控制器连接
    NotificationCenter.default.addObserver(
        forName: .GCControllerDidConnect,
        object: nil, queue: .main
    ) { notification in
        guard let controller = notification.object as? GCController else { return }
        // 设置按钮按下回调
        controller.extendedGamepad?.buttonA.pressedChangedHandler = {
            button, value, pressed in
            if pressed {
                // 按钮被按下
                self.playerJump()
            }
        }
        // 设置摇杆读取
        controller.extendedGamepad?.leftThumbstick.valueChangedHandler = {
            joystick, xValue, yValue in
            // xValue/yValue 范围 [-1, 1]
            self.movePlayer(dx: CGFloat(xValue), dy: CGFloat(yValue))
        }
    }
}

DualSense 自适应扳机

import GameController

func setupAdaptiveTrigger(for controller: GCController) {
    guard let gamepad = controller.extendedGamepad else { return }
    // 设置右扳机的阻力曲线(模拟拉弓)
    // 从轻到重的渐进阻力
    gamepad.rightTrigger.setAdaptiveTrigger(
        .init(mode: .feedback) { mode in
            // 前半段轻,后半段突然变重
            mode.setPositionsAndResistances(
                [0.0, 0.5, 1.0],
                resistances: [0.2, 0.2, 0.9]
            )
        }
    )
}

最佳实践

  1. 虚拟控制器的默认布局要参考主流手游的操作方式。左边摇杆 + 右边按钮是大多数玩家的肌肉记忆,不要标新立异搞反直觉的布局。
  2. 同时支持虚拟和物理控制器输入。用 GCController.controllers() 检测是否有物理设备连接,有则隐藏虚拟控制器,无则显示。不要两者同时显示浪费屏幕空间。
  3. 触觉反馈要有节制。持续的高强度震动让玩家手麻。只在关键时刻(击杀、碰撞、技能释放)触发短促的 haptic burst。

还有什么值得关注

  • 虚拟控制器支持用户自定义布局并保存偏好,重启 app 后恢复。
  • tvOS 上虚拟控制器配合 Siri Remote 可以实现更灵活的输入方案。
  • GCEventViewController 让你可以在游戏画面上同时接收触摸和控制器事件,互不干扰。
WWDC 2021