implement on screen virtual controller for iOS

This commit is contained in:
Alex Zenla 2024-09-14 23:39:46 -04:00
parent d7cb051fb7
commit 3b2a6ffb6a
No known key found for this signature in database
GPG Key ID: 067B238899B51269
3 changed files with 90 additions and 1 deletions

View File

@ -2,6 +2,10 @@ import Foundation
import SDL3
import QuartzCore.CAMetalLayer
#if canImport(GameController)
import GameController
#endif
public class Application {
private let cfg: ApplicationConfiguration
private var del: GameDelegate!
@ -12,12 +16,24 @@ public class Application {
private var lastCounter: UInt64 = 0
private var time: Duration = .zero
#if os(iOS)
private var onScreenVirtualController: GCVirtualController? = nil
private var onScreenVirtualControllerShown: Bool = false
#endif
public init(delegate: GameDelegate, configuration: ApplicationConfiguration) {
self.cfg = configuration
self.del = delegate
}
private func initialize() -> ApplicationExecutionState {
#if os(iOS)
if cfg.flags.contains(.onScreenVirtualController) {
onScreenVirtualController = initializeOnScreenVirtualController()
self.showVirtualGameController(true)
}
#endif
guard SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD) else {
printErr("SDL_Init() error: \(String(cString: SDL_GetError()))")
return .exitFailure
@ -31,6 +47,12 @@ public class Application {
if cfg.flags.contains(.highDPI) {
windowFlags |= SDL_WindowFlags(SDL_WINDOW_HIGH_PIXEL_DENSITY)
}
if cfg.flags.contains(.borderless) {
windowFlags |= SDL_WindowFlags(SDL_WINDOW_BORDERLESS)
}
if cfg.flags.contains(.fullscreen) {
windowFlags |= SDL_WindowFlags(SDL_WINDOW_FULLSCREEN)
}
window = SDL_CreateWindow(cfg.title, cfg.frame.w, cfg.frame.h, windowFlags)
guard window != nil else {
printErr("SDL_CreateWindow() error: \(String(cString: SDL_GetError()))")
@ -70,6 +92,22 @@ public class Application {
return .running
}
#if os(iOS)
private func initializeOnScreenVirtualController() -> GCVirtualController {
let configuration = GCVirtualController.Configuration()
configuration.elements = [
GCInputLeftThumbstick,
GCInputRightThumbstick,
GCInputLeftTrigger,
GCInputRightTrigger,
GCInputButtonA,
GCInputButtonB,
]
let controller = GCVirtualController(configuration: configuration)
return controller
}
#endif
private func deinitialize() {
self.del = nil
self.renderer = nil
@ -141,6 +179,33 @@ public class Application {
}
}
private func showVirtualGameController(_ shown: Bool) {
#if os(iOS)
guard let onScreenVirtualController = self.onScreenVirtualController else {
return
}
if shown {
if !onScreenVirtualControllerShown {
let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
Task.detached {
try? await onScreenVirtualController.connect()
semaphore.signal()
}
}
semaphore.wait()
onScreenVirtualControllerShown = true
}
} else {
if onScreenVirtualControllerShown {
onScreenVirtualController.disconnect()
onScreenVirtualControllerShown = false
}
}
#endif
}
private func update() -> ApplicationExecutionState {
let deltaTime = getDeltaTime()
time += deltaTime
@ -201,6 +266,9 @@ public struct ApplicationConfiguration {
static let resizable = Flags(rawValue: 1 << 0)
static let highDPI = Flags(rawValue: 1 << 1)
static let borderless = Flags(rawValue: 1 << 2)
static let fullscreen = Flags(rawValue: 1 << 3)
static let onScreenVirtualController = Flags(rawValue: 1 << 4)
}
public enum VSyncMode: Equatable {

View File

@ -36,5 +36,7 @@
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
</dict>
</plist>

View File

@ -4,14 +4,33 @@ import Foundation
@objc public static func run() -> Int32 {
Thread.current.qualityOfService = .userInteractive
var flags: ApplicationConfiguration.Flags = [ .resizable, .highDPI, .onScreenVirtualController ]
if enableFullscreenWindow() {
flags = flags.union(.fullscreen)
}
let app = Application(
delegate: Game(),
configuration: ApplicationConfiguration(
frame: Size(1280, 720),
title: "Voxelotl Demo",
flags: [ .resizable, .highDPI ],
flags: flags,
vsyncMode: .on(interval: 1)))
return app.run()
}
static func enableFullscreenWindow() -> Bool {
return Program.isFrontAndCenterGamingDevice()
}
static func isFrontAndCenterGamingDevice() -> Bool {
#if os(iOS)
return !(ProcessInfo.processInfo.isiOSAppOnMac || ProcessInfo.processInfo.isMacCatalystApp)
#elseif os(tvOS)
return true
#else
return false
#endif
}
}