Highlight
Apple 在 Game Controller 框架中加入 GCPhysicalInputProfile、手柄 Core Haptics、当前手柄、运动传感器、灯光、电池、输入重映射和 SF Symbols 按键图标,让游戏能适配 Xbox Elite、Xbox Adaptive Controller 和 DualShock 4 的差异化硬件能力。
核心内容
做一款支持手柄的游戏,以前最麻烦的地方往往不在 A/B/X/Y。标准按键可以抽象,难处理的是每个手柄多出来的能力:Xbox Elite 的 paddle buttons、DualShock 4 的 touchpad、手柄震动马达、陀螺仪、灯光和电池状态。游戏如果只写死标准布局,这些硬件优势就会闲置。
WWDC 2020 的改变,是把这些差异放回 Game Controller 框架。每个 GCController 都有 physicalInputProfile,游戏可以在运行时查询手柄上真实存在的输入。Core Haptics 也能驱动支持的手柄震动。玩家在 iOS 14 和 tvOS 14 中做的全局或单 App 按键重映射,还能反映到游戏 UI 的 SF Symbols 按键图标里。
这个设计直接解决了两个痛点。开发者不用为每一种手柄写一套分支,也不用自己维护所有按键图素材。游戏先支持标准控制,再把额外按钮、触控板、运动、灯光和震动作为增强能力接入。没有这些硬件的手柄仍然能玩,有这些硬件的玩家得到更完整的体验。
详细内容
可扩展输入:运行时读取手柄真实按钮
(02:53)GCPhysicalInputProfile 表示一个控制器上的所有物理输入,包括按钮、trigger、D-pad 和 thumbstick。GCExtendedGamepad 与 GCMicroGamepad 都成为它的子类。游戏仍可用 GCExtendedGamepad 判断标准布局,再通过 physical input profile 读取非标准输入。
// Extensible input API example
var attackComboBtn: GCControllerButtonInput?
var mapBtn: GCControllerButtonInput?
var mappedButtons = Set<GCControllerButtonInput>()
var unmappedButtons = Set<GCControllerButtonInput>()
func setupConnectedController(_ controller: GCController) {
let input = controller.physicalInputProfile
// Set up standard button mapping
setupBasicControls(input)
// Map a shortcut to the player's special combo attack
attackComboBtn = input.buttons["Paddle 1"]
if (attackComboBtn != nil) { mappedButtons.insert(attackComboBtn!) }
// Map a shortcut to the in-game map
mapBtn = input.buttons[GCInputDualShockTouchpadButton]
if (mapBtn != nil) { mappedButtons.insert(mapBtn!) }
// Find buttons that havent' been mapped to any actions yet
unmappedButtons = input.allButtons.filter { !mappedButtons.contains($0) }
}
关键点:
controller.physicalInputProfile是所有控制器都有的入口,用来查询当前硬件提供的输入。setupBasicControls(input)先完成标准按钮映射,保证普通手柄有完整控制。input.buttons["Paddle 1"]读取 Xbox Elite Wireless Controller Series 2 的 paddle button;不存在时返回nil。input.buttons[GCInputDualShockTouchpadButton]读取 DualShock 4 touchpad button。input.allButtons.filter找出还没有绑定动作的按钮,可用于游戏内按键设置界面。
手柄震动:用 Core Haptics 播放可缩放反馈
(08:45)Game Controller 与 Core Haptics 结合后,游戏可以为支持的手柄创建 CHHapticEngine。locality 决定震动目标位置,例如 handles 或 impulse trigger。
private func createAndStartHapticEngine() {
// Create and configure a haptic engine for the active controller
guard let controller = activeController else { return }
hapticEngine = controller.haptics?.createEngine(withLocality: .handles)
guard let engine = hapticEngine else {
print("Active controller does not support handle haptics")
return
}
关键点:
activeController是当前要输出震动的手柄。controller.haptics?.createEngine(withLocality: .handles)为手柄握把创建 haptic engine。guard let engine = hapticEngine处理不支持对应 haptics locality 的手柄。- 真实项目还需要启动 engine,并处理启动失败或外部停止的情况。
(09:05)当玩家受到伤害时,示例根据伤害强度创建 pattern player,然后立即播放。
// Play haptics whenever the player is damaged
private func playerWasDamaged(damage: Float) {
do {
// Calculate the magnitude of damage as percentage of range [0, maxPossibleDamage]
let damageMagnitude: Float = ...
// Create a haptic pattern player for the player being hit by an enemy
let hapticPlayer = try patternPlayerForPlayerDamage(damageMagnitude)
// Start player, "fire and forget".
try hapticPlayer?.start(atTime: CHHapticTimeImmediate)
} catch let error {
print("Haptic Playback Error: \(error)")
}
}
关键点:
damageMagnitude把本次伤害转换成 0 到 1 范围内的强度。patternPlayerForPlayerDamage在播放前创建对应强度的 haptic pattern。CHHapticTimeImmediate表示尽快播放,适合受击这类低延迟反馈。- 函数没有保存
hapticPlayer,因为 pattern 会自行播放到结束。
(09:49)pattern 本身由 continuous event 与 transient event 组成。连续事件提供基础震动,两次瞬态事件把伤害强度表现为冲击。
// Create a haptic pattern that scales to the damage dealt to the player
private func patternPlayerForPlayerDamage(_ damage: Float) throws -> CHHapticPatternPlayer? {
let continuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
], relativeTime: 0, duration: 0.6)
let firstTransientEvent = CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9 * damage),
], relativeTime: 0.2)
let secondTransientEvent = CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9 * damage),
], relativeTime: 0.4)
let pattern = try CHHapticPattern(events: [continuousEvent, firstTransientEvent,
secondTransientEvent], parameters: [])
return try engine.makePlayer(with: pattern)
}
关键点:
.hapticContinuous创建持续 0.6 秒的基础反馈。.hapticTransient创建短促冲击,不设置 duration。0.9 * damage把受击强度映射到瞬态事件 intensity。CHHapticPattern(events:parameters:)把多个事件组合成一次受击反馈。engine.makePlayer(with:)用当前手柄的 haptic engine 创建播放器。
把旧式 rumble 循环迁移到动态参数
(12:28)很多游戏引擎原本每帧直接设置马达强度。迁移到 Core Haptics 时,可以先创建长时间运行的 continuous pattern,再在每帧发送 CHHapticDynamicParameter 改变强度。
// Update the state of the connected controller's haptics every frame
private func update() {
updateHaptics()
}
private func updateHaptics() {
// Update the controller's haptics by sending a dynamic intensity parameter each frame
do {
// Create dynamic parameter for the intensity.
let intensityParam = CHHapticDynamicParameter(parameterID: .hapticIntensityControl,
value: hapticEngineMotorIntensity,
relativeTime: 0)
// Send parameter to the pattern player.
try hapticsUpdateLoopPatternPlayer?.sendParameters([intensityParam],
atTime: CHHapticTimeImmediate)
} catch let error {
print("Dynamic Parameter Error: \(error)")
}
}
关键点:
update()代表游戏主循环中的每帧更新。.hapticIntensityControl控制正在播放的 pattern 的整体强度。hapticEngineMotorIntensity是引擎为当前帧计算出的 0 到 1 强度。sendParameters(_:atTime:)把新的动态参数发送给已存在的 pattern player。- 如果要独立控制多个马达,需要为每个目标重复这一套做法。
当前手柄、运动、灯光和按键图标
(14:11)单人游戏经常同时看到 Siri Remote 与游戏手柄。GCController.current 返回最近使用的控制器,两个通知让 UI 随当前输入设备变化。
NSNotification.Name.GCControllerDidBecomeCurrent
NSNotification.Name.GCControllerDidStopBeingCurrentNotification
关键点:
GCController.current适合单人游戏判断当前输入来源。GCControllerDidBecomeCurrent表示某个手柄成为当前手柄。GCControllerDidStopBeingCurrentNotification表示某个手柄不再是当前手柄。- UI 可以在这些通知中切换提示图标、教程文字和控制状态。
(15:25)DualShock 4 的 motion sensors 通过 GCMotion 暴露。为了节省电量,有些控制器要求 App 手动启用传感器。
if motion.sensorsRequireManualActivation {
motion.sensorsActive = true
}
关键点:
sensorsRequireManualActivation告诉你是否需要手动管理传感器开关。sensorsActive = true启用 motion sensors。- 不需要 motion 的界面应及时关闭传感器,避免消耗电量。
(15:44)控制器不一定能把重力和用户加速度拆开。示例先判断能力,再选择对应数据源。
if motion.hasGravityAndUserAcceleration {
handleMotion(gravity: motion.gravity, userAccel: motion.userAcceleration)
} else {
handleMotion(totalAccel: motion.acceleration)
}
关键点:
hasGravityAndUserAcceleration表示是否能拆分 gravity 与 user acceleration。- 支持拆分时,游戏可以分别处理姿态与玩家动作。
- 不支持拆分时,使用
motion.acceleration读取总加速度。
(16:27)DualShock 4 lightbar 通过 GCDeviceLight 暴露。游戏可以把颜色和游戏状态绑定,例如受伤、低血量或玩家编号。
guard let controller = GCController.current else { return }
controller.light?.color = GCColor.init(red: 1.0, green: 0, blue: 0)
关键点:
GCController.current获取最近使用的控制器。controller.light只在控制器支持灯光时存在。GCColor设置 lightbar 的颜色。
(22:36)输入重映射会改变玩家看到的 UI 提示。iOS 14 在 SF Symbols 中加入 game controller glyphs,sfSymbolsName 会返回适合当前手柄与重映射状态的符号名。
let xboxButtonY = xboxController.physicalInputProfile[GCInputButtonY]!
guard let xboxSfSymbolsName = xboxButtonY.sfSymbolsName else { return }
let xboxButtonYGlyph = UIImage(systemName: xboxSfSymbolsName)
let ds4ButtonY = ds4Controller.physicalInputProfile[GCInputButtonY]!
guard let ds4SfSymbolsName = ds4ButtonY.sfSymbolsName else { return }
let ds4ButtonYGlyph = UIImage(systemName: ds4SfSymbolsName)
关键点:
physicalInputProfile[GCInputButtonY]取出当前手柄的 Y button 输入元素。sfSymbolsName返回与输入元素匹配的 SF Symbols 名称。- Xbox 上可能显示圆圈里的 Y,DualShock 4 上对应 triangle glyph。
- 如果玩家把 Y 重映射到 X,游戏引用 Y button 时 UI 会反映重映射后的 glyph。
核心启发
-
做什么:给动作游戏增加 Elite paddle 快捷键。 为什么值得做:
GCPhysicalInputProfile能在运行时查询Paddle 1,没有 paddle 的手柄仍走标准按键。 怎么开始:先用setupBasicControls(input)绑定基本控制,再把input.buttons["Paddle 1"]映射到闪避、装填或组合技。 -
做什么:把玩家受击、开车路面和爆炸距离做成分层手柄震动。 为什么值得做:Core Haptics 可以在手柄上播放 continuous 与 transient event,并按伤害或距离缩放 intensity。 怎么开始:为当前
GCController创建 haptic engine,再用CHHapticPattern组合基础震动和短促冲击。 -
做什么:为射击游戏加入 DualShock 4 陀螺仪微调瞄准。 为什么值得做:Session 明确提到用 gyroscope 做细粒度 camera aiming,thumbstick 负责粗略移动。 怎么开始:检查
motion.sensorsRequireManualActivation,启用 sensors 后读取 gravity/user acceleration 或 total acceleration。 -
做什么:让教程和 HUD 自动显示玩家当前手柄的按键图标。 为什么值得做:
sfSymbolsName会根据 MFi、Xbox、DualShock 4 以及输入重映射返回正确 glyph。 怎么开始:从physicalInputProfile取出输入元素,拿到sfSymbolsName后用UIImage(systemName:)渲染。 -
做什么:在 Apple TV 单人游戏中自动跟随最近使用的控制器。 为什么值得做:玩家可能同时连接 Siri Remote 和游戏手柄,
GCController.current能减少自写输入仲裁逻辑。 怎么开始:监听GCControllerDidBecomeCurrent与GCControllerDidStopBeingCurrentNotification,在通知里刷新 UI 和输入状态。
关联 Session
- Bring keyboard and mouse gaming to iPad — 同样使用 Game Controller 框架,把键盘、鼠标和触控板纳入 iPad 游戏输入。
- Tap into Game Center: Dashboard, Access Point, and Profile — 手柄输入解决控制层后,这场讲游戏内 Game Center 入口和玩家资料。
- Tap into Game Center: Leaderboards, Achievements, and Multiplayer — 继续补齐排行榜、成就和多人能力,适合与手柄适配一起规划。
- SF Symbols 2 — 了解 SF Symbols 的图标体系,可配合
sfSymbolsName呈现手柄按键 glyph。
评论
GitHub Issues · utterances