WWDC Quick Look 💓 By SwiftGGTeam
Advancements in Game Controllers

Advancements in Game Controllers

Watch original video

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:53GCPhysicalInputProfileRepresents 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 savedhapticPlayer, 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 supportedmotion.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 currentGCControllerCreate 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: Checkmotion.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.

Comments

GitHub Issues · utterances