Highlight
Apple has added GCPhysicalInputProfile, controller Core Haptics, current controller, motion sensor, light, battery, input remapping and SF Symbols button icons to the Game Controller framework, allowing games to adapt to the differentiated hardware capabilities of Xbox Elite, Xbox Adaptive Controller and DualShock 4.
Core Content
When making a game that supports controllers, the most troublesome part in the past was often not A/B/X/Y. Standard buttons can be abstracted, but what is difficult to deal with are the extra capabilities of each controller: Xbox Elite paddle buttons, DualShock 4 touchpad, controller vibration motor, gyroscope, lights, and battery status. If the game only hardcodes the standard layout, these hardware advantages will go unused.
The change at WWDC 2020 is to put these differences back into the Game Controller framework. eachGCControllerAllphysicalInputProfile, the game can query the actual input on the controller at runtime. Core Haptics can also drive supported controller vibrations. Global or single-App button remapping done by players in iOS 14 and tvOS 14 can also be reflected in the SF Symbols button icons in the game UI.
This design directly solves two pain points. Developers don’t need to write a set of branches for each type of controller, nor do they need to maintain all the button map materials themselves. The game first supports standard controls, and then adds additional buttons, trackpads, motion, lights, and vibrations as enhancements. Controllers without these hardware can still play, and players with these hardware get a more complete experience.
Detailed Content
Extensible input: read the real buttons of the controller at runtime
(02:53)GCPhysicalInputProfileRepresents all physical inputs on a controller, including buttons, triggers, D-pad, and thumbstick.GCExtendedGamepadandGCMicroGamepadAll become its subcategories. The game is still availableGCExtendedGamepadDetermine the standard layout, and then read non-standard input through the 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) }
}
Key points:
controller.physicalInputProfileIt is an entrance that all controllers have and is used to query the input provided by the current hardware. -setupBasicControls(input)Complete the standard button mapping first to ensure complete control of the ordinary handle. -input.buttons["Paddle 1"]Reads the Xbox Elite Wireless Controller Series 2 paddle button; returns if it does not existnil。input.buttons[GCInputDualShockTouchpadButton]Read DualShock 4 touchpad button. -input.allButtons.filterFind buttons that have no bound actions and can be used in the in-game button setting interface.
Controller Vibration: Playing Scalable Feedback with Core Haptics
(08:45) Game Controller When combined with Core Haptics, games can be created for supported controllersCHHapticEngine. locality determines the vibration target location, such as handles or 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
}
Key points:
activeControllerIs the controller currently to output vibration. -controller.haptics?.createEngine(withLocality: .handles)Create a haptic engine for the handle grip. -guard let engine = hapticEngineHandling handles corresponding to haptics locality are not supported.- Real projects also need to start the engine and handle the situation of startup failure or external stop.
(09:05) When the player takes damage, the example creates a pattern player based on the damage intensity, and then plays it immediately.
// 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)")
}
}
Key points:
damageMagnitudeConverts this damage to a strength ranging from 0 to 1. -patternPlayerForPlayerDamageCreate a haptic pattern of corresponding intensity before playing. -CHHapticTimeImmediateIndicates playing as quickly as possible, suitable for low-latency feedback such as being hit.- function not saved
hapticPlayer, because the pattern will play itself to the end.
(09:49) The pattern itself consists of continuous events and transient events. The continuous event provides the base shock, and the two transient events express the damage intensity as impact.
// 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)
}
Key points:
.hapticContinuousCreates a base feedback lasting 0.6 seconds. -.hapticTransientCreates a short impact without setting duration. -0.9 * damageMap hit intensity to transient event intensity. -CHHapticPattern(events:parameters:)Combine multiple events into one hit feedback. -engine.makePlayer(with:)Create a player using the current controller’s haptic engine.
Migrate old rumble loops to dynamic parameters
(12:28) Many game engines originally set motor strength directly per frame. When migrating to Core Haptics, you can create long-running continuous patterns and send them every frameCHHapticDynamicParameterChange the intensity.
// 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)")
}
}
Key points:
update()Represents each frame update in the main game loop. -.hapticIntensityControlControls the overall intensity of the pattern being played. -hapticEngineMotorIntensityis the 0 to 1 intensity calculated by the engine for the current frame. -sendParameters(_:atTime:)Send new dynamic parameters to the existing pattern player.- If you want to control multiple motors independently, you need to repeat this process for each target.
Current controller, movement, light and button icons
(14:11) Single-player games often see the Siri Remote paired with a gamepad.GCController.currentReturns the most recently used controller, and two notifications allow the UI to change based on the current input device.
NSNotification.Name.GCControllerDidBecomeCurrent
NSNotification.Name.GCControllerDidStopBeingCurrentNotification
Key points:
GCController.currentSuitable for single-player games to determine the current input source. -GCControllerDidBecomeCurrentIndicates that a handle becomes the current handle. -GCControllerDidStopBeingCurrentNotificationIndicates that a handle is no longer the current handle.- UI can toggle prompt icons, tutorial text and control status in these notifications.
(15:25) DualShock 4’s motion sensors passGCMotionexposed. To save power, some controllers require the app to manually enable the sensor.
if motion.sensorsRequireManualActivation {
motion.sensorsActive = true
}
Key points:
sensorsRequireManualActivationTells you if you need to manually manage sensor switches. -sensorsActive = trueEnable motion sensors.- Interfaces that do not require motion should turn off the sensor in time to avoid consuming power.
(15:44) Controllers may not be able to separate gravity from user acceleration. The example first determines capabilities and then selects the corresponding data source.
if motion.hasGravityAndUserAcceleration {
handleMotion(gravity: motion.gravity, userAccel: motion.userAcceleration)
} else {
handleMotion(totalAccel: motion.acceleration)
}
Key points:
hasGravityAndUserAccelerationIndicates whether gravity and user acceleration can be split.- When splitting is supported, the game can handle gestures and player actions separately.
- Use when splitting is not supported
motion.accelerationRead the total acceleration.
(16:27) DualShock 4 lightbar passedGCDeviceLightexposed. Games can tie colors to game status, such as injury, low health, or player number.
guard let controller = GCController.current else { return }
controller.light?.color = GCColor.init(red: 1.0, green: 0, blue: 0)
Key points:
GCController.currentGet the most recently used controller. -controller.lightOnly present if the controller supports lights. -GCColorSet the color of the lightbar.
(22:36) Input remapping changes the UI prompts players see. iOS 14 adds game controller glyphs to SF Symbols,sfSymbolsNameWill return the symbol name appropriate for the current controller and remapping state.
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)
Key points:
physicalInputProfile[GCInputButtonY]Get the Y button input element of the current handle. -sfSymbolsNameReturns the names of SF Symbols that match the input elements.- May show a circled Y on Xbox, corresponding to the triangle glyph on DualShock 4.
- If the player remaps Y to X, the UI will reflect the remapped glyph when the game references the Y button.
Core Takeaways
-
What to do: Add Elite paddle shortcut keys to action games. Why it’s worth doing:
GCPhysicalInputProfileCan be queried at runtimePaddle 1, the handle without paddle still uses standard buttons. How to start: Use firstsetupBasicControls(input)Bind the basic control, and theninput.buttons["Paddle 1"]Maps to dodge, reload, or combo. -
What to do: Make the player’s hit, driving surface and explosion distance into layered controller vibrations. Why it’s worth it: Core Haptics can play continuous and transient events on the controller, and scale intensity by damage or distance. How to start: For the current
GCControllerCreate a haptic engine and then use itCHHapticPatternCombination of basic shock and short shock. -
What: Adds DualShock 4 gyroscopic fine-tuned aiming to shooters. Why it’s worth doing: Session clearly mentioned using gyroscope for fine-grained camera aiming, and thumbstick for coarse movement. How to get started: Check
motion.sensorsRequireManualActivation, read gravity/user acceleration or total acceleration after enabling sensors. -
What: Have the tutorial and HUD automatically display button icons for the player’s current controller. Why it’s worth doing:
sfSymbolsNameCorrect glyphs are returned based on MFi, Xbox, DualShock 4, and input remapping. How to start: FromphysicalInputProfileTake out the input element and getsfSymbolsNamefor later useUIImage(systemName:)render. -
What: Automatically follow the most recently used controller in single-player games on Apple TV. Why it’s worth it: Players may have both a Siri Remote and a gamepad connected at the same time,
GCController.currentCan reduce self-written input arbitration logic. How to start: MonitoringGCControllerDidBecomeCurrentandGCControllerDidStopBeingCurrentNotification, refresh the UI and input status in the notification.
Related Sessions
- Bring keyboard and mouse gaming to iPad — Also use the Game Controller framework to incorporate keyboard, mouse, and trackpad into iPad game input.
- Tap into Game Center: Dashboard, Access Point, and Profile — After the controller input resolves the control layer, this section talks about the in-game Game Center entrance and player information.
- Tap into Game Center: Leaderboards, Achievements, and Multiplayer — Continue to complete the rankings, achievements and multiplayer capabilities, suitable for planning together with controller adaptation.
- SF Symbols 2 — Understand the icon system of SF Symbols and can cooperate with
sfSymbolsNameRenders the controller key glyph.
Comments
GitHub Issues · utterances