import SDL3 public class GameController { public struct Pad { public enum Axes { case leftStickX, leftStickY case rightStickX, rightStickY case leftTrigger, rightTrigger internal var sdlEnum: SDL_GamepadAxis { switch self { case .leftStickX: SDL_GAMEPAD_AXIS_LEFTX case .leftStickY: SDL_GAMEPAD_AXIS_LEFTY case .rightStickX: SDL_GAMEPAD_AXIS_RIGHTX case .rightStickY: SDL_GAMEPAD_AXIS_RIGHTY case .leftTrigger: SDL_GAMEPAD_AXIS_LEFT_TRIGGER case .rightTrigger: SDL_GAMEPAD_AXIS_RIGHT_TRIGGER } } } public struct Buttons: OptionSet { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } static let east = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_EAST.rawValue) static let south = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_SOUTH.rawValue) static let north = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_NORTH.rawValue) static let west = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_WEST.rawValue) static let back = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_BACK.rawValue) static let start = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_START.rawValue) static let guide = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_GUIDE.rawValue) static let leftStick = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_STICK.rawValue) static let rightStick = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK.rawValue) static let leftBumper = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER.rawValue) static let rightBumper = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER.rawValue) static let dpadLeft = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_LEFT.rawValue) static let dpadRight = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_RIGHT.rawValue) static let dpadUp = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_UP.rawValue) static let dpadDown = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_DPAD_DOWN.rawValue) static let misc1 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC1.rawValue) static let misc2 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC2.rawValue) static let misc3 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC3.rawValue) static let misc4 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC4.rawValue) static let misc5 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC5.rawValue) static let misc6 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_MISC6.rawValue) static let paddle1 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1.rawValue) static let paddle2 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE1.rawValue) static let paddle3 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2.rawValue) static let paddle4 = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_LEFT_PADDLE2.rawValue) static let touchPad = Self(rawValue: 1 << SDL_GAMEPAD_BUTTON_TOUCHPAD.rawValue) } public struct State { private let _axes: [Int16] private let _btns: Buttons, _btnImpulse: Buttons internal init(axes: [Int16], btns: Buttons, btnImpulse: Buttons) { self._axes = axes self._btns = btns self._btnImpulse = btnImpulse } public func axis(_ axis: Axes) -> Float { let raw = rawAxis(axis) let rescale = raw < 0 ? 1 / Float(-Int(Int16.min)) : 1 / Float(Int16.max) return Float(raw) * rescale } @inline(__always) func rawAxis(_ axis: Axes) -> Int16 { _axes[Int(axis.sdlEnum.rawValue)] } public func down(_ btn: Buttons) -> Bool { btn.isSubset(of: _btns) } public func pressed(_ btn: Buttons) -> Bool { btn.isSubset(of: _btns.intersection(_btnImpulse)) } public func released(_ btn: Buttons) -> Bool { btn.isSubset(of: _btnImpulse.subtracting(_btns)) } } public var name: String { String(cString: SDL_GetGamepadName(_sdlPad)) } public var state: State { .init( axes: self._axes, btns: self._btnCur, btnImpulse: self._btnCur.symmetricDifference(self._btnPrv)) } //MARK: - Private private var _joyInstance: SDL_JoystickID, _sdlPad: OpaquePointer private var _axes = [Int16](repeating: 0, count: Int(SDL_GAMEPAD_AXIS_COUNT.rawValue)) private var _btnCur: Buttons = [], _btnPrv: Buttons = [] internal var instanceID: SDL_JoystickID { _joyInstance } private init(instance: SDL_JoystickID, pad: OpaquePointer) { self._joyInstance = instance self._sdlPad = pad } internal static func open(joystickID: SDL_JoystickID) -> Self? { return if let sdlPad = SDL_OpenGamepad(joystickID) { .init(instance: joystickID, pad: sdlPad) } else { nil } } internal func close() { SDL_CloseGamepad(self._sdlPad) } internal mutating func buttonEvent(_ btn: SDL_GamepadButton, _ down: Bool) { if down { self._btnCur.formUnion(.init(rawValue: 1 << btn.rawValue)) } else { self._btnCur.subtract(.init(rawValue: 1 << btn.rawValue)) } } internal mutating func axisEvent(_ axis: SDL_GamepadAxis, _ value: Int16) { self._axes[Int(axis.rawValue)] = value } internal mutating func newTick() { self._btnPrv = self._btnCur } } public static func getPad(id: Int32) -> Pad? { _instance._pads[SDL_JoystickID(id)] ?? nil } @inline(__always) public static var current: Pad? { getPad(id: Int32(_instance._firstID)) } //MARK: - Private private static let _instance = GameController() public static var instance: GameController { _instance } private var _pads = Dictionary() private var _firstID: SDL_JoystickID = 0 internal func connectedEvent(id: SDL_JoystickID) { if _pads.keys.contains(id) { return } if let pad = Pad.open(joystickID: id) { printErr("Using gamepad #\(pad.instanceID), \"\(pad.name)\"") if self._firstID == 0 { self._firstID = id } self._pads[id] = pad } } internal func removedEvent(id: SDL_JoystickID) { if let pad = self._pads.removeValue(forKey: id) { pad.close() } if id == _firstID { _firstID = _pads.keys.first ?? 0 } } internal func buttonEvent(id: SDL_JoystickID, btn: SDL_GamepadButton, state: Bool) { _pads[id]?.buttonEvent(btn, state) } internal func axisEvent(id: SDL_JoystickID, axis: SDL_GamepadAxis, value: Int16) { _pads[id]?.axisEvent(axis, value) } internal func newFrame() { for idx in _pads.values.indices { _pads.values[idx].newTick() } } } //MARK: - Stick convenience functions public extension GameController.Pad.State { var leftStick: SIMD2 { .init(axis(.leftStickX), axis(.leftStickY)) } var rightStick: SIMD2 { .init(axis(.rightStickX), axis(.rightStickY)) } var leftTrigger: Float { axis(.leftTrigger) } var rightTrigger: Float { axis(.rightTrigger) } } public extension FloatingPoint { @inline(__always) internal func axisDeadzone(_ min: Self, _ max: Self) -> Self { let x = abs(self) return if x <= min { 0 } else if x >= max { .init(signOf: self, magnitudeOf: 1) } else { .init(signOf: self, magnitudeOf: x - min) / (max - min) } } } public extension SIMD2 where Scalar: FloatingPoint { func cardinalDeadzone(min: Scalar, max: Scalar) -> Self { .init(self.x.axisDeadzone(min, max), self.y.axisDeadzone(min, max)) } func radialDeadzone(min: Scalar, max: Scalar) -> Self { let magnitude = (x * x + y * y).squareRoot() if magnitude == .zero || magnitude < min { return .zero } else if magnitude > max { return self / magnitude } else { let rescale = (magnitude - min) / (max - min) return self / magnitude * rescale } } }