2020 System ServicesGraphics & Games
WWDC20 · 25 min · System Services / Graphics & Games
Advancements in Game Controllers
Let’s rumble! Discover how you can bring third-party game controllers and custom haptics into your games on iPhone, iPad, Mac, and Apple TV. We’ll show you how to add support for the latest controllers — including Xbox’s Elite Wireless Controller Series 2 and Adaptive Controller — and map your game’s controls accordingly. Learn how you can use the Game Controller framework in tandem with Core Haptics to enable rumble feedback. And find out how you can take your gaming experience to the next level with custom button mapping, nonstandard inputs, and control over specialty features like motion sensors, lights, and battery level. To get the most out of this session, you should be familiar with the Game Controller framework. Check the documentation link for a primer. And if you build games for iPad, be sure to check out "Bring keyboard and mouse gaming to iPad” for a guide on integrating keyboard, mouse, and trackpad inputs into your experience.
Watch at developer.apple.com ↗Code shown on screen · 10 snippets
Extensible input API
// 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) }
} Starting the Haptic Engine
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
} Play haptics
// 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)")
}
} Creating a haptic pattern
// 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)
} Updating haptics every frame
// 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)")
}
} Current controller notifications
NSNotification.Name.GCControllerDidBecomeCurrent
NSNotification.Name.GCControllerDidStopBeingCurrentNotification Manual activation
if motion.sensorsRequireManualActivation {
motion.sensorsActive = true
} Using total acceleration
if motion.hasGravityAndUserAcceleration {
handleMotion(gravity: motion.gravity, userAccel: motion.userAcceleration)
} else {
handleMotion(totalAccel: motion.acceleration)
} Setting up the lightbar
guard let controller = GCController.current else { return }
controller.light?.color = GCColor.init(red: 1.0, green: 0, blue: 0) Input glyphs with SF Symbols
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) Resources
Related sessions
-
12 min -
15 min -
27 min -
23 min -
15 min -
18 min -
14 min -
56 min -
17 min