深入虚拟与物理游戏控制器
Tap into virtual and physical game controllers
2021年6月10日
一句话判断
虚拟控制器(Virtual Controller)API 终于让非游戏 app 也能获得手柄级别的精确输入,但真正重要的是按钮自定义能力和触觉反馈的整合 —— 移动端手游操作体验被抬到了一个新高度。
这场 Session 讲了什么
这集全面讲解了 iOS/tvOS/macOS 上游戏控制器的更新。苹果在 Game Controller 框架中增强了虚拟控制器的支持,允许开发者在触摸屏上创建自定义的虚拟按钮、摇杆和方向键布局。
物理控制器方面,Session 介绍了对新版 Xbox 和 PlayStation DualSense 控制器的支持增强,包括触觉反馈(Haptic Feedback)、自适应扳机(Adaptive Triggers,仅 DualSense)和按钮映射的自定义。
还讨论了控制器输入的事件处理模型,包括如何同时响应物理和虚拟控制器的输入、如何在游戏中切换控制方案、以及如何处理控制器连接/断开事件。
值得深挖的点
Virtual Controller 的布局自定义。GCVirtualController 允许你通过配置字典定义虚拟按钮的位置、大小和类型。你可以创建包含摇杆、按钮组和方向键的复杂布局,位置是相对于安全区域的百分比坐标。但布局编辑器是用户可调的 —— 玩家可以拖动按钮位置,你的配置只是默认值。这是一个很好的设计决策,但意味着你不能假设按钮在精确位置。
DualSense 自适应扳机的实现。GCController 的 extendedGamepad 属性新增了 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]
)
}
)
}
最佳实践
- 虚拟控制器的默认布局要参考主流手游的操作方式。左边摇杆 + 右边按钮是大多数玩家的肌肉记忆,不要标新立异搞反直觉的布局。
- 同时支持虚拟和物理控制器输入。用
GCController.controllers()检测是否有物理设备连接,有则隐藏虚拟控制器,无则显示。不要两者同时显示浪费屏幕空间。 - 触觉反馈要有节制。持续的高强度震动让玩家手麻。只在关键时刻(击杀、碰撞、技能释放)触发短促的 haptic burst。
还有什么值得关注
- 虚拟控制器支持用户自定义布局并保存偏好,重启 app 后恢复。
- tvOS 上虚拟控制器配合 Siri Remote 可以实现更灵活的输入方案。
GCEventViewController让你可以在游戏画面上同时接收触摸和控制器事件,互不干扰。