From 204fbb18147281116feecb24f80ff2402c9dbf72 Mon Sep 17 00:00:00 2001 From: Harold Serrano Date: Sat, 17 Jan 2026 06:32:21 -0700 Subject: [PATCH 1/3] [Patch]fixed crash with loading scene with async --- .../UntoldEngine/Scenes/SceneSerializer.swift | 74 ++++++++++--------- .../Systems/RegistrationSystem.swift | 69 +++++++++-------- 2 files changed, 81 insertions(+), 62 deletions(-) diff --git a/Sources/UntoldEngine/Scenes/SceneSerializer.swift b/Sources/UntoldEngine/Scenes/SceneSerializer.swift index 255e2ba5..0708772e 100644 --- a/Sources/UntoldEngine/Scenes/SceneSerializer.swift +++ b/Sources/UntoldEngine/Scenes/SceneSerializer.swift @@ -652,25 +652,29 @@ public func deserializeScene(sceneData: SceneData, meshLoadingMode: MeshLoadingM } case .asyncDefault: setEntityMeshAsync(entityId: entityId, filename: filename, withExtension: withExtension, assetName: nil) { success in - if success { - Logger.log(message: "✅ Asset instance '\(sceneDataEntity.name)' loaded") - // Apply overrides after async import completes - applyAssetInstanceOverrides(entityId: entityId, overrides: assetInstance.overrides) - - // Setup animations (skeleton is now available) - if sceneDataEntity.hasAnimationComponent == true { - for animations in sceneDataEntity.animations { - let animationFilename = animations.deletingPathExtension().lastPathComponent - let animationFilenameExt = animations.pathExtension - setEntityAnimations(entityId: entityId, filename: animationFilename, withExtension: animationFilenameExt, name: animationFilename) - changeAnimation(entityId: entityId, name: animationFilename) - } - if let animationComponent = scene.get(component: AnimationComponent.self, for: entityId) { - animationComponent.animationsFilenames = sceneDataEntity.animations + Task { + await MainActor.run { + if success { + Logger.log(message: "✅ Asset instance '\(sceneDataEntity.name)' loaded") + // Apply overrides after async import completes + applyAssetInstanceOverrides(entityId: entityId, overrides: assetInstance.overrides) + + // Setup animations (skeleton is now available) + if sceneDataEntity.hasAnimationComponent == true { + for animations in sceneDataEntity.animations { + let animationFilename = animations.deletingPathExtension().lastPathComponent + let animationFilenameExt = animations.pathExtension + setEntityAnimations(entityId: entityId, filename: animationFilename, withExtension: animationFilenameExt, name: animationFilename) + changeAnimation(entityId: entityId, name: animationFilename) + } + if let animationComponent = scene.get(component: AnimationComponent.self, for: entityId) { + animationComponent.animationsFilenames = sceneDataEntity.animations + } + } + } else { + Logger.logWarning(message: "❌ Asset instance '\(sceneDataEntity.name)' failed to load") } } - } else { - Logger.logWarning(message: "❌ Asset instance '\(sceneDataEntity.name)' failed to load") } } } @@ -699,24 +703,28 @@ public func deserializeScene(sceneData: SceneData, meshLoadingMode: MeshLoadingM let fallbackLabel = withExtension.isEmpty ? filename : "\(filename).\(withExtension)" let meshLabel = sceneDataEntity.name.isEmpty ? fallbackLabel : sceneDataEntity.name setEntityMeshAsync(entityId: entityId, filename: filename, withExtension: withExtension, assetName: sceneDataEntity.assetName) { success in - applyLocalTransform() - if success { - Logger.log(message: "✅ Mesh loaded for \(meshLabel)") - - // Setup animations (skeleton is now available) - if sceneDataEntity.hasAnimationComponent == true { - for animations in sceneDataEntity.animations { - let animationFilename = animations.deletingPathExtension().lastPathComponent - let animationFilenameExt = animations.pathExtension - setEntityAnimations(entityId: entityId, filename: animationFilename, withExtension: animationFilenameExt, name: animationFilename) - changeAnimation(entityId: entityId, name: animationFilename) - } - if let animationComponent = scene.get(component: AnimationComponent.self, for: entityId) { - animationComponent.animationsFilenames = sceneDataEntity.animations + Task { + await MainActor.run { + applyLocalTransform() + if success { + Logger.log(message: "✅ Mesh loaded for \(meshLabel)") + + // Setup animations (skeleton is now available) + if sceneDataEntity.hasAnimationComponent == true { + for animations in sceneDataEntity.animations { + let animationFilename = animations.deletingPathExtension().lastPathComponent + let animationFilenameExt = animations.pathExtension + setEntityAnimations(entityId: entityId, filename: animationFilename, withExtension: animationFilenameExt, name: animationFilename) + changeAnimation(entityId: entityId, name: animationFilename) + } + if let animationComponent = scene.get(component: AnimationComponent.self, for: entityId) { + animationComponent.animationsFilenames = sceneDataEntity.animations + } + } + } else { + Logger.logWarning(message: "❌ Mesh failed for \(meshLabel)") } } - } else { - Logger.logWarning(message: "❌ Mesh failed for \(meshLabel)") } } } diff --git a/Sources/UntoldEngine/Systems/RegistrationSystem.swift b/Sources/UntoldEngine/Systems/RegistrationSystem.swift index 3c1c7178..a6411e66 100644 --- a/Sources/UntoldEngine/Systems/RegistrationSystem.swift +++ b/Sources/UntoldEngine/Systems/RegistrationSystem.swift @@ -227,7 +227,9 @@ public func setEntityMeshAsync( handleError(.filenameNotFound, filename) await loadFallbackMesh(entityId: entityId, filename: filename) await AssetLoadingState.shared.finishLoading(entityId: entityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } @@ -235,7 +237,9 @@ public func setEntityMeshAsync( handleError(.fileTypeNotSupported, url.pathExtension) await loadFallbackMesh(entityId: entityId, filename: filename) await AssetLoadingState.shared.finishLoading(entityId: entityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } @@ -256,7 +260,9 @@ public func setEntityMeshAsync( handleError(.assetDataMissing, filename) await loadFallbackMesh(entityId: entityId, filename: filename) await AssetLoadingState.shared.finishLoading(entityId: entityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } @@ -269,7 +275,9 @@ public func setEntityMeshAsync( handleError(.assetDataMissing, "No mesh with asset name \(assetNameExist)") await loadFallbackMesh(entityId: entityId, filename: filename) await AssetLoadingState.shared.finishLoading(entityId: entityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } } @@ -375,7 +383,9 @@ public func setEntityMeshAsync( } await AssetLoadingState.shared.finishLoading(entityId: entityId) - completion?(true) + await MainActor.run { + completion?(true) + } } } @@ -486,14 +496,18 @@ public func loadSceneAsync( guard let url = LoadingSystem.shared.resourceURL(forResource: filename, withExtension: withExtension, subResource: nil) else { handleError(.filenameNotFound, filename) await AssetLoadingState.shared.finishLoading(entityId: sceneLoadEntityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } if url.pathExtension == "dae" { handleError(.fileTypeNotSupported, url.pathExtension) await AssetLoadingState.shared.finishLoading(entityId: sceneLoadEntityId) - completion?(false) + await MainActor.run { + completion?(false) + } return } @@ -510,39 +524,36 @@ public func loadSceneAsync( } // Process on main thread + var didLoadMeshes = true await MainActor.run { if meshes.isEmpty { handleError(.assetDataMissing, filename) - Task { - await AssetLoadingState.shared.finishLoading(entityId: sceneLoadEntityId) - } - completion?(false) + didLoadMeshes = false return } - for mesh in meshes { - if mesh.count > 0 { - let entityId = createEntity() + for mesh in meshes where mesh.count > 0 { + let entityId = createEntity() - if hasComponent(entityId: entityId, componentType: LocalTransformComponent.self) == false { - registerTransformComponent(entityId: entityId) - } - - if hasComponent(entityId: entityId, componentType: ScenegraphComponent.self) == false { - registerSceneGraphComponent(entityId: entityId) - } + if hasComponent(entityId: entityId, componentType: LocalTransformComponent.self) == false { + registerTransformComponent(entityId: entityId) + } - associateMeshesToEntity(entityId: entityId, meshes: mesh) - registerRenderComponent(entityId: entityId, meshes: mesh, url: url, assetName: mesh.first!.assetName) - setEntityName(entityId: entityId, name: mesh.first!.assetName) - setEntitySkeleton(entityId: entityId, filename: filename, withExtension: withExtension) + if hasComponent(entityId: entityId, componentType: ScenegraphComponent.self) == false { + registerSceneGraphComponent(entityId: entityId) } - } - Task { - await AssetLoadingState.shared.finishLoading(entityId: sceneLoadEntityId) + associateMeshesToEntity(entityId: entityId, meshes: mesh) + registerRenderComponent(entityId: entityId, meshes: mesh, url: url, assetName: mesh.first!.assetName) + setEntityName(entityId: entityId, name: mesh.first!.assetName) + setEntitySkeleton(entityId: entityId, filename: filename, withExtension: withExtension) } - completion?(true) + } + + await AssetLoadingState.shared.finishLoading(entityId: sceneLoadEntityId) + + await MainActor.run { + completion?(didLoadMeshes) } } } From 41d8f83fbe3ebefdc7814b1a257c53952c660b1d Mon Sep 17 00:00:00 2001 From: Harold Serrano Date: Sat, 17 Jan 2026 07:06:56 -0700 Subject: [PATCH 2/3] [Patch]added support for gamepad for more keys --- .../UntoldEngine/Systems/InputSystem.swift | 97 ++++++++++++++++--- .../InputSystemExtensionsTest.swift | 4 +- Tests/UntoldEngineTests/InputSystemTest.swift | 6 +- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/Sources/UntoldEngine/Systems/InputSystem.swift b/Sources/UntoldEngine/Systems/InputSystem.swift index 3bae70f5..e0115717 100644 --- a/Sources/UntoldEngine/Systems/InputSystem.swift +++ b/Sources/UntoldEngine/Systems/InputSystem.swift @@ -11,10 +11,32 @@ import Foundation import GameController import simd -public struct GamePadState { +public struct GameControllerState { public var aPressed = false public var bPressed = false + public var xPressed = false + public var yPressed = false + + public var dpadUpPressed = false + public var dpadDownPressed = false + public var dpadLeftPressed = false + public var dpadRightPressed = false + + public var leftShoulderPressed = false + public var rightShoulderPressed = false + public var leftTriggerPressed = false + public var rightTriggerPressed = false + public var leftTriggerValue: Float = 0 + public var rightTriggerValue: Float = 0 + + public var leftThumbstickX: Float = 0 + public var leftThumbstickY: Float = 0 + public var rightThumbstickX: Float = 0 + public var rightThumbstickY: Float = 0 public var leftThumbStickActive = false + public var rightThumbStickActive = false + public var leftThumbstickPressed = false + public var rightThumbstickPressed = false } public enum PanGestureState { case began, changed, ended } @@ -37,8 +59,8 @@ public final class InputSystem { public let kVK_ANSI_Space: UInt16 = 49, kVK_ANSI_J: UInt16 = 38, kVK_ANSI_K: UInt16 = 40 public var keyState = KeyState() - public var gamePadState = GamePadState() - public var currentGamepad: GCExtendedGamepad? + public var gameControllerState = GameControllerState() + public var currentGameController: GCExtendedGamepad? // Shared state public var currentPanGestureState: PanGestureState? @@ -55,9 +77,9 @@ public final class InputSystem { public var pinchDelta: simd_float3 = .init(0, 0, 0) public var previousScale: CGFloat = 1 - init() { setupGameController() } + init() { registerGameControllerEvents() } - private func setupGameController() { + public func registerGameControllerEvents() { NotificationCenter.default.addObserver(self, selector: #selector(controllerDidConnect(_:)), name: .GCControllerDidConnect, object: nil) @@ -70,19 +92,68 @@ public final class InputSystem { } @objc private func controllerDidConnect(_ note: Notification) { - guard let controller = note.object as? GCController, let gamepad = controller.extendedGamepad else { return } - currentGamepad = gamepad - configureGamepadHandlers(gamepad) + guard let controller = note.object as? GCController, let gameController = controller.extendedGamepad else { return } + currentGameController = gameController + configureGameControllerHandlers(gameController) + Logger.log(message: "Game Controller \(controller.vendorName ?? "unknown vendor") connected and Configured") } @objc private func controllerDidDisconnect(_ note: Notification) { guard let controller = note.object as? GCController else { return } - if currentGamepad === controller.extendedGamepad { currentGamepad = nil } + if currentGameController === controller.extendedGamepad { currentGameController = nil } + Logger.log(message: "Game Controller \(controller.vendorName ?? "unknown vendor") disconnected") } - private func configureGamepadHandlers(_ gamepad: GCExtendedGamepad) { - gamepad.buttonA.pressedChangedHandler = { [weak self] _, _, pressed in self?.gamePadState.aPressed = pressed } - gamepad.buttonB.pressedChangedHandler = { [weak self] _, _, pressed in self?.gamePadState.bPressed = pressed } - // add thumbstick/trigger mapping as needed… + private func configureGameControllerHandlers(_ gameController: GCExtendedGamepad) { + gameController.buttonA.pressedChangedHandler = { [weak self] _, _, pressed in self?.gameControllerState.aPressed = pressed } + gameController.buttonB.pressedChangedHandler = { [weak self] _, _, pressed in self?.gameControllerState.bPressed = pressed } + gameController.buttonX.pressedChangedHandler = { [weak self] _, _, pressed in self?.gameControllerState.xPressed = pressed } + gameController.buttonY.pressedChangedHandler = { [weak self] _, _, pressed in self?.gameControllerState.yPressed = pressed } + + gameController.dpad.valueChangedHandler = { [weak self] _, xValue, yValue in + guard let self else { return } + gameControllerState.dpadUpPressed = yValue > 0.5 + gameControllerState.dpadDownPressed = yValue < -0.5 + gameControllerState.dpadLeftPressed = xValue < -0.5 + gameControllerState.dpadRightPressed = xValue > 0.5 + } + + gameController.leftShoulder.pressedChangedHandler = { [weak self] _, _, pressed in + self?.gameControllerState.leftShoulderPressed = pressed + } + gameController.rightShoulder.pressedChangedHandler = { [weak self] _, _, pressed in + self?.gameControllerState.rightShoulderPressed = pressed + } + + gameController.leftTrigger.valueChangedHandler = { [weak self] _, value, pressed in + guard let self else { return } + gameControllerState.leftTriggerValue = value + gameControllerState.leftTriggerPressed = pressed + } + gameController.rightTrigger.valueChangedHandler = { [weak self] _, value, pressed in + guard let self else { return } + gameControllerState.rightTriggerValue = value + gameControllerState.rightTriggerPressed = pressed + } + + gameController.leftThumbstick.valueChangedHandler = { [weak self] _, xValue, yValue in + guard let self else { return } + gameControllerState.leftThumbstickX = xValue + gameControllerState.leftThumbstickY = yValue + gameControllerState.leftThumbStickActive = abs(xValue) > 0.1 || abs(yValue) > 0.1 + } + gameController.rightThumbstick.valueChangedHandler = { [weak self] _, xValue, yValue in + guard let self else { return } + gameControllerState.rightThumbstickX = xValue + gameControllerState.rightThumbstickY = yValue + gameControllerState.rightThumbStickActive = abs(xValue) > 0.1 || abs(yValue) > 0.1 + } + + gameController.leftThumbstickButton?.pressedChangedHandler = { [weak self] _, _, pressed in + self?.gameControllerState.leftThumbstickPressed = pressed + } + gameController.rightThumbstickButton?.pressedChangedHandler = { [weak self] _, _, pressed in + self?.gameControllerState.rightThumbstickPressed = pressed + } } } diff --git a/Tests/UntoldEngineTests/InputSystemExtensionsTest.swift b/Tests/UntoldEngineTests/InputSystemExtensionsTest.swift index 1cc8bfa5..726116a1 100644 --- a/Tests/UntoldEngineTests/InputSystemExtensionsTest.swift +++ b/Tests/UntoldEngineTests/InputSystemExtensionsTest.swift @@ -19,8 +19,8 @@ final class InputSystemExtensionsTests: XCTestCase { input.delegate = nil input.keyState = KeyState() - input.gamePadState = GamePadState() - input.currentGamepad = nil + input.gameControllerState = GameControllerState() + input.currentGameController = nil input.currentPanGestureState = nil input.currentPinchGestureState = nil diff --git a/Tests/UntoldEngineTests/InputSystemTest.swift b/Tests/UntoldEngineTests/InputSystemTest.swift index 123dd4c4..7e53d4d7 100644 --- a/Tests/UntoldEngineTests/InputSystemTest.swift +++ b/Tests/UntoldEngineTests/InputSystemTest.swift @@ -19,8 +19,8 @@ final class InputSystemTests: XCTestCase { input.delegate = nil input.keyState = KeyState() - input.gamePadState = GamePadState() - input.currentGamepad = nil + input.gameControllerState = GameControllerState() + input.currentGameController = nil input.currentPanGestureState = nil input.currentPinchGestureState = nil @@ -146,7 +146,7 @@ final class InputSystemTests: XCTestCase { // MARK: - GamePadState (no hardware) func test_manual_gamepad_state_flip() { - var gp = InputSystem.shared.gamePadState + var gp = InputSystem.shared.gameControllerState XCTAssertFalse(gp.aPressed) XCTAssertFalse(gp.bPressed) XCTAssertFalse(gp.leftThumbStickActive) From 67cbebdab52c18378065b01046d1c5446adbde43 Mon Sep 17 00:00:00 2001 From: Harold Serrano Date: Sat, 17 Jan 2026 07:25:47 -0700 Subject: [PATCH 3/3] [docs] added game controller input docs --- .../03-Engine Systems/UsingInputSystem.md | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md b/docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md index 4c994f85..1c73ff69 100644 --- a/docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md +++ b/docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md @@ -9,21 +9,39 @@ sidebar_position: 6 The Input System in the Untold Engine allows you to detect user inputs, such as keystrokes and mouse movements, to control entities and interact with the game. This guide will explain how to use the Input System effectively. -## How to Use the Input System +## How to Use the Input System (Keyboard) ### Step 1: Detect Keystrokes + To detect if a specific key is pressed, use the keyState object from the Input System. Example: Detecting the 'W' Key ```swift -if inputSystem.keyState.wPressed == true { - // Your code here +func init(){ +// Make sure that you have enabled keyevents in your init function: +InputSystem.shared.registerKeyboardEvents() +} + +// Then in the handleInput callback, you can do this: + +func handleInput() { + // Skip logic if not in game mode + if gameMode == false { return } + + let inputSystem = InputSystem.shared + + // Handle input here + if inputSystem.keyState.wPressed{ + Logger.log(message: "w pressed") + } } ``` You can use the same logic for other keys like A, S, and D: ```swift +let inputSystem = InputSystem.shared + if inputSystem.keyState.aPressed == true { // Move left } @@ -43,6 +61,9 @@ Here’s an example function that moves a car entity based on keyboard inputs: ```swift func moveCar(entityId: EntityID, dt: Float) { + + let inputSystem = InputSystem.shared + // Ensure we are in game mode if gameMode == false { return @@ -75,6 +96,32 @@ func moveCar(entityId: EntityID, dt: Float) { } ``` +## How to Use the Input System with a Game Controller + +To detect if a specific button is pressed, use the gameControllerState object from the Input System. + +Example: Detecting the 'A' button + +```swift +func init(){ +// Make sure that you have enabled game controller events in your init function: + InputSystem.shared.registerGameControllerEvents() +} + +// Then in the handleInput callback, you can do this: + +func handleInput() { + // Skip logic if not in game mode + if gameMode == false { return } + let inputSystem = InputSystem.shared + + // Handle input here + if inputSystem.gameControllerState.aPressed { + Logger.log(message: "Pressed A key") + } +} +``` + --- ## Tips and Best Practices