Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

Code shown on screen · 10 snippets

Extensible input API swift · at 2:53 ↗
// 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 swift · at 8:45 ↗
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 swift · at 9:05 ↗
// 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 swift · at 9:49 ↗
// 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 swift · at 12:28 ↗
// 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 swift · at 14:11 ↗
NSNotification.Name.GCControllerDidBecomeCurrent
NSNotification.Name.GCControllerDidStopBeingCurrentNotification
Manual activation swift · at 15:25 ↗
if motion.sensorsRequireManualActivation {
    motion.sensorsActive = true
}
Using total acceleration swift · at 15:44 ↗
if motion.hasGravityAndUserAcceleration {
    handleMotion(gravity: motion.gravity, userAccel: motion.userAcceleration)
} else {
    handleMotion(totalAccel: motion.acceleration)
}
Setting up the lightbar swift · at 16:27 ↗
guard let controller = GCController.current else { return }
controller.light?.color = GCColor.init(red: 1.0, green: 0, blue: 0)
Input glyphs with SF Symbols swift · at 22:36 ↗
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