[Unity]macOSでバックグラウンド(非フォーカス)時にキーボードの入力を受け取る

# [Unity]macOSでバックグラウンド(非フォーカス)時にキー入力を受け取る
UnityEngine.Inputではバックグラウンド（非フォーカス）時にキー入力を受け取れなかったので、バックグラウンド時にキー入力を受け取れるUnityのNative Pluginを作成しました
テキストエディットのアプリにフォーカスが当たって入力してますが、Unityでもキーボードの入力を検出できています

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">macのunityでフォーカス当たってない時もキーボードの入力の検知するやつ公開しましたっ！<a href="https://t.co/danGo8WHgG">https://t.co/danGo8WHgG</a> <a href="https://t.co/uKtZb6JKNX">pic.twitter.com/uKtZb6JKNX</a></p>&mdash; ふじき (@fzkqi) <a href="https://twitter.com/fzkqi/status/1548268632546824200?ref_src=twsrc%5Etfw">July 16, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 作成したもの
ソースコードとunitypackageを公開しています

https://github.com/fuziki/UnityAppEventMonitor


# macOSでバックグラウンド(非フォーカス)時にキー入力を受け取る実装
[NSEvent.addGlobalMonitorForEvents](https://developer.apple.com/documentation/appkit/nsevent/1535472-addglobalmonitorforevents)を使うとmacOSでバックグラウンドでキー入力を受け取る事が可能です

```swift
import AppKit
// start
let eventMonitor: Any? = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { (event: NSEvent) in
    print("event: \(event)")
}
// stop
NSEvent.removeMonitor(eventMonitor)
```

## バックグラウンド時にキー入力を受け取るロジックの実装

jsonで設定を受け取って、addGlobalMonitorForEventsし、モニタリングしたeventをjsonにして返します

```swift
import AppKit
import Foundation

public struct UnityAppEventMonitorConfiguration: Decodable {
    public let targets: UInt64
}

public struct UnityAppEventMonitorEvent: Encodable {
    public let characters: String
}

public class UnityAppEventMonitor {
    public static let shared = UnityAppEventMonitor()

    private var handler: ((String) -> Void)?
    private var eventMonitor: Any?

    public func start(configuration: String,
                      handler: @escaping (String) -> Void) {
        stop()
        guard let data = configuration.data(using: .utf8),
              let config = try? JSONDecoder().decode(UnityAppEventMonitorConfiguration.self, from: data) else {
            return
        }
        self.handler = handler
        let mask = NSEvent.EventTypeMask(rawValue: config.targets)
        addGlobalMonitor(matching: mask)
    }

    private func addGlobalMonitor(matching mask: NSEvent.EventTypeMask) {
        eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { [weak self] (event: NSEvent) in
            let res = UnityAppEventMonitorEvent(characters: event.characters ?? "")
            guard let data = try? JSONEncoder().encode(res),
                  let str = String(data: data, encoding: .utf8) else {
                return
            }
            self?.handler?(str)
        }
    }

    public func stop() {
        if let eventMonitor = eventMonitor {
            NSEvent.removeMonitor(eventMonitor)
        }
        handler = nil
        eventMonitor = nil
    }
}
```

## UnityのInterfaceの実装

上で作成したAppEventMonitorを呼び出すためのCのインタフェースを定義します

```swift
@_cdecl("AppEventMonitor_Start")
public func AppEventMonitor_Start(_ configStrPtr: UnsafePointer<CChar>?,
                                  _ handler: @escaping @convention(c) (UnsafePointer<CChar>) -> Void) {
    let configStr = String(cString: configStrPtr!)
    UnityAppEventMonitor.shared.start(configuration: configStr) { (resStr: String) in
        let resNsStr = resStr as NSString
        let resNsStrPtr: UnsafePointer<CChar> = resNsStr.utf8String!
        let resNsStrPtrDup: UnsafeMutablePointer<CChar> = strdup(resNsStrPtr)
        handler(resNsStrPtrDup)
    }
}
@_cdecl("AppEventMonitor_Stop")
public func AppEventMonitor_Stop() {
    UnityAppEventMonitor.shared.stop()
}
```

## bundleの作成
bundleターゲットを追加してビルドします
詳しくはこちら↓

https://qiita.com/fuziki/items/974f70b663ebfadfb136

## C#の実装

Swiftで用意したCのインタフェースの関数の定義と、コールバック用の関数を作成します
コールバックが呼ばれたらパースしてActionに流します

```c#
using AOT;
using System;
using System.Runtime.InteropServices;
using UnityEngine;

namespace AppEventMonitor
{
    public static class AppEventMonitorManager
    {
        [Serializable]
        class AppEventMonitorEvent
        {
            public string characters;
        }

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate void AppEventMonitor_CallbackType([MarshalAs(UnmanagedType.LPStr), In] string message);

        [DllImport("AppEventMonitor.bundle")]
        private static extern void AppEventMonitor_Start(string config, [MarshalAs(UnmanagedType.FunctionPtr)] AppEventMonitor_CallbackType callback);

        [MonoPInvokeCallback(typeof(AppEventMonitor_CallbackType))]
        private static void AppEventMonitor_CallbackTypeHandler(string message)
        {
            var e = JsonUtility.FromJson<AppEventMonitorEvent>(message);
            OnKeyDown.Invoke(e.characters);
        }

        [DllImport("AppEventMonitor.bundle")]
        private static extern void AppEventMonitor_Stop();

        /// <summary>
        /// Detect on key down.
        /// </summary>
        public static event Action<string> OnKeyDown;

        /// <summary>
        /// Start monitoring.
        /// </summary>
        public static void Start()
        {
            // detect keydown
            string config = "{ \"targets\" : 1024 }";
            AppEventMonitor_Start(config, AppEventMonitor_CallbackTypeHandler);
        }

        /// <summary>
        /// Stop monitoring.
        /// </summary>
        public static void Stop()
        {
            AppEventMonitor_Stop();
        }
    }
}
```

## 作成した関数の呼び出し

Actionを登録してStartします

```C#
using AppEventMonitor;

public class Cube : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        AppEventMonitorManager.OnKeyDown += OnKeyDown;

        Debug.Log("Start!!");
        AppEventMonitorManager.Start();
    }

    void OnDestroy()
    {
        Debug.Log("Stop!!");
        AppEventMonitorManager.Stop();
    }

    private void OnKeyDown(string key)
    {
        Debug.Log($"OnKeyDown: {key}");
    }
}
```

## アクセシビリティの登録

開いている間は設定が更新されないので、Unity.app（とUnity Hub.app）を終了します
システム環境設定 > セキュリティとプライバシー > プライバシー > アクセシビリティにUnity.appを追加します
もしUnity Hubを利用している場合は、Unity Hub.appも追加します

![accessibility.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/3ca3853b-6e72-2d00-f2c3-56bb932734ff.png)

## 動作確認

Unityを実行し、Unity以外にフォーカスがある時にキーボードに入力した時に検出できていれば完成です

# 終わりに

Zoomにアバターで参加している時にアバターを動かすために、バックグランドでもUnityにキー入力をしたくて実装を始めました
アクセシビリティ周りの話があるので、複雑かなと思ったのですが、Unity.app追加するだけで良く、安心しました
キーボードの押下だけでなく、キーボードを離した時やマウスの動きでも検出できるようなので、拡張していきたいです