"[GCController controllers]" does not contain any controllers that were connected prior to application launch
TLDR;
I am trying to implement gamepad input on macOS using the Game Controller Framework. When invoked in my code, [GameController controllers]
always returns an empty list until new controllers are connected. It never reflects gamepads connected to macOS prior to application launch, except if you disconnect them and reconnect them while the app is running. Does anyone know what I need to do to make controllers
populate with pre-launch connections?
Full question
Now that Apple has added support for Xbox and Playstation controllers to the GameController framework, I'm trying to use it for gamepad input on a C++ game engine I'm developing. I'm using the framework instead of IOKit in order to "future-proof" my games to support additional controller types in the future, as well as to simplify my own input handling code.
Like many other game engines, I've foregone using NSApplicationMain()
and nib files in favor of implementing my own event loop and setting up my game window programmatically. While my "Windows style" event loop appears to be working correctly, I've discovered that [GCController controllers]
does not. The array it returns is always empty at launch, and will only ever reflect controllers that are connected while the game is running. Disconnecting a pre-connected controller does not trigger my GCControllerDidDisconnectNotification
callback.
Here is a simplified version of my event loop:
int main(int argc, const char * argv[])
{
@autoreleasepool
{
// Create application
[NSApplication sharedApplication];
// Set up custom app delegate
CustomAppDelegate * delegate = [[CustomAppDelegate alloc] init];
[NSApp setDelegate:delegate];
// Activate and launch app
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp setPresentationOptions:NSApplicationPresentationDefault];
[NSApp activateIgnoringOtherApps:YES]; // Strictly speaking, not necessary
[NSApp finishLaunching]; // NSMenu is set up at this point in applicationWillFinishLaunching:.
// Initialize game engine (window is created here)
GenericEngineCode_Init(); // <-- Where I want to call [GCController controllers]
NSEvent *e;
do
{
do
{
// Pump messages
e = [NSApp nextEventMatchingMask: NSEventMaskAny
untilDate: nil
inMode: NSDefaultRunLoopMode
dequeue: YES];
if (e)
{
[NSApp sendEvent: e];
[NSApp updateWindows];
}
} while (e);
} while (GenericEngineCode_Run()); // Steps the engine, returns false when quitting.
GenericEngineCode_Cleanup();
}
return 0;
}
I've confirmed that even when using [NSApp run]
instead of [NSApp finishLaunching]
, the behavior is the same. As best as I can tell, the problem is that there's something NSApplicationMain()
does that I'm not doing, but that function is a black box -- I can't identify what I need to do to get controllers
to populate correctly. Does anyone know what I'm missing?
The closest thing I could find to an explanation of this problem is this answer, which suggests that my app isn't getting didBecomeActive
notifications, or that at the least, the private _GCControllerManager
isn't getting a CBApplicationDidBecomeActive
message. I'm not a professional macOS developer, though: I don't know if this actually applies to my situation, or how I'd go about correcting the problem if it does.
After a huge amount of time searching, I found the answer on my own. It turns out that my code wasn't the problem -- the problem was that my Info.plist
file was having its CFBundleIdentifier
value stripped out due to a problem with my build system. It appears that the Game Controller Framework needs the bundle identifier to correctly populate [GCController controllers]
at launch. While a missing CFBundleIdentifier
would have been a problem anyway, as a Windows person it didn't occur to me that the identifier might be used for things besides the App Store, so I let it slide until now.
If someone else has this problem, make sure that CFBundleIdentifier
isn't missing or empty in Info.plist
in your assembled app bundle. In my case with Premake, I had to manually set PRODUCT_BUNDLE_IDENTIFIER
with xcodebuildsettings
so that $(PRODUCT_BUNDLE_IDENTIFIER)
would get properly replaced in Info.plist
.