mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-02 21:00:57 +00:00
227 lines
7.8 KiB
Swift
227 lines
7.8 KiB
Swift
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<SDL_JoystickID, Pad>()
|
|
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<Float> {
|
|
.init(axis(.leftStickX), axis(.leftStickY))
|
|
}
|
|
var rightStick: SIMD2<Float> {
|
|
.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
|
|
}
|
|
}
|
|
}
|