VisionOS -> Gamepads with Input System vs. Input Manager?

For sanity purposes, can anyone confirm whether gamepad (Playstation or Xbox) controllers work in Unity/VisionOS using Unity’s newer Input System method? I can only seem to get the older Input Manager to connect to my controllers on the AVP in a packaged build.

Gamepad with new input system is working well in my game. (Tested Xbox controller)
PolySpatial 1.1, 1.2, 1.3 and visionOS 1 & 2

1 Like

Thank you. So weird I can’t get it to work… I’m on pre.11 but I can’t imagine that is what is blocking me. Will try in a new project. :slight_smile:

By any chance, in non-metal compositor builds (not full immersion) do you have any issues with input getting highjacked by gaze using mixed or volumes? I literally have to look away to get my controllers to get picked up… the opposite of what you want think… I would expect I could look at something and the input on the controllerworks… but I can only look away from object, move the controller and look back and the controller is working.

I had the same problem with VisionOS 2.0. Game controller input would no longer be sent to my window. If I turned to look away from the window, started pressing buttons or moving the stick, and then turned back then input would work. This is with Unity 2022 and 6.0.

The problem is VisionOS 2.0 added the ability to use the game controller with the OS UI. To enable game views to also receive input, you need to add a GCEventInteraction to your view’s interactions array. It’s mentioned in the VisionOS 2.0 release notes here.

Here is the code I added to my UnityAppController to fix it. This is in the startUnity function.

GCEventInteraction* gamepadInteraction = [[GCEventInteraction alloc] init];
gamepadInteraction.handledEventTypes = GCUIEventTypeGamepad;
NSMutableArray* interactions = [NSMutableArray array];
[interactions addObject:gamepadInteraction];
[interactions addObjectsFromArray:_rootView.interactions];
_rootView.interactions = interactions;

Sounds like this might be the same problem you are running into so hopefully this helps.

2 Likes

Yes! This is exactly what i was looking for! Thank you for passsing along. :slight_smile:

Is this where it goes? :slight_smile:

- (void)startUnity:(UIApplication*)application
{
    NSAssert(_unityAppReady == NO, @"[UnityAppController startUnity:] called after Unity has been initialized");

    UnityInitApplicationGraphics();

#if !PLATFORM_VISIONOS
    // we make sure that first level gets correct display list and orientation
    [[DisplayManager Instance] updateDisplayListCacheInUnity];
    
    GCEventInteraction* gamepadInteraction = [[GCEventInteraction alloc] init];
    gamepadInteraction.handledEventTypes = GCUIEventTypeGamepad;
    NSMutableArray* interactions = [NSMutableArray array];
    [interactions addObject:gamepadInteraction];
    [interactions addObjectsFromArray:_rootView.interactions];
    _rootView.interactions = interactions;
    
#endif

Yes, that should work.

Weird. I’ll give it another shot with clean build but when I tested this I still had to look away to get focus. Clean build might help. Thanks again!

You probably want to put it outside the “#if !PLATFORM_VISIONOS” block. Otherwise, it won’t compile on visionOS.

#if !PLATFORM_VISIONOS
    // we make sure that first level gets correct display list and orientation
    [[DisplayManager Instance] updateDisplayListCacheInUnity];
#endif

    GCEventInteraction* gamepadInteraction = [[GCEventInteraction alloc] init];
    gamepadInteraction.handledEventTypes = GCUIEventTypeGamepad;
    NSMutableArray* interactions = [NSMutableArray array];
    [interactions addObject:gamepadInteraction];
    [interactions addObjectsFromArray:_rootView.interactions];
    _rootView.interactions = interactions;

I tried a clean build and it didn’t work. When I moved the block outside of the if visionos I get this error. Doesn’t recognize GCEventInteraction

Try importing <GameController/GameController.h> at the top of the file:

#import <GameController/GameController.h>
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CADisplayLink.h>
#import <Availability.h>
#import <AVFoundation/AVFoundation.h>
1 Like

I gave it another try and managed to reproduce the issue with my game. Here’s how I resolved it:

  • In MainApp/UnityVisionOSSettings.swift add this: import GameController
  • For each PolySpatialContentViewWrapper add this modifier: .handlesGameControllerEvents(matching: .gamepad)

Done!

So for example:

WindowGroup(id: "Bounded-0.500x0.500x0.500", for: UUID.self) { uuid in
    PolySpatialContentViewWrapper()
        .environment(\.pslWindow, PolySpatialWindow(uuid.wrappedValue, "Bounded-0.500x0.500x0.500", .init(0.500, 0.500, 0.500)))
        .handlesGameControllerEvents(matching: .gamepad)
    
    KeyboardTextField().frame(width: 0, height: 0).modifier(LifeCycleHandlerModifier())
} defaultValue: { UUID() } .windowStyle(.volumetric).defaultSize(width: 0.500, height: 0.500, depth: 0.500, in: .meters) .upperLimbVisibility(.visible)

ImmersiveSpace(id: "Unbounded", for: UUID.self) { uuid in
    PolySpatialContentViewWrapper()
        .environment(\.pslWindow, PolySpatialWindow(uuid.wrappedValue, "Unbounded", .init(1.000, 1.000, 1.000)))
        .handlesGameControllerEvents(matching: .gamepad)
    
    KeyboardTextField().frame(width: 0, height: 0).modifier(LifeCycleHandlerModifier())
} defaultValue: { UUID() }  .upperLimbVisibility(.visible)
.immersionStyle(selection: .constant(.mixed), in: .mixed)

For more information see.

1 Like

Better Solution
This post-build script automatically applies the gamepad fix while maintaining compatibility with visionOS versions prior to 2.0:

    public class VisionOSGameControllerFixBuildProcessor : IPostprocessBuildWithReport
    {
        public int callbackOrder => 999;

        public void OnPostprocessBuild(BuildReport report)
        {
            DoPostprocessBuild(report);
        }

        [Conditional("UNITY_VISIONOS")]
        private static void DoPostprocessBuild(BuildReport report)
        {
            var outputPath = report.summary.outputPath;
            var swiftFilePath = Path.Combine(outputPath, "MainApp", "UnityVisionOSSettings.swift");

            var swiftContent = File.ReadAllText(swiftFilePath);

            var modifier = $@"struct GameControllerCompatibilityModifier: ViewModifier {{
        func body(content: Content) -> some View {{
            if #available(visionOS 2, *) {{
                content.handlesGameControllerEvents(matching: .gamepad)
            }} else {{
                content
            }}
        }}
    }}
}}
";
            var i = swiftContent.LastIndexOf("}", StringComparison.Ordinal);
            swiftContent = swiftContent.Remove(i, 1);
            swiftContent += modifier;

            swiftContent = "import GameController\n" + swiftContent;
            swiftContent = swiftContent.Replace("PolySpatialContentViewWrapper()",
                "PolySpatialContentViewWrapper().modifier(GameControllerCompatibilityModifier())");

            File.WriteAllText(swiftFilePath, swiftContent);
        }
    }
1 Like

Unfortunately, the call to PolySpatialContentViewWrapper in my generated code has parameters and a couple of modifiers:

                        PolySpatialContentViewWrapper(minSize: .init(1.000, 1.000, 1.000), maxSize: .init(1.000, 1.000, 1.000))
                            .environment(\.pslWindow, PolySpatialWindow(uuid.wrappedValue, "Unbounded", .init(1.000, 1.000, 1.000)))
                            .onImmersionChange() { oldContext, newContext in
                                PolySpatialWindowManagerAccess.onImmersionChange(oldContext.amount, newContext.amount)
                            }

I ended up replacing:

            swiftContent = swiftContent.Replace("PolySpatialContentViewWrapper()",
                "PolySpatialContentViewWrapper().modifier(GameControllerCompatibilityModifier())");

with:

            swiftContent = Regex.Replace(swiftContent, @"(newContext\.amount\)\n\s*})",
                "$1\n                            .modifier(GameControllerCompatibilityModifier())\n");

This was the only tweak I needed. Otherwise your code worked great and saved my day. Thanks!

1 Like