如何从 macOS 命令行工具使用 Apple 的 GameController 框架?

Pat*_*gan 6 macos cocoa objective-c core-foundation gamecontroller

我正在尝试将以下代码用作 macOS 命令行工具。重要的是这不是 Cocoa 应用程序,所以这不是一个选项。

相同的代码在具有 Cocoa App 目标的同一个项目中完美运行并检测到兼容的控制器,但是当作为命令行工具目标运行时,没有任何反应并且 API 显示没有连接控制器。

显然,其中一些是人为设计的……这只是我可以将其归结为最简单的方法,并在实际工作时对发生的事情有一些指示。

#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>


int main( int argc, const char * argv[] )
{
    @autoreleasepool
    {
        NSApplication * application = [NSApplication sharedApplication];

        NSNotificationCenter * center = [NSNotificationCenter defaultCenter];

        [center addObserverForName: GCControllerDidConnectNotification
                            object: nil
                             queue: nil
                        usingBlock: ^(NSNotification * note) {
                            GCController * controller = note.object;
                            printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
                        }
         ];

        [application finishLaunching];

        bool shouldKeepRunning = true;
        while (shouldKeepRunning)
        {
            printf( "." );

            while (true)
            {
                NSEvent * event = [application
                                   nextEventMatchingMask: NSEventMaskAny
                                   untilDate: nil
                                   inMode: NSDefaultRunLoopMode
                                   dequeue: YES];
                if (event == NULL)
                {
                    break;
                }
                else
                {
                    [application sendEvent: event];
                }
            }

            usleep( 100 * 1000 );
        }
    }

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我猜这与 Cocoa 应用程序的设置方式或事件循环的处理方式有关。或者可能有一些内部触发器初始化 GameController 框架。API 似乎没有任何明确的方法来初始化它。

https://developer.apple.com/documentation/gamecontroller?language=objc

任何人都可以阐明我如何让这个工作?

最终,这段代码真的需要在 Core Foundation 包中工作,所以如果它真的可以与 Core Foundation 运行循环一起工作,那将是理想的。

- 编辑 -

我做了一个测试项目来更清楚地说明问题。有两个构建目标。Cocoa 应用程序构建目标工作并接收控制器连接事件。另一个构建目标,只是一个简单的 CLI 应用程序,不起作用。它们都使用相同的源文件。它还包括两个代码路径,一个是传统的【NSApp run】,第二个是上面的手动事件循环。结果是一样的。

https://www.dropbox.com/s/a6fw3nuegq7bg8x/ControllerTest.zip?dl=0

Mig*_*nal 1

尽管每个线程都会创建一个运行循环(NSRunLoop对于 Cocoa 应用程序)来处理输入事件,但该循环不会自动启动。下面的代码使其随调用运行[application run]。当运行循环处理正确的事件时,将引发通知。我将观察者安装在应用程序委托中只是为了确保所有其他系统此时已完成初始化。

#import <Cocoa/Cocoa.h>
#import <GameController/GameController.h>

@interface AppDelegate : NSObject <NSApplicationDelegate> @end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSNotificationCenter * center = [NSNotificationCenter defaultCenter];
    [center addObserverForName: GCControllerDidConnectNotification
                        object: nil
                         queue: nil
                    usingBlock: ^(NSNotification * note) {
                        GCController * controller = note.object;
                        printf( "ATTACHED: %s\n", controller.vendorName.UTF8String );
                    }
     ];
}

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSApplication * application = [NSApplication sharedApplication]; // You can get rid of the variable and just use the global NSApp below instead
        AppDelegate *delegate = [[AppDelegate alloc] init];
        [application setDelegate:delegate];
        [application run];
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

更新 抱歉,我误解了这个问题。上面的代码适用于连接和断开控制器,但它无法使用[GCController controllers]应用程序启动时已连接的设备正确初始化阵列。

正如您所指出的,连接的设备在 Cocoa 应用程序上发送具有相同代码的通知,但在命令行上则不然。不同之处在于 Cocoa 应用程序会获取didBecomeActive通知,这会导致私有对象_GCControllerManager(负责处理由NSXPCConnection发布的 s 的对象GameControllerDaemon)接收CBApplicationDidBecomeActive填充controllers数组的消息。

无论如何,我尝试使命令行应用程序处于活动状态,以便它路由这些消息,但这不起作用;应用程序需要didBecomeActive在启动过程中尽早发送消息。

然后我尝试创建自己的_GCGameController并手动发送CBApplicationDidBecomeActive;这种方法是有效的,只是应用程序最终有两个这样的控制器,并且连接会重复。

我需要的是访问私有_GCGameController对象,但我不知道谁拥有它,所以我无法直接引用它。

所以最后,我采用了方法调配。下面的代码更改了终端应用程序初始化时调用的最后一个方法,_GCGameController startIdleWatchTimer因此它CBApplicationDidBecomeActive随后发送。

我知道使用各种苹果内部代码并不是一个很好的解决方案,但也许它可以帮助某人得到更好的东西。将以下代码添加到上一个代码中:

#import <objc/runtime.h>


@interface _GCControllerManager : NSObject
-(void) CBApplicationDidBecomeActive;
-(void) startIdleWatchTimer;
@end

@implementation _GCControllerManager (Extras)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(startIdleWatchTimer);
        SEL swizzledSelector = @selector(myStartIdleWatchTimer);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void) myStartIdleWatchTimer {
    [self myStartIdleWatchTimer];
    [self CBApplicationDidBecomeActive];
}

@end
Run Code Online (Sandbox Code Playgroud)