LoginSignup
3
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-17

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

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

作成したもの

ソースコードとunitypackageを公開しています

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

NSEvent.addGlobalMonitorForEventsを使うとmacOSでバックグラウンドでキー入力を受け取る事が可能です

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

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

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

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のインタフェースを定義します

@_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ターゲットを追加してビルドします
詳しくはこちら↓

C#の実装

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

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します

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

動作確認

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

終わりに

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2