diff --git a/Sources/Voxelotl/Application.swift b/Sources/Voxelotl/Application.swift index 1288ed9..3c57e72 100644 --- a/Sources/Voxelotl/Application.swift +++ b/Sources/Voxelotl/Application.swift @@ -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 { diff --git a/Sources/Voxelotl/Info.plist.in b/Sources/Voxelotl/Info.plist.in index 4977f0d..480fae9 100644 --- a/Sources/Voxelotl/Info.plist.in +++ b/Sources/Voxelotl/Info.plist.in @@ -36,5 +36,7 @@ UIApplicationSupportsIndirectInputEvents + UIRequiresFullScreen + diff --git a/Sources/Voxelotl/Program.swift b/Sources/Voxelotl/Program.swift index 576e156..b6fb183 100644 --- a/Sources/Voxelotl/Program.swift +++ b/Sources/Voxelotl/Program.swift @@ -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 + } }