502 lines
21 KiB
C#
502 lines
21 KiB
C#
|
#if UNITY_EDITOR
|
||
|
using System;
|
||
|
using System.Runtime.InteropServices;
|
||
|
using UnityEditor;
|
||
|
using UnityEngine.InputSystem.LowLevel;
|
||
|
|
||
|
namespace UnityEngine.InputSystem
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Adds support for processing input-related messages sent from the <c>Unite Remote</c> app.
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// 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.
|
||
|
/// </remarks>
|
||
|
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<Touchscreen>();
|
||
|
s_State.touchscreen.m_DeviceFlags |= InputDevice.DeviceFlags.Remote;
|
||
|
s_State.accelerometer = InputSystem.AddDevice<Accelerometer>();
|
||
|
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<Gyroscope>();
|
||
|
s_State.attitude = InputSystem.AddDevice<AttitudeSensor>();
|
||
|
s_State.gravity = InputSystem.AddDevice<GravitySensor>();
|
||
|
s_State.linearAcceleration = InputSystem.AddDevice<LinearAccelerationSensor>();
|
||
|
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<InputDevice, InputDeviceChange> 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
|