#if UNITY_EDITOR using System; using System.Runtime.InteropServices; using UnityEditor; using UnityEngine.InputSystem.LowLevel; namespace UnityEngine.InputSystem { /// /// Adds support for processing input-related messages sent from the Unite Remote app. /// /// /// A hook in the Unity runtime allows us to observe messages received from the remote (see Modules/GenericRemoteEditor). /// We get the binary blob of each message and a shot at processing the message instead of /// the native code doing it. /// internal static class UnityRemoteSupport { public static bool isConnected => s_State.connected; public static void Initialize() { InputRuntime.s_Instance.onUnityRemoteMessage = ProcessMessageFromUnityRemote; InputSystem.onSettingsChange += () => { if (InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kDisableUnityRemoteSupport)) { InputRuntime.s_Instance.onUnityRemoteMessage = null; if (s_State.connected) Disconnect(); } else InputRuntime.s_Instance.onUnityRemoteMessage = ProcessMessageFromUnityRemote; }; } private static unsafe bool ProcessMessageFromUnityRemote(IntPtr messageData) { var messageHeader = (MessageHeader*)messageData; switch (messageHeader->type) { case (byte)MessageType.Hello: if (s_State.connected) break; // Install handlers. s_State.deviceChangeHandler = OnDeviceChange; s_State.deviceCommandHandler = OnDeviceCommand; ////REVIEW: We really should have a way of installing a handler just for a specific device. InputSystem.onDeviceChange += s_State.deviceChangeHandler; InputSystem.onDeviceCommand += s_State.deviceCommandHandler; // Add devices. s_State.touchscreen = InputSystem.AddDevice(); s_State.touchscreen.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; s_State.accelerometer = InputSystem.AddDevice(); s_State.accelerometer.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; // Gryo etc. added only when we receive GyroSettingsMessage. s_State.connected = true; Debug.Log("Unity Remote connected to input!"); break; case (byte)MessageType.Goodbye: if (!s_State.connected) break; Disconnect(); Debug.Log("Unity Remote disconnected from input!"); break; case (byte)MessageType.Options: var optionsMessage = (OptionsMessage*)messageData; s_State.screenSize = DetermineScreenSize(optionsMessage->dimension1, optionsMessage->dimension2); break; case (byte)MessageType.TouchInput: if (s_State.touchscreen == null) break; // Android Remote seems to not be sending the last two fields (azimuthAngle and attitudeAngle). if (messageHeader->length < 56) break; var touchMessage = (TouchInputMessage*)messageData; var phase = TouchPhase.None; switch (touchMessage->phase) { case (int)UnityEngine.TouchPhase.Began: phase = TouchPhase.Began; break; case (int)UnityEngine.TouchPhase.Canceled: phase = TouchPhase.Canceled; break; case (int)UnityEngine.TouchPhase.Ended: phase = TouchPhase.Ended; break; case (int)UnityEngine.TouchPhase.Moved: phase = TouchPhase.Moved; break; // Ignore stationary. } if (phase == default) break; InputSystem.QueueStateEvent(s_State.touchscreen, new TouchState { touchId = touchMessage->id + 1, phase = phase, position = MapRemoteTouchCoordinatesToLocal(new Vector2(touchMessage->positionX, touchMessage->positionY)), radius = new Vector2(touchMessage->radius, touchMessage->radius), pressure = touchMessage->pressure }); break; case (byte)MessageType.GyroSettings: var gyroSettingsMessage = (GyroSettingsMessage*)messageData; if (!s_State.gyroInitialized) { // Message itself indicates presence of a gyro. Add the devices. s_State.gyroscope = InputSystem.AddDevice(); s_State.attitude = InputSystem.AddDevice(); s_State.gravity = InputSystem.AddDevice(); s_State.linearAcceleration = InputSystem.AddDevice(); s_State.gyroscope.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; s_State.attitude.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; s_State.gravity.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; s_State.linearAcceleration.m_DeviceFlags |= InputDevice.DeviceFlags.Remote; s_State.gyroInitialized = true; } // Disable them if they are not currently enabled. if (gyroSettingsMessage->enabled == 0) { InputSystem.DisableDevice(s_State.gyroscope); InputSystem.DisableDevice(s_State.attitude); InputSystem.DisableDevice(s_State.gravity); InputSystem.DisableDevice(s_State.linearAcceleration); } else { s_State.gyroEnabled = true; } s_State.gyroUpdateInterval = gyroSettingsMessage->receivedGyroUpdateInternal; break; case (byte)MessageType.GyroInput: var gyroInputMessage = (GyroInputMessage*)messageData; if (s_State.attitude != null && s_State.attitude.enabled) { InputSystem.QueueStateEvent(s_State.attitude, new AttitudeState { attitude = new Quaternion(gyroInputMessage->attitudeX, gyroInputMessage->attitudeY, gyroInputMessage->attitudeZ, gyroInputMessage->attitudeW) }); } if (s_State.gyroscope != null && s_State.gyroscope.enabled) { InputSystem.QueueStateEvent(s_State.gyroscope, new GyroscopeState { angularVelocity = new Vector3(gyroInputMessage->rotationRateX, gyroInputMessage->rotationRateY, gyroInputMessage->rotationRateZ) }); } if (s_State.gravity != null && s_State.gravity.enabled) { InputSystem.QueueStateEvent(s_State.gravity, new GravityState { gravity = new Vector3(gyroInputMessage->gravityX, gyroInputMessage->gravityY, gyroInputMessage->gravityZ) }); } if (s_State.linearAcceleration != null && s_State.linearAcceleration.enabled) { InputSystem.QueueStateEvent(s_State.linearAcceleration, new LinearAccelerationState { acceleration = new Vector3(gyroInputMessage->userAccelerationX, gyroInputMessage->userAccelerationY, gyroInputMessage->userAccelerationZ) }); } break; case (byte)MessageType.AccelerometerInput: if (s_State.accelerometer == null) break; var accelerometerMessage = (AccelerometerInputMessage*)messageData; InputSystem.QueueStateEvent(s_State.accelerometer, new AccelerometerState { acceleration = new Vector3(accelerometerMessage->accelerationX, accelerometerMessage->accelerationY, accelerometerMessage->accelerationZ) }); break; } return false; } private static void Disconnect() { InputSystem.RemoveDevice(s_State.touchscreen); InputSystem.RemoveDevice(s_State.accelerometer); if (s_State.gyroscope != null) InputSystem.RemoveDevice(s_State.gyroscope); if (s_State.attitude != null) InputSystem.RemoveDevice(s_State.attitude); if (s_State.gravity != null) InputSystem.RemoveDevice(s_State.gravity); if (s_State.linearAcceleration != null) InputSystem.RemoveDevice(s_State.linearAcceleration); ResetGlobalState(); } private static void OnDeviceChange(InputDevice device, InputDeviceChange change) { switch (change) { case InputDeviceChange.Removed: // Deal with someone manually removing one of our devices. if (device == s_State.accelerometer) s_State.accelerometer = null; else if (device == s_State.attitude) s_State.accelerometer = null; else if (device == s_State.gravity) s_State.gravity = null; else if (device == s_State.gyroscope) s_State.gyroscope = null; else if (device == s_State.touchscreen) s_State.touchscreen = null; else if (device == s_State.linearAcceleration) s_State.linearAcceleration = null; break; case InputDeviceChange.Enabled: case InputDeviceChange.Disabled: // If it's any of our devices that make up the remote gyro, // send a message to the remote. if (device == s_State.attitude || device == s_State.gravity || device == s_State.gyroscope || device == s_State.linearAcceleration) { SyncGyroEnabledInRemote(); } break; } } private static unsafe long? OnDeviceCommand(InputDevice device, InputDeviceCommand* command) { if (device != s_State.attitude && device != s_State.gyroscope && device != s_State.gravity && device != s_State.linearAcceleration) return null; if (command->type == SetSamplingFrequencyCommand.Type) { s_State.gyroUpdateInterval = ((SetSamplingFrequencyCommand*)command)->frequency; InputRuntime.s_Instance.SetUnityRemoteGyroUpdateInterval(s_State.gyroUpdateInterval); return InputDeviceCommand.GenericSuccess; } if (command->type == QuerySamplingFrequencyCommand.Type) { ((QuerySamplingFrequencyCommand*)command)->frequency = s_State.gyroUpdateInterval; return InputDeviceCommand.GenericSuccess; } return InputDeviceCommand.GenericFailure; } private static void SyncGyroEnabledInRemote() { var enabled = (s_State.attitude?.enabled ?? false) || (s_State.gravity?.enabled ?? false) || (s_State.gyroscope?.enabled ?? false) || (s_State.linearAcceleration?.enabled ?? false); if (enabled != s_State.gyroEnabled) { s_State.gyroEnabled = enabled; InputRuntime.s_Instance.SetUnityRemoteGyroEnabled(enabled); } } // This is taken from HandleOptionsMessage() in GenericRemote.cpp. private static Vector2 DetermineScreenSize(int dimension1, int dimension2) { const float kMaxPixels = 640 * 480; // limit the resolution to VGA float screenPixels = dimension1 * dimension2; var divider = (int)Mathf.Ceil(Mathf.Sqrt(screenPixels / kMaxPixels)); if (divider == 0) return default; var hdim1 = dimension1 / divider; var hdim2 = dimension2 / divider; // GetConfigValue is private. Reflect around it. var getConfigValueMethod = typeof(EditorSettings).GetMethod("GetConfigValue"); if (getConfigValueMethod != null && "Normal".Equals(getConfigValueMethod.Invoke(null, new[] { "UnityRemoteResolution" }))) { hdim1 = dimension1; hdim2 = dimension2; } if (hdim1 >= 1 && hdim2 >= 1) return new Vector2(dimension1, dimension2); return default; } private static Vector2 MapRemoteTouchCoordinatesToLocal(Vector2 position) { var screenSizeRemote = s_State.screenSize; var screenSizeLocal = InputRuntime.s_Instance.screenSize; return new Vector2( position.x / screenSizeRemote.x * screenSizeLocal.x, position.y = position.y / screenSizeRemote.y * screenSizeLocal.y); } // See Editor/Src/RemoteInput/GenericRemote.cpp internal enum MessageType : byte { Invalid = 0, Hello = 1, Options = 2, GyroSettings = 3, DeviceOrientation = 4, DeviceFeatures = 5, TouchInput = 10, AccelerometerInput = 11, TrackBallInput = 12, Key = 13, GyroInput = 14, MousePresence = 15, JoystickInput = 16, JoystickNames = 17, WebCamDeviceList = 20, WebCamStream = 21, LocationServiceData = 30, CompassData = 31, Goodbye = 32, Reserved = 255, } internal interface IUnityRemoteMessage { byte staticType { get; } } [StructLayout(LayoutKind.Explicit, Size = 5)] internal struct MessageHeader { // Unfortunately, the header has an odd 5 byte length and everything // coming after it is misaligned. Reason is that native reads is as a stream // and wants to pack tightly. [FieldOffset(0)] public byte type; [FieldOffset(1)] public int length; } [StructLayout(LayoutKind.Explicit)] internal unsafe struct HelloMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public uint protocolIdLength; [FieldOffset(9)] public fixed char protocolId[11]; [FieldOffset(20)] public int protocolVersion; public byte staticType => (byte)MessageType.Hello; public static HelloMessage Create() { var msg = default(HelloMessage); msg.protocolIdLength = 11; msg.protocolId[0] = 'U'; msg.protocolId[1] = 'n'; msg.protocolId[2] = 'i'; msg.protocolId[3] = 't'; msg.protocolId[4] = 'y'; msg.protocolId[5] = 'R'; msg.protocolId[6] = 'e'; msg.protocolId[7] = 'm'; msg.protocolId[8] = 'o'; msg.protocolId[9] = 't'; msg.protocolId[10] = 'e'; msg.protocolVersion = 0; return msg; } } [StructLayout(LayoutKind.Explicit)] internal struct OptionsMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public int dimension1; [FieldOffset(9)] public int dimension2; public byte staticType => (byte)MessageType.Options; } [StructLayout(LayoutKind.Explicit)] internal struct GoodbyeMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; public byte staticType => (byte)MessageType.Goodbye; } [StructLayout(LayoutKind.Explicit)] internal struct TouchInputMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public float positionX; [FieldOffset(9)] public float positionY; [FieldOffset(13)] public ulong frame; [FieldOffset(21)] public int id; [FieldOffset(25)] public int phase; [FieldOffset(29)] public int tapCount; [FieldOffset(33)] public float radius; [FieldOffset(37)] public float radiusVariance; [FieldOffset(41)] public int type; [FieldOffset(45)] public float pressure; [FieldOffset(49)] public float maximumPossiblePressure; [FieldOffset(53)] public float azimuthAngle; [FieldOffset(57)] public float altitudeAngle; public byte staticType => (byte)MessageType.TouchInput; } [StructLayout(LayoutKind.Explicit)] internal struct GyroSettingsMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public int enabled; [FieldOffset(9)] public float receivedGyroUpdateInternal; public byte staticType => (byte)MessageType.GyroSettings; } [StructLayout(LayoutKind.Explicit)] internal struct GyroInputMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public float rotationRateX; [FieldOffset(9)] public float rotationRateY; [FieldOffset(13)] public float rotationRateZ; [FieldOffset(17)] public float rotationRateUnbiasedX; [FieldOffset(21)] public float rotationRateUnbiasedY; [FieldOffset(25)] public float rotationRateUnbiasedZ; [FieldOffset(29)] public float gravityX; [FieldOffset(33)] public float gravityY; [FieldOffset(37)] public float gravityZ; [FieldOffset(41)] public float userAccelerationX; [FieldOffset(45)] public float userAccelerationY; [FieldOffset(49)] public float userAccelerationZ; [FieldOffset(53)] public float attitudeX; [FieldOffset(57)] public float attitudeY; [FieldOffset(61)] public float attitudeZ; [FieldOffset(65)] public float attitudeW; public byte staticType => (byte)MessageType.GyroInput; } [StructLayout(LayoutKind.Explicit)] internal struct AccelerometerInputMessage : IUnityRemoteMessage { [FieldOffset(0)] public MessageHeader header; [FieldOffset(5)] public float accelerationX; [FieldOffset(9)] public float accelerationY; [FieldOffset(13)] public float accelerationZ; [FieldOffset(17)] public float deltaTime; public byte staticType => (byte)MessageType.AccelerometerInput; } private struct State { public bool connected; public bool gyroInitialized; public bool gyroEnabled; public float gyroUpdateInterval; public Vector2 screenSize; public Action deviceChangeHandler; public InputDeviceCommandDelegate deviceCommandHandler; // Devices that we create for receiving input from the remote. public Touchscreen touchscreen; public Accelerometer accelerometer; public Gyroscope gyroscope; public AttitudeSensor attitude; public GravitySensor gravity; public LinearAccelerationSensor linearAcceleration; } private static State s_State; ////TODO: hook this into Hakan's new cleanup mechanism internal static void ResetGlobalState() { if (s_State.deviceChangeHandler != null) InputSystem.onDeviceChange -= s_State.deviceChangeHandler; if (s_State.deviceCommandHandler != null) InputSystem.onDeviceCommand -= s_State.deviceCommandHandler; s_State = default; } } } #endif