UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e5769f689f15dce4a9f92aff27659c54
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,228 @@
#if UNITY_EDITOR || UNITY_ANDROID || PACKAGE_DOCS_GENERATION
using System;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Android.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Android.LowLevel
{
/// <summary>
/// Enum used to identity the axis type in the Android motion input event. See <see cref="AndroidGameControllerState.axis"/>.
/// See https://developer.android.com/reference/android/view/MotionEvent#constants_1 for more details.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags", Justification = "False positive")]
public enum AndroidAxis
{
/// <summary>
/// X axis of a motion event.
/// </summary>
X = 0,
/// <summary>
/// Y axis of a motion event.
/// </summary>
Y = 1,
/// <summary>
/// Pressure axis of a motion event.
/// </summary>
Pressure = 2,
/// <summary>
/// Size axis of a motion event.
/// </summary>
Size = 3,
/// <summary>
/// TouchMajor axis of a motion event.
/// </summary>
TouchMajor = 4,
/// <summary>
/// TouchMinor axis of a motion event.
/// </summary>
TouchMinor = 5,
/// <summary>
/// ToolMajor axis of a motion event.
/// </summary>
ToolMajor = 6,
/// <summary>
/// ToolMinor axis of a motion event.
/// </summary>
ToolMinor = 7,
/// <summary>
/// Orientation axis of a motion event.
/// </summary>
Orientation = 8,
/// <summary>
/// Vertical Scroll of a motion event.
/// </summary>
Vscroll = 9,
/// <summary>
/// Horizontal Scroll axis of a motion event.
/// </summary>
Hscroll = 10,
/// <summary>
/// Z axis of a motion event.
/// </summary>
Z = 11,
/// <summary>
/// X Rotation axis of a motion event.
/// </summary>
Rx = 12,
/// <summary>
/// Y Rotation axis of a motion event.
/// </summary>
Ry = 13,
/// <summary>
/// Z Rotation axis of a motion event.
/// </summary>
Rz = 14,
/// <summary>
/// Hat X axis of a motion event.
/// </summary>
HatX = 15,
/// <summary>
/// Hat Y axis of a motion event.
/// </summary>
HatY = 16,
/// <summary>
/// Left Trigger axis of a motion event.
/// </summary>
Ltrigger = 17,
/// <summary>
/// Right Trigger axis of a motion event.
/// </summary>
Rtrigger = 18,
/// <summary>
/// Throttle axis of a motion event.
/// </summary>
Throttle = 19,
/// <summary>
/// Rudder axis of a motion event.
/// </summary>
Rudder = 20,
/// <summary>
/// Wheel axis of a motion event.
/// </summary>
Wheel = 21,
/// <summary>
/// Gas axis of a motion event.
/// </summary>
Gas = 22,
/// <summary>
/// Break axis of a motion event.
/// </summary>
Brake = 23,
/// <summary>
/// Distance axis of a motion event.
/// </summary>
Distance = 24,
/// <summary>
/// Tilt axis of a motion event.
/// </summary>
Tilt = 25,
/// <summary>
/// Generic 1 axis of a motion event.
/// </summary>
Generic1 = 32,
/// <summary>
/// Generic 2 axis of a motion event.
/// </summary>
Generic2 = 33,
/// <summary>
/// Generic 3 axis of a motion event.
/// </summary>
Generic3 = 34,
/// <summary>
/// Generic 4 axis of a motion event.
/// </summary>
Generic4 = 35,
/// <summary>
/// Generic 5 axis of a motion event.
/// </summary>
Generic5 = 36,
/// <summary>
/// Generic 6 axis of a motion event.
/// </summary>
Generic6 = 37,
/// <summary>
/// Generic 7 axis of a motion event.
/// </summary>
Generic7 = 38,
/// <summary>
/// Generic 8 axis of a motion event.
/// </summary>
Generic8 = 39,
/// <summary>
/// Generic 9 axis of a motion event.
/// </summary>
Generic9 = 40,
/// <summary>
/// Generic 10 axis of a motion event.
/// </summary>
Generic10 = 41,
/// <summary>
/// Generic 11 axis of a motion event.
/// </summary>
Generic11 = 42,
/// <summary>
/// Generic 12 axis of a motion event.
/// </summary>
Generic12 = 43,
/// <summary>
/// Generic 13 axis of a motion event.
/// </summary>
Generic13 = 44,
/// <summary>
/// Generic 14 axis of a motion event.
/// </summary>
Generic14 = 45,
/// <summary>
/// Generic 15 axis of a motion event.
/// </summary>
Generic15 = 46,
/// <summary>
/// Generic 16 axis of a motion event.
/// </summary>
Generic16 = 47,
}
}
#endif // UNITY_EDITOR || UNITY_ANDROID

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15fa4fb6e47739e43b36a70fce024b6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,242 @@
#if UNITY_EDITOR || UNITY_ANDROID || PACKAGE_DOCS_GENERATION
using System;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Android.LowLevel;
using UnityEngine.InputSystem.DualShock;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Android.LowLevel
{
/// <summary>
/// Default state layout for Android game controller.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct AndroidGameControllerState : IInputStateTypeInfo
{
public const int MaxAxes = 48;
public const int MaxButtons = 220;
public class Variants
{
public const string Gamepad = "Gamepad";
public const string Joystick = "Joystick";
public const string DPadAxes = "DpadAxes";
public const string DPadButtons = "DpadButtons";
}
internal const uint kAxisOffset = sizeof(uint) * (uint)((MaxButtons + 31) / 32);
public static FourCC kFormat = new FourCC('A', 'G', 'C', ' ');
[InputControl(name = "dpad", layout = "Dpad", bit = (uint)AndroidKeyCode.DpadUp, sizeInBits = 4, variants = Variants.DPadButtons)]
[InputControl(name = "dpad/up", bit = (uint)AndroidKeyCode.DpadUp, variants = Variants.DPadButtons)]
[InputControl(name = "dpad/down", bit = (uint)AndroidKeyCode.DpadDown, variants = Variants.DPadButtons)]
[InputControl(name = "dpad/left", bit = (uint)AndroidKeyCode.DpadLeft, variants = Variants.DPadButtons)]
[InputControl(name = "dpad/right", bit = (uint)AndroidKeyCode.DpadRight, variants = Variants.DPadButtons)]
[InputControl(name = "buttonSouth", bit = (uint)AndroidKeyCode.ButtonA, variants = Variants.Gamepad)]
[InputControl(name = "buttonWest", bit = (uint)AndroidKeyCode.ButtonX, variants = Variants.Gamepad)]
[InputControl(name = "buttonNorth", bit = (uint)AndroidKeyCode.ButtonY, variants = Variants.Gamepad)]
[InputControl(name = "buttonEast", bit = (uint)AndroidKeyCode.ButtonB, variants = Variants.Gamepad)]
[InputControl(name = "leftStickPress", bit = (uint)AndroidKeyCode.ButtonThumbl, variants = Variants.Gamepad)]
[InputControl(name = "rightStickPress", bit = (uint)AndroidKeyCode.ButtonThumbr, variants = Variants.Gamepad)]
[InputControl(name = "leftShoulder", bit = (uint)AndroidKeyCode.ButtonL1, variants = Variants.Gamepad)]
[InputControl(name = "rightShoulder", bit = (uint)AndroidKeyCode.ButtonR1, variants = Variants.Gamepad)]
[InputControl(name = "start", bit = (uint)AndroidKeyCode.ButtonStart, variants = Variants.Gamepad)]
[InputControl(name = "select", bit = (uint)AndroidKeyCode.ButtonSelect, variants = Variants.Gamepad)]
public fixed uint buttons[(MaxButtons + 31) / 32];
[InputControl(name = "dpad", layout = "Dpad", offset = (uint)AndroidAxis.HatX * sizeof(float) + kAxisOffset, format = "VEC2", sizeInBits = 64, variants = Variants.DPadAxes)]
[InputControl(name = "dpad/right", offset = 0, bit = 0, sizeInBits = 32, format = "FLT", parameters = "clamp=3,clampConstant=0,clampMin=0,clampMax=1", variants = Variants.DPadAxes)]
[InputControl(name = "dpad/left", offset = 0, bit = 0, sizeInBits = 32, format = "FLT", parameters = "clamp=3,clampConstant=0,clampMin=-1,clampMax=0,invert", variants = Variants.DPadAxes)]
[InputControl(name = "dpad/down", offset = ((uint)AndroidAxis.HatY - (uint)AndroidAxis.HatX) * sizeof(float), bit = 0, sizeInBits = 32, format = "FLT", parameters = "clamp=3,clampConstant=0,clampMin=0,clampMax=1", variants = Variants.DPadAxes)]
[InputControl(name = "dpad/up", offset = ((uint)AndroidAxis.HatY - (uint)AndroidAxis.HatX) * sizeof(float), bit = 0, sizeInBits = 32, format = "FLT", parameters = "clamp=3,clampConstant=0,clampMin=-1,clampMax=0,invert", variants = Variants.DPadAxes)]
[InputControl(name = "leftTrigger", offset = (uint)AndroidAxis.Brake * sizeof(float) + kAxisOffset, parameters = "clamp=1,clampMin=0,clampMax=1.0", variants = Variants.Gamepad)]
[InputControl(name = "rightTrigger", offset = (uint)AndroidAxis.Gas * sizeof(float) + kAxisOffset, parameters = "clamp=1,clampMin=0,clampMax=1.0", variants = Variants.Gamepad)]
[InputControl(name = "leftStick", variants = Variants.Gamepad)]
[InputControl(name = "leftStick/y", variants = Variants.Gamepad, parameters = "invert")]
[InputControl(name = "leftStick/up", variants = Variants.Gamepad, parameters = "invert,clamp=1,clampMin=-1.0,clampMax=0.0")]
[InputControl(name = "leftStick/down", variants = Variants.Gamepad, parameters = "invert=false,clamp=1,clampMin=0,clampMax=1.0")]
////FIXME: state for this control is not contiguous
[InputControl(name = "rightStick", offset = (uint)AndroidAxis.Z * sizeof(float) + kAxisOffset, sizeInBits = ((uint)AndroidAxis.Rz - (uint)AndroidAxis.Z + 1) * sizeof(float) * 8, variants = Variants.Gamepad)]
[InputControl(name = "rightStick/x", variants = Variants.Gamepad)]
[InputControl(name = "rightStick/y", offset = ((uint)AndroidAxis.Rz - (uint)AndroidAxis.Z) * sizeof(float), variants = Variants.Gamepad, parameters = "invert")]
[InputControl(name = "rightStick/up", offset = ((uint)AndroidAxis.Rz - (uint)AndroidAxis.Z) * sizeof(float), variants = Variants.Gamepad, parameters = "invert,clamp=1,clampMin=-1.0,clampMax=0.0")]
[InputControl(name = "rightStick/down", offset = ((uint)AndroidAxis.Rz - (uint)AndroidAxis.Z) * sizeof(float), variants = Variants.Gamepad, parameters = "invert=false,clamp=1,clampMin=0,clampMax=1.0")]
public fixed float axis[MaxAxes];
public FourCC format
{
get { return kFormat; }
}
public AndroidGameControllerState WithButton(AndroidKeyCode code, bool value = true)
{
fixed(uint* buttonsPtr = buttons)
{
if (value)
buttonsPtr[(int)code / 32] |= 1U << ((int)code % 32);
else
buttonsPtr[(int)code / 32] &= ~(1U << ((int)code % 32));
}
return this;
}
public AndroidGameControllerState WithAxis(AndroidAxis axis, float value)
{
fixed(float* axisPtr = this.axis)
{
axisPtr[(int)axis] = value;
}
return this;
}
}
// See https://developer.android.com/reference/android/view/InputDevice.html for input source values
internal enum AndroidInputSource
{
Keyboard = 257,
Dpad = 513,
Gamepad = 1025,
Touchscreen = 4098,
Mouse = 8194,
Stylus = 16386,
Trackball = 65540,
Touchpad = 1048584,
Joystick = 16777232
}
[Serializable]
internal struct AndroidDeviceCapabilities
{
public string deviceDescriptor;
public int productId;
public int vendorId;
public bool isVirtual;
public AndroidAxis[] motionAxes;
public AndroidInputSource inputSources;
public string ToJson()
{
return JsonUtility.ToJson(this);
}
public static AndroidDeviceCapabilities FromJson(string json)
{
if (json == null)
throw new ArgumentNullException(nameof(json));
return JsonUtility.FromJson<AndroidDeviceCapabilities>(json);
}
public override string ToString()
{
return
$"deviceDescriptor = {deviceDescriptor}, productId = {productId}, vendorId = {vendorId}, isVirtual = {isVirtual}, motionAxes = {(motionAxes == null ? "<null>" : String.Join(",", motionAxes.Select(i => i.ToString()).ToArray()))}, inputSources = {inputSources}";
}
}
}
namespace UnityEngine.InputSystem.Android
{
/// <summary>
/// Represents a gamepad device on Android, providing unified support for various controller types.
/// </summary>
/// <remarks>
/// This layout covers multiple Android-supported gamepads, including but not limited to:
/// - ELAN PLAYSTATION(R)3 Controller
/// - My-Power CO., LTD. PS(R) Controller Adaptor
/// - Sony Interactive Entertainment Wireless (PS4 DualShock)
/// - Xbox Wireless Controller (Xbox One)
/// - NVIDIA Controller v01.03/v01.04
/// - (More may be added later)
///
/// ### Typical Android Axis and Key Mappings
/// | Control | Mapping |
/// |------------------------|----------------------------------------------------|
/// | Left Stick | AXIS_X(0) / AXIS_Y(1) |
/// | Right Stick | AXIS_Z(11) / AXIS_RZ(14) |
/// | L1 | KEYCODE_BUTTON_L1(102) |
/// | R1 | KEYCODE_BUTTON_R1(103) |
/// | L2 | AXIS_BRAKE(23) |
/// | R2 | AXIS_GAS(22) |
/// | Left Thumb | KEYCODE_BUTTON_THUMBL(106) |
/// | Right Thumb | KEYCODE_BUTTON_THUMBR(107) |
/// | X | KEYCODE_BUTTON_X(99) |
/// | Y | KEYCODE_BUTTON_Y(100) |
/// | B | KEYCODE_BUTTON_B(97) |
/// | A | KEYCODE_BUTTON_A(96) |
/// | DPAD | AXIS_HAT_X(15), AXIS_HAT_Y(16) or KEYCODE_DPAD_* |
///
/// ### Notes
/// - **NVIDIA Shield Console**
/// - The L2 and R2 triggers generate both `AXIS_BRAKE` / `AXIS_GAS` and `AXIS_LTRIGGER` / `AXIS_RTRIGGER` events.
/// - On most Android phones, only `AXIS_BRAKE` and `AXIS_GAS` are reported; `AXIS_LTRIGGER` and `AXIS_RTRIGGER` are not invoked.
/// - For consistency across devices, triggers are mapped exclusively to `AXIS_BRAKE` and `AXIS_GAS`.
/// - The Shield also reports `KEYCODE_BACK` instead of `KEYCODE_BUTTON_SELECT`, causing the **Options** (Xbox), **View** (DualShock), or **Select** buttons to be non-functional.
///
/// - **PS4 Controller Compatibility**
/// - Official PS4 controller support is available starting from **Android 10 and later**
/// (see: https://playstation.com/en-us/support/hardware/ps4-pair-dualshock-4-wireless-with-sony-xperia-and-android).
/// - On older Android versions, driver implementations vary by manufacturer. Some vendors have partially fixed DualShock support in custom drivers, leading to inconsistent mappings.
///
/// - **Driver-Dependent Behavior**
/// - Gamepad mappings may differ even between devices running the *same Android version*.
/// - For example, on **Android 8.0**:
/// - **NVIDIA Shield Console:** buttons map correctly according to `AndroidGameControllerState` (for example, `L1 → ButtonL1`, `R1 → ButtonR1`).
/// - **Samsung Galaxy S9 / S8** and **Xiaomi Mi Note2:** mappings are inconsistent (for example, `L1 → ButtonY`, `R1 → ButtonZ`).
/// - These discrepancies stem from device-specific **driver differences**, not the Android OS itself.
///
/// Because mapping inconsistencies depend on vendor-specific drivers, its impractical to maintain per-device remaps.
/// </remarks>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), variants = AndroidGameControllerState.Variants.Gamepad)]
public class AndroidGamepad : Gamepad
{
}
/// <summary>
/// Generic controller with Dpad axes
/// </summary>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), hideInUI = true,
variants = AndroidGameControllerState.Variants.Gamepad + InputControlLayout.VariantSeparator + AndroidGameControllerState.Variants.DPadAxes)]
public class AndroidGamepadWithDpadAxes : AndroidGamepad
{
}
/// <summary>
/// Generic controller with Dpad buttons
/// </summary>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), hideInUI = true,
variants = AndroidGameControllerState.Variants.Gamepad + InputControlLayout.VariantSeparator + AndroidGameControllerState.Variants.DPadButtons)]
public class AndroidGamepadWithDpadButtons : AndroidGamepad
{
}
/// <summary>
/// Joystick on Android.
/// </summary>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), variants = AndroidGameControllerState.Variants.Joystick)]
public class AndroidJoystick : Joystick
{
}
/// <summary>
/// A PlayStation DualShock 4 controller connected to an Android device.
/// </summary>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), displayName = "Android DualShock 4 Gamepad",
variants = AndroidGameControllerState.Variants.Gamepad + InputControlLayout.VariantSeparator + AndroidGameControllerState.Variants.DPadAxes)]
public class DualShock4GamepadAndroid : DualShockGamepad
{
}
/// <summary>
/// An XboxOne controller connected to an Android device.
/// </summary>
[InputControlLayout(stateType = typeof(AndroidGameControllerState), displayName = "Android Xbox One Controller",
variants = AndroidGameControllerState.Variants.Gamepad + InputControlLayout.VariantSeparator + AndroidGameControllerState.Variants.DPadAxes)]
public class XboxOneGamepadAndroid : XInput.XInputController
{
}
}
#endif // UNITY_EDITOR || UNITY_ANDROID

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7c6472eb810ba445be34c69325e4fea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ede17f8b7e9dfcf42b560545a8102eeb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,293 @@
#if UNITY_EDITOR || UNITY_ANDROID || PACKAGE_DOCS_GENERATION
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Android.LowLevel;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Processors;
////TODO: make all the sensor class types internal
namespace UnityEngine.InputSystem.Android.LowLevel
{
internal enum AndroidSensorType
{
None = 0,
Accelerometer = 1,
MagneticField = 2,
Orientation = 3, // Was deprecated in API 8 https://developer.android.com/reference/android/hardware/Sensor#TYPE_ORIENTATION
Gyroscope = 4,
Light = 5,
Pressure = 6,
Temperature = 7, // Was deprecated in API 14 https://developer.android.com/reference/android/hardware/Sensor#TYPE_TEMPERATURE
Proximity = 8,
Gravity = 9,
LinearAcceleration = 10,
RotationVector = 11,
RelativeHumidity = 12,
AmbientTemperature = 13,
MagneticFieldUncalibrated = 14,
GameRotationVector = 15,
GyroscopeUncalibrated = 16,
SignificantMotion = 17,
StepDetector = 18,
StepCounter = 19,
GeomagneticRotationVector = 20,
HeartRate = 21,
Pose6DOF = 28,
StationaryDetect = 29,
MotionDetect = 30,
HeartBeat = 31,
LowLatencyOffBodyDetect = 34,
AccelerometerUncalibrated = 35,
HingeAngle = 36
}
[Serializable]
internal struct AndroidSensorCapabilities
{
public AndroidSensorType sensorType;
public string ToJson()
{
return JsonUtility.ToJson(this);
}
public static AndroidSensorCapabilities FromJson(string json)
{
if (json == null)
throw new ArgumentNullException(nameof(json));
return JsonUtility.FromJson<AndroidSensorCapabilities>(json);
}
public override string ToString()
{
return $"type = {sensorType.ToString()}";
}
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct AndroidSensorState : IInputStateTypeInfo
{
public static FourCC kFormat = new FourCC('A', 'S', 'S', ' ');
////FIXME: Sensors to check if values matches old system
// Accelerometer - OK
// MagneticField - no alternative in old system
// Gyroscope - OK
// Light - no alternative in old system
// Pressure - no alternative in old system
// Proximity - no alternative in old system
// Gravity - OK
// LinearAcceleration - need to check
// RotationVector - OK
// RelativeHumidity - no alternative in old system
// AmbientTemperature - no alternative in old system
// GameRotationVector - no alternative in old system
// StepCounter - no alternative in old system
// GeomagneticRotationVector - no alternative in old system
// HeartRate - no alternative in old system
[InputControl(name = "acceleration", layout = "Vector3", processors = "AndroidCompensateDirection", variants = "Accelerometer")]
[InputControl(name = "magneticField", layout = "Vector3", variants = "MagneticField")]
// Note: Using CompensateDirection instead of AndroidCompensateDirection, because we don't need to normalize velocity
[InputControl(name = "angularVelocity", layout = "Vector3", processors = "CompensateDirection", variants = "Gyroscope")]
[InputControl(name = "lightLevel", layout = "Axis", variants = "Light")]
[InputControl(name = "atmosphericPressure", layout = "Axis", variants = "Pressure")]
[InputControl(name = "distance", layout = "Axis", variants = "Proximity")]
[InputControl(name = "gravity", layout = "Vector3", processors = "AndroidCompensateDirection", variants = "Gravity")]
[InputControl(name = "acceleration", layout = "Vector3", processors = "AndroidCompensateDirection", variants = "LinearAcceleration")]
[InputControl(name = "attitude", layout = "Quaternion", processors = "AndroidCompensateRotation", variants = "RotationVector")]
[InputControl(name = "relativeHumidity", layout = "Axis", variants = "RelativeHumidity")]
[InputControl(name = "ambientTemperature", layout = "Axis", variants = "AmbientTemperature")]
[InputControl(name = "attitude", layout = "Quaternion", processors = "AndroidCompensateRotation", variants = "GameRotationVector")]
[InputControl(name = "stepCounter", layout = "Integer", variants = "StepCounter")]
[InputControl(name = "rotation", layout = "Quaternion", processors = "AndroidCompensateRotation", variants = "GeomagneticRotationVector")]
[InputControl(name = "rate", layout = "Axis", variants = "HeartRate")]
[InputControl(name = "angle", layout = "Axis", variants = nameof(AndroidSensorType.HingeAngle))]
public fixed float data[16];
public AndroidSensorState WithData(params float[] data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
for (var i = 0; i < data.Length && i < 16; i++)
this.data[i] = data[i];
// Fill the rest with zeroes
for (var i = data.Length; i < 16; i++)
this.data[i] = 0.0f;
return this;
}
public FourCC format => kFormat;
}
[DesignTimeVisible(false)]
internal class AndroidCompensateDirectionProcessor : CompensateDirectionProcessor
{
// Taken from platforms\android-<API>\arch-arm\usr\include\android\sensor.h
private const float kSensorStandardGravity = 9.80665f;
private const float kAccelerationMultiplier = -1.0f / kSensorStandardGravity;
public override Vector3 Process(Vector3 vector, InputControl control)
{
return base.Process(vector * kAccelerationMultiplier, control);
}
}
[DesignTimeVisible(false)]
internal class AndroidCompensateRotationProcessor : CompensateRotationProcessor
{
public override Quaternion Process(Quaternion value, InputControl control)
{
// https://developer.android.com/reference/android/hardware/SensorEvent#values
// "...The rotation vector represents the orientation of the device as a combination of an angle and an axis, in which the device has rotated through an angle theta around an axis <x, y, z>."
// "...The three elements of the rotation vector are < x * sin(theta / 2), y* sin(theta / 2), z* sin(theta / 2)>, such that the magnitude of the rotation vector is equal to sin(theta / 2), and the direction of the rotation vector is equal to the direction of the axis of rotation."
// "...The three elements of the rotation vector are equal to the last three components of a unit quaternion < cos(theta / 2), x* sin(theta/ 2), y* sin(theta / 2), z* sin(theta/ 2)>."
//
// In other words, axis + rotation is combined into Vector3, to recover the quaternion from it, we must compute 4th component as 1 - sqrt(x*x + y*y + z*z)
var sinRho2 = value.x * value.x + value.y * value.y + value.z * value.z;
value.w = (sinRho2 < 1.0f) ? Mathf.Sqrt(1.0f - sinRho2) : 0.0f;
return base.Process(value, control);
}
}
}
namespace UnityEngine.InputSystem.Android
{
/// <summary>
/// Accelerometer device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_ACCELEROMETER"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Accelerometer", hideInUI = true)]
public class AndroidAccelerometer : Accelerometer
{
}
/// <summary>
/// Magnetic field sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_MAGNETIC_FIELD"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "MagneticField", hideInUI = true)]
public class AndroidMagneticFieldSensor : MagneticFieldSensor
{
}
/// <summary>
/// Gyroscope device on android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_GYROSCOPE"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Gyroscope", hideInUI = true)]
public class AndroidGyroscope : Gyroscope
{
}
/// <summary>
/// Light sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_LIGHT"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Light", hideInUI = true)]
public class AndroidLightSensor : LightSensor
{
}
/// <summary>
/// Pressure sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_PRESSURE"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Pressure", hideInUI = true)]
public class AndroidPressureSensor : PressureSensor
{
}
/// <summary>
/// Proximity sensor type on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_PROXIMITY"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Proximity", hideInUI = true)]
public class AndroidProximity : ProximitySensor
{
}
/// <summary>
/// Gravity sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_GRAVITY"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "Gravity", hideInUI = true)]
public class AndroidGravitySensor : GravitySensor
{
}
/// <summary>
/// Linear acceleration sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_LINEAR_ACCELERATION"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "LinearAcceleration", hideInUI = true)]
public class AndroidLinearAccelerationSensor : LinearAccelerationSensor
{
}
/// <summary>
/// Rotation vector sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_ROTATION_VECTOR"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "RotationVector", hideInUI = true)]
public class AndroidRotationVector : AttitudeSensor
{
}
/// <summary>
/// Relative humidity sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_RELATIVE_HUMIDITY"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "RelativeHumidity", hideInUI = true)]
public class AndroidRelativeHumidity : HumiditySensor
{
}
/// <summary>
/// Ambient temperature sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_AMBIENT_TEMPERATURE"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "AmbientTemperature", hideInUI = true)]
public class AndroidAmbientTemperature : AmbientTemperatureSensor
{
}
/// <summary>
/// Game rotation vector sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_GAME_ROTATION_VECTOR"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "GameRotationVector", hideInUI = true)]
public class AndroidGameRotationVector : AttitudeSensor
{
}
/// <summary>
/// Step counter sensor device on Android.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_STEP_COUNTER"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = "StepCounter", hideInUI = true)]
public class AndroidStepCounter : StepCounter
{
}
/// <summary>
/// Hinge angle sensor device on Android.
/// This sensor is usually available on foldable devices.
/// </summary>
/// <seealso href="https://developer.android.com/reference/android/hardware/Sensor#TYPE_HINGE_ANGLE"/>
[InputControlLayout(stateType = typeof(AndroidSensorState), variants = nameof(AndroidSensorType.HingeAngle), hideInUI = true)]
public class AndroidHingeAngle : HingeAngle
{
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 830105192d35a17469c56711468d1e8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,174 @@
#if UNITY_EDITOR || UNITY_ANDROID
using System.Linq;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Android.LowLevel;
namespace UnityEngine.InputSystem.Android
{
/// <summary>
/// Initializes custom android devices.
/// You can use 'adb shell dumpsys input' from terminal to output information about all input devices.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
class AndroidSupport
{
internal const string kAndroidInterface = "Android";
public static void Initialize()
{
InputSystem.RegisterLayout<AndroidGamepad>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidGameController"));
InputSystem.RegisterLayout<AndroidJoystick>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidGameController"));
InputSystem.RegisterLayout<DualShock4GamepadAndroid>();
InputSystem.RegisterLayout<XboxOneGamepadAndroid>();
////TODO: capability matching does not yet support bitmasking so these remain handled by OnFindLayoutForDevice for now
InputSystem.RegisterLayout<AndroidGamepadWithDpadAxes>();
InputSystem.RegisterLayout<AndroidGamepadWithDpadButtons>();
InputSystem.RegisterProcessor<AndroidCompensateDirectionProcessor>();
InputSystem.RegisterProcessor<AndroidCompensateRotationProcessor>();
// Add sensors
InputSystem.RegisterLayout<AndroidAccelerometer>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Accelerometer));
InputSystem.RegisterLayout<AndroidMagneticFieldSensor>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.MagneticField));
InputSystem.RegisterLayout<AndroidGyroscope>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Gyroscope));
InputSystem.RegisterLayout<AndroidLightSensor>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Light));
InputSystem.RegisterLayout<AndroidPressureSensor>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Pressure));
InputSystem.RegisterLayout<AndroidProximity>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Proximity));
InputSystem.RegisterLayout<AndroidGravitySensor>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.Gravity));
InputSystem.RegisterLayout<AndroidLinearAccelerationSensor>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.LinearAcceleration));
InputSystem.RegisterLayout<AndroidRotationVector>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.RotationVector));
InputSystem.RegisterLayout<AndroidRelativeHumidity>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.RelativeHumidity));
InputSystem.RegisterLayout<AndroidAmbientTemperature>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.AmbientTemperature));
InputSystem.RegisterLayout<AndroidGameRotationVector>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.GameRotationVector));
InputSystem.RegisterLayout<AndroidStepCounter>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.StepCounter));
InputSystem.RegisterLayout<AndroidHingeAngle>(
matches: new InputDeviceMatcher()
.WithInterface(kAndroidInterface)
.WithDeviceClass("AndroidSensor")
.WithCapability("sensorType", AndroidSensorType.HingeAngle));
InputSystem.onFindLayoutForDevice += OnFindLayoutForDevice;
}
internal static string OnFindLayoutForDevice(ref InputDeviceDescription description,
string matchedLayout, InputDeviceExecuteCommandDelegate executeCommandDelegate)
{
// If we already have a matching layout, someone registered a better match.
// We only want to act as a fallback.
if (!string.IsNullOrEmpty(matchedLayout) && matchedLayout != "AndroidGamepad" && matchedLayout != "AndroidJoystick")
return null;
if (description.interfaceName != "Android" || string.IsNullOrEmpty(description.capabilities))
return null;
////TODO: these should just be Controller and Sensor; the interface is already Android
switch (description.deviceClass)
{
case "AndroidGameController":
{
var caps = AndroidDeviceCapabilities.FromJson(description.capabilities);
// Note: Gamepads have both AndroidInputSource.Gamepad and AndroidInputSource.Joystick in input source, while
// Joysticks don't have AndroidInputSource.Gamepad in their input source
if ((caps.inputSources & AndroidInputSource.Gamepad) != AndroidInputSource.Gamepad)
return "AndroidJoystick";
if (caps.motionAxes == null)
return "AndroidGamepadWithDpadButtons";
// Vendor Ids, Product Ids can be found here http://www.linux-usb.org/usb.ids
const int kVendorMicrosoft = 0x045e;
const int kVendorSony = 0x054c;
// Tested with controllers: PS4 DualShock; XboxOne; Nvidia Shield
// Tested on devices: Shield console Android 9; Galaxy s9+ Android 10
if (caps.motionAxes.Contains(AndroidAxis.Z) &&
caps.motionAxes.Contains(AndroidAxis.Rz) &&
caps.motionAxes.Contains(AndroidAxis.HatX) &&
caps.motionAxes.Contains(AndroidAxis.HatY))
{
if (caps.vendorId == kVendorMicrosoft)
return "XboxOneGamepadAndroid";
if (caps.vendorId == kVendorSony)
return "DualShock4GamepadAndroid";
}
// Fallback to generic gamepads
if (caps.motionAxes.Contains(AndroidAxis.HatX) &&
caps.motionAxes.Contains(AndroidAxis.HatY))
return "AndroidGamepadWithDpadAxes";
return "AndroidGamepadWithDpadButtons";
}
default:
return null;
}
}
}
}
#endif // UNITY_EDITOR || UNITY_ANDROID

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0c8442e8094ce974598eacc0302ddabb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6eefa91e7a89647338245ee3818141d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.Scripting;
////TODO: speaker, touchpad
////TODO: move gyro here
namespace UnityEngine.InputSystem.DualShock
{
/// <summary>
/// A Sony DualShock/DualSense controller.
/// </summary>
[InputControlLayout(displayName = "PlayStation Controller")]
public class DualShockGamepad : Gamepad, IDualShockHaptics
{
/// <summary>
/// Button that is triggered when the touchbar on the controller is pressed down.
/// </summary>
/// <value>Control representing the touchbar button.</value>
[InputControl(name = "buttonWest", displayName = "Square", shortDisplayName = "Square")]
[InputControl(name = "buttonNorth", displayName = "Triangle", shortDisplayName = "Triangle")]
[InputControl(name = "buttonEast", displayName = "Circle", shortDisplayName = "Circle")]
[InputControl(name = "buttonSouth", displayName = "Cross", shortDisplayName = "Cross")]
[InputControl]
public ButtonControl touchpadButton { get; protected set; }
/// <summary>
/// The right side button in the middle section of the controller. Equivalent to
/// <see cref="Gamepad.startButton"/>.
/// </summary>
/// <value>Same as <see cref="Gamepad.startButton"/>.</value>
[InputControl(name = "start", displayName = "Options")]
public ButtonControl optionsButton { get; protected set; }
/// <summary>
/// The left side button in the middle section of the controller. Equivalent to
/// <see cref="Gamepad.selectButton"/>
/// </summary>
/// <value>Same as <see cref="Gamepad.selectButton"/>.</value>
[InputControl(name = "select", displayName = "Share")]
public ButtonControl shareButton { get; protected set; }
/// <summary>
/// The left shoulder button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.leftShoulder"/>.</value>
[InputControl(name = "leftShoulder", displayName = "L1", shortDisplayName = "L1")]
public ButtonControl L1 { get; protected set; }
/// <summary>
/// The right shoulder button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.rightShoulder"/>.</value>
[InputControl(name = "rightShoulder", displayName = "R1", shortDisplayName = "R1")]
public ButtonControl R1 { get; protected set; }
/// <summary>
/// The left trigger button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.leftTrigger"/>.</value>
[InputControl(name = "leftTrigger", displayName = "L2", shortDisplayName = "L2")]
public ButtonControl L2 { get; protected set; }
/// <summary>
/// The right trigger button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.rightTrigger"/>.</value>
[InputControl(name = "rightTrigger", displayName = "R2", shortDisplayName = "R2")]
public ButtonControl R2 { get; protected set; }
/// <summary>
/// The left stick press button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.leftStickButton"/>.</value>
[InputControl(name = "leftStickPress", displayName = "L3", shortDisplayName = "L3")]
public ButtonControl L3 { get; protected set; }
/// <summary>
/// The right stick press button.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.rightStickButton"/>.</value>
[InputControl(name = "rightStickPress", displayName = "R3", shortDisplayName = "R3")]
public ButtonControl R3 { get; protected set; }
/// <summary>
/// The last used/added DualShock controller.
/// </summary>
/// <value>Equivalent to <see cref="Gamepad.leftTrigger"/>.</value>
public new static DualShockGamepad current { get; private set; }
/// <summary>
/// If the controller is connected over HID, returns <see cref="HID.HID.HIDDeviceDescriptor"/> data parsed from <see cref="InputDeviceDescription.capabilities"/>.
/// </summary>
internal HID.HID.HIDDeviceDescriptor hidDescriptor { get; private set; }
/// <inheritdoc />
public override void MakeCurrent()
{
base.MakeCurrent();
current = this;
}
/// <inheritdoc />
protected override void OnRemoved()
{
base.OnRemoved();
if (current == this)
current = null;
}
/// <inheritdoc />
protected override void FinishSetup()
{
base.FinishSetup();
touchpadButton = GetChildControl<ButtonControl>("touchpadButton");
optionsButton = startButton;
shareButton = selectButton;
L1 = leftShoulder;
R1 = rightShoulder;
L2 = leftTrigger;
R2 = rightTrigger;
L3 = leftStickButton;
R3 = rightStickButton;
if (m_Description.capabilities != null && m_Description.interfaceName == "HID")
hidDescriptor = HID.HID.HIDDeviceDescriptor.FromJson(m_Description.capabilities);
}
/// <inheritdoc />
public virtual void SetLightBarColor(Color color)
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df88ce51eaa14f2f8975023f7f45d2d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7225419e3d446c5982afd5ba9cce319
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,73 @@
using UnityEngine.InputSystem.Layouts;
namespace UnityEngine.InputSystem.DualShock
{
/// <summary>
/// Adds support for PS4 DualShock controllers.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class DualShockSupport
{
public static void Initialize()
{
InputSystem.RegisterLayout<DualShockGamepad>();
// HID version for platforms where we pick up the controller as a raw HID.
// This works without any PS4-specific drivers but does not support the full
// range of capabilities of the controller (the HID format is undocumented
// and only partially understood).
//
// NOTE: We match by PID and VID here as that is the most reliable way. The product
// and manufacturer strings we get from APIs often return inconsistent results
// or none at all. E.g. when connected via Bluetooth on OSX, the DualShock will
// not return anything from IOHIDDevice_GetProduct() and IOHIDevice_GetManufacturer()
// even though it will report the expected results when plugged in via USB.
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_WSA || UNITY_EDITOR || UNITY_STANDALONE_LINUX
InputSystem.RegisterLayout<DualSenseGamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0xDF2)); // Dual Sense Edge
InputSystem.RegisterLayout<DualSenseGamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0xCE6)); // Dual Sense
InputSystem.RegisterLayout<DualShock4GamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0x9CC)); // Wireless controller.
InputSystem.RegisterLayoutMatcher<DualShock4GamepadHID>(
new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0x5C4)); // Wireless controller.
// Just to make sure, also set up a matcher that goes by strings so that we cover
// all bases.
InputSystem.RegisterLayoutMatcher<DualShock4GamepadHID>(
new InputDeviceMatcher()
.WithInterface("HID")
.WithManufacturerContains("Sony")
.WithProduct("Wireless Controller"));
InputSystem.RegisterLayout<DualShock3GamepadHID>(
matches: new InputDeviceMatcher()
.WithInterface("HID")
.WithCapability("vendorId", 0x54C) // Sony Entertainment.
.WithCapability("productId", 0x268)); // PLAYSTATION(R)3 Controller.
InputSystem.RegisterLayoutMatcher<DualShock3GamepadHID>(
new InputDeviceMatcher()
.WithInterface("HID")
.WithManufacturerContains("Sony")
.WithProduct("PLAYSTATION(R)3 Controller", supportRegex: false));
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa26399d579948dd90c6ff298cdb0570
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using UnityEngine.InputSystem.Haptics;
namespace UnityEngine.InputSystem.DualShock
{
/// <summary>
/// Extended haptics interface for DualShock controllers.
/// </summary>
public interface IDualShockHaptics : IDualMotorRumble
{
/// <summary>
/// Set the color of the light bar on the back of the controller.
/// </summary>
/// <param name="color">Color to use for the light bar. Alpha component is ignored. Also,
/// RBG values are clamped into [0..1] range.</param>
void SetLightBarColor(Color color);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e235e01f7e6408f87e899ac373c85d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ef4b66ea93dd243a783c32ab1da39fc4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,213 @@
using System;
using System.Diagnostics;
using UnityEngine.InputSystem.LowLevel;
#if UNITY_EDITOR
using UnityEditor;
#endif
////REVIEW: this *really* should be renamed to TouchPolling or something like that
////REVIEW: Should this auto-enable itself when the API is used? Problem with this is that it means the first touch inputs will get missed
//// as by the time the API is polled, we're already into the first frame.
////TODO: gesture support
////TODO: high-frequency touch support
////REVIEW: have TouchTap, TouchSwipe, etc. wrapper MonoBehaviours like LeanTouch?
////TODO: as soon as we can break the API, remove the EnhancedTouchSupport class altogether and rename UnityEngine.InputSystem.EnhancedTouch to TouchPolling
////FIXME: does not survive domain reloads
namespace UnityEngine.InputSystem.EnhancedTouch
{
/// <summary>
/// API to control enhanced touch facilities like <see cref="Touch"/> that are not
/// enabled by default.
/// </summary>
/// <remarks>
/// Enhanced touch support provides automatic finger tracking and touch history recording.
/// It is an API designed for polling, i.e. for querying touch state directly in methods
/// such as <c>MonoBehaviour.Update</c>. Enhanced touch support cannot be used in combination
/// with <see cref="InputAction"/>s though both can be used side-by-side.
///
/// <example>
/// <code>
/// public class MyBehavior : MonoBehaviour
/// {
/// protected void OnEnable()
/// {
/// EnhancedTouchSupport.Enable();
/// }
///
/// protected void OnDisable()
/// {
/// EnhancedTouchSupport.Disable();
/// }
///
/// protected void Update()
/// {
/// var activeTouches = Touch.activeTouches;
/// for (var i = 0; i &lt; activeTouches.Count; ++i)
/// Debug.Log("Active touch: " + activeTouches[i]);
/// }
/// }
/// </code>
/// </example>
/// </remarks>
/// <seealso cref="Touch"/>
/// <seealso cref="Finger"/>
public static class EnhancedTouchSupport
{
/// <summary>
/// Whether enhanced touch support is currently enabled.
/// </summary>
/// <value>True if EnhancedTouch support has been enabled.</value>
public static bool enabled => s_Enabled > 0;
private static int s_Enabled;
private static InputSettings.UpdateMode s_UpdateMode;
/// <summary>
/// Enable enhanced touch support.
/// </summary>
/// <remarks>
/// Calling this method is necessary to enable the functionality provided
/// by <see cref="Touch"/> and <see cref="Finger"/>. These APIs add extra
/// processing to touches and are thus disabled by default.
///
/// Calls to <c>Enable</c> and <see cref="Disable"/> balance each other out.
/// If <c>Enable</c> is called repeatedly, it will take as many calls to
/// <see cref="Disable"/> to disable the system again.
/// </remarks>
public static void Enable()
{
++s_Enabled;
if (s_Enabled > 1)
return;
InputSystem.onDeviceChange += OnDeviceChange;
InputSystem.onBeforeUpdate += Touch.BeginUpdate;
InputSystem.onSettingsChange += OnSettingsChange;
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeDomainReload;
#endif
SetUpState();
}
/// <summary>
/// Disable enhanced touch support.
/// </summary>
/// <remarks>
/// This method only undoes a single call to <see cref="Enable"/>.
/// </remarks>
public static void Disable()
{
if (!enabled)
return;
--s_Enabled;
if (s_Enabled > 0)
return;
InputSystem.onDeviceChange -= OnDeviceChange;
InputSystem.onBeforeUpdate -= Touch.BeginUpdate;
InputSystem.onSettingsChange -= OnSettingsChange;
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
#endif
TearDownState();
}
internal static void Reset()
{
Touch.s_GlobalState.touchscreens = default;
Touch.s_GlobalState.playerState.Destroy();
Touch.s_GlobalState.playerState = default;
#if UNITY_EDITOR
Touch.s_GlobalState.editorState.Destroy();
Touch.s_GlobalState.editorState = default;
#endif
s_Enabled = 0;
}
private static void SetUpState()
{
Touch.s_GlobalState.playerState.updateMask = InputUpdateType.Dynamic | InputUpdateType.Manual | InputUpdateType.Fixed;
#if UNITY_EDITOR
Touch.s_GlobalState.editorState.updateMask = InputUpdateType.Editor;
#endif
s_UpdateMode = InputSystem.settings.updateMode;
foreach (var device in InputSystem.devices)
OnDeviceChange(device, InputDeviceChange.Added);
}
internal static void TearDownState()
{
foreach (var device in InputSystem.devices)
OnDeviceChange(device, InputDeviceChange.Removed);
Touch.s_GlobalState.playerState.Destroy();
#if UNITY_EDITOR
Touch.s_GlobalState.editorState.Destroy();
#endif
Touch.s_GlobalState.playerState = default;
#if UNITY_EDITOR
Touch.s_GlobalState.editorState = default;
#endif
}
private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
switch (change)
{
case InputDeviceChange.Added:
{
if (device is Touchscreen touchscreen)
Touch.AddTouchscreen(touchscreen);
break;
}
case InputDeviceChange.Removed:
{
if (device is Touchscreen touchscreen)
Touch.RemoveTouchscreen(touchscreen);
break;
}
}
}
private static void OnSettingsChange()
{
var currentUpdateMode = InputSystem.settings.updateMode;
if (s_UpdateMode == currentUpdateMode)
return;
TearDownState();
SetUpState();
}
#if UNITY_EDITOR
private static void OnBeforeDomainReload()
{
// We need to release NativeArrays we're holding before losing track of them during domain reloads.
Touch.s_GlobalState.playerState.Destroy();
Touch.s_GlobalState.editorState.Destroy();
}
#endif
[Conditional("DEVELOPMENT_BUILD")]
[Conditional("UNITY_EDITOR")]
internal static void CheckEnabled()
{
if (!enabled)
throw new InvalidOperationException("EnhancedTouch API is not enabled; call EnhancedTouchSupport.Enable()");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3cf42e49077e442fd8b9dc29c328d345
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,266 @@
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.EnhancedTouch
{
/// <summary>
/// A source of touches (<see cref="Touch"/>).
/// </summary>
/// <remarks>
/// Each <see cref="Touchscreen"/> has a limited number of fingers it supports corresponding to the total number of concurrent
/// touches supported by the screen. Unlike a <see cref="Touch"/>, a <see cref="Finger"/> will stay the same and valid for the
/// lifetime of its <see cref="Touchscreen"/>.
///
/// Note that a Finger does not represent an actual physical finger in the world. That is, the same Finger instance might be used,
/// for example, for a touch from the index finger at one point and then for a touch from the ring finger. Each Finger simply
/// corresponds to the Nth touch on the given screen.
/// </remarks>
/// <seealso cref="Touch"/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
Justification = "Holds on to internally managed memory which should not be disposed by the user.")]
public class Finger
{
// This class stores pretty much all the data that is kept by the enhanced touch system. All
// the finger and history tracking is found here.
/// <summary>
/// The screen that the finger is associated with.
/// </summary>
/// <value>Touchscreen associated with the touch.</value>
public Touchscreen screen { get; }
/// <summary>
/// Index of the finger on <see cref="screen"/>. Each finger corresponds to the Nth touch on a screen.
/// </summary>
public int index { get; }
/// <summary>
/// Whether the finger is currently touching the screen.
/// </summary>
public bool isActive => currentTouch.valid;
/// <summary>
/// The current position of the finger on the screen or <c>default(Vector2)</c> if there is no
/// ongoing touch.
/// </summary>
public Vector2 screenPosition
{
get
{
////REVIEW: should this work off of currentTouch instead of lastTouch?
var touch = lastTouch;
if (!touch.valid)
return default;
return touch.screenPosition;
}
}
////REVIEW: should lastTouch and currentTouch have accumulated deltas? would that be confusing?
/// <summary>
/// The last touch that happened on the finger or <c>default(Touch)</c> (with <see cref="Touch.valid"/> being
/// false) if no touch has been registered on the finger yet.
/// </summary>
/// <remarks>
/// A given touch will be returned from this property for as long as no new touch has been started. As soon as a
/// new touch is registered on the finger, the property switches to the new touch.
/// </remarks>
public Touch lastTouch
{
get
{
var count = m_StateHistory.Count;
if (count == 0)
return default;
return new Touch(this, m_StateHistory[count - 1]);
}
}
/// <summary>
/// The currently ongoing touch for the finger or <c>default(Touch)</c> (with <see cref="Touch.valid"/> being false)
/// if no touch is currently in progress on the finger.
/// </summary>
public Touch currentTouch
{
get
{
var touch = lastTouch;
if (!touch.valid)
return default;
if (touch.isInProgress)
return touch;
// Ended touches stay current in the frame they ended in.
if (touch.updateStepCount == InputUpdate.s_UpdateStepCount)
return touch;
return default;
}
}
/// <summary>
/// The full touch history of the finger.
/// </summary>
/// <remarks>
/// The history is capped at <see cref="Touch.maxHistoryLengthPerFinger"/>. Once full, newer touch records will start
/// overwriting older entries. Note that this means that a given touch will not trace all the way back to its beginning
/// if it runs past the max history size.
/// </remarks>
public TouchHistory touchHistory => new TouchHistory(this, m_StateHistory);
internal readonly InputStateHistory<TouchState> m_StateHistory;
internal Finger(Touchscreen screen, int index, InputUpdateType updateMask)
{
this.screen = screen;
this.index = index;
// Set up history recording.
m_StateHistory = new InputStateHistory<TouchState>(screen.touches[index])
{
historyDepth = Touch.maxHistoryLengthPerFinger,
extraMemoryPerRecord = UnsafeUtility.SizeOf<Touch.ExtraDataPerTouchState>(),
onRecordAdded = OnTouchRecorded,
onShouldRecordStateChange = ShouldRecordTouch,
updateMask = updateMask,
};
m_StateHistory.StartRecording();
// record the current state if touch is already in progress
if (screen.touches[index].isInProgress)
m_StateHistory.RecordStateChange(screen.touches[index], screen.touches[index].value);
}
private static unsafe bool ShouldRecordTouch(InputControl control, double time, InputEventPtr eventPtr)
{
// We only want to record changes that come from events. We ignore internal state
// changes that Touchscreen itself generates. This includes the resetting of deltas.
if (!eventPtr.valid)
return false;
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
return false;
// Direct memory access for speed.
var currentTouchState = (TouchState*)((byte*)control.currentStatePtr + control.stateBlock.byteOffset);
// Touchscreen will record a button down and button up on a TouchControl when a tap occurs.
// We only want to record the button down, not the button up.
if (currentTouchState->isTapRelease)
return false;
return true;
}
private unsafe void OnTouchRecorded(InputStateHistory.Record record)
{
var recordIndex = record.recordIndex;
var touchHeader = m_StateHistory.GetRecordUnchecked(recordIndex);
var touchState = (TouchState*)touchHeader->statePtrWithoutControlIndex; // m_StateHistory is bound to a single TouchControl.
touchState->updateStepCount = InputUpdate.s_UpdateStepCount;
// Invalidate activeTouches.
Touch.s_GlobalState.playerState.haveBuiltActiveTouches = false;
// Record the extra data we maintain for each touch.
var extraData = (Touch.ExtraDataPerTouchState*)((byte*)touchHeader + m_StateHistory.bytesPerRecord -
UnsafeUtility.SizeOf<Touch.ExtraDataPerTouchState>());
extraData->uniqueId = ++Touch.s_GlobalState.playerState.lastId;
// We get accumulated deltas from Touchscreen. Store the accumulated
// value and "unaccumulate" the value we store on delta.
extraData->accumulatedDelta = touchState->delta;
if (touchState->phase != TouchPhase.Began)
{
// Inlined (instead of just using record.previous) for speed. Bypassing
// the safety checks here.
if (recordIndex != m_StateHistory.m_HeadIndex)
{
var previousRecordIndex = recordIndex == 0 ? m_StateHistory.historyDepth - 1 : recordIndex - 1;
var previousTouchHeader = m_StateHistory.GetRecordUnchecked(previousRecordIndex);
var previousTouchState = (TouchState*)previousTouchHeader->statePtrWithoutControlIndex;
touchState->delta -= previousTouchState->delta;
touchState->beganInSameFrame = previousTouchState->beganInSameFrame &&
previousTouchState->updateStepCount == touchState->updateStepCount;
}
}
else
{
touchState->beganInSameFrame = true;
}
// Trigger callback.
switch (touchState->phase)
{
case TouchPhase.Began:
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerDown, this, "Touch.onFingerDown");
break;
case TouchPhase.Moved:
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerMove, this, "Touch.onFingerMove");
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
DelegateHelpers.InvokeCallbacksSafe(ref Touch.s_GlobalState.onFingerUp, this, "Touch.onFingerUp");
break;
}
}
private unsafe Touch FindTouch(uint uniqueId)
{
Debug.Assert(uniqueId != default, "0 is not a valid ID");
foreach (var record in m_StateHistory)
{
if (((Touch.ExtraDataPerTouchState*)record.GetUnsafeExtraMemoryPtrUnchecked())->uniqueId == uniqueId)
return new Touch(this, record);
}
return default;
}
internal unsafe TouchHistory GetTouchHistory(Touch touch)
{
Debug.Assert(touch.finger == this);
// If the touch is not pointing to our history, it's probably a touch we copied for
// activeTouches. We know the unique ID of the touch so go and try to find the touch
// in our history.
var touchRecord = touch.m_TouchRecord;
if (touchRecord.owner != m_StateHistory)
{
touch = FindTouch(touch.uniqueId);
if (!touch.valid)
return default;
}
var touchId = touch.touchId;
var startIndex = touch.m_TouchRecord.index;
// If the current touch isn't the beginning of the touch, search back through the
// history for all touches belonging to the same contact.
var count = 0;
if (touch.phase != TouchPhase.Began)
{
for (var previousRecord = touch.m_TouchRecord.previous; previousRecord.valid; previousRecord = previousRecord.previous)
{
var touchState = (TouchState*)previousRecord.GetUnsafeMemoryPtr();
// Stop if the touch doesn't belong to the same contact.
if (touchState->touchId != touchId)
break;
++count;
// Stop if we've found the beginning of the touch.
if (touchState->phase == TouchPhase.Began)
break;
}
}
if (count == 0)
return default;
// We don't want to include the touch we started with.
--startIndex;
return new TouchHistory(this, m_StateHistory, startIndex, count);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71f3c0e630f0148b496f565843b5b272
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,999 @@
using System;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Utilities;
////TODO: recorded times are baked *external* times; reset touch when coming out of play mode
////REVIEW: record velocity on touches? or add method to very easily get the data?
////REVIEW: do we need to keep old touches around on activeTouches like the old UnityEngine touch API?
namespace UnityEngine.InputSystem.EnhancedTouch
{
/// <summary>
/// A high-level representation of a touch which automatically keeps track of a touch
/// over time.
/// </summary>
/// <remarks>
/// This API obsoletes the need for manually keeping tracking of touch IDs (<see cref="TouchControl.touchId"/>)
/// and touch phases (<see cref="TouchControl.phase"/>) in order to tell one touch apart from another.
///
/// Also, this class protects against losing touches. If a touch is shorter-lived than a single input update,
/// <see cref="Touchscreen"/> may overwrite it with a new touch coming in the same update whereas this class
/// will retain all changes that happened on the touchscreen in any particular update.
///
/// The API makes a distinction between "fingers" and "touches". A touch refers to one contact state change event, that is, a
/// finger beginning to touch the screen (<see cref="TouchPhase.Began"/>), moving on the screen (<see cref="TouchPhase.Moved"/>),
/// or being lifted off the screen (<see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>).
/// A finger, on the other hand, always refers to the Nth contact on the screen.
///
/// A Touch instance is a struct which only contains a reference to the actual data which is stored in unmanaged
/// memory.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem.EnhancedTouch;
///
/// // Alias EnhancedTouch.Touch to "Touch" for less typing.
/// using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
/// using TouchPhase = UnityEngine.InputSystem.TouchPhase;
///
/// public class Example : MonoBehaviour
/// {
/// void Awake()
/// {
/// // Note that enhanced touch support needs to be explicitly enabled.
/// EnhancedTouchSupport.Enable();
/// }
///
/// void Update()
/// {
/// // Illustrates how to examine all active touches once per frame and show their last recorded position
/// // in the associated screen-space.
/// foreach (var touch in Touch.activeTouches)
/// {
/// switch (touch.phase)
/// {
/// case TouchPhase.Began:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} started this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Ended:
/// Debug.Log($"Frame {Time.frameCount}:Touch {touch} ended this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Moved:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} moved this frame to ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Canceled:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} was canceled this frame");
/// break;
/// case TouchPhase.Stationary:
/// Debug.Log($"Frame {Time.frameCount}: ouch {touch} was not updated this frame");
/// break;
/// }
/// }
/// }
/// }
/// </code>
/// </example>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
public struct Touch : IEquatable<Touch>
{
// The way this works is that at the core, it simply attaches one InputStateHistory per "<Touchscreen>/touch*"
// control and then presents a public API that crawls over the recorded touch history in various ways.
/// <summary>
/// Whether this touch record holds valid data.
/// </summary>
/// <remarks>
/// <para>
/// Touch data is stored in unmanaged memory as a circular input buffer. This means that when
/// the buffer runs out of capacity, older touch entries will get reused. When this happens,
/// existing <c>Touch</c> instances referring to the record become invalid.
/// </para>
/// <para>
/// This property can be used to determine whether the record held on to by the <c>Touch</c>
/// instance is still valid.
/// </para>
/// <para>
/// This property will be <c>false</c> for default-initialized <c>Touch</c> instances.
/// </para>
/// <para>
/// Note that accessing most of the other properties on this struct when the touch is
/// invalid will trigger <see cref="InvalidOperationException"/>.
/// </para>
/// </remarks>
public bool valid => m_TouchRecord.valid;
/// <summary>
/// The finger used for the touch contact.
/// </summary>
/// <remarks>
/// <para>
/// Note that this is only <c>null</c> for default-initialized instances of the struct.
/// </para>
/// <para>
/// See <see cref="activeFingers"/> for how to access all active fingers.
/// </para>
/// </remarks>
public Finger finger => m_Finger;
/// <summary>
/// The current touch phase of the touch indicating its current state in the phase cycle.
/// </summary>
/// <remarks>
/// <para>
/// Every touch goes through a predefined cycle that starts with <see cref="TouchPhase.Began"/>,
/// then potentially <see cref="TouchPhase.Moved"/> and/or <see cref="TouchPhase.Stationary"/>,
/// and finally concludes with either <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
/// </para>
/// <para>
/// This property indicates where in the cycle the touch is and is based on <see cref="TouchControl.phase"/>.
/// </para>
/// <para>
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
/// </para>
/// </remarks>
public TouchPhase phase => state.phase;
/// <summary>
/// Whether the touch has begun this frame, i.e. whether <see cref="phase"/> is <see cref="TouchPhase.Began"/>.
/// </summary>
/// <remarks>
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
/// </remarks>
public bool began => phase == TouchPhase.Began;
/// <summary>
/// Whether the touch is currently in progress, i.e. whether <see cref="phase"/> is either
/// <see cref="TouchPhase.Moved"/>, <see cref="TouchPhase.Stationary"/>, or <see cref="TouchPhase.Began"/>.
/// </summary>
public bool inProgress => phase == TouchPhase.Moved || phase == TouchPhase.Stationary || phase == TouchPhase.Began;
/// <summary>
/// Whether the touch has ended this frame, i.e. whether <see cref="phase"/> is either
/// <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
/// </summary>
/// <remarks>
/// Use <see cref="isInProgress"/> to more conveniently evaluate whether this touch is currently active or not.
/// </remarks>
public bool ended => phase == TouchPhase.Ended || phase == TouchPhase.Canceled;
/// <summary>
/// Unique ID of the touch as (usually) assigned by the platform.
/// </summary>
/// <remarks>
/// <para>
/// Each touch contact that is made with the screen receives its own unique, non-zero ID which is
/// normally assigned by the underlying platform via <see cref="TouchControl.touchId"/>.
/// </para>
/// <para>
/// Note a platform may reuse touch IDs after their respective touches have finished.
/// This means that the guarantee of uniqueness is only made with respect to <see cref="activeTouches"/>.
/// </para>
/// <para>
/// In particular, all touches in <see cref="history"/> will have the same ID whereas
/// touches in the finger's <see cref="Finger.touchHistory"/> may end up having the same
/// touch ID even though constituting different physical touch contacts.
/// </para>
/// </remarks>
public int touchId => state.touchId;
/// <summary>
/// Normalized pressure of the touch against the touch surface.
/// </summary>
/// <remarks>
/// <para>
/// Not all touchscreens are pressure-sensitive. If unsupported, this property will
/// always return 0.
/// </para>
/// <para>
/// In general, touch pressure is supported on mobile platforms only.
/// </para>
/// <para>
/// Touch pressure may also be retrieved directly from the device control via <see cref="TouchControl.pressure"/>.
/// </para>
/// <para>
/// Note that it is possible for the value to go above 1 even though it is considered normalized. The reason is
/// that calibration on the system can put the maximum pressure point below the physically supported maximum value.
/// </para>
/// </remarks>
public float pressure => state.pressure;
/// <summary>
/// Screen-space radius of the touch which define its horizontal and vertical extents.
/// </summary>
/// <remarks>
/// <para>
/// If supported by the underlying device, this reports the size of the touch contact based on its
/// <see cref="screenPosition"/> center point. If not supported, this will be <c>default(Vector2)</c>.
/// </para>
/// <para>
/// Touch radius may also be retrieved directly from the device control via <see cref="TouchControl.radius"/>.
/// </para>
/// </remarks>
public Vector2 radius => state.radius;
/// <summary>
/// Start time of the touch in seconds on the same timeline as <c>Time.realTimeSinceStartup</c>.
/// </summary>
/// <remarks>
/// This is the value of <see cref="InputEvent.time"/> when the touch started with
/// <see cref="phase"/> <see cref="TouchPhase.Began"/>. Note that start time may also be retrieved directly
/// from the device control via <see cref="TouchControl.startTime"/>.
/// </remarks>
public double startTime => state.startTime;
/// <summary>
/// Time the touch record was reported on the same timeline as <see cref="UnityEngine.Time.realtimeSinceStartup"/>.
/// </summary>
/// <remarks>
/// This is the value <see cref="InputEvent.time"/> of the event that signaled the current state
/// change for the touch.
/// </remarks>
public double time => m_TouchRecord.time;
/// <summary>
/// The <see cref="Touchscreen"/> associated with the touch contact.
/// </summary>
public Touchscreen screen => finger.screen;
/// <summary>
/// Screen-space position of the touch.
/// </summary>
/// <remarks>Also see <see cref="TouchControl.position"/> for retrieving position directly from a device
/// control.</remarks>
public Vector2 screenPosition => state.position;
/// <summary>
/// Screen-space position where the touch started.
/// </summary>
/// <remarks>Also see <see cref="TouchControl.startPosition"/> for retrieving start position directly from
/// a device control.</remarks>
public Vector2 startScreenPosition => state.startPosition;
/// <summary>
/// Screen-space motion delta of the touch.
/// </summary>
/// <remarks>
/// <para>
/// Note that deltas have behaviors attached to them different from most other
/// controls. See <see cref="Pointer.delta"/> for details.
/// </para>
/// <para>
/// Also see <see cref="TouchControl.delta"/> for retrieving delta directly from a device control.
/// </para>
/// </remarks>
public Vector2 delta => state.delta;
/// <summary>
/// Number of times that the touch has been tapped in succession.
/// </summary>
/// <value>Indicates how many taps have been performed one after the other.</value>
/// <remarks>
/// <para>
/// Successive taps have to come within <see cref="InputSettings.multiTapDelayTime"/> for them
/// to increase the tap count. I.e. if a new tap finishes within that time after <see cref="startTime"/>
/// of the previous touch, the tap count is increased by one. If more than <see cref="InputSettings.multiTapDelayTime"/>
/// passes after a tap with no successive tap, the tap count is reset to zero.
/// </para>
/// <para>
/// Also see <see cref="TouchControl.tapCount"/> for retrieving tap count directly from a device control.
/// </para>
/// </remarks>
public int tapCount => state.tapCount;
/// <summary>
/// Whether the touch has performed a tap.
/// </summary>
/// <value>Indicates whether the touch has tapped the screen.</value>
/// <remarks>
/// <para>
/// A tap is defined as a touch that begins and ends within <see cref="InputSettings.defaultTapTime"/> and
/// stays within <see cref="InputSettings.tapRadius"/> of its <see cref="startScreenPosition"/>. If this
/// is the case for a touch, this button is set to 1 at the time the touch goes to <see cref="phase"/>
/// <see cref="TouchPhase.Ended"/>.
/// </para>
/// <para>
/// Resets to 0 only when another touch is started on the control or when the control is reset.
/// </para>
/// <para>
/// Use <see cref="tapCount"/> to determine if there were multiple taps occurring during the frame.
/// Also note that <see cref="TouchControl.tap"/> may be used to determine whether there was a tap.
/// </para>
/// </remarks>
public bool isTap => state.isTap;
/// <summary>
/// The index of the display containing the touch.
/// </summary>
/// <remarks>
/// <para>
/// A zero based number representing the display index of the <see cref="Display"/> that contains the touch.
/// </para>
/// <para>
/// Also see <see cref="TouchControl.displayIndex"/> for retrieving display index directly from a device
/// control.
/// </para>
/// </remarks>
public int displayIndex => state.displayIndex;
/// <summary>
/// Whether the touch is currently in progress.
/// </summary>
/// <remarks>This is effectively equivalent to checking if <see cref="phase"/> is equal to either of:
/// <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Moved"/>, or <see cref="TouchPhase.Stationary"/>.
/// </remarks>
public bool isInProgress
{
get
{
switch (phase)
{
case TouchPhase.Began:
case TouchPhase.Moved:
case TouchPhase.Stationary:
return true;
}
return false;
}
}
internal uint updateStepCount => state.updateStepCount;
internal uint uniqueId => extraData.uniqueId;
private unsafe ref TouchState state => ref *(TouchState*)m_TouchRecord.GetUnsafeMemoryPtr();
private unsafe ref ExtraDataPerTouchState extraData =>
ref *(ExtraDataPerTouchState*)m_TouchRecord.GetUnsafeExtraMemoryPtr();
/// <summary>
/// History touch readings for this specific touch contact.
/// </summary>
/// <remarks>
/// Unlike <see cref="Finger.touchHistory"/>, this gives the history of this touch only.
/// </remarks>
public TouchHistory history
{
get
{
if (!valid)
throw new InvalidOperationException("Touch is invalid");
return finger.GetTouchHistory(this);
}
}
/// <summary>
/// All touches that are either ongoing as of the current frame or have ended in the current frame.
/// </summary>
/// <remarks>
/// A touch that begins in a frame will always have its phase set to <see cref="TouchPhase.Began"/> even
/// if there was also movement (or even an end/cancellation) for the touch in the same frame.
///
/// A touch that begins and ends in the same frame will have its <see cref="TouchPhase.Began"/> surface
/// in that frame and then another entry with <see cref="TouchPhase.Ended"/> surface in the
/// <em>next</em> frame. This logic implies that there can be more active touches than concurrent touches
/// supported by the hardware/platform.
///
/// A touch that begins and moves in the same frame will have its <see cref="TouchPhase.Began"/> surface
/// in that frame and then another entry with <see cref="TouchPhase.Moved"/> and the screen motion
/// surface in the <em>next</em> frame <em>except</em> if the touch also ended in the frame (in which
/// case <see cref="phase"/> will be <see cref="TouchPhase.Ended"/> instead of <see cref="TouchPhase.Moved"/>).
///
/// Note that the touches reported by this API do <em>not</em> necessarily have to match the contents of
/// <a href="https://docs.unity3d.com/ScriptReference/Input-touches.html">UnityEngine.Input.touches</a>.
/// The reason for this is that the <c>UnityEngine.Input</c> API and the Input System API flush their input
/// queues at different points in time and may thus have a different view on available input. In particular,
/// the Input System event queue is flushed <em>later</em> in the frame than inputs for <c>UnityEngine.Input</c>
/// and may thus have newer inputs available. On Android, for example, touch input is gathered from a separate
/// UI thread and fed into the input system via a "background" event queue that can gather input asynchronously.
/// Due to this setup, touch events that will reach <c>UnityEngine.Input</c> only in the next frame may have
/// already reached the Input System.
///
/// In order to evaluate all active touches on a per-frame basis see <see cref="activeFingers"/>.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem.EnhancedTouch;
///
/// // Alias EnhancedTouch.Touch to "Touch" for less typing.
/// using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
/// using TouchPhase = UnityEngine.InputSystem.TouchPhase;
///
/// public class Example : MonoBehaviour
/// {
/// void Awake()
/// {
/// // Note that enhanced touch support needs to be explicitly enabled.
/// EnhancedTouchSupport.Enable();
/// }
///
/// void Update()
/// {
/// // Illustrates how to examine all active touches once per frame and show their last recorded position
/// // in the associated screen-space.
/// foreach (var touch in Touch.activeTouches)
/// {
/// switch (touch.phase)
/// {
/// case TouchPhase.Began:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} started this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Ended:
/// Debug.Log($"Frame {Time.frameCount}:Touch {touch} ended this frame at ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Moved:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} moved this frame to ({touch.screenPosition.x}, {touch.screenPosition.y})");
/// break;
/// case TouchPhase.Canceled:
/// Debug.Log($"Frame {Time.frameCount}: Touch {touch} was canceled this frame");
/// break;
/// case TouchPhase.Stationary:
/// Debug.Log($"Frame {Time.frameCount}: ouch {touch} was not updated this frame");
/// break;
/// }
/// }
/// }
/// }
/// </code>
/// </example>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static ReadOnlyArray<Touch> activeTouches
{
get
{
EnhancedTouchSupport.CheckEnabled();
// We lazily construct the array of active touches.
s_GlobalState.playerState.UpdateActiveTouches();
return new ReadOnlyArray<Touch>(s_GlobalState.playerState.activeTouches, 0, s_GlobalState.playerState.activeTouchCount);
}
}
/// <summary>
/// An array of all possible concurrent touch contacts, i.e. all concurrent touch contacts regardless of whether
/// they are currently active or not.
/// </summary>
/// <remarks>
/// For querying only active fingers, use <see cref="activeFingers"/>.
/// For querying only active touches, use <see cref="activeTouches"/>.
///
/// The length of this array will always correspond to the maximum number of concurrent touches supported by the system.
/// Note that the actual number of physically supported concurrent touches as determined by the current hardware and
/// operating system may be lower than this number.
/// </remarks>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static ReadOnlyArray<Finger> fingers
{
get
{
EnhancedTouchSupport.CheckEnabled();
return new ReadOnlyArray<Finger>(s_GlobalState.playerState.fingers, 0, s_GlobalState.playerState.totalFingerCount);
}
}
/// <summary>
/// Set of currently active fingers, i.e. touch contacts that currently have an active touch (as defined by <see cref="activeTouches"/>).
/// </summary>
/// <remarks>
/// To instead get a collection of all fingers (not only currently active) use <see cref="fingers"/>.
/// </remarks>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static ReadOnlyArray<Finger> activeFingers
{
get
{
EnhancedTouchSupport.CheckEnabled();
// We lazily construct the array of active fingers.
s_GlobalState.playerState.UpdateActiveFingers();
return new ReadOnlyArray<Finger>(s_GlobalState.playerState.activeFingers, 0, s_GlobalState.playerState.activeFingerCount);
}
}
/// <summary>
/// Return the set of <see cref="Touchscreen"/>s on which touch input is monitored.
/// </summary>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static IEnumerable<Touchscreen> screens
{
get
{
EnhancedTouchSupport.CheckEnabled();
return s_GlobalState.touchscreens;
}
}
/// <summary>
/// Event that is invoked when a finger touches a <see cref="Touchscreen"/>.
/// </summary>
/// <remarks>
/// In order to react to a finger being moved or released, see <see cref="onFingerMove"/> and
/// <see cref="onFingerUp"/> respectively.
/// </remarks>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static event Action<Finger> onFingerDown
{
add
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerDown.AddCallback(value);
}
remove
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerDown.RemoveCallback(value);
}
}
/// <summary>
/// Event that is invoked when a finger stops touching a <see cref="Touchscreen"/>.
/// </summary>
/// <remarks>
/// In order to react to a finger that touches a <see cref="Touchscreen"/> or a finger that is being moved
/// use <see cref="onFingerDown"/> and <see cref="onFingerMove"/> respectively.
/// </remarks>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static event Action<Finger> onFingerUp
{
add
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerUp.AddCallback(value);
}
remove
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerUp.RemoveCallback(value);
}
}
/// <summary>
/// Event that is invoked when a finger that is in contact with a <see cref="Touchscreen"/> moves
/// on the screen.
/// </summary>
/// <remarks>
/// In order to react to a finger that touches a <see cref="Touchscreen"/> or a finger that stops touching
/// a <see cref="Touchscreen"/>, use <see cref="onFingerDown"/> and <see cref="onFingerUp"/> respectively.
/// </remarks>
/// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
public static event Action<Finger> onFingerMove
{
add
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerMove.AddCallback(value);
}
remove
{
EnhancedTouchSupport.CheckEnabled();
if (value == null)
throw new ArgumentNullException(nameof(value));
s_GlobalState.onFingerMove.RemoveCallback(value);
}
}
/*
public static Action<Finger> onFingerTap
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
*/
/// <summary>
/// The amount of history kept for each single touch.
/// </summary>
/// <remarks>
/// By default, this is zero meaning that no history information is kept for
/// touches. Setting this to <c>Int32.maxValue</c> will cause all history from
/// the beginning to the end of a touch being kept.
/// </remarks>
public static int maxHistoryLengthPerFinger
{
get => s_GlobalState.historyLengthPerFinger;
////TODO
/*set { throw new NotImplementedException(); }*/
}
internal Touch(Finger finger, InputStateHistory<TouchState>.Record touchRecord)
{
m_Finger = finger;
m_TouchRecord = touchRecord;
}
/// <inheritdoc/>
public override string ToString()
{
if (!valid)
return "<None>";
return $"{{id={touchId} finger={finger.index} phase={phase} position={screenPosition} delta={delta} time={time}}}";
}
/// <summary>
/// Compares this touch for equality with another instance <paramref name="other"/>.
/// </summary>
/// <param name="other">The other instance to compare with.</param>
/// <returns><c>true</c> if this touch and <paramref name="other"/> represents the same finger and maps to the
/// same touch record, otherwise <c>false</c></returns>
public bool Equals(Touch other)
{
return Equals(m_Finger, other.m_Finger) && m_TouchRecord.Equals(other.m_TouchRecord);
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
return obj is Touch other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
{
return ((m_Finger != null ? m_Finger.GetHashCode() : 0) * 397) ^ m_TouchRecord.GetHashCode();
}
}
internal static void AddTouchscreen(Touchscreen screen)
{
Debug.Assert(!s_GlobalState.touchscreens.ContainsReference(screen), "Already added touchscreen");
s_GlobalState.touchscreens.AppendWithCapacity(screen, capacityIncrement: 5);
// Add finger tracking to states.
s_GlobalState.playerState.AddFingers(screen);
#if UNITY_EDITOR
s_GlobalState.editorState.AddFingers(screen);
#endif
}
internal static void RemoveTouchscreen(Touchscreen screen)
{
Debug.Assert(s_GlobalState.touchscreens.ContainsReference(screen), "Did not add touchscreen");
// Remove from list.
var index = s_GlobalState.touchscreens.IndexOfReference(screen);
s_GlobalState.touchscreens.RemoveAtWithCapacity(index);
// Remove fingers from states.
s_GlobalState.playerState.RemoveFingers(screen);
#if UNITY_EDITOR
s_GlobalState.editorState.RemoveFingers(screen);
#endif
}
////TODO: only have this hooked when we actually need it
internal static void BeginUpdate()
{
#if UNITY_EDITOR
if ((InputState.currentUpdateType == InputUpdateType.Editor && s_GlobalState.playerState.updateMask != InputUpdateType.Editor) ||
(InputState.currentUpdateType != InputUpdateType.Editor && s_GlobalState.playerState.updateMask == InputUpdateType.Editor))
{
// Either swap in editor state and retain currently active player state in s_EditorState
// or swap player state back in.
MemoryHelpers.Swap(ref s_GlobalState.playerState, ref s_GlobalState.editorState);
}
#endif
// If we have any touches in activeTouches that are ended or canceled,
// we need to clear them in the next frame.
if (s_GlobalState.playerState.haveActiveTouchesNeedingRefreshNextUpdate)
s_GlobalState.playerState.haveBuiltActiveTouches = false;
}
private readonly Finger m_Finger;
internal InputStateHistory<TouchState>.Record m_TouchRecord;
/// <summary>
/// Holds global (static) touch state.
/// </summary>
internal struct GlobalState
{
internal InlinedArray<Touchscreen> touchscreens;
internal int historyLengthPerFinger;
internal CallbackArray<Action<Finger>> onFingerDown;
internal CallbackArray<Action<Finger>> onFingerMove;
internal CallbackArray<Action<Finger>> onFingerUp;
internal FingerAndTouchState playerState;
#if UNITY_EDITOR
internal FingerAndTouchState editorState;
#endif
}
private static GlobalState CreateGlobalState()
{ // Convenient method since parameterized construction is default
return new GlobalState { historyLengthPerFinger = 64 };
}
internal static GlobalState s_GlobalState = CreateGlobalState();
internal static ISavedState SaveAndResetState()
{
// Save current state
var savedState = new SavedStructState<GlobalState>(
ref s_GlobalState,
(ref GlobalState state) => s_GlobalState = state,
() => { /* currently nothing to dispose */ });
// Reset global state
s_GlobalState = CreateGlobalState();
return savedState;
}
// In scenarios where we have to support multiple different types of input updates (e.g. in editor or in
// player when both dynamic and fixed input updates are enabled), we need more than one copy of touch state.
// We encapsulate the state in this struct so that we can easily swap it.
//
// NOTE: Finger instances are per state. This means that you will actually see different Finger instances for
// the same finger in two different update types.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
Justification = "Managed internally")]
internal struct FingerAndTouchState
{
public InputUpdateType updateMask;
public Finger[] fingers;
public Finger[] activeFingers;
public Touch[] activeTouches;
public int activeFingerCount;
public int activeTouchCount;
public int totalFingerCount;
public uint lastId;
public bool haveBuiltActiveTouches;
public bool haveActiveTouchesNeedingRefreshNextUpdate;
// `activeTouches` adds yet another view of input state that is different from "normal" recorded
// state history. In this view, touches become stationary in the next update and deltas reset
// between updates. We solve this by storing state separately for active touches. We *only* do
// so when `activeTouches` is actually queried meaning that `activeTouches` has no overhead if
// not used.
public InputStateHistory<TouchState> activeTouchState;
public void AddFingers(Touchscreen screen)
{
var touchCount = screen.touches.Count;
ArrayHelpers.EnsureCapacity(ref fingers, totalFingerCount, touchCount);
for (var i = 0; i < touchCount; ++i)
{
var finger = new Finger(screen, i, updateMask);
ArrayHelpers.AppendWithCapacity(ref fingers, ref totalFingerCount, finger);
}
}
public void RemoveFingers(Touchscreen screen)
{
var touchCount = screen.touches.Count;
for (var i = 0; i < fingers.Length; ++i)
{
if (fingers[i].screen != screen)
continue;
// Release unmanaged memory.
for (var n = 0; n < touchCount; ++n)
fingers[i + n].m_StateHistory.Dispose();
////REVIEW: leave Fingers in place and reuse the instances?
ArrayHelpers.EraseSliceWithCapacity(ref fingers, ref totalFingerCount, i, touchCount);
break;
}
// Force rebuilding of active touches.
haveBuiltActiveTouches = false;
}
public void Destroy()
{
for (var i = 0; i < totalFingerCount; ++i)
fingers[i].m_StateHistory.Dispose();
activeTouchState?.Dispose();
activeTouchState = null;
}
public void UpdateActiveFingers()
{
////TODO: do this only once per update per activeFingers getter
activeFingerCount = 0;
for (var i = 0; i < totalFingerCount; ++i)
{
var finger = fingers[i];
var lastTouch = finger.currentTouch;
if (lastTouch.valid)
ArrayHelpers.AppendWithCapacity(ref activeFingers, ref activeFingerCount, finger);
}
}
public unsafe void UpdateActiveTouches()
{
if (haveBuiltActiveTouches)
return;
// Clear activeTouches state.
if (activeTouchState == null)
{
activeTouchState = new InputStateHistory<TouchState>
{
extraMemoryPerRecord = UnsafeUtility.SizeOf<ExtraDataPerTouchState>()
};
}
else
{
activeTouchState.Clear();
activeTouchState.m_ControlCount = 0;
activeTouchState.m_Controls.Clear();
}
activeTouchCount = 0;
haveActiveTouchesNeedingRefreshNextUpdate = false;
var currentUpdateStepCount = InputUpdate.s_UpdateStepCount;
////OPTIMIZE: Handle touchscreens that have no activity more efficiently
////FIXME: This is sensitive to history size; we probably need to ensure that the Begans and Endeds/Canceleds of touches are always available to us
//// (instead of rebuild activeTouches from scratch each time, may be more useful to update it)
// Go through fingers and for each one, get the touches that were active this update.
for (var i = 0; i < totalFingerCount; ++i)
{
ref var finger = ref fingers[i];
// NOTE: Many of the operations here are inlined in order to not perform the same
// checks/computations repeatedly.
var history = finger.m_StateHistory;
var touchRecordCount = history.Count;
if (touchRecordCount == 0)
continue;
// We're walking newest-first through the touch history but want the resulting list of
// active touches to be oldest first (so that a record for an ended touch comes before
// a record of a new touch started on the same finger). To achieve that, we insert
// new touch entries for any finger always at the same index (i.e. we prepend rather
// than append).
var insertAt = activeTouchCount;
// Go back in time through the touch records on the finger and collect any touch
// active in the current frame. Note that this may yield *multiple* touches for the
// finger as there may be touches that have ended in the frame while in the same
// frame, a new touch was started.
var currentTouchId = 0;
var currentTouchState = default(TouchState*);
var touchRecordIndex = history.UserIndexToRecordIndex(touchRecordCount - 1); // Start with last record.
var touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
var touchRecordSize = history.bytesPerRecord;
var extraMemoryOffset = touchRecordSize - history.extraMemoryPerRecord;
for (var n = 0; n < touchRecordCount; ++n)
{
if (n != 0)
{
--touchRecordIndex;
if (touchRecordIndex < 0)
{
// We're wrapping around so buffer must be full. Go to last record in buffer.
//touchRecordIndex = history.historyDepth - history.m_HeadIndex - 1;
touchRecordIndex = history.historyDepth - 1;
touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
}
else
{
touchRecordHeader = (InputStateHistory.RecordHeader*)((byte*)touchRecordHeader - touchRecordSize);
}
}
// Skip if part of an ongoing touch we've already recorded.
var touchState = (TouchState*)touchRecordHeader->statePtrWithoutControlIndex; // History is tied to a single TouchControl.
var wasUpdatedThisFrame = touchState->updateStepCount == currentUpdateStepCount;
if (touchState->touchId == currentTouchId && !touchState->phase.IsEndedOrCanceled())
{
// If this is the Began record for the touch and that one happened in
// the current frame, we force the touch phase to Began.
if (wasUpdatedThisFrame && touchState->phase == TouchPhase.Began)
{
Debug.Assert(currentTouchState != null, "Must have current touch record at this point");
currentTouchState->phase = TouchPhase.Began;
currentTouchState->position = touchState->position;
currentTouchState->delta = default;
haveActiveTouchesNeedingRefreshNextUpdate = true;
}
// Need to continue here as there may still be Ended touches that need to
// be taken into account (as in, there may actually be multiple active touches
// for the same finger due to how the polling API works).
continue;
}
// If the touch is older than the current frame and it's a touch that has
// ended, we don't need to look further back into the history as anything
// coming before that will be equally outdated.
if (touchState->phase.IsEndedOrCanceled())
{
// An exception are touches that both began *and* ended in the previous frame.
// For these, we surface the Began in the previous update and the Ended in the
// current frame.
if (!(touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1) &&
!wasUpdatedThisFrame)
break;
}
// Make a copy of the touch so that we can modify data like deltas and phase.
// NOTE: Again, not using AddRecord() for speed.
// NOTE: Unlike `history`, `activeTouchState` stores control indices as each active touch
// will correspond to a different TouchControl.
var touchExtraState = (ExtraDataPerTouchState*)((byte*)touchRecordHeader + extraMemoryOffset);
var newRecordHeader = activeTouchState.AllocateRecord(out var newRecordIndex);
var newRecordState = (TouchState*)newRecordHeader->statePtrWithControlIndex;
var newRecordExtraState = (ExtraDataPerTouchState*)((byte*)newRecordHeader + activeTouchState.bytesPerRecord - UnsafeUtility.SizeOf<ExtraDataPerTouchState>());
newRecordHeader->time = touchRecordHeader->time;
newRecordHeader->controlIndex = ArrayHelpers.AppendWithCapacity(ref activeTouchState.m_Controls,
ref activeTouchState.m_ControlCount, finger.m_StateHistory.controls[0]);
UnsafeUtility.MemCpy(newRecordState, touchState, UnsafeUtility.SizeOf<TouchState>());
UnsafeUtility.MemCpy(newRecordExtraState, touchExtraState, UnsafeUtility.SizeOf<ExtraDataPerTouchState>());
// If the touch hasn't moved this frame, mark it stationary.
// EXCEPT: If we are looked at a Moved touch that also began in the same frame and that
// frame is the one immediately preceding us. In that case, we want to surface the Moved
// as if it happened this frame.
var phase = touchState->phase;
if ((phase == TouchPhase.Moved || phase == TouchPhase.Began) &&
!wasUpdatedThisFrame && !(phase == TouchPhase.Moved && touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1))
{
newRecordState->phase = TouchPhase.Stationary;
newRecordState->delta = default;
}
// If the touch wasn't updated this frame, zero out its delta.
else if (!wasUpdatedThisFrame && !touchState->beganInSameFrame)
{
newRecordState->delta = default;
}
else
{
// We want accumulated deltas only on activeTouches.
newRecordState->delta = newRecordExtraState->accumulatedDelta;
}
var newRecord = new InputStateHistory<TouchState>.Record(activeTouchState, newRecordIndex, newRecordHeader);
var newTouch = new Touch(finger, newRecord);
ArrayHelpers.InsertAtWithCapacity(ref activeTouches, ref activeTouchCount, insertAt, newTouch);
currentTouchId = touchState->touchId;
currentTouchState = newRecordState;
// For anything but stationary touches on the activeTouches list, we need a subsequent
// update in the next frame.
if (newTouch.phase != TouchPhase.Stationary)
haveActiveTouchesNeedingRefreshNextUpdate = true;
}
}
haveBuiltActiveTouches = true;
}
}
internal struct ExtraDataPerTouchState
{
public Vector2 accumulatedDelta;
public uint uniqueId; // Unique ID for touch *record* (i.e. multiple TouchStates having the same touchId will still each have a unique ID).
////TODO
//public uint tapCount;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6261dd2a83757498188548c57e2492bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem.LowLevel;
namespace UnityEngine.InputSystem.EnhancedTouch
{
/// <summary>
/// A fixed-size buffer of <see cref="Touch"/> records used to trace the history of touches.
/// </summary>
/// <remarks>
/// This struct provides access to a recorded list of touches.
/// </remarks>
public struct TouchHistory : IReadOnlyList<Touch>
{
private readonly InputStateHistory<TouchState> m_History;
private readonly Finger m_Finger;
private readonly int m_Count;
private readonly int m_StartIndex;
private readonly uint m_Version;
internal TouchHistory(Finger finger, InputStateHistory<TouchState> history, int startIndex = -1, int count = -1)
{
m_Finger = finger;
m_History = history;
m_Version = history.version;
m_Count = count >= 0 ? count : m_History.Count;
m_StartIndex = startIndex >= 0 ? startIndex : m_History.Count - 1;
}
/// <summary>
/// Enumerate touches in the history. Goes from newest records to oldest.
/// </summary>
/// <returns>Enumerator over the touches in the history.</returns>
public IEnumerator<Touch> GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Number of history records available.
/// </summary>
public int Count => m_Count;
/// <summary>
/// Return a history record by index. Indexing starts at 0 == newest to <see cref="Count"/> - 1 == oldest.
/// </summary>
/// <param name="index">Index of history record.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than 0 or >= <see cref="Count"/>.</exception>
public Touch this[int index]
{
get
{
CheckValid();
if (index < 0 || index >= Count)
throw new ArgumentOutOfRangeException(
$"Index {index} is out of range for history with {Count} entries", nameof(index));
// History records oldest-first but we index newest-first.
return new Touch(m_Finger, m_History[m_StartIndex - index]);
}
}
internal void CheckValid()
{
if (m_Finger == null || m_History == null)
throw new InvalidOperationException("Touch history not initialized");
if (m_History.version != m_Version)
throw new InvalidOperationException(
"Touch history is no longer valid; the recorded history has been changed");
}
private class Enumerator : IEnumerator<Touch>
{
private readonly TouchHistory m_Owner;
private int m_Index;
internal Enumerator(TouchHistory owner)
{
m_Owner = owner;
m_Index = -1;
}
public bool MoveNext()
{
if (m_Index >= m_Owner.Count - 1)
return false;
++m_Index;
return true;
}
public void Reset()
{
m_Index = -1;
}
public Touch Current => m_Owner[m_Index];
object IEnumerator.Current => Current;
public void Dispose()
{
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4cc0c6a7fa1c14a518fec64497f4937f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,411 @@
using System;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.InputSystem.Editor;
#endif
////TODO: add pressure support
////REVIEW: extend this beyond simulating from Pointers only? theoretically, we could simulate from any means of generating positions and presses
////REVIEW: I think this is a workable first attempt but overall, not a sufficient take on input simulation. ATM this uses InputState.Change
//// to shove input directly into Touchscreen. Also, it uses state change notifications to set off the simulation. The latter leads
//// to touch input potentially changing multiple times in response to a single pointer event. And the former leads to the simulated
//// touch input not being visible at the event level -- which leaves Touch and Finger slightly unhappy, for example.
//// I think being able to cycle simulated input fully through the event loop would result in a setup that is both simpler and more robust.
//// Also, it would allow *disabling* the source devices as long as we don't disable them in the backend, too.
//// Finally, the fact that we spin off input *from* events here and feed that into InputState.Change() by passing the event along
//// means that places that make sure we process input only once (e.g. binding composites which will remember the event ID they have
//// been triggered from) may reject the simulated input when they have already seen the non-simulated input (which may be okay
//// behavior).
namespace UnityEngine.InputSystem.EnhancedTouch
{
/// <summary>
/// Adds a <see cref="Touchscreen"/> with input simulated from other types of <see cref="Pointer"/> devices (e.g. <see cref="Mouse"/>
/// or <see cref="Pen"/>).
/// </summary>
[AddComponentMenu("Input/Debug/Touch Simulation")]
[ExecuteInEditMode]
[HelpURL(InputSystem.kDocUrl + "/manual/Touch.html#touch-simulation")]
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public class TouchSimulation : MonoBehaviour, IInputStateChangeMonitor
{
public Touchscreen simulatedTouchscreen { get; private set; }
public static TouchSimulation instance => s_Instance;
public static void Enable()
{
if (instance == null)
{
////TODO: find instance
var hiddenGO = new GameObject();
hiddenGO.SetActive(false);
hiddenGO.hideFlags = HideFlags.HideAndDontSave;
s_Instance = hiddenGO.AddComponent<TouchSimulation>();
instance.gameObject.SetActive(true);
}
instance.enabled = true;
}
public static void Disable()
{
if (instance != null)
instance.enabled = false;
}
public static void Destroy()
{
Disable();
if (s_Instance != null)
{
Destroy(s_Instance.gameObject);
s_Instance = null;
}
}
protected void AddPointer(Pointer pointer)
{
if (pointer == null)
throw new ArgumentNullException(nameof(pointer));
// Ignore if already added.
if (m_Pointers.ContainsReference(m_NumPointers, pointer))
return;
// Add to list.
ArrayHelpers.AppendWithCapacity(ref m_Pointers, ref m_NumPointers, pointer);
ArrayHelpers.Append(ref m_CurrentPositions, default(Vector2));
ArrayHelpers.Append(ref m_CurrentDisplayIndices, default(int));
InputSystem.DisableDevice(pointer, keepSendingEvents: true);
}
protected void RemovePointer(Pointer pointer)
{
if (pointer == null)
throw new ArgumentNullException(nameof(pointer));
// Ignore if not added.
var pointerIndex = m_Pointers.IndexOfReference(pointer, m_NumPointers);
if (pointerIndex == -1)
return;
// Cancel all ongoing touches from the pointer.
for (var i = 0; i < m_Touches.Length; ++i)
{
var button = m_Touches[i];
if (button != null && button.device != pointer)
continue;
UpdateTouch(i, pointerIndex, TouchPhase.Canceled);
}
// Remove from list.
m_Pointers.EraseAtWithCapacity(ref m_NumPointers, pointerIndex);
ArrayHelpers.EraseAt(ref m_CurrentPositions, pointerIndex);
ArrayHelpers.EraseAt(ref m_CurrentDisplayIndices, pointerIndex);
// Re-enable the device (only in case it's still added to the system).
if (pointer.added)
InputSystem.EnableDevice(pointer);
}
private unsafe void OnEvent(InputEventPtr eventPtr, InputDevice device)
{
if (device == simulatedTouchscreen)
{
// Avoid processing events queued by this simulation device
return;
}
var pointerIndex = m_Pointers.IndexOfReference(device, m_NumPointers);
if (pointerIndex < 0)
return;
var eventType = eventPtr.type;
if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
return;
////REVIEW: should we have specialized paths for MouseState and PenState here? (probably can only use for StateEvents)
Pointer pointer = m_Pointers[pointerIndex];
// Read pointer position.
var positionControl = pointer.position;
var positionStatePtr = positionControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
if (positionStatePtr != null)
m_CurrentPositions[pointerIndex] = positionControl.ReadValueFromState(positionStatePtr);
// Read display index.
var displayIndexControl = pointer.displayIndex;
var displayIndexStatePtr = displayIndexControl.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
if (displayIndexStatePtr != null)
m_CurrentDisplayIndices[pointerIndex] = displayIndexControl.ReadValueFromState(displayIndexStatePtr);
// End touches for which buttons are no longer pressed.
////REVIEW: There must be a better way to do this
for (var i = 0; i < m_Touches.Length; ++i)
{
var button = m_Touches[i];
if (button == null || button.device != device)
continue;
var buttonStatePtr = button.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
if (buttonStatePtr == null)
{
// Button is not contained in event. If we do have a position update, issue
// a move on the button's corresponding touch. This makes us deal with delta
// events that only update pointer positions.
if (positionStatePtr != null)
UpdateTouch(i, pointerIndex, TouchPhase.Moved, eventPtr);
}
else if (button.ReadValueFromState(buttonStatePtr) < (ButtonControl.s_GlobalDefaultButtonPressPoint * ButtonControl.s_GlobalDefaultButtonReleaseThreshold))
UpdateTouch(i, pointerIndex, TouchPhase.Ended, eventPtr);
}
// Add/update touches for buttons that are pressed.
foreach (var control in eventPtr.EnumerateControls(InputControlExtensions.Enumerate.IgnoreControlsInDefaultState, device))
{
if (!control.isButton)
continue;
// Check if it's pressed.
var buttonStatePtr = control.GetStatePtrFromStateEventUnchecked(eventPtr, eventType);
Debug.Assert(buttonStatePtr != null, "Button returned from EnumerateControls() must be found in event");
var value = 0f;
control.ReadValueFromStateIntoBuffer(buttonStatePtr, UnsafeUtility.AddressOf(ref value), 4);
if (value <= ButtonControl.s_GlobalDefaultButtonPressPoint)
continue; // Not in default state but also not pressed.
// See if we have an ongoing touch for the button.
var touchIndex = m_Touches.IndexOfReference(control);
if (touchIndex < 0)
{
// No, so add it.
touchIndex = m_Touches.IndexOfReference((ButtonControl)null);
if (touchIndex >= 0) // If negative, we're at max touch count and can't add more.
{
m_Touches[touchIndex] = (ButtonControl)control;
UpdateTouch(touchIndex, pointerIndex, TouchPhase.Began, eventPtr);
}
}
else
{
// Yes, so update it.
UpdateTouch(touchIndex, pointerIndex, TouchPhase.Moved, eventPtr);
}
}
eventPtr.handled = true;
}
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
// If someone removed our simulated touchscreen, disable touch simulation.
if (device == simulatedTouchscreen && change == InputDeviceChange.Removed)
{
Disable();
return;
}
switch (change)
{
case InputDeviceChange.Added:
{
if (device is Pointer pointer)
{
if (device is Touchscreen)
return; ////TODO: decide what to do
AddPointer(pointer);
}
break;
}
case InputDeviceChange.Removed:
{
if (device is Pointer pointer)
RemovePointer(pointer);
break;
}
}
}
protected void OnEnable()
{
if (simulatedTouchscreen != null)
{
if (!simulatedTouchscreen.added)
InputSystem.AddDevice(simulatedTouchscreen);
}
else
{
simulatedTouchscreen = InputSystem.GetDevice("Simulated Touchscreen") as Touchscreen;
if (simulatedTouchscreen == null)
simulatedTouchscreen = InputSystem.AddDevice<Touchscreen>("Simulated Touchscreen");
}
if (m_Touches == null)
m_Touches = new ButtonControl[simulatedTouchscreen.touches.Count];
if (m_TouchIds == null)
m_TouchIds = new int[simulatedTouchscreen.touches.Count];
foreach (var device in InputSystem.devices)
OnDeviceChange(device, InputDeviceChange.Added);
if (m_OnDeviceChange == null)
m_OnDeviceChange = OnDeviceChange;
if (m_OnEvent == null)
m_OnEvent = OnEvent;
InputSystem.onDeviceChange += m_OnDeviceChange;
InputSystem.onEvent += m_OnEvent;
}
protected void OnDisable()
{
if (simulatedTouchscreen != null && simulatedTouchscreen.added)
InputSystem.RemoveDevice(simulatedTouchscreen);
// Re-enable all pointers we disabled.
for (var i = 0; i < m_NumPointers; ++i)
InputSystem.EnableDevice(m_Pointers[i]);
m_Pointers.Clear(m_NumPointers);
m_Touches.Clear();
m_NumPointers = 0;
m_LastTouchId = 0;
InputSystem.onDeviceChange -= m_OnDeviceChange;
InputSystem.onEvent -= m_OnEvent;
}
private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase phase, InputEventPtr eventPtr = default)
{
Vector2 position = m_CurrentPositions[pointerIndex];
Debug.Assert(m_CurrentDisplayIndices[pointerIndex] <= byte.MaxValue, "Display index was larger than expected");
byte displayIndex = (byte)m_CurrentDisplayIndices[pointerIndex];
// We need to partially set TouchState in a similar way that the Native side would do, but deriving that
// data from the Pointer events.
// The handling of the remaining fields is done by the Touchscreen.OnStateEvent() callback.
var touch = new TouchState
{
phase = phase,
position = position,
displayIndex = displayIndex
};
if (phase == TouchPhase.Began)
{
touch.startTime = eventPtr.valid ? eventPtr.time : InputState.currentTime;
touch.startPosition = position;
touch.touchId = ++m_LastTouchId;
m_TouchIds[touchIndex] = m_LastTouchId;
}
else
{
touch.touchId = m_TouchIds[touchIndex];
}
//NOTE: Processing these events still happen in the current frame.
InputSystem.QueueStateEvent(simulatedTouchscreen, touch);
if (phase.IsEndedOrCanceled())
{
m_Touches[touchIndex] = null;
}
}
[NonSerialized] private int m_NumPointers;
[NonSerialized] private Pointer[] m_Pointers;
[NonSerialized] private Vector2[] m_CurrentPositions;
[NonSerialized] private int[] m_CurrentDisplayIndices;
[NonSerialized] private ButtonControl[] m_Touches;
[NonSerialized] private int[] m_TouchIds;
[NonSerialized] private int m_LastTouchId;
[NonSerialized] private Action<InputDevice, InputDeviceChange> m_OnDeviceChange;
[NonSerialized] private Action<InputEventPtr, InputDevice> m_OnEvent;
internal static TouchSimulation s_Instance;
#if UNITY_EDITOR
static TouchSimulation()
{
// We're a MonoBehaviour so our cctor may get called as part of the MonoBehaviour being
// created. We don't want to trigger InputSystem initialization from there so delay-execute
// the code here.
EditorApplication.delayCall +=
() =>
{
InputSystem.onSettingsChange += OnSettingsChanged;
InputSystem.onBeforeUpdate += ReEnableAfterDomainReload;
};
}
private static void ReEnableAfterDomainReload()
{
OnSettingsChanged();
InputSystem.onBeforeUpdate -= ReEnableAfterDomainReload;
}
private static void OnSettingsChanged()
{
if (InputEditorUserSettings.simulateTouch)
Enable();
else
Disable();
}
[CustomEditor(typeof(TouchSimulation))]
private class TouchSimulationEditor : UnityEditor.Editor
{
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.TouchSimulation).Send();
}
}
#endif // UNITY_EDITOR
////TODO: Remove IInputStateChangeMonitor from this class when we can break the API
void IInputStateChangeMonitor.NotifyControlStateChanged(InputControl control, double time, InputEventPtr eventPtr, long monitorIndex)
{
}
void IInputStateChangeMonitor.NotifyTimerExpired(InputControl control, double time, long monitorIndex, int timerIndex)
{
}
// Disable warnings about unused parameters.
#pragma warning disable CA1801
////TODO: [Obsolete]
protected void InstallStateChangeMonitors(int startIndex = 0)
{
}
////TODO: [Obsolete]
protected void OnSourceControlChangedValue(InputControl control, double time, InputEventPtr eventPtr,
long sourceDeviceAndButtonIndex)
{
}
////TODO: [Obsolete]
protected void UninstallStateChangeMonitors(int startIndex = 0)
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e31057ef324a04478bb4f7d469b3cdc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a71161af1440e4b1fbbefefb3aaffce0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8806f46699a44c39a6a66c5d07772871
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,239 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
#if UNITY_6000_2_OR_NEWER
using TreeView = UnityEditor.IMGUI.Controls.TreeView<int>;
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
#endif
////TODO: use two columns for treeview and separate name and value
namespace UnityEngine.InputSystem.HID.Editor
{
/// <summary>
/// A window that dumps a raw HID descriptor in a tree view.
/// </summary>
/// <remarks>
/// Not specific to InputDevices of type <see cref="HID"/> so that it can work with
/// any <see cref="InputDevice"/> created for a device using the "HID" interface.
/// </remarks>
internal class HIDDescriptorWindow : EditorWindow, ISerializationCallbackReceiver
{
public static void CreateOrShowExisting(int deviceId, InputDeviceDescription deviceDescription)
{
// See if we have an existing window for the device and if so pop it
// in front.
if (s_OpenWindows != null)
{
for (var i = 0; i < s_OpenWindows.Count; ++i)
{
var existingWindow = s_OpenWindows[i];
if (existingWindow.m_DeviceId == deviceId)
{
existingWindow.Show();
existingWindow.Focus();
return;
}
}
}
// No, so create a new one.
var window = CreateInstance<HIDDescriptorWindow>();
window.InitializeWith(deviceId, deviceDescription);
window.minSize = new Vector2(270, 200);
window.Show();
window.titleContent = new GUIContent("HID Descriptor");
}
public void Awake()
{
AddToList();
}
public void OnDestroy()
{
RemoveFromList();
}
public void OnGUI()
{
if (!m_Initialized)
InitializeWith(m_DeviceId, m_DeviceDescription);
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label(m_Label, GUILayout.MinWidth(100), GUILayout.ExpandWidth(true));
GUILayout.EndHorizontal();
var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
m_TreeView.OnGUI(rect);
}
private void InitializeWith(int deviceId, InputDeviceDescription deviceDescription)
{
m_DeviceId = deviceId;
m_DeviceDescription = deviceDescription;
m_Initialized = true;
// Set up tree view for HID descriptor.
var hidDescriptor = HID.ReadHIDDeviceDescriptor(ref m_DeviceDescription,
(ref InputDeviceCommand command) => InputRuntime.s_Instance.DeviceCommand(m_DeviceId, ref command));
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState();
m_TreeView = new HIDDescriptorTreeView(m_TreeViewState, hidDescriptor);
m_TreeView.SetExpanded(1, true);
m_Label = new GUIContent(
$"HID Descriptor for '{deviceDescription.manufacturer} {deviceDescription.product}'");
}
[NonSerialized] private bool m_Initialized;
[NonSerialized] private HIDDescriptorTreeView m_TreeView;
[NonSerialized] private GUIContent m_Label;
[SerializeField] private int m_DeviceId;
[SerializeField] private InputDeviceDescription m_DeviceDescription;
[SerializeField] private TreeViewState m_TreeViewState;
private void AddToList()
{
if (s_OpenWindows == null)
s_OpenWindows = new List<HIDDescriptorWindow>();
if (!s_OpenWindows.Contains(this))
s_OpenWindows.Add(this);
}
private void RemoveFromList()
{
if (s_OpenWindows != null)
s_OpenWindows.Remove(this);
}
private static List<HIDDescriptorWindow> s_OpenWindows;
private class HIDDescriptorTreeView : TreeView
{
private HID.HIDDeviceDescriptor m_Descriptor;
public HIDDescriptorTreeView(TreeViewState state, HID.HIDDeviceDescriptor descriptor)
: base(state)
{
m_Descriptor = descriptor;
Reload();
}
protected override TreeViewItem BuildRoot()
{
var id = 0;
var root = new TreeViewItem
{
id = id++,
depth = -1
};
var item = BuildDeviceItem(m_Descriptor, ref id);
root.AddChild(item);
return root;
}
private TreeViewItem BuildDeviceItem(HID.HIDDeviceDescriptor device, ref int id)
{
var item = new TreeViewItem
{
id = id++,
depth = 0,
displayName = "Device"
};
AddChild(item, string.Format("Vendor ID: 0x{0:X}", device.vendorId), ref id);
AddChild(item, string.Format("Product ID: 0x{0:X}", device.productId), ref id);
AddChild(item, string.Format("Usage Page: 0x{0:X} ({1})", (uint)device.usagePage, device.usagePage), ref id);
AddChild(item, string.Format("Usage: 0x{0:X}", device.usage), ref id);
AddChild(item, "Input Report Size: " + device.inputReportSize, ref id);
AddChild(item, "Output Report Size: " + device.outputReportSize, ref id);
AddChild(item, "Feature Report Size: " + device.featureReportSize, ref id);
// Elements.
if (device.elements != null)
{
var elementCount = device.elements.Length;
var elements = AddChild(item, elementCount + " Elements", ref id);
for (var i = 0; i < elementCount; ++i)
BuildElementItem(i, elements, device.elements[i], ref id);
}
else
AddChild(item, "0 Elements", ref id);
////TODO: collections
return item;
}
private TreeViewItem BuildElementItem(int index, TreeViewItem parent, HID.HIDElementDescriptor element, ref int id)
{
var item = AddChild(parent, string.Format("Element {0} ({1})", index, element.reportType), ref id);
string usagePageString = HID.UsagePageToString(element.usagePage);
string usageString = HID.UsageToString(element.usagePage, element.usage);
AddChild(item, string.Format("Usage Page: 0x{0:X} ({1})", (uint)element.usagePage, usagePageString), ref id);
if (usageString != null)
AddChild(item, string.Format("Usage: 0x{0:X} ({1})", element.usage, usageString), ref id);
else
AddChild(item, string.Format("Usage: 0x{0:X}", element.usage), ref id);
AddChild(item, "Report Type: " + element.reportType, ref id);
AddChild(item, "Report ID: " + element.reportId, ref id);
AddChild(item, "Report Size in Bits: " + element.reportSizeInBits, ref id);
AddChild(item, "Report Bit Offset: " + element.reportOffsetInBits, ref id);
AddChild(item, "Collection Index: " + element.collectionIndex, ref id);
AddChild(item, string.Format("Unit: {0:X}", element.unit), ref id);
AddChild(item, string.Format("Unit Exponent: {0:X}", element.unitExponent), ref id);
AddChild(item, "Logical Min: " + element.logicalMin, ref id);
AddChild(item, "Logical Max: " + element.logicalMax, ref id);
AddChild(item, "Physical Min: " + element.physicalMin, ref id);
AddChild(item, "Physical Max: " + element.physicalMax, ref id);
AddChild(item, "Has Null State?: " + element.hasNullState, ref id);
AddChild(item, "Has Preferred State?: " + element.hasPreferredState, ref id);
AddChild(item, "Is Array?: " + element.isArray, ref id);
AddChild(item, "Is Non-Linear?: " + element.isNonLinear, ref id);
AddChild(item, "Is Relative?: " + element.isRelative, ref id);
AddChild(item, "Is Constant?: " + element.isConstant, ref id);
AddChild(item, "Is Wrapping?: " + element.isWrapping, ref id);
return item;
}
private TreeViewItem AddChild(TreeViewItem parent, string displayName, ref int id)
{
var item = new TreeViewItem
{
id = id++,
depth = parent.depth + 1,
displayName = displayName
};
parent.AddChild(item);
return item;
}
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
AddToList();
}
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ae42a3986cf84fe2aa1df4aa984dd806
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,480 @@
using System;
using System.Collections.Generic;
////TODO: array support
////TODO: delimiter support
////TODO: designator support
#pragma warning disable CS0649
namespace UnityEngine.InputSystem.HID
{
/// <summary>
/// Turns binary HID descriptors into <see cref="HID.HIDDeviceDescriptor"/> instances.
/// </summary>
/// <remarks>
/// For information about the format, see the <a href="http://www.usb.org/developers/hidpage/HID1_11.pdf">
/// Device Class Definition for Human Interface Devices</a> section 6.2.2.
/// </remarks>
internal static class HIDParser
{
/// <summary>
/// Parse a HID report descriptor as defined by section 6.2.2 of the
/// <a href="http://www.usb.org/developers/hidpage/HID1_11.pdf">HID
/// specification</a> and add the elements and collections from the
/// descriptor to the given <paramref name="deviceDescriptor"/>.
/// </summary>
/// <param name="buffer">Buffer containing raw HID report descriptor.</param>
/// <param name="deviceDescriptor">HID device descriptor to complete with the information
/// from the report descriptor. Elements and collections will get added to this descriptor.</param>
/// <returns>True if the report descriptor was successfully parsed.</returns>
/// <remarks>
/// Will also set <see cref="HID.HIDDeviceDescriptor.inputReportSize"/>,
/// <see cref="HID.HIDDeviceDescriptor.outputReportSize"/>, and
/// <see cref="HID.HIDDeviceDescriptor.featureReportSize"/>.
/// </remarks>
public static unsafe bool ParseReportDescriptor(byte[] buffer, ref HID.HIDDeviceDescriptor deviceDescriptor)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
fixed(byte* bufferPtr = buffer)
{
return ParseReportDescriptor(bufferPtr, buffer.Length, ref deviceDescriptor);
}
}
public unsafe static bool ParseReportDescriptor(byte* bufferPtr, int bufferLength, ref HID.HIDDeviceDescriptor deviceDescriptor)
{
// Item state.
var localItemState = new HIDItemStateLocal();
var globalItemState = new HIDItemStateGlobal();
// Lists where we accumulate the data from the HID items.
var reports = new List<HIDReportData>();
var elements = new List<HID.HIDElementDescriptor>();
var collections = new List<HID.HIDCollectionDescriptor>();
var currentCollection = -1;
// Parse the linear list of items.
var endPtr = bufferPtr + bufferLength;
var currentPtr = bufferPtr;
while (currentPtr < endPtr)
{
var firstByte = *currentPtr;
////TODO
if (firstByte == 0xFE)
throw new NotImplementedException("long item support");
// Read item header.
var itemSize = (byte)(firstByte & 0x3);
var itemTypeAndTag = (byte)(firstByte & 0xFC);
++currentPtr;
// Process item.
switch (itemTypeAndTag)
{
// ------------ Global Items --------------
// These set item state permanently until it is reset.
// Usage Page
case (int)HIDItemTypeAndTag.UsagePage:
globalItemState.usagePage = ReadData(itemSize, currentPtr, endPtr);
break;
// Report Count
case (int)HIDItemTypeAndTag.ReportCount:
globalItemState.reportCount = ReadData(itemSize, currentPtr, endPtr);
break;
// Report Size
case (int)HIDItemTypeAndTag.ReportSize:
globalItemState.reportSize = ReadData(itemSize, currentPtr, endPtr);
break;
// Report ID
case (int)HIDItemTypeAndTag.ReportID:
globalItemState.reportId = ReadData(itemSize, currentPtr, endPtr);
break;
// Logical Minimum
case (int)HIDItemTypeAndTag.LogicalMinimum:
globalItemState.logicalMinimum = ReadData(itemSize, currentPtr, endPtr);
break;
// Logical Maximum
case (int)HIDItemTypeAndTag.LogicalMaximum:
globalItemState.logicalMaximum = ReadData(itemSize, currentPtr, endPtr);
break;
// Physical Minimum
case (int)HIDItemTypeAndTag.PhysicalMinimum:
globalItemState.physicalMinimum = ReadData(itemSize, currentPtr, endPtr);
break;
// Physical Maximum
case (int)HIDItemTypeAndTag.PhysicalMaximum:
globalItemState.physicalMaximum = ReadData(itemSize, currentPtr, endPtr);
break;
// Unit Exponent
case (int)HIDItemTypeAndTag.UnitExponent:
globalItemState.unitExponent = ReadData(itemSize, currentPtr, endPtr);
break;
// Unit
case (int)HIDItemTypeAndTag.Unit:
globalItemState.unit = ReadData(itemSize, currentPtr, endPtr);
break;
// ------------ Local Items --------------
// These set the state for the very next elements to be generated.
// Usage
case (int)HIDItemTypeAndTag.Usage:
localItemState.SetUsage(ReadData(itemSize, currentPtr, endPtr));
break;
// Usage Minimum
case (int)HIDItemTypeAndTag.UsageMinimum:
localItemState.usageMinimum = ReadData(itemSize, currentPtr, endPtr);
break;
// Usage Maximum
case (int)HIDItemTypeAndTag.UsageMaximum:
localItemState.usageMaximum = ReadData(itemSize, currentPtr, endPtr);
break;
// ------------ Main Items --------------
// These emit things into the descriptor based on the local and global item state.
// Collection
case (int)HIDItemTypeAndTag.Collection:
// Start new collection.
var parentCollection = currentCollection;
currentCollection = collections.Count;
collections.Add(new HID.HIDCollectionDescriptor
{
type = (HID.HIDCollectionType)ReadData(itemSize, currentPtr, endPtr),
parent = parentCollection,
usagePage = globalItemState.GetUsagePage(0, ref localItemState),
usage = localItemState.GetUsage(0),
firstChild = elements.Count
});
HIDItemStateLocal.Reset(ref localItemState);
break;
// EndCollection
case (int)HIDItemTypeAndTag.EndCollection:
if (currentCollection == -1)
return false;
// Close collection.
var collection = collections[currentCollection];
collection.childCount = elements.Count - collection.firstChild;
collections[currentCollection] = collection;
// Switch back to parent collection (if any).
currentCollection = collection.parent;
HIDItemStateLocal.Reset(ref localItemState);
break;
// Input/Output/Feature
case (int)HIDItemTypeAndTag.Input:
case (int)HIDItemTypeAndTag.Output:
case (int)HIDItemTypeAndTag.Feature:
// Determine report type.
var reportType = itemTypeAndTag == (int)HIDItemTypeAndTag.Input
? HID.HIDReportType.Input
: itemTypeAndTag == (int)HIDItemTypeAndTag.Output
? HID.HIDReportType.Output
: HID.HIDReportType.Feature;
// Find report.
var reportIndex = HIDReportData.FindOrAddReport(globalItemState.reportId, reportType, reports);
var report = reports[reportIndex];
// If we have a report ID, then reports start with an 8 byte report ID.
// Shift our offsets accordingly.
if (report.currentBitOffset == 0 && globalItemState.reportId.HasValue)
report.currentBitOffset = 8;
// Add elements to report.
var reportCount = globalItemState.reportCount.GetValueOrDefault(1);
var flags = ReadData(itemSize, currentPtr, endPtr);
for (var i = 0; i < reportCount; ++i)
{
var element = new HID.HIDElementDescriptor
{
usage = localItemState.GetUsage(i) & 0xFFFF, // Mask off usage page, if set.
usagePage = globalItemState.GetUsagePage(i, ref localItemState),
reportType = reportType,
reportSizeInBits = globalItemState.reportSize.GetValueOrDefault(8),
reportOffsetInBits = report.currentBitOffset,
reportId = globalItemState.reportId.GetValueOrDefault(1),
flags = (HID.HIDElementFlags)flags,
logicalMin = globalItemState.logicalMinimum.GetValueOrDefault(0),
logicalMax = globalItemState.logicalMaximum.GetValueOrDefault(0),
physicalMin = globalItemState.GetPhysicalMin(),
physicalMax = globalItemState.GetPhysicalMax(),
unitExponent = globalItemState.unitExponent.GetValueOrDefault(0),
unit = globalItemState.unit.GetValueOrDefault(0),
};
report.currentBitOffset += element.reportSizeInBits;
elements.Add(element);
}
reports[reportIndex] = report;
HIDItemStateLocal.Reset(ref localItemState);
break;
}
if (itemSize == 3)
currentPtr += 4;
else
currentPtr += itemSize;
}
deviceDescriptor.elements = elements.ToArray();
deviceDescriptor.collections = collections.ToArray();
// Set usage and usage page on device descriptor to what's
// on the toplevel application collection.
foreach (var collection in collections)
{
if (collection.parent == -1 && collection.type == HID.HIDCollectionType.Application)
{
deviceDescriptor.usage = collection.usage;
deviceDescriptor.usagePage = collection.usagePage;
break;
}
}
return true;
}
private unsafe static int ReadData(int itemSize, byte* currentPtr, byte* endPtr)
{
if (itemSize == 0)
return 0;
// Read byte.
if (itemSize == 1)
{
if (currentPtr >= endPtr)
return 0;
return (sbyte)*currentPtr;
}
// Read short.
if (itemSize == 2)
{
if (currentPtr + 2 >= endPtr)
return 0;
var data1 = *currentPtr;
var data2 = *(currentPtr + 1);
return (short)((data2 << 8) | data1);
}
// Read int.
if (itemSize == 3) // Item size 3 means 4 bytes!
{
if (currentPtr + 4 >= endPtr)
return 0;
var data1 = *currentPtr;
var data2 = *(currentPtr + 1);
var data3 = *(currentPtr + 2);
var data4 = *(currentPtr + 3);
return (data4 << 24) | (data3 << 16) | (data2 << 8) | data1;
}
Debug.Assert(false, "Should not reach here");
return 0;
}
private struct HIDReportData
{
public int reportId;
public HID.HIDReportType reportType;
public int currentBitOffset;
public static int FindOrAddReport(int? reportId, HID.HIDReportType reportType, List<HIDReportData> reports)
{
var id = 1;
if (reportId.HasValue)
id = reportId.Value;
for (var i = 0; i < reports.Count; ++i)
{
if (reports[i].reportId == id && reports[i].reportType == reportType)
return i;
}
reports.Add(new HIDReportData
{
reportId = id,
reportType = reportType
});
return reports.Count - 1;
}
}
// All types and tags with size bits (low order two bits) masked out (i.e. being 0).
private enum HIDItemTypeAndTag
{
Input = 0x80,
Output = 0x90,
Feature = 0xB0,
Collection = 0xA0,
EndCollection = 0xC0,
UsagePage = 0x04,
LogicalMinimum = 0x14,
LogicalMaximum = 0x24,
PhysicalMinimum = 0x34,
PhysicalMaximum = 0x44,
UnitExponent = 0x54,
Unit = 0x64,
ReportSize = 0x74,
ReportID = 0x84,
ReportCount = 0x94,
Push = 0xA4,
Pop = 0xB4,
Usage = 0x08,
UsageMinimum = 0x18,
UsageMaximum = 0x28,
DesignatorIndex = 0x38,
DesignatorMinimum = 0x48,
DesignatorMaximum = 0x58,
StringIndex = 0x78,
StringMinimum = 0x88,
StringMaximum = 0x98,
Delimiter = 0xA8,
}
// State that needs to be defined for each main item separately.
// See section 6.2.2.8
private struct HIDItemStateLocal
{
public int? usage;
public int? usageMinimum;
public int? usageMaximum;
public int? designatorIndex;
public int? designatorMinimum;
public int? designatorMaximum;
public int? stringIndex;
public int? stringMinimum;
public int? stringMaximum;
public List<int> usageList;
// Wipe state but preserve usageList allocation.
public static void Reset(ref HIDItemStateLocal state)
{
var usageList = state.usageList;
state = new HIDItemStateLocal();
if (usageList != null)
{
usageList.Clear();
state.usageList = usageList;
}
}
// Usage can be set repeatedly to provide an enumeration of usages.
public void SetUsage(int value)
{
if (usage.HasValue)
{
if (usageList == null)
usageList = new List<int>();
usageList.Add(usage.Value);
}
usage = value;
}
// Get usage for Nth element in [0-reportCount] list.
public int GetUsage(int index)
{
// If we have minimum and maximum usage, interpolate between that.
if (usageMinimum.HasValue && usageMaximum.HasValue)
{
var min = usageMinimum.Value;
var max = usageMaximum.Value;
var range = max - min;
if (range < 0)
return 0;
if (index >= range)
return max;
return min + index;
}
// If we have a list of usages, index into that.
if (usageList != null && usageList.Count > 0)
{
var usageCount = usageList.Count;
if (index >= usageCount)
return usage.Value;
return usageList[index];
}
if (usage.HasValue)
return usage.Value;
////TODO: min/max
return 0;
}
}
// State that is carried over from main item to main item.
// See section 6.2.2.7
private struct HIDItemStateGlobal
{
public int? usagePage;
public int? logicalMinimum;
public int? logicalMaximum;
public int? physicalMinimum;
public int? physicalMaximum;
public int? unitExponent;
public int? unit;
public int? reportSize;
public int? reportCount;
public int? reportId;
public HID.UsagePage GetUsagePage(int index, ref HIDItemStateLocal localItemState)
{
if (!usagePage.HasValue)
{
var usage = localItemState.GetUsage(index);
return (HID.UsagePage)(usage >> 16);
}
return (HID.UsagePage)usagePage.Value;
}
public int GetPhysicalMin()
{
if (physicalMinimum == null || physicalMaximum == null ||
(physicalMinimum.Value == 0 && physicalMaximum.Value == 0))
return logicalMinimum.GetValueOrDefault(0);
return physicalMinimum.Value;
}
public int GetPhysicalMax()
{
if (physicalMinimum == null || physicalMaximum == null ||
(physicalMinimum.Value == 0 && physicalMaximum.Value == 0))
return logicalMaximum.GetValueOrDefault(0);
return physicalMaximum.Value;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a736049645e8c4781aedf679c2ee6be6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,154 @@
using System.Linq;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.InputSystem.Editor;
using UnityEngine.InputSystem.HID.Editor;
#endif
namespace UnityEngine.InputSystem.HID
{
using ShouldCreateHIDCallback = System.Func<HID.HIDDeviceDescriptor, bool?>;
/// <summary>
/// Adds support for generic HID devices to the input system.
/// </summary>
/// <remarks>
/// Even without this module, HIDs can be used on platforms where we
/// support HID has a native backend (Windows and OSX, at the moment).
/// However, each supported HID requires a layout specifically targeting
/// it as a product.
///
/// What this module adds is the ability to turn any HID with usable
/// controls into an InputDevice. It will make a best effort to figure
/// out a suitable class for the device and will use the HID elements
/// present in the HID report descriptor to populate the device.
///
/// If there is an existing product-specific layout for a HID, it will
/// take precedence and HIDSupport will leave the device alone.
/// </remarks>
public static class HIDSupport
{
/// <summary>
/// A pair of HID usage page and HID usage number.
/// </summary>
/// <remarks>
/// Used to describe a HID usage for the <see cref="supportedHIDUsages"/> property.
/// </remarks>
public struct HIDPageUsage
{
/// <summary>
/// The usage page.
/// </summary>
public HID.UsagePage page;
/// <summary>
/// A number specifying the usage on the usage page.
/// </summary>
public int usage;
/// <summary>
/// Create a HIDPageUsage struct by specifying a page and usage.
/// </summary>
public HIDPageUsage(HID.UsagePage page, int usage)
{
this.page = page;
this.usage = usage;
}
/// <summary>
/// Create a HIDPageUsage struct from the GenericDesktop usage page by specifying the usage.
/// </summary>
public HIDPageUsage(HID.GenericDesktop usage)
{
page = HID.UsagePage.GenericDesktop;
this.usage = (int)usage;
}
}
private static HIDPageUsage[] s_SupportedHIDUsages;
/// <summary>
/// An array of HID usages the input is configured to support.
/// </summary>
/// <remarks>
/// The input system will only create <see cref="InputDevice"/>s for HIDs with usages
/// listed in this array. Any other HID will be ignored. This saves the input system from
/// spending resources on creating layouts and devices for HIDs which are not supported or
/// not usable for game input.
///
/// By default, this includes only <see cref="HID.GenericDesktop.Joystick"/>,
/// <see cref="HID.GenericDesktop.Gamepad"/> and <see cref="HID.GenericDesktop.MultiAxisController"/>,
/// but you can set this property to include any other HID usages.
///
/// Note that currently on macOS, the only HID usages which can be enabled are
/// <see cref="HID.GenericDesktop.Joystick"/>, <see cref="HID.GenericDesktop.Gamepad"/>,
/// <see cref="HID.GenericDesktop.MultiAxisController"/>, <see cref="HID.GenericDesktop.TabletPCControls"/>,
/// and <see cref="HID.GenericDesktop.AssistiveControl"/>.
/// </remarks>
public static ReadOnlyArray<HIDPageUsage> supportedHIDUsages
{
get => s_SupportedHIDUsages;
set
{
s_SupportedHIDUsages = value.ToArray();
// Add HIDs we now support.
InputSystem.s_Manager.AddAvailableDevicesThatAreNowRecognized();
// Remove HIDs we no longer support.
for (var i = 0; i < InputSystem.devices.Count; ++i)
{
var device = InputSystem.devices[i];
if (device is HID hid && !s_SupportedHIDUsages.Contains(new HIDPageUsage(hid.hidDescriptor.usagePage, hid.hidDescriptor.usage)))
{
// Remove the entire generated layout. This will also remove all devices based on it.
InputSystem.RemoveLayout(device.layout);
--i;
}
}
}
}
/// <summary>
/// Add support for generic HIDs to InputSystem.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static void Initialize()
{
s_SupportedHIDUsages = new[]
{
new HIDPageUsage(HID.GenericDesktop.Joystick),
new HIDPageUsage(HID.GenericDesktop.Gamepad),
new HIDPageUsage(HID.GenericDesktop.MultiAxisController),
};
InputSystem.RegisterLayout<HID>();
InputSystem.onFindLayoutForDevice += HID.OnFindLayoutForDevice;
// Add toolbar button to any devices using the "HID" interface. Opens
// a windows to browse the HID descriptor of the device.
#if UNITY_EDITOR
InputDeviceDebuggerWindow.onToolbarGUI +=
device =>
{
if (device.description.interfaceName == HID.kHIDInterface)
{
if (GUILayout.Button(s_HIDDescriptor, EditorStyles.toolbarButton))
{
HIDDescriptorWindow.CreateOrShowExisting(device.deviceId, device.description);
}
}
};
#endif
}
#if UNITY_EDITOR
private static readonly GUIContent s_HIDDescriptor = new GUIContent("HID Descriptor");
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f0b0cfb7c484e43823c1a038c97a5f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 96500a17645c67442a0ccdca007e6d28
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,6 @@
using System.Runtime.CompilerServices;
using UnityEngine.Scripting;
[assembly: InternalsVisibleTo("UnityEngine.InputForUIVisualizer")]
[assembly: InternalsVisibleTo("Unity.InputSystem.Tests")]
[assembly: AlwaysLinkAssembly]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65c861c7ef9bee2479c728694e87ea45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
#if UNITY_EDITOR && ENABLE_INPUT_SYSTEM && UNITY_2023_2_OR_NEWER
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.InputSystem.Editor;
namespace UnityEngine.InputSystem.Plugins.InputForUI
{
// Unlike InputSystemProvider we want the verifier to register itself directly on domain reload in editor.
[InitializeOnLoad]
internal class InputActionAssetVerifier : ProjectWideActionsAsset.IInputActionAssetVerifier
{
public enum ReportPolicy
{
ReportAll,
SuppressChildErrors
}
// Note: This is intentionally not a constant to avoid dead code warning in tests while this remains
// as a setting type of value.
public static ReportPolicy DefaultReportPolicy = ReportPolicy.SuppressChildErrors;
static InputActionAssetVerifier()
{
// Register an InputActionAsset verifier for this plugin.
ProjectWideActionsAsset.RegisterInputActionAssetVerifier(() => new InputActionAssetVerifier());
InputSystemProvider.SetOnRegisterActions((asset) => { ProjectWideActionsAsset.Verify(asset); });
}
#region ProjectWideActionsAsset.IInputActionAssetVerifier
public void Verify(InputActionAsset asset,
ProjectWideActionsAsset.IReportInputActionAssetVerificationErrors reporter)
{
// We don't want to log warnings if no UI action map is present as we default in this case.
if (asset.FindActionMap("UI", false) == null)
{
return;
}
// Note:
// PWA has initial state check true for "Point" action, DefaultActions do not, does it matter?
//
// Additionally note that "Submit" and "Cancel" are indirectly expected to be of Button action type.
// This is not available in UI configuration, but InputActionRebindingExtensions suggests this.
//
// Additional "LeftClick" has initial state check set in PWA, but not "MiddleClick" and "RightClick".
// Is this intentional? Are requirements different?
var context = new Context(asset, reporter, DefaultReportPolicy);
context.Verify(actionNameOrId: InputSystemProvider.Actions.PointAction, actionType: InputActionType.PassThrough, expectedControlType: nameof(Vector2));
context.Verify(actionNameOrId: InputSystemProvider.Actions.MoveAction, actionType: InputActionType.PassThrough, expectedControlType: nameof(Vector2));
context.Verify(actionNameOrId: InputSystemProvider.Actions.SubmitAction, actionType: InputActionType.Button, expectedControlType: "Button");
context.Verify(actionNameOrId: InputSystemProvider.Actions.CancelAction, actionType: InputActionType.Button, expectedControlType: "Button");
context.Verify(actionNameOrId: InputSystemProvider.Actions.LeftClickAction, actionType: InputActionType.PassThrough, expectedControlType: "Button");
context.Verify(actionNameOrId: InputSystemProvider.Actions.MiddleClickAction, actionType: InputActionType.PassThrough, expectedControlType: "Button");
context.Verify(actionNameOrId: InputSystemProvider.Actions.RightClickAction, actionType: InputActionType.PassThrough, expectedControlType: "Button");
context.Verify(actionNameOrId: InputSystemProvider.Actions.ScrollWheelAction, actionType: InputActionType.PassThrough, expectedControlType: nameof(Vector2));
}
#endregion
private struct Context
{
const string errorSuffix = "The Input System's runtime UI integration relies on certain required input action definitions, some of which are missing. This means some runtime UI input may not work correctly. See <a href=\"https://docs.unity3d.com/Packages/com.unity.inputsystem@latest/index.html?subfolder=/manual/UISupport.html#required-actions-for-ui\">Input System Manual - UI Support</a> for guidance on required actions for UI integration or see <a href=\"https://docs.unity3d.com/Packages/com.unity.inputsystem@latest/index.html?subfolder=/manual/ProjectWideActions.html#the-default-actions\">how to revert to defaults</a>.";
public Context(InputActionAsset asset,
ProjectWideActionsAsset.IReportInputActionAssetVerificationErrors reporter,
ReportPolicy policy)
{
this.asset = asset;
this.missingPaths = new HashSet<string>();
this.reporter = reporter;
this.policy = policy;
}
private string GetAssetReference()
{
var path = AssetDatabase.GetAssetPath(asset);
return string.IsNullOrEmpty(path) ? asset.name : path;
}
private void ActionMapWarning(string actionMap, string problem)
{
reporter.Report($"InputActionMap with path '{actionMap}' in asset \"{GetAssetReference()}\" {problem}. {errorSuffix}");
}
private void ActionWarning(string actionNameOrId, string problem)
{
reporter.Report($"InputAction with path '{actionNameOrId}' in asset \"{GetAssetReference()}\" {problem}. {errorSuffix}");
}
public void Verify(string actionNameOrId, InputActionType actionType, string expectedControlType)
{
var action = asset.FindAction(actionNameOrId);
if (action == null)
{
const string kCouldNotBeFound = "could not be found";
// Check if the map (if any) exists
var noMapOrMapExists = true;
var index = actionNameOrId.IndexOf('/');
if (index > 0)
{
var path = actionNameOrId.Substring(0, index);
if (asset.FindActionMap(path) == null)
{
if (missingPaths == null)
missingPaths = new HashSet<string>(1);
if (missingPaths.Add(path))
ActionMapWarning(path, kCouldNotBeFound);
noMapOrMapExists = false;
}
}
if (!noMapOrMapExists && policy == ReportPolicy.SuppressChildErrors)
return;
ActionWarning(actionNameOrId, kCouldNotBeFound);
}
else if (action.bindings.Count == 0)
ActionWarning(actionNameOrId, "do not have any configured bindings");
else if (action.type != actionType)
ActionWarning(actionNameOrId, $"has 'type' set to '{nameof(InputActionType)}.{action.type}', but '{nameof(InputActionType)}.{actionType}' was expected");
else if (!string.IsNullOrEmpty(expectedControlType) && !string.IsNullOrEmpty(action.expectedControlType) && action.expectedControlType != expectedControlType)
ActionWarning(actionNameOrId, $"has 'expectedControlType' set to '{action.expectedControlType}', but '{expectedControlType}' was expected");
}
private readonly InputActionAsset asset;
private readonly ProjectWideActionsAsset.IReportInputActionAssetVerificationErrors reporter;
private HashSet<string> missingPaths; // Avoids generating multiple warnings around missing map
private ReportPolicy policy;
}
}
}
#endif // UNITY_EDITOR && ENABLE_INPUT_SYSTEM && UNITY_2023_2_OR_NEWER

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1fe35221cd5f4ca9b9df947d34e7e1f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
{
"name": "Unity.InputSystem.ForUI",
"references": [
"Unity.InputSystem"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 09dcc9c0126b6b34ab2a877b8f2277f5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,712 @@
#if UNITY_2023_2_OR_NEWER // UnityEngine.InputForUI Module unavailable in earlier releases
using System;
using System.Collections.Generic;
using Unity.IntegerTime;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputForUI;
namespace UnityEngine.InputSystem.Plugins.InputForUI
{
using Event = UnityEngine.InputForUI.Event;
using EventModifiers = UnityEngine.InputForUI.EventModifiers;
using EventProvider = UnityEngine.InputForUI.EventProvider;
internal class InputSystemProvider : IEventProviderImpl
{
InputEventPartialProvider m_InputEventPartialProvider;
DefaultInputActions m_DefaultInputActions;
InputActionAsset m_InputActionAsset;
// Note that these are plain action references instead since InputActionReference do
// not provide any value when this integration doesn't have any UI. If this integration
// later gets a UI replace these by InputActionReference and remember to check for e.g.
// m_ActionReference != null && m_ActionReference.action != null.
InputAction m_PointAction;
InputAction m_MoveAction;
InputAction m_SubmitAction;
InputAction m_CancelAction;
InputAction m_LeftClickAction;
InputAction m_MiddleClickAction;
InputAction m_RightClickAction;
InputAction m_ScrollWheelAction;
InputAction m_NextPreviousAction;
List<Event> m_Events = new List<Event>();
PointerState m_MouseState;
PointerState m_PenState;
bool m_SeenPenEvents;
PointerState m_TouchState;
bool m_SeenTouchEvents;
const float k_SmallestReportedMovementSqrDist = 0.01f;
NavigationEventRepeatHelper m_RepeatHelper = new();
bool m_ResetSeenEventsOnUpdate;
#if UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA
const float kScrollUGUIScaleFactor = UIElements.WheelEvent.scrollDeltaPerTick;
#else
const float kScrollUGUIScaleFactor = 3.0f;
#endif
static Action<InputActionAsset> s_OnRegisterActions;
static InputSystemProvider()
{
// Only if InputSystem is enabled in the PlayerSettings do we set it as the provider.
// This includes situations where both InputManager and InputSystem are enabled.
#if ENABLE_INPUT_SYSTEM
EventProvider.SetInputSystemProvider(new InputSystemProvider());
#endif // ENABLE_INPUT_SYSTEM
}
[RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.SubsystemRegistration)]
static void Bootstrap() {} // Empty function. Exists only to invoke the static class constructor in Runtime Players
EventModifiers m_EventModifiers => m_InputEventPartialProvider._eventModifiers;
DiscreteTime m_CurrentTime => (DiscreteTime)Time.timeAsRational;
const uint k_DefaultPlayerId = 0u;
public void Initialize()
{
m_InputEventPartialProvider ??= new InputEventPartialProvider();
m_InputEventPartialProvider.Initialize();
m_Events.Clear();
m_MouseState.Reset();
m_PenState.Reset();
m_SeenPenEvents = false;
m_TouchState.Reset();
m_SeenTouchEvents = false;
SelectInputActionAsset();
RegisterActions();
// TODO make it configurable as it is not part of default config
// The Next/Previous action is not part of the input actions asset
RegisterFixedActions();
InputSystem.onActionsChange += OnActionsChange;
}
public void Shutdown()
{
UnregisterActions();
UnregisterFixedActions();
m_InputEventPartialProvider.Shutdown();
m_InputEventPartialProvider = null;
if (m_DefaultInputActions != null)
{
m_DefaultInputActions.Dispose();
m_DefaultInputActions = null;
}
InputSystem.onActionsChange -= OnActionsChange;
}
public void OnActionsChange()
{
UnregisterActions();
SelectInputActionAsset();
RegisterActions();
}
public void Update()
{
#if UNITY_EDITOR
// Ensure we are in a good (initialized) state before running updates.
// This could be in a bad state for a duration while the build pipeline is running
// when building tests to run in the Standalone Player.
if (m_InputActionAsset == null)
return;
#endif
m_InputEventPartialProvider.Update();
// Sort events added by input actions callbacks, based on type.
// This is necessary to ensure that events are dispatched in the correct order.
// If all events are of the PointerEvents type, sorting is based on reverse order of the EventSource enum.
// Touch -> Pen -> Mouse.
m_Events.Sort((a, b) => SortEvents(a, b));
var currentTime = (DiscreteTime)Time.timeAsRational;
DirectionNavigation(currentTime);
foreach (var ev in m_Events)
{
// We need to ignore some pointer events based on priority (Touch->Pen->Mouse)
// This is mostly used to filter out simulated input, e.g. when pen is active it also generates mouse input
if (m_SeenTouchEvents && ev.type == Event.Type.PointerEvent && ev.eventSource == EventSource.Pen)
m_PenState.Reset();
else if ((m_SeenTouchEvents || m_SeenPenEvents) &&
ev.type == Event.Type.PointerEvent && (ev.eventSource == EventSource.Mouse || ev.eventSource == EventSource.Unspecified))
m_MouseState.Reset();
else
EventProvider.Dispatch(ev);
}
// Sometimes single lower priority events can be received when using Touch or Pen, on a different frame.
// To avoid dispatching them, the seen event flags aren't reset in between calls to OnPointerPerformed.
// Essentially, if we're moving with Touch or Pen, lower priority events aren't dispatch as well.
// Once OnClickPerformed is called, the seen flags are reset
if (m_ResetSeenEventsOnUpdate)
{
ResetSeenEvents();
m_ResetSeenEventsOnUpdate = false;
}
m_Events.Clear();
}
void ResetSeenEvents()
{
m_SeenTouchEvents = false;
m_SeenPenEvents = false;
}
public bool ActionAssetIsNotNull()
{
return m_InputActionAsset != null;
}
//TODO: Refactor as there is no need for having almost the same implementation in the IM and ISX?
void DirectionNavigation(DiscreteTime currentTime)
{
var(move, axesButtonWerePressed) = ReadCurrentNavigationMoveVector();
var direction = NavigationEvent.DetermineMoveDirection(move);
// Checks for next/previous directions if no movement was detected
if (direction == NavigationEvent.Direction.None)
{
direction = ReadNextPreviousDirection();
axesButtonWerePressed = m_NextPreviousAction.WasPressedThisFrame();
}
if (direction == NavigationEvent.Direction.None)
{
m_RepeatHelper.Reset();
}
else
{
if (m_RepeatHelper.ShouldSendMoveEvent(currentTime, direction, axesButtonWerePressed))
{
EventProvider.Dispatch(Event.From(new NavigationEvent
{
type = NavigationEvent.Type.Move,
direction = direction,
timestamp = currentTime,
eventSource = GetEventSource(GetActiveDeviceFromDirection(direction)),
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
}
}
}
InputDevice GetActiveDeviceFromDirection(NavigationEvent.Direction direction)
{
switch (direction)
{
case NavigationEvent.Direction.Left:
case NavigationEvent.Direction.Up:
case NavigationEvent.Direction.Right:
case NavigationEvent.Direction.Down:
if (m_MoveAction != null)
return m_MoveAction.activeControl.device;
break;
case NavigationEvent.Direction.Next:
case NavigationEvent.Direction.Previous:
if (m_NextPreviousAction != null)
return m_NextPreviousAction.activeControl.device;
break;
case NavigationEvent.Direction.None:
default:
break;
}
return Keyboard.current;
}
(Vector2, bool) ReadCurrentNavigationMoveVector()
{
// In case action has not been configured we return defaults
if (m_MoveAction == null)
return (default, default);
var move = m_MoveAction.ReadValue<Vector2>();
// Check if the action was "pressed" this frame to deal with repeating events
var axisWasPressed = m_MoveAction.WasPressedThisFrame();
return (move, axisWasPressed);
}
NavigationEvent.Direction ReadNextPreviousDirection()
{
if (m_NextPreviousAction.IsPressed()) // Note: never null since created through code
{
//TODO: For now it only deals with Keyboard, needs to deal with other devices if we can add bindings
// for Gamepad, etc
//TODO: An alternative could be to have an action for next and for previous since shortcut support does
// not work properly
if (m_NextPreviousAction.activeControl.device is Keyboard)
{
var keyboard = m_NextPreviousAction.activeControl.device as Keyboard;
// Return direction based on whether shift is pressed or not
return keyboard.shiftKey.isPressed ? NavigationEvent.Direction.Previous : NavigationEvent.Direction.Next;
}
}
return NavigationEvent.Direction.None;
}
static int SortEvents(Event a, Event b)
{
return Event.CompareType(a, b);
}
public void OnFocusChanged(bool focus)
{
m_InputEventPartialProvider.OnFocusChanged(focus);
}
public bool RequestCurrentState(Event.Type type)
{
if (m_InputEventPartialProvider.RequestCurrentState(type))
return true;
switch (type)
{
case Event.Type.PointerEvent:
{
if (m_TouchState.LastPositionValid)
EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_TouchState, EventSource.Touch)));
if (m_PenState.LastPositionValid)
EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_PenState, EventSource.Pen)));
if (m_MouseState.LastPositionValid)
EventProvider.Dispatch(Event.From(ToPointerStateEvent(m_CurrentTime, m_MouseState, EventSource.Mouse)));
else
{
// TODO maybe it's reasonable to poll and dispatch mouse state here anyway?
}
return m_TouchState.LastPositionValid ||
m_PenState.LastPositionValid ||
m_MouseState.LastPositionValid;
}
// TODO
case Event.Type.IMECompositionEvent:
default:
return false;
}
}
public uint playerCount => 1; // TODO
// copied from UIElementsRuntimeUtility.cs
internal static Vector2 ScreenBottomLeftToPanelPosition(Vector2 position, int targetDisplay)
{
// Flip positions Y axis between input and UITK
var screenHeight = Screen.height;
if (targetDisplay > 0 && targetDisplay < Display.displays.Length)
screenHeight = Display.displays[targetDisplay].systemHeight;
position.y = screenHeight - position.y;
return position;
}
PointerEvent ToPointerStateEvent(DiscreteTime currentTime, in PointerState state, EventSource eventSource)
{
return new PointerEvent
{
type = PointerEvent.Type.State,
pointerIndex = 0,
position = state.LastPosition,
deltaPosition = Vector2.zero,
scroll = Vector2.zero,
displayIndex = state.LastDisplayIndex,
// TODO
// tilt = eventSource == EventSource.Pen ? _lastPenData.tilt : Vector2.zero,
// twist = eventSource == EventSource.Pen ? _lastPenData.twist : 0.0f,
// pressure = eventSource == EventSource.Pen ? _lastPenData.pressure : 0.0f,
// isInverted = eventSource == EventSource.Pen && ((_lastPenData.penStatus & PenStatus.Inverted) != 0),
button = 0,
buttonsState = state.ButtonsState,
clickCount = 0,
timestamp = currentTime,
eventSource = eventSource,
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
};
}
EventSource GetEventSource(InputAction.CallbackContext ctx)
{
var device = ctx.control.device;
return GetEventSource(device);
}
EventSource GetEventSource(InputDevice device)
{
if (device is Touchscreen)
return EventSource.Touch;
if (device is Pen)
return EventSource.Pen;
if (device is Mouse)
return EventSource.Mouse;
if (device is Keyboard)
return EventSource.Keyboard;
if (device is Gamepad)
return EventSource.Gamepad;
return EventSource.Unspecified;
}
ref PointerState GetPointerStateForSource(EventSource eventSource)
{
switch (eventSource)
{
case EventSource.Touch:
return ref m_TouchState;
case EventSource.Pen:
return ref m_PenState;
default:
return ref m_MouseState;
}
}
void DispatchFromCallback(in Event ev)
{
m_Events.Add(ev);
}
static int FindTouchFingerIndex(Touchscreen touchscreen, InputAction.CallbackContext ctx)
{
if (touchscreen == null)
return 0;
var asVector2Control = ctx.control is Vector2Control ? (Vector2Control)ctx.control : null;
var asTouchPressControl = ctx.control is TouchPressControl ? (TouchPressControl)ctx.control : null;
var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null;
// Finds the index of the matching control type in the Touchscreen device lost of touch controls (touches)
for (var i = 0; i < touchscreen.touches.Count; ++i)
{
if (asVector2Control != null && asVector2Control == touchscreen.touches[i].position)
return i;
if (asTouchPressControl != null && asTouchPressControl == touchscreen.touches[i].press)
return i;
if (asTouchControl != null && asTouchControl == touchscreen.touches[i])
return i;
}
return 0;
}
void OnPointerPerformed(InputAction.CallbackContext ctx)
{
var eventSource = GetEventSource(ctx);
ref var pointerState = ref GetPointerStateForSource(eventSource);
// Overall I'm not happy how leaky this is, we're using input actions to have flexibility to bind to different controls,
// but instead we just kinda abuse it to bind to different devices ...
var asPointerDevice = ctx.control.device is Pointer ? (Pointer)ctx.control.device : null;
var asPenDevice = ctx.control.device is Pen ? (Pen)ctx.control.device : null;
var asTouchscreenDevice = ctx.control.device is Touchscreen ? (Touchscreen)ctx.control.device : null;
var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null;
var pointerIndex = FindTouchFingerIndex(asTouchscreenDevice, ctx);
m_ResetSeenEventsOnUpdate = false;
if (asTouchControl != null || asTouchscreenDevice != null)
m_SeenTouchEvents = true;
else if (asPenDevice != null)
m_SeenPenEvents = true;
var positionISX = ctx.ReadValue<Vector2>();
var targetDisplay = asPointerDevice != null ? asPointerDevice.displayIndex.ReadValue() : (asTouchscreenDevice != null ? asTouchscreenDevice.displayIndex.ReadValue() : (asPenDevice != null ? asPenDevice.displayIndex.ReadValue() : 0));
var position = ScreenBottomLeftToPanelPosition(positionISX, targetDisplay);
var delta = pointerState.LastPositionValid ? position - pointerState.LastPosition : Vector2.zero;
var tilt = asPenDevice != null ? asPenDevice.tilt.ReadValue() : Vector2.zero;
var twist = asPenDevice != null ? asPenDevice.twist.ReadValue() : 0.0f;
var pressure = asPenDevice != null
? asPenDevice.pressure.ReadValue()
: (asTouchControl != null ? asTouchControl.pressure.ReadValue() : 0.0f);
var isInverted = asPenDevice != null
? asPenDevice.eraser.isPressed
: false; // TODO any way to detect that pen is inverted but not touching?
if (delta.sqrMagnitude >= k_SmallestReportedMovementSqrDist)
{
DispatchFromCallback(Event.From(new PointerEvent
{
type = PointerEvent.Type.PointerMoved,
pointerIndex = pointerIndex,
position = position,
deltaPosition = delta,
scroll = Vector2.zero,
displayIndex = targetDisplay,
tilt = tilt,
twist = twist,
pressure = pressure,
isInverted = isInverted,
button = 0,
buttonsState = pointerState.ButtonsState,
clickCount = 0,
timestamp = m_CurrentTime,
eventSource = eventSource,
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
// only record if we send an event
pointerState.OnMove(m_CurrentTime, position, targetDisplay);
}
else if (!pointerState.LastPositionValid)
pointerState.OnMove(m_CurrentTime, position, targetDisplay);
}
void OnSubmitPerformed(InputAction.CallbackContext ctx)
{
DispatchFromCallback(Event.From(new NavigationEvent
{
type = NavigationEvent.Type.Submit,
direction = NavigationEvent.Direction.None,
timestamp = m_CurrentTime,
eventSource = GetEventSource(ctx),
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
}
void OnCancelPerformed(InputAction.CallbackContext ctx)
{
DispatchFromCallback(Event.From(new NavigationEvent
{
type = NavigationEvent.Type.Cancel,
direction = NavigationEvent.Direction.None,
timestamp = m_CurrentTime,
eventSource = GetEventSource(ctx),
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
}
void OnClickPerformed(InputAction.CallbackContext ctx, EventSource eventSource, PointerEvent.Button button)
{
ref var state = ref GetPointerStateForSource(eventSource);
var asTouchscreenDevice = ctx.control.device is Touchscreen ? (Touchscreen)ctx.control.device : null;
var asTouchControl = ctx.control is TouchControl ? (TouchControl)ctx.control : null;
var pointerIndex = FindTouchFingerIndex(asTouchscreenDevice, ctx);
m_ResetSeenEventsOnUpdate = true;
if (asTouchControl != null || asTouchscreenDevice != null)
m_SeenTouchEvents = true;
var wasPressed = state.ButtonsState.Get(button);
var isPressed = ctx.ReadValueAsButton();
state.OnButtonChange(m_CurrentTime, button, wasPressed, isPressed);
DispatchFromCallback(Event.From(new PointerEvent
{
type = isPressed ? PointerEvent.Type.ButtonPressed : PointerEvent.Type.ButtonReleased,
pointerIndex = pointerIndex,
position = state.LastPosition,
deltaPosition = Vector2.zero,
scroll = Vector2.zero,
displayIndex = state.LastDisplayIndex,
tilt = Vector2.zero,
twist = 0.0f,
pressure = 0.0f,
isInverted = false,
button = button,
buttonsState = state.ButtonsState,
clickCount = state.ClickCount,
timestamp = m_CurrentTime,
eventSource = eventSource,
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
}
void OnLeftClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseLeft);
void OnMiddleClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseMiddle);
void OnRightClickPerformed(InputAction.CallbackContext ctx) => OnClickPerformed(ctx, GetEventSource(ctx), PointerEvent.Button.MouseRight);
void OnScrollWheelPerformed(InputAction.CallbackContext ctx)
{
// ISXB-704: convert input value to uniform ticks before sending them to UI.
var scrollTicks = ctx.ReadValue<Vector2>() / InputSystem.scrollWheelDeltaPerTick;
if (scrollTicks.sqrMagnitude < k_SmallestReportedMovementSqrDist)
return;
var eventSource = GetEventSource(ctx);
ref var state = ref GetPointerStateForSource(eventSource);
var position = Vector2.zero;
var targetDisplay = 0;
if (state.LastPositionValid)
{
position = state.LastPosition;
targetDisplay = state.LastDisplayIndex;
}
else if (eventSource == EventSource.Mouse && Mouse.current != null)
{
position = Mouse.current.position.ReadValue();
targetDisplay = Mouse.current.displayIndex.ReadValue();
}
// Make scrollDelta look similar to IMGUI event scroll values.
var scrollDelta = new Vector2
{
x = scrollTicks.x * kScrollUGUIScaleFactor,
y = -scrollTicks.y * kScrollUGUIScaleFactor
};
DispatchFromCallback(Event.From(new PointerEvent
{
type = PointerEvent.Type.Scroll,
pointerIndex = 0,
position = position,
deltaPosition = Vector2.zero,
scroll = scrollDelta,
displayIndex = targetDisplay,
tilt = Vector2.zero,
twist = 0.0f,
pressure = 0.0f,
isInverted = false,
button = 0,
buttonsState = state.ButtonsState,
clickCount = 0,
timestamp = m_CurrentTime,
eventSource = EventSource.Mouse,
playerId = k_DefaultPlayerId,
eventModifiers = m_EventModifiers
}));
}
void RegisterFixedActions()
{
m_NextPreviousAction = new InputAction(name: "nextPreviousAction", type: InputActionType.Button);
// TODO add more default bindings, or make them configurable
m_NextPreviousAction.AddBinding("<Keyboard>/tab");
m_NextPreviousAction.Enable();
}
void UnregisterFixedActions()
{
// The Next/Previous action is not part of the input actions asset
if (m_NextPreviousAction != null)
{
m_NextPreviousAction.Disable();
m_NextPreviousAction = null;
}
}
InputAction FindActionAndRegisterCallback(string actionNameOrId, Action<InputAction.CallbackContext> callback = null)
{
var action = m_InputActionAsset.FindAction(actionNameOrId);
if (action != null && callback != null)
action.performed += callback;
return action;
}
void RegisterActions()
{
// Invoke potential lister observing registration
s_OnRegisterActions?.Invoke(m_InputActionAsset);
m_PointAction = FindActionAndRegisterCallback(Actions.PointAction, OnPointerPerformed);
m_MoveAction = FindActionAndRegisterCallback(Actions.MoveAction); // No callback for this action
m_SubmitAction = FindActionAndRegisterCallback(Actions.SubmitAction, OnSubmitPerformed);
m_CancelAction = FindActionAndRegisterCallback(Actions.CancelAction, OnCancelPerformed);
m_LeftClickAction = FindActionAndRegisterCallback(Actions.LeftClickAction, OnLeftClickPerformed);
m_MiddleClickAction = FindActionAndRegisterCallback(Actions.MiddleClickAction, OnMiddleClickPerformed);
m_RightClickAction = FindActionAndRegisterCallback(Actions.RightClickAction, OnRightClickPerformed);
m_ScrollWheelAction = FindActionAndRegisterCallback(Actions.ScrollWheelAction, OnScrollWheelPerformed);
// When adding new actions, don't forget to add them to UnregisterActions
if (InputSystem.actions == null)
{
// If we've not loaded a user-created set of actions, just enable the UI actions from our defaults.
m_InputActionAsset.FindActionMap("UI", true).Enable();
}
else
m_InputActionAsset.Enable();
}
void UnregisterAction(ref InputAction action, Action<InputAction.CallbackContext> callback = null)
{
if (action != null && callback != null)
action.performed -= callback;
action = null;
}
void UnregisterActions()
{
UnregisterAction(ref m_PointAction, OnPointerPerformed);
UnregisterAction(ref m_MoveAction); // No callback for this action
UnregisterAction(ref m_SubmitAction, OnSubmitPerformed);
UnregisterAction(ref m_CancelAction, OnCancelPerformed);
UnregisterAction(ref m_LeftClickAction, OnLeftClickPerformed);
UnregisterAction(ref m_MiddleClickAction, OnMiddleClickPerformed);
UnregisterAction(ref m_RightClickAction, OnRightClickPerformed);
UnregisterAction(ref m_ScrollWheelAction, OnScrollWheelPerformed);
if (m_InputActionAsset != null)
m_InputActionAsset.Disable();
}
void SelectInputActionAsset()
{
// Only use default actions asset configuration if (ISX-1954):
// - Project-wide Input Actions have not been configured, OR
// - Project-wide Input Actions have been configured but contains no UI action map.
var projectWideInputActions = InputSystem.actions;
var useProjectWideInputActions =
projectWideInputActions != null &&
projectWideInputActions.FindActionMap("UI") != null;
// Use InputSystem.actions (Project-wide Actions) if available, else use default asset if
// user didn't specifically set one, so that UI functions still work (ISXB-811).
if (useProjectWideInputActions)
m_InputActionAsset = InputSystem.actions;
else
{
if (m_DefaultInputActions is null)
m_DefaultInputActions = new DefaultInputActions();
m_InputActionAsset = m_DefaultInputActions.asset;
}
}
public static class Actions
{
public readonly static string PointAction = "UI/Point";
public readonly static string MoveAction = "UI/Navigate";
public readonly static string SubmitAction = "UI/Submit";
public readonly static string CancelAction = "UI/Cancel";
public readonly static string LeftClickAction = "UI/Click";
public readonly static string MiddleClickAction = "UI/MiddleClick";
public readonly static string RightClickAction = "UI/RightClick";
public readonly static string ScrollWheelAction = "UI/ScrollWheel";
}
internal static void SetOnRegisterActions(Action<InputActionAsset> callback)
{
s_OnRegisterActions = callback;
}
}
}
#endif // UNITY_2023_2_OR_NEWER

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6965b068653ebe45986cb10b38854d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1c597fa0f94a3d54b9c0e85f56e5f821
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,146 @@
#if UNITY_EDITOR || UNITY_STANDALONE_LINUX
using System;
namespace UnityEngine.InputSystem.Linux
{
// These structures are not explicitly assigned, but they are filled in via JSON serialization coming from matching structs in native.
#pragma warning disable 0649
internal enum JoystickFeatureType
{
Invalid = 0,
Axis,
Ball,
Button,
Hat,
Max
}
internal enum SDLAxisUsage
{
Unknown = 0,
X,
Y,
Z,
RotateX,
RotateY,
RotateZ,
Throttle,
Rudder,
Wheel,
Gas,
Brake,
Hat0X,
Hat0Y,
Hat1X,
Hat1Y,
Hat2X,
Hat2Y,
Hat3X,
Hat3Y,
Count
}
internal enum SDLButtonUsage
{
Unknown = 0,
Trigger,
Thumb,
Thumb2,
Top,
Top2,
Pinkie,
Base,
Base2,
Base3,
Base4,
Base5,
Base6,
Dead,
A,
B,
X,
Y,
Z,
TriggerLeft,
TriggerRight,
TriggerLeft2,
TriggerRight2,
Select,
Start,
Mode,
ThumbLeft,
ThumbRight,
Count
}
// JSON must match JoystickFeatureDefinition in native.
[Serializable]
internal struct SDLFeatureDescriptor
{
public JoystickFeatureType featureType;
public int usageHint;
public int featureSize;
public int offset;
public int bit;
public int min;
public int max;
}
[Serializable]
internal class SDLDeviceDescriptor
{
public SDLFeatureDescriptor[] controls;
internal string ToJson()
{
return JsonUtility.ToJson(this);
}
internal static SDLDeviceDescriptor FromJson(string json)
{
return JsonUtility.FromJson<SDLDeviceDescriptor>(json);
}
}
#pragma warning restore 0649
/// <summary>
/// A small helper class to aid in initializing and registering SDL devices and layout builders.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class LinuxSupport
{
/// <summary>
/// The current interface code sent with devices to identify as Linux SDL devices.
/// </summary>
internal const string kInterfaceName = "Linux";
public static string GetAxisNameFromUsage(SDLAxisUsage usage)
{
return Enum.GetName(typeof(SDLAxisUsage), usage);
}
public static string GetButtonNameFromUsage(SDLButtonUsage usage)
{
return Enum.GetName(typeof(SDLButtonUsage), usage);
}
/// <summary>
/// Registers all initial templates and the generalized layout builder with the InputSystem.
/// </summary>
public static void Initialize()
{
InputSystem.onFindLayoutForDevice += SDLLayoutBuilder.OnFindLayoutForDevice;
}
}
}
#endif // UNITY_EDITOR || UNITY_STANDALONE_LINUX

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3da49be2d645b418c8ee308bb1a011c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,288 @@
#if UNITY_EDITOR || UNITY_STANDALONE_LINUX
using System;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using System.Text;
using UnityEngine.InputSystem.Layouts;
namespace UnityEngine.InputSystem.Linux
{
[Serializable]
internal class SDLLayoutBuilder
{
[SerializeField] private string m_ParentLayout;
[SerializeField] private SDLDeviceDescriptor m_Descriptor;
internal static string OnFindLayoutForDevice(ref InputDeviceDescription description, string matchedLayout,
InputDeviceExecuteCommandDelegate executeCommandDelegate)
{
if (description.interfaceName != LinuxSupport.kInterfaceName)
return null;
if (string.IsNullOrEmpty(description.capabilities))
return null;
// Try to parse the SDL descriptor.
SDLDeviceDescriptor deviceDescriptor;
try
{
deviceDescriptor = SDLDeviceDescriptor.FromJson(description.capabilities);
}
catch (Exception exception)
{
Debug.LogError($"{exception} while trying to parse descriptor for SDL device: {description.capabilities}");
return null;
}
if (deviceDescriptor == null)
return null;
string layoutName;
if (string.IsNullOrEmpty(description.manufacturer))
{
layoutName = $"{SanitizeName(description.interfaceName)}::{SanitizeName(description.product)}";
}
else
{
layoutName =
$"{SanitizeName(description.interfaceName)}::{SanitizeName(description.manufacturer)}::{SanitizeName(description.product)}";
}
var layout = new SDLLayoutBuilder { m_Descriptor = deviceDescriptor, m_ParentLayout = matchedLayout };
InputSystem.RegisterLayoutBuilder(() => layout.Build(), layoutName, matchedLayout);
return layoutName;
}
private static string SanitizeName(string originalName)
{
var stringLength = originalName.Length;
var sanitizedName = new StringBuilder(stringLength);
for (var i = 0; i < stringLength; i++)
{
var letter = originalName[i];
if (char.IsUpper(letter) || char.IsLower(letter) || char.IsDigit(letter))
sanitizedName.Append(letter);
}
return sanitizedName.ToString();
}
private static bool IsAxis(SDLFeatureDescriptor feature, SDLAxisUsage axis)
{
return feature.featureType == JoystickFeatureType.Axis
&& feature.usageHint == (int)axis;
}
private static void BuildStickFeature(ref InputControlLayout.Builder builder, SDLFeatureDescriptor xFeature, SDLFeatureDescriptor yFeature)
{
int byteOffset;
if (xFeature.offset <= yFeature.offset)
byteOffset = xFeature.offset;
else
byteOffset = yFeature.offset;
const string stickName = "Stick";
builder.AddControl(stickName)
.WithLayout("Stick")
.WithByteOffset((uint)byteOffset)
.WithSizeInBits((uint)xFeature.featureSize * 8 + (uint)yFeature.featureSize * 8)
.WithUsages(CommonUsages.Primary2DMotion);
builder.AddControl(stickName + "/x")
.WithFormat(InputStateBlock.FormatInt)
.WithByteOffset(0)
.WithSizeInBits((uint)xFeature.featureSize * 8)
.WithParameters("clamp=1,clampMin=-1,clampMax=1,scale,scaleFactor=65538");
builder.AddControl(stickName + "/y")
.WithFormat(InputStateBlock.FormatInt)
.WithByteOffset(4)
.WithSizeInBits((uint)xFeature.featureSize * 8)
.WithParameters("clamp=1,clampMin=-1,clampMax=1,scale,scaleFactor=65538,invert");
builder.AddControl(stickName + "/up")
.WithParameters("clamp=1,clampMin=-1,clampMax=0,scale,scaleFactor=65538,invert");
builder.AddControl(stickName + "/down")
.WithParameters("clamp=1,clampMin=0,clampMax=1,scale,scaleFactor=65538,invert=false");
builder.AddControl(stickName + "/left")
.WithParameters("clamp=1,clampMin=-1,clampMax=0,scale,scaleFactor=65538,invert");
builder.AddControl(stickName + "/right")
.WithParameters("clamp=1,clampMin=0,clampMax=1,scale,scaleFactor=65538");
}
private static bool IsHatX(SDLFeatureDescriptor feature)
{
return feature.featureType == JoystickFeatureType.Hat
&& (feature.usageHint == (int)SDLAxisUsage.Hat0X
|| feature.usageHint == (int)SDLAxisUsage.Hat1X
|| feature.usageHint == (int)SDLAxisUsage.Hat2X
|| feature.usageHint == (int)SDLAxisUsage.Hat3X);
}
private static bool IsHatY(SDLFeatureDescriptor feature)
{
return feature.featureType == JoystickFeatureType.Hat
&& (feature.usageHint == (int)SDLAxisUsage.Hat0Y
|| feature.usageHint == (int)SDLAxisUsage.Hat1Y
|| feature.usageHint == (int)SDLAxisUsage.Hat2Y
|| feature.usageHint == (int)SDLAxisUsage.Hat3Y);
}
private static int HatNumber(SDLFeatureDescriptor feature)
{
Debug.Assert(feature.featureType == JoystickFeatureType.Hat);
return 1 + (feature.usageHint - (int)SDLAxisUsage.Hat0X) / 2;
}
private static void BuildHatFeature(ref InputControlLayout.Builder builder, SDLFeatureDescriptor xFeature, SDLFeatureDescriptor yFeature)
{
Debug.Assert(xFeature.offset < yFeature.offset, "Order of features must be X followed by Y");
var hat = HatNumber(xFeature);
var hatName = hat > 1 ? $"Hat{hat}" : "Hat";
builder.AddControl(hatName)
.WithLayout("Dpad")
.WithByteOffset((uint)xFeature.offset)
.WithSizeInBits((uint)xFeature.featureSize * 8 + (uint)yFeature.featureSize * 8)
.WithUsages(CommonUsages.Hatswitch);
builder.AddControl(hatName + "/up")
.WithFormat(InputStateBlock.FormatInt)
.WithParameters("scale,scaleFactor=2147483647,clamp,clampMin=-1,clampMax=0,invert")
.WithByteOffset(4)
.WithBitOffset(0)
.WithSizeInBits((uint)yFeature.featureSize * 8);
builder.AddControl(hatName + "/down")
.WithFormat(InputStateBlock.FormatInt)
.WithParameters("scale,scaleFactor=2147483647,clamp,clampMin=0,clampMax=1")
.WithByteOffset(4)
.WithBitOffset(0)
.WithSizeInBits((uint)yFeature.featureSize * 8);
builder.AddControl(hatName + "/left")
.WithFormat(InputStateBlock.FormatInt)
.WithParameters("scale,scaleFactor=2147483647,clamp,clampMin=-1,clampMax=0,invert")
.WithByteOffset(0)
.WithBitOffset(0)
.WithSizeInBits((uint)xFeature.featureSize * 8);
builder.AddControl(hatName + "/right")
.WithFormat(InputStateBlock.FormatInt)
.WithParameters("scale,scaleFactor=2147483647,clamp,clampMin=0,clampMax=1")
.WithByteOffset(0)
.WithBitOffset(0)
.WithSizeInBits((uint)xFeature.featureSize * 8);
}
internal InputControlLayout Build()
{
var builder = new InputControlLayout.Builder
{
stateFormat = new FourCC('L', 'J', 'O', 'Y'),
extendsLayout = m_ParentLayout
};
for (var i = 0; i < m_Descriptor.controls.LengthSafe(); i++)
{
var feature = m_Descriptor.controls[i];
switch (feature.featureType)
{
case JoystickFeatureType.Axis:
{
var usage = (SDLAxisUsage)feature.usageHint;
var featureName = LinuxSupport.GetAxisNameFromUsage(usage);
var parameters = "scale,scaleFactor=65538,clamp=1,clampMin=-1,clampMax=1";
// If X is followed by Y, build a stick out of the two.
if (IsAxis(feature, SDLAxisUsage.X) && i + 1 < m_Descriptor.controls.Length)
{
var nextFeature = m_Descriptor.controls[i + 1];
if (IsAxis(nextFeature, SDLAxisUsage.Y))
{
BuildStickFeature(ref builder, feature, nextFeature);
++i;
continue;
}
}
if (IsAxis(feature, SDLAxisUsage.Y))
parameters += ",invert";
var control = builder.AddControl(featureName)
.WithLayout("Analog")
.WithByteOffset((uint)feature.offset)
.WithFormat(InputStateBlock.FormatInt)
.WithParameters(parameters);
if (IsAxis(feature, SDLAxisUsage.RotateZ))
control.WithUsages(CommonUsages.Twist);
break;
}
case JoystickFeatureType.Ball:
{
//TODO
break;
}
case JoystickFeatureType.Button:
{
var usage = (SDLButtonUsage)feature.usageHint;
var featureName = LinuxSupport.GetButtonNameFromUsage(usage);
if (featureName != null)
{
builder.AddControl(featureName)
.WithLayout("Button")
.WithByteOffset((uint)feature.offset)
.WithBitOffset((uint)feature.bit)
.WithFormat(InputStateBlock.FormatBit);
}
break;
}
case JoystickFeatureType.Hat:
{
var usage = (SDLAxisUsage)feature.usageHint;
var featureName = LinuxSupport.GetAxisNameFromUsage(usage);
var parameters = "scale,scaleFactor=2147483647,clamp=1,clampMin=-1,clampMax=1";
if (i + 1 < m_Descriptor.controls.Length)
{
var nextFeature = m_Descriptor.controls[i + 1];
if (IsHatY(nextFeature) && HatNumber(feature) == HatNumber(nextFeature))
{
BuildHatFeature(ref builder, feature, nextFeature);
++i;
continue;
}
}
if (IsHatY(feature))
parameters += ",invert";
builder.AddControl(featureName)
.WithLayout("Analog")
.WithByteOffset((uint)feature.offset)
.WithFormat(InputStateBlock.FormatInt)
.WithParameters(parameters);
break;
}
default:
{
throw new NotImplementedException(
$"SDLLayoutBuilder.Build: Trying to build an SDL device with an unknown feature of type {feature.featureType}.");
}
}
}
return builder.Build();
}
}
}
#endif // UNITY_EDITOR || UNITY_STANDALONE_LINUX

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2ab54adba6d259742a9022c514b04149
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea65eddf43398654fa32b724723c71a4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
#if UNITY_EDITOR || UNITY_STANDALONE_OSX || PACKAGE_DOCS_GENERATION
using System.Runtime.InteropServices;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.InputSystem.OSX.LowLevel;
using UnityEngine.InputSystem.Controls;
namespace UnityEngine.InputSystem.OSX.LowLevel
{
/// <summary>
/// Structure of HID input reports for SteelSeries Nimbus+ controllers supported
/// via HID on OSX.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 8)]
internal struct NimbusPlusHIDInputReport : IInputStateTypeInfo
{
/// <summary>
/// A dummy vendor ID made available by OSX when supporting Nimbus+ via HID.
/// This is exposed by OSX instead of the true SteelSeries vendor ID 0x1038.
/// </summary>
public const int OSXVendorId = 0xd;
/// <summary>
/// A dummy product ID made available by OSX when supporting Nimbus+ via HID.
/// This is exposed by OSX instead of the true Nimbus+ product ID 0x1422.
/// </summary>
public const int OSXProductId = 0x0;
public FourCC format => new FourCC('H', 'I', 'D');
[InputControl(name = "leftStick", layout = "Stick", format = "VC2B")]
[InputControl(name = "leftStick/x", offset = 0, format = "SBYT", parameters = "")]
[InputControl(name = "leftStick/left", offset = 0, format = "SBYT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "leftStick/right", offset = 0, format = "SBYT", parameters = "clamp=1,clampMin=0,clampMax=1")]
[InputControl(name = "leftStick/y", offset = 1, format = "SBYT", parameters = "")]
[InputControl(name = "leftStick/up", offset = 1, format = "SBYT", parameters = "clamp=1,clampMin=0,clampMax=1")]
[InputControl(name = "leftStick/down", offset = 1, format = "SBYT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert")]
[FieldOffset(0)] public sbyte leftStickX;
[FieldOffset(1)] public sbyte leftStickY;
[InputControl(name = "rightStick", layout = "Stick", format = "VC2B")]
[InputControl(name = "rightStick/x", offset = 0, format = "SBYT")]
[InputControl(name = "rightStick/left", offset = 0, format = "SBYT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "rightStick/right", offset = 0, format = "SBYT", parameters = "clamp=1,clampMin=0,clampMax=1")]
[InputControl(name = "rightStick/y", offset = 1, format = "SBYT")]
[InputControl(name = "rightStick/up", offset = 1, format = "SBYT", parameters = "clamp=1,clampMin=0,clampMax=1")]
[InputControl(name = "rightStick/down", offset = 1, format = "SBYT", parameters = "clamp=1,clampMin=-1,clampMax=0,invert")]
[FieldOffset(2)] public sbyte rightStickX;
[FieldOffset(3)] public sbyte rightStickY;
[InputControl(name = "leftTrigger", format = "BYTE")]
[FieldOffset(4)] public byte leftTrigger;
[InputControl(name = "rightTrigger", format = "BYTE")]
[FieldOffset(5)] public byte rightTrigger;
[InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4)]
[InputControl(name = "dpad/up", format = "BIT", bit = 0)]
[InputControl(name = "dpad/right", format = "BIT", bit = 1)]
[InputControl(name = "dpad/down", format = "BIT", bit = 2)]
[InputControl(name = "dpad/left", format = "BIT", bit = 3)]
[InputControl(name = "buttonSouth", displayName = "A", bit = 4)]
[InputControl(name = "buttonEast", displayName = "B", bit = 5)]
[InputControl(name = "buttonWest", displayName = "X", bit = 6)]
[InputControl(name = "buttonNorth", displayName = "Y", bit = 7)]
[FieldOffset(6)] public byte buttons1;
[InputControl(name = "leftShoulder", bit = 0)]
[InputControl(name = "rightShoulder", bit = 1)]
[InputControl(name = "leftStickPress", bit = 2)]
[InputControl(name = "rightStickPress", bit = 3)]
[InputControl(name = "homeButton", layout = "Button", bit = 4)]
[InputControl(name = "select", bit = 5)]
[InputControl(name = "start", bit = 6)]
[FieldOffset(7)] public byte buttons2;
}
}
namespace UnityEngine.InputSystem.OSX
{
/// <summary>
/// Steel Series Nimbus+ uses iOSGameController MFI when on iOS but
/// is just a standard HID on OSX. Note that the gamepad is made available
/// with incorrect VID/PID by OSX instead of the true VID/PID registred with
/// USB.org for this device.
/// </summary>
[InputControlLayout(stateType = typeof(NimbusPlusHIDInputReport), displayName = "Nimbus+ Gamepad")]
[Scripting.Preserve]
public class NimbusGamepadHid : Gamepad
{
/// <summary>
/// The center button in the middle section of the controller.
/// </summary>
/// <remarks>
/// Note that this button is also picked up by OS.
/// </remarks>
[InputControl(name = "homeButton", displayName = "Home", shortDisplayName = "Home")]
public ButtonControl homeButton { get; protected set; }
/// <inheritdoc />
protected override void FinishSetup()
{
homeButton = GetChildControl<ButtonControl>("homeButton");
Debug.Assert(homeButton != null);
base.FinishSetup();
}
}
}
#endif // UNITY_EDITOR || UNITY_STANDALONE_OSX

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9817b64a59aab947a96a4cf6247d018
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
#if UNITY_EDITOR || UNITY_STANDALONE_OSX
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.OSX.LowLevel;
namespace UnityEngine.InputSystem.OSX
{
/// <summary>
/// A small helper class to aid in initializing and registering HID device layout builders.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class OSXSupport
{
/// <summary>
/// Registers HID device layouts for OSX.
/// </summary>
public static void Initialize()
{
// Note that OSX reports manufacturer "Unknown" and a bogus VID/PID according
// to matcher below.
InputSystem.RegisterLayout<NimbusGamepadHid>(
matches: new InputDeviceMatcher()
.WithProduct("Nimbus+", supportRegex: false)
.WithCapability("vendorId", NimbusPlusHIDInputReport.OSXVendorId)
.WithCapability("productId", NimbusPlusHIDInputReport.OSXProductId));
}
}
}
#endif // UNITY_EDITOR || UNITY_STANDALONE_OSX

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec5b5d14e5b51f44abf18ab8f288cdb5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0e49b0a8b53e45445979b3fa9604a35c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.Layouts;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
#endif
////TODO: custom icon for OnScreenButton component
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// A button that is visually represented on-screen and triggered by touch or other pointer
/// input.
/// </summary>
[AddComponentMenu("Input/On-Screen Button")]
[HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-buttons")]
public class OnScreenButton : OnScreenControl, IPointerDownHandler, IPointerUpHandler
{
public void OnPointerUp(PointerEventData eventData)
{
SendValueToControl(0.0f);
}
public void OnPointerDown(PointerEventData eventData)
{
SendValueToControl(1.0f);
}
////TODO: pressure support
/*
/// <summary>
/// If true, the button's value is driven from the pressure value of touch or pen input.
/// </summary>
/// <remarks>
/// This essentially allows having trigger-like buttons as on-screen controls.
/// </remarks>
[SerializeField] private bool m_UsePressure;
*/
[InputControl(layout = "Button")]
[SerializeField]
private string m_ControlPath;
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(OnScreenButton))]
internal class OnScreenButtonEditor : UnityEditor.Editor
{
private UnityEditor.SerializedProperty m_ControlPathInternal;
public void OnEnable()
{
m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenButton.m_ControlPath));
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.OnScreenButton).Send();
}
public override void OnInspectorGUI()
{
// Current implementation has UGUI dependencies (ISXB-915, ISXB-916)
UGUIOnScreenControlEditorUtils.ShowWarningIfNotPartOfCanvasHierarchy((OnScreenButton)target);
UnityEditor.EditorGUILayout.PropertyField(m_ControlPathInternal);
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d54531977ecb194c95e2d3aa7a5d72a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,369 @@
using System;
using Unity.Collections;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
////REVIEW: should we make this ExecuteInEditMode?
////TODO: handle display strings for this in some form; shouldn't display generic gamepad binding strings, for example, for OSCs
////TODO: give more control over when an OSC creates a new devices; going simply by name of layout only is inflexible
////TODO: make this survive domain reloads
////TODO: allow feeding into more than one control
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// Base class for on-screen controls.
/// </summary>
/// <remarks>
/// The set of on-screen controls together forms a device. A control layout
/// is automatically generated from the set and a device using the layout is
/// added to the system when the on-screen controls are enabled.
///
/// The layout that the generated layout is based on is determined by the
/// control paths chosen for each on-screen control. If, for example, an
/// on-screen control chooses the 'a' key from the "Keyboard" layout as its
/// path, a device layout is generated that is based on the "Keyboard" layout
/// and the on-screen control becomes the 'a' key in that layout.
///
/// If a <see cref="GameObject"/> has multiple on-screen controls that reference different
/// types of device layouts (e.g. one control references 'buttonWest' on
/// a gamepad and another references 'leftButton' on a mouse), then a device
/// is created for each type referenced by the setup.
///
/// The <see cref="OnScreenControl"/> works by simulating events from the device specified in the <see cref="OnScreenControl.controlPath"/>
/// property. Some parts of the Input System, such as the <see cref="PlayerInput"/> component, can be set up to
/// auto-switch <see cref="PlayerInput.neverAutoSwitchControlSchemes"/> to a new device when input from them is detected.
/// When a device is switched, any currently running inputs from the previously active device are cancelled.
///
/// To avoid this situation, you need to ensure, depending on your case, that the Mouse, Pen, Touchsceen and/or XRController devices are not used in a concurent
/// control schemes of the simulated device.
/// </remarks>
public abstract class OnScreenControl : MonoBehaviour
{
/// <summary>
/// The control path (see <see cref="InputControlPath"/>) for the control that the on-screen
/// control will feed input into.
/// </summary>
/// <remarks>
/// A device will be created from the device layout referenced by the control path (see
/// <see cref="InputControlPath.TryGetDeviceLayout"/>). The path is then used to look up
/// <see cref="control"/> on the device. The resulting control will be fed values from
/// the on-screen control.
///
/// Multiple on-screen controls sharing the same device layout will together create a single
/// virtual device. If, for example, one component uses <c>"&lt;Gamepad&gt;/buttonSouth"</c>
/// and another uses <c>"&lt;Gamepad&gt;/leftStick"</c> as the control path, a single
/// <see cref="Gamepad"/> will be created and the first component will feed data to
/// <see cref="Gamepad.buttonSouth"/> and the second component will feed data to
/// <see cref="Gamepad.leftStick"/>.
/// </remarks>
/// <seealso cref="InputControlPath"/>
public string controlPath
{
get => controlPathInternal;
set
{
controlPathInternal = value;
if (isActiveAndEnabled)
SetupInputControl();
}
}
/// <summary>
/// The actual control that is fed input from the on-screen control.
/// </summary>
/// <remarks>
/// This is only valid while the on-screen control is enabled. Otherwise, it is <c>null</c>. Also,
/// if no <see cref="controlPath"/> has been set, this will remain <c>null</c> even if the component is enabled.
/// </remarks>
public InputControl control => m_Control;
private InputControl m_Control;
private OnScreenControl m_NextControlOnDevice;
private InputEventPtr m_InputEventPtr;
/// <summary>
/// Accessor for the <see cref="controlPath"/> of the component. Must be implemented by subclasses.
/// </summary>
/// <remarks>
/// Moving the definition of how the control path is stored into subclasses allows them to
/// apply their own <see cref="InputControlAttribute"/> attributes to them and thus set their
/// own layout filters.
/// </remarks>
protected abstract string controlPathInternal { get; set; }
private void SetupInputControl()
{
Debug.Assert(m_Control == null, "InputControl already initialized");
Debug.Assert(m_NextControlOnDevice == null, "Previous InputControl has not been properly uninitialized (m_NextControlOnDevice still set)");
Debug.Assert(!m_InputEventPtr.valid, "Previous InputControl has not been properly uninitialized (m_InputEventPtr still set)");
// Nothing to do if we don't have a control path.
var path = controlPathInternal;
if (string.IsNullOrEmpty(path))
return;
// Determine what type of device to work with.
var layoutName = InputControlPath.TryGetDeviceLayout(path);
if (layoutName == null)
{
Debug.LogError(
$"Cannot determine device layout to use based on control path '{path}' used in {GetType().Name} component",
this);
return;
}
// Try to find existing on-screen device that matches.
var internedLayoutName = new InternedString(layoutName);
var deviceInfoIndex = -1;
for (var i = 0; i < s_OnScreenDevices.length; ++i)
{
////FIXME: this does not take things such as different device usages into account
if (s_OnScreenDevices[i].device.m_Layout == internedLayoutName)
{
deviceInfoIndex = i;
break;
}
}
// If we don't have a matching one, create a new one.
InputDevice device;
if (deviceInfoIndex == -1)
{
// Try to create device.
try
{
device = InputSystem.AddDevice(layoutName);
}
catch (Exception exception)
{
Debug.LogError(
$"Could not create device with layout '{layoutName}' used in '{GetType().Name}' component");
Debug.LogException(exception);
return;
}
InputSystem.AddDeviceUsage(device, "OnScreen");
// Create event buffer.
var buffer = StateEvent.From(device, out var eventPtr, Allocator.Persistent);
// Add to list.
deviceInfoIndex = s_OnScreenDevices.Append(new OnScreenDeviceInfo
{
eventPtr = eventPtr,
buffer = buffer,
device = device,
});
}
else
{
device = s_OnScreenDevices[deviceInfoIndex].device;
}
// Try to find control on device.
m_Control = InputControlPath.TryFindControl(device, path);
if (m_Control == null)
{
Debug.LogError(
$"Cannot find control with path '{path}' on device of type '{layoutName}' referenced by component '{GetType().Name}'",
this);
// Remove the device, if we just created one.
if (s_OnScreenDevices[deviceInfoIndex].firstControl == null)
{
s_OnScreenDevices[deviceInfoIndex].Destroy();
s_OnScreenDevices.RemoveAt(deviceInfoIndex);
}
return;
}
m_InputEventPtr = s_OnScreenDevices[deviceInfoIndex].eventPtr;
// We have all we need. Permanently add us.
s_OnScreenDevices[deviceInfoIndex] =
s_OnScreenDevices[deviceInfoIndex].AddControl(this);
}
protected void SendValueToControl<TValue>(TValue value)
where TValue : struct
{
if (m_Control == null)
return;
if (!(m_Control is InputControl<TValue> control))
throw new ArgumentException(
$"The control path {controlPath} yields a control of type {m_Control.GetType().Name} which is not an InputControl with value type {typeof(TValue).Name}", nameof(value));
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime;
control.WriteValueIntoEvent(value, m_InputEventPtr);
InputSystem.QueueEvent(m_InputEventPtr);
}
protected void SentDefaultValueToControl()
{
if (m_Control == null)
return;
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime;
m_Control.ResetToDefaultStateInEvent(m_InputEventPtr);
InputSystem.QueueEvent(m_InputEventPtr);
}
// Used by PlayerInput auto switch for scheme to prevent using Pointer device.
internal static bool HasAnyActive => s_nbActiveInstances != 0;
private static int s_nbActiveInstances = 0;
protected virtual void OnEnable()
{
++s_nbActiveInstances;
SetupInputControl();
if (m_Control == null)
return;
// if we are in single player and if it the first active switch to the target device.
if (s_nbActiveInstances == 1 &&
PlayerInput.isSinglePlayer)
{
var firstPlayer = PlayerInput.GetPlayerByIndex(0);
if (firstPlayer?.neverAutoSwitchControlSchemes == false)
{
var devices = firstPlayer.devices;
bool deviceFound = false;
// skip is the device is already part of the current scheme
foreach (var device in devices)
{
if (m_Control.device.deviceId == device.deviceId)
{
deviceFound = true;
break;
}
}
if (!deviceFound)
{
firstPlayer.SwitchCurrentControlScheme(m_Control.device);
}
}
}
}
protected virtual void OnDisable()
{
--s_nbActiveInstances;
if (m_Control == null)
return;
var device = m_Control.device;
for (var i = 0; i < s_OnScreenDevices.length; ++i)
{
if (s_OnScreenDevices[i].device != device)
continue;
var deviceInfo = s_OnScreenDevices[i].RemoveControl(this);
if (deviceInfo.firstControl == null)
{
// We're the last on-screen control on this device. Remove the device.
s_OnScreenDevices[i].Destroy();
s_OnScreenDevices.RemoveAt(i);
}
else
{
s_OnScreenDevices[i] = deviceInfo;
// We're keeping the device but we're disabling the on-screen representation
// for one of its controls. If the control isn't in default state, reset it
// to that now. This is what ensures that if, for example, OnScreenButton is
// disabled after OnPointerDown, we reset its button control to zero even
// though we will not see an OnPointerUp.
if (!m_Control.CheckStateIsAtDefault())
SentDefaultValueToControl();
}
m_Control = null;
m_InputEventPtr = new InputEventPtr();
Debug.Assert(m_NextControlOnDevice == null);
break;
}
}
private struct OnScreenDeviceInfo
{
public InputEventPtr eventPtr;
public NativeArray<byte> buffer;
public InputDevice device;
public OnScreenControl firstControl;
public OnScreenDeviceInfo AddControl(OnScreenControl control)
{
control.m_NextControlOnDevice = firstControl;
firstControl = control;
return this;
}
public OnScreenDeviceInfo RemoveControl(OnScreenControl control)
{
if (firstControl == control)
firstControl = control.m_NextControlOnDevice;
else
{
for (OnScreenControl current = firstControl.m_NextControlOnDevice, previous = firstControl;
current != null; previous = current, current = current.m_NextControlOnDevice)
{
if (current != control)
continue;
previous.m_NextControlOnDevice = current.m_NextControlOnDevice;
break;
}
}
control.m_NextControlOnDevice = null;
return this;
}
public void Destroy()
{
if (buffer.IsCreated)
buffer.Dispose();
if (device != null)
InputSystem.RemoveDevice(device);
device = null;
buffer = new NativeArray<byte>();
}
}
private static InlinedArray<OnScreenDeviceInfo> s_OnScreenDevices;
internal string GetWarningMessage()
{
return $"{GetType()} needs to be attached as a child to a UI Canvas and have a RectTransform component to function properly.";
}
}
internal static class UGUIOnScreenControlUtils
{
public static RectTransform GetCanvasRectTransform(Transform transform)
{
var parentTransform = transform.parent;
return parentTransform != null ? transform.parent.GetComponentInParent<RectTransform>() : null;
}
}
#if UNITY_EDITOR
internal static class UGUIOnScreenControlEditorUtils
{
public static void ShowWarningIfNotPartOfCanvasHierarchy(OnScreenControl target)
{
if (UGUIOnScreenControlUtils.GetCanvasRectTransform(target.transform) == null)
UnityEditor.EditorGUILayout.HelpBox(target.GetWarningMessage(), UnityEditor.MessageType.Warning);
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92ae37f8da4c25141a0c43e136275e46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,596 @@
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UI;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AnimatedValues;
using UnityEngine.InputSystem.Editor;
#endif
////TODO: custom icon for OnScreenStick component
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// A stick control displayed on screen and moved around by touch or other pointer
/// input.
/// </summary>
/// <remarks>
/// The <see cref="OnScreenStick"/> works by simulating events from the device specified in the <see cref="OnScreenControl.controlPath"/>
/// property. Some parts of the Input System, such as the <see cref="PlayerInput"/> component, can be set up to
/// auto-switch <see cref="PlayerInput.neverAutoSwitchControlSchemes"/> to a new device when input from them is detected.
/// When a device is switched, any currently running inputs from the previously active device are cancelled.
/// In the case of <see cref="OnScreenStick"/>, this can mean that the <see cref="IPointerUpHandler.OnPointerUp"/> method will be called
/// and the stick will jump back to center, even though the pointer input has not physically been released.
///
/// To avoid this situation, set the <see cref="useIsolatedInputActions"/> property to true. This will create a set of local
/// Input Actions to drive the stick that are not cancelled when device switching occurs.
/// You might also need to ensure, depending on your case, that the Mouse, Pen, Touchsceen and/or XRController devices are not used in a concurent
/// control schemes of the simulated device.
/// </remarks>
[AddComponentMenu("Input/On-Screen Stick")]
[HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-sticks")]
public class OnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
private const string kDynamicOriginClickable = "DynamicOriginClickable";
/// <summary>
/// Callback to handle OnPointerDown UI events.
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
if (eventData == null)
throw new System.ArgumentNullException(nameof(eventData));
BeginInteraction(eventData.position, eventData.pressEventCamera);
}
/// <summary>
/// Callback to handle OnDrag UI events.
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
if (eventData == null)
throw new System.ArgumentNullException(nameof(eventData));
MoveStick(eventData.position, eventData.pressEventCamera);
}
/// <summary>
/// Callback to handle OnPointerUp UI events.
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
EndInteraction();
}
private void Start()
{
if (m_UseIsolatedInputActions)
{
// avoid allocations every time the pointer down event fires by allocating these here
// and re-using them
m_RaycastResults = new List<RaycastResult>();
m_PointerEventData = new PointerEventData(EventSystem.current);
// if the pointer actions have no bindings (the default), add some
if (m_PointerDownAction == null || m_PointerDownAction.bindings.Count == 0)
{
if (m_PointerDownAction == null)
m_PointerDownAction = new InputAction(type: InputActionType.PassThrough);
// ensure PassThrough mode
else if (m_PointerDownAction.m_Type != InputActionType.PassThrough)
m_PointerDownAction.m_Type = InputActionType.PassThrough;
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = true;
#endif
m_PointerDownAction.AddBinding("<Mouse>/leftButton");
m_PointerDownAction.AddBinding("<Pen>/tip");
m_PointerDownAction.AddBinding("<Touchscreen>/touch*/press");
m_PointerDownAction.AddBinding("<XRController>/trigger");
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = false;
#endif
}
if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0)
{
if (m_PointerMoveAction == null)
m_PointerMoveAction = new InputAction();
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = true;
#endif
m_PointerMoveAction.AddBinding("<Mouse>/position");
m_PointerMoveAction.AddBinding("<Pen>/position");
m_PointerMoveAction.AddBinding("<Touchscreen>/touch*/position");
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = false;
#endif
}
m_PointerDownAction.performed += OnPointerChanged;
m_PointerDownAction.Enable();
m_PointerMoveAction.Enable();
}
// Unable to setup elements according to settings if a RectTransform is not available (ISXB-915, ISXB-916).
if (!(transform is RectTransform))
return;
m_StartPos = ((RectTransform)transform).anchoredPosition;
if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
m_PointerDownPos = m_StartPos;
var dynamicOrigin = new GameObject(kDynamicOriginClickable, typeof(Image));
dynamicOrigin.transform.SetParent(transform);
var image = dynamicOrigin.GetComponent<Image>();
image.color = new Color(1, 1, 1, 0);
var rectTransform = (RectTransform)dynamicOrigin.transform;
rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
rectTransform.localScale = new Vector3(1, 1, 0);
rectTransform.anchoredPosition3D = Vector3.zero;
image.sprite = SpriteUtilities.CreateCircleSprite(16, new Color32(255, 255, 255, 255));
image.alphaHitTestMinimumThreshold = 0.5f;
}
private void OnDestroy()
{
if (m_UseIsolatedInputActions)
{
m_PointerDownAction.performed -= OnPointerChanged;
}
}
private void BeginInteraction(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRectTransform = UGUIOnScreenControlUtils.GetCanvasRectTransform(transform);
if (canvasRectTransform == null)
{
Debug.LogError(GetWarningMessage());
return;
}
switch (m_Behaviour)
{
case Behaviour.RelativePositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out m_PointerDownPos);
break;
case Behaviour.ExactPositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out m_PointerDownPos);
MoveStick(pointerPosition, uiCamera);
break;
case Behaviour.ExactPositionWithDynamicOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out var pointerDown);
m_PointerDownPos = ((RectTransform)transform).anchoredPosition = pointerDown;
break;
}
}
private void MoveStick(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRectTransform = UGUIOnScreenControlUtils.GetCanvasRectTransform(transform);
if (canvasRectTransform == null)
{
Debug.LogError(GetWarningMessage());
return;
}
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out var position);
var delta = position - m_PointerDownPos;
switch (m_Behaviour)
{
case Behaviour.RelativePositionWithStaticOrigin:
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
break;
case Behaviour.ExactPositionWithStaticOrigin:
delta = position - (Vector2)m_StartPos;
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
break;
case Behaviour.ExactPositionWithDynamicOrigin:
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = m_PointerDownPos + delta;
break;
}
var newPos = new Vector2(delta.x / movementRange, delta.y / movementRange);
SendValueToControl(newPos);
}
private void EndInteraction()
{
((RectTransform)transform).anchoredPosition = m_PointerDownPos = m_StartPos;
SendValueToControl(Vector2.zero);
}
private void OnPointerDown(InputAction.CallbackContext ctx)
{
if (m_IsIsolationActive) { return; }
Debug.Assert(EventSystem.current != null);
var screenPosition = Vector2.zero;
TouchControl touchControl = null;
if (ctx.control?.parent is TouchControl touch)
{
touchControl = touch;
screenPosition = touch.position.ReadValue();
}
else if (ctx.control?.device is Pointer pointer)
{
screenPosition = pointer.position.ReadValue();
}
m_PointerEventData.position = screenPosition;
EventSystem.current.RaycastAll(m_PointerEventData, m_RaycastResults);
if (m_RaycastResults.Count == 0)
return;
var stickSelected = false;
foreach (var result in m_RaycastResults)
{
if (result.gameObject != gameObject) continue;
stickSelected = true;
break;
}
if (!stickSelected)
return;
BeginInteraction(screenPosition, GetCameraFromCanvas());
if (touchControl != null)
{
m_TouchControl = touchControl;
m_PointerMoveAction.ApplyBindingOverride($"{touchControl.path}/position", path: "<Touchscreen>/touch*/position");
}
m_PointerMoveAction.performed += OnPointerMove;
m_IsIsolationActive = true;
}
private void OnPointerChanged(InputAction.CallbackContext ctx)
{
if (ctx.control.IsPressed())
OnPointerDown(ctx);
else
OnPointerUp(ctx);
}
private void OnPointerMove(InputAction.CallbackContext ctx)
{
// only pointer devices are allowed
Debug.Assert(ctx.control?.device is Pointer);
Vector2 screenPosition;
// If it's a finger take the value from the finger that initiated the change
if (m_TouchControl != null)
{
// if the finger is up ignore the move
if (m_TouchControl.isInProgress == false)
{
return;
}
screenPosition = m_TouchControl.position.ReadValue();
}
else
{
screenPosition = ((Pointer)ctx.control.device).position.ReadValue();
}
MoveStick(screenPosition, GetCameraFromCanvas());
}
private void OnPointerUp(InputAction.CallbackContext ctx)
{
if (!m_IsIsolationActive) return;
// if it's a finger ensure that is the one that get released
if (m_TouchControl != null)
{
if (m_TouchControl.isInProgress) return;
m_PointerMoveAction.ApplyBindingOverride(null, path: "<Touchscreen>/touch*/position");
m_TouchControl = null;
}
EndInteraction();
m_PointerMoveAction.performed -= OnPointerMove;
m_IsIsolationActive = false;
}
private Camera GetCameraFromCanvas()
{
var canvas = GetComponentInParent<Canvas>();
var renderMode = canvas?.renderMode;
if (renderMode == RenderMode.ScreenSpaceOverlay
|| (renderMode == RenderMode.ScreenSpaceCamera && canvas?.worldCamera == null))
return null;
return canvas?.worldCamera ?? Camera.main;
}
private void OnDrawGizmosSelected()
{
// This will not produce meaningful results unless we have a rect transform (ISXB-915, ISXB-916).
var parentRectTransform = transform.parent as RectTransform;
if (parentRectTransform == null)
return;
Gizmos.matrix = parentRectTransform.localToWorldMatrix;
var startPos = parentRectTransform.anchoredPosition;
if (Application.isPlaying)
startPos = m_StartPos;
Gizmos.color = new Color32(84, 173, 219, 255);
var center = startPos;
if (Application.isPlaying && m_Behaviour == Behaviour.ExactPositionWithDynamicOrigin)
center = m_PointerDownPos;
DrawGizmoCircle(center, m_MovementRange);
if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
Gizmos.color = new Color32(158, 84, 219, 255);
DrawGizmoCircle(startPos, m_DynamicOriginRange);
}
private void DrawGizmoCircle(Vector2 center, float radius)
{
for (var i = 0; i < 32; i++)
{
var radians = i / 32f * Mathf.PI * 2;
var nextRadian = (i + 1) / 32f * Mathf.PI * 2;
Gizmos.DrawLine(
new Vector3(center.x + Mathf.Cos(radians) * radius, center.y + Mathf.Sin(radians) * radius, 0),
new Vector3(center.x + Mathf.Cos(nextRadian) * radius, center.y + Mathf.Sin(nextRadian) * radius, 0));
}
}
private void UpdateDynamicOriginClickableArea()
{
var dynamicOriginTransform = transform.Find(kDynamicOriginClickable);
if (dynamicOriginTransform)
{
var rectTransform = (RectTransform)dynamicOriginTransform;
rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
}
}
/// <summary>
/// The distance from the onscreen control's center of origin, around which the control can move.
/// </summary>
public float movementRange
{
get => m_MovementRange;
set => m_MovementRange = value;
}
/// <summary>
/// Defines the circular region where the onscreen control may have it's origin placed.
/// </summary>
/// <remarks>
/// This only applies if <see cref="behaviour"/> is set to <see cref="Behaviour.ExactPositionWithDynamicOrigin"/>.
/// When the first press is within this region, then the control will appear at that position and have it's origin of motion placed there.
/// Otherwise, if pressed outside of this region the control will ignore it.
/// This property defines the radius of the circular region. The center point being defined by the component position in the scene.
/// </remarks>
public float dynamicOriginRange
{
get => m_DynamicOriginRange;
set
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (m_DynamicOriginRange != value)
{
m_DynamicOriginRange = value;
UpdateDynamicOriginClickableArea();
}
}
}
/// <summary>
/// Prevents stick interactions from getting cancelled due to device switching.
/// </summary>
/// <remarks>
/// This property is useful for scenarios where the active device switches automatically
/// based on the most recently actuated device. A common situation where this happens is
/// when using a <see cref="PlayerInput"/> component with Auto-switch set to true. Imagine
/// a mobile game where an on-screen stick simulates the left stick of a gamepad device.
/// When the on-screen stick is moved, the Input System will see an input event from a gamepad
/// and switch the active device to it. This causes any active actions to be cancelled, including
/// the pointer action driving the on screen stick, which results in the stick jumping back to
/// the center as though it had been released.
///
/// In isolated mode, the actions driving the stick are not cancelled because they are
/// unique Input Action instances that don't share state with any others.
/// </remarks>
public bool useIsolatedInputActions
{
get => m_UseIsolatedInputActions;
set => m_UseIsolatedInputActions = value;
}
[FormerlySerializedAs("movementRange")]
[SerializeField]
[Min(0)]
private float m_MovementRange = 50;
[SerializeField]
[Tooltip("Defines the circular region where the onscreen control may have it's origin placed.")]
[Min(0)]
private float m_DynamicOriginRange = 100;
[InputControl(layout = "Vector2")]
[SerializeField]
private string m_ControlPath;
[SerializeField]
[Tooltip("Choose how the onscreen stick will move relative to it's origin and the press position.\n\n" +
"RelativePositionWithStaticOrigin: The control's center of origin is fixed. " +
"The control will begin un-actuated at it's centered position and then move relative to the pointer or finger motion.\n\n" +
"ExactPositionWithStaticOrigin: The control's center of origin is fixed. The stick will immediately jump to the " +
"exact position of the click or touch and begin tracking motion from there.\n\n" +
"ExactPositionWithDynamicOrigin: The control's center of origin is determined by the initial press position. " +
"The stick will begin un-actuated at this center position and then track the current pointer or finger position.")]
private Behaviour m_Behaviour;
[SerializeField]
[Tooltip("Set this to true to prevent cancellation of pointer events due to device switching. Cancellation " +
"will appear as the stick jumping back and forth between the pointer position and the stick center.")]
private bool m_UseIsolatedInputActions;
[SerializeField]
[Tooltip("The action that will be used to detect pointer down events on the stick control. Note that if no bindings " +
"are set, default ones will be provided.")]
private InputAction m_PointerDownAction;
[SerializeField]
[Tooltip("The action that will be used to detect pointer movement on the stick control. Note that if no bindings " +
"are set, default ones will be provided.")]
private InputAction m_PointerMoveAction;
private Vector3 m_StartPos;
private Vector2 m_PointerDownPos;
[NonSerialized]
private List<RaycastResult> m_RaycastResults;
[NonSerialized]
private PointerEventData m_PointerEventData;
[NonSerialized]
private TouchControl m_TouchControl;
[NonSerialized]
private bool m_IsIsolationActive;
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
/// <summary>Defines how the onscreen stick will move relative to it's origin and the press position.</summary>
public Behaviour behaviour
{
get => m_Behaviour;
set => m_Behaviour = value;
}
/// <summary>Defines how the onscreen stick will move relative to it's center of origin and the press position.</summary>
public enum Behaviour
{
/// <summary>The control's center of origin is fixed in the scene.
/// The control will begin un-actuated at it's centered position and then move relative to the press motion.</summary>
RelativePositionWithStaticOrigin,
/// <summary>The control's center of origin is fixed in the scene.
/// The control may begin from an actuated position to ensure it is always tracking the current press position.</summary>
ExactPositionWithStaticOrigin,
/// <summary>The control's center of origin is determined by the initial press position.
/// The control will begin unactuated at this center position and then track the current press position.</summary>
ExactPositionWithDynamicOrigin
}
#if UNITY_EDITOR
[CustomEditor(typeof(OnScreenStick))]
internal class OnScreenStickEditor : UnityEditor.Editor
{
private AnimBool m_ShowDynamicOriginOptions;
private AnimBool m_ShowIsolatedInputActions;
private SerializedProperty m_UseIsolatedInputActions;
private SerializedProperty m_Behaviour;
private SerializedProperty m_ControlPathInternal;
private SerializedProperty m_MovementRange;
private SerializedProperty m_DynamicOriginRange;
private SerializedProperty m_PointerDownAction;
private SerializedProperty m_PointerMoveAction;
public void OnEnable()
{
m_ShowDynamicOriginOptions = new AnimBool(false);
m_ShowIsolatedInputActions = new AnimBool(false);
m_UseIsolatedInputActions = serializedObject.FindProperty(nameof(OnScreenStick.m_UseIsolatedInputActions));
m_Behaviour = serializedObject.FindProperty(nameof(OnScreenStick.m_Behaviour));
m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenStick.m_ControlPath));
m_MovementRange = serializedObject.FindProperty(nameof(OnScreenStick.m_MovementRange));
m_DynamicOriginRange = serializedObject.FindProperty(nameof(OnScreenStick.m_DynamicOriginRange));
m_PointerDownAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerDownAction));
m_PointerMoveAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerMoveAction));
}
public void OnDisable()
{
// Report analytics
new InputComponentEditorAnalytic(InputSystemComponent.OnScreenStick).Send();
new OnScreenStickEditorAnalytic(this).Send();
}
public override void OnInspectorGUI()
{
// Current implementation has UGUI dependencies (ISXB-915, ISXB-916)
UGUIOnScreenControlEditorUtils.ShowWarningIfNotPartOfCanvasHierarchy((OnScreenStick)target);
EditorGUILayout.PropertyField(m_MovementRange);
EditorGUILayout.PropertyField(m_ControlPathInternal);
EditorGUILayout.PropertyField(m_Behaviour);
m_ShowDynamicOriginOptions.target = ((OnScreenStick)target).behaviour ==
Behaviour.ExactPositionWithDynamicOrigin;
if (EditorGUILayout.BeginFadeGroup(m_ShowDynamicOriginOptions.faded))
{
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_DynamicOriginRange);
if (EditorGUI.EndChangeCheck())
{
((OnScreenStick)target).UpdateDynamicOriginClickableArea();
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFadeGroup();
EditorGUILayout.PropertyField(m_UseIsolatedInputActions);
m_ShowIsolatedInputActions.target = m_UseIsolatedInputActions.boolValue;
if (EditorGUILayout.BeginFadeGroup(m_ShowIsolatedInputActions.faded))
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_PointerDownAction);
EditorGUILayout.PropertyField(m_PointerMoveAction);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFadeGroup();
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9d677f1681015749b15c436eec6d880
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
#if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS || UNITY_WSA || UNITY_VISIONOS
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// Support for various forms of on-screen controls.
/// </summary>
/// <remarks>
/// On-screen input visually represents control elements either through (potentially) built-in
/// mechanisms like <see cref="OnScreenKeyboard"/> or through manually arranged control setups
/// in the form of <see cref="OnScreenControl">OnScreenControls</see>.
/// </remarks>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class OnScreenSupport
{
public static void Initialize()
{
////TODO: OnScreenKeyboard support
//InputSystem.RegisterLayout<OnScreenKeyboard>();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 74405005f3013c2498103a79211ba93b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1cccd07fcea7f491793d4e7a5adc17a2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc69a9a42102dea4dafad4147e3ed98e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,839 @@
{
"version": 1,
"name": "DefaultInputActions",
"maps": [
{
"name": "Player",
"id": "df70fa95-8a34-4494-b137-73ab6b9c7d37",
"actions": [
{
"name": "Move",
"type": "Value",
"id": "351f2ccd-1f9f-44bf-9bec-d62ac5c5f408",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Look",
"type": "Value",
"id": "6b444451-8a00-4d00-a97e-f47457f736a8",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Fire",
"type": "Button",
"id": "6c2ab1b8-8984-453a-af3d-a3c78ae1679a",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "978bfe49-cc26-4a3d-ab7b-7d7a29327403",
"path": "<Gamepad>/leftStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "WASD",
"id": "00ca640b-d935-4593-8157-c05846ea39b3",
"path": "Dpad",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "e2062cb9-1b15-46a2-838c-2f8d72a0bdd9",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "8180e8bd-4097-4f4e-ab88-4523101a6ce9",
"path": "<Keyboard>/upArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "320bffee-a40b-4347-ac70-c210eb8bc73a",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "1c5327b5-f71c-4f60-99c7-4e737386f1d1",
"path": "<Keyboard>/downArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "d2581a9b-1d11-4566-b27d-b92aff5fabbc",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "2e46982e-44cc-431b-9f0b-c11910bf467a",
"path": "<Keyboard>/leftArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "fcfe95b8-67b9-4526-84b5-5d0bc98d6400",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "77bff152-3580-4b21-b6de-dcd0c7e41164",
"path": "<Keyboard>/rightArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "1635d3fe-58b6-4ba9-a4e2-f4b964f6b5c8",
"path": "<XRController>/{Primary2DAxis}",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "3ea4d645-4504-4529-b061-ab81934c3752",
"path": "<Joystick>/stick",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "c1f7a91b-d0fd-4a62-997e-7fb9b69bf235",
"path": "<Gamepad>/rightStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8c8e490b-c610-4785-884f-f04217b23ca4",
"path": "<Pointer>/delta",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse;Touch",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "3e5f5442-8668-4b27-a940-df99bad7e831",
"path": "<Joystick>/{Hatswitch}",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "143bb1cd-cc10-4eca-a2f0-a3664166fe91",
"path": "<Gamepad>/rightTrigger",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "05f6913d-c316-48b2-a6bb-e225f14c7960",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "886e731e-7071-4ae4-95c0-e61739dad6fd",
"path": "<Touchscreen>/primaryTouch/tap",
"interactions": "",
"processors": "",
"groups": ";Touch",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "ee3d0cd2-254e-47a7-a8cb-bc94d9658c54",
"path": "<Joystick>/trigger",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8255d333-5683-4943-a58a-ccb207ff1dce",
"path": "<XRController>/{PrimaryAction}",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
}
]
},
{
"name": "UI",
"id": "272f6d14-89ba-496f-b7ff-215263d3219f",
"actions": [
{
"name": "Navigate",
"type": "PassThrough",
"id": "c95b2375-e6d9-4b88-9c4c-c5e76515df4b",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Submit",
"type": "Button",
"id": "7607c7b6-cd76-4816-beef-bd0341cfe950",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Cancel",
"type": "Button",
"id": "15cef263-9014-4fd5-94d9-4e4a6234a6ef",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Point",
"type": "PassThrough",
"id": "32b35790-4ed0-4e9a-aa41-69ac6d629449",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Click",
"type": "PassThrough",
"id": "3c7022bf-7922-4f7c-a998-c437916075ad",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "ScrollWheel",
"type": "PassThrough",
"id": "0489e84a-4833-4c40-bfae-cea84b696689",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "MiddleClick",
"type": "PassThrough",
"id": "dad70c86-b58c-4b17-88ad-f5e53adf419e",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "RightClick",
"type": "PassThrough",
"id": "44b200b1-1557-4083-816c-b22cbdf77ddf",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "TrackedDevicePosition",
"type": "PassThrough",
"id": "24908448-c609-4bc3-a128-ea258674378a",
"expectedControlType": "Vector3",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "TrackedDeviceOrientation",
"type": "PassThrough",
"id": "9caa3d8a-6b2f-4e8e-8bad-6ede561bd9be",
"expectedControlType": "Quaternion",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "Gamepad",
"id": "809f371f-c5e2-4e7a-83a1-d867598f40dd",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "14a5d6e8-4aaf-4119-a9ef-34b8c2c548bf",
"path": "<Gamepad>/leftStick/up",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "9144cbe6-05e1-4687-a6d7-24f99d23dd81",
"path": "<Gamepad>/rightStick/up",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2db08d65-c5fb-421b-983f-c71163608d67",
"path": "<Gamepad>/leftStick/down",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "58748904-2ea9-4a80-8579-b500e6a76df8",
"path": "<Gamepad>/rightStick/down",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "8ba04515-75aa-45de-966d-393d9bbd1c14",
"path": "<Gamepad>/leftStick/left",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "712e721c-bdfb-4b23-a86c-a0d9fcfea921",
"path": "<Gamepad>/rightStick/left",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "fcd248ae-a788-4676-a12e-f4d81205600b",
"path": "<Gamepad>/leftStick/right",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "1f04d9bc-c50b-41a1-bfcc-afb75475ec20",
"path": "<Gamepad>/rightStick/right",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "fb8277d4-c5cd-4663-9dc7-ee3f0b506d90",
"path": "<Gamepad>/dpad",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Joystick",
"id": "e25d9774-381c-4a61-b47c-7b6b299ad9f9",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "3db53b26-6601-41be-9887-63ac74e79d19",
"path": "<Joystick>/stick/up",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "0cb3e13e-3d90-4178-8ae6-d9c5501d653f",
"path": "<Joystick>/stick/down",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "0392d399-f6dd-4c82-8062-c1e9c0d34835",
"path": "<Joystick>/stick/left",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "942a66d9-d42f-43d6-8d70-ecb4ba5363bc",
"path": "<Joystick>/stick/right",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "Keyboard",
"id": "ff527021-f211-4c02-933e-5976594c46ed",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "563fbfdd-0f09-408d-aa75-8642c4f08ef0",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "eb480147-c587-4a33-85ed-eb0ab9942c43",
"path": "<Keyboard>/upArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2bf42165-60bc-42ca-8072-8c13ab40239b",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "85d264ad-e0a0-4565-b7ff-1a37edde51ac",
"path": "<Keyboard>/downArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "74214943-c580-44e4-98eb-ad7eebe17902",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "cea9b045-a000-445b-95b8-0c171af70a3b",
"path": "<Keyboard>/leftArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "8607c725-d935-4808-84b1-8354e29bab63",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "4cda81dc-9edd-4e03-9d7c-a71a14345d0b",
"path": "<Keyboard>/rightArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "9e92bb26-7e3b-4ec4-b06b-3c8f8e498ddc",
"path": "*/{Submit}",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR",
"action": "Submit",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "82627dcc-3b13-4ba9-841d-e4b746d6553e",
"path": "*/{Cancel}",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR",
"action": "Cancel",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "c52c8e0b-8179-41d3-b8a1-d149033bbe86",
"path": "<Mouse>/position",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "e1394cbc-336e-44ce-9ea8-6007ed6193f7",
"path": "<Pen>/position",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "5693e57a-238a-46ed-b5ae-e64e6e574302",
"path": "<Touchscreen>/touch*/position",
"interactions": "",
"processors": "",
"groups": "Touch",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "4faf7dc9-b979-4210-aa8c-e808e1ef89f5",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8d66d5ba-88d7-48e6-b1cd-198bbfef7ace",
"path": "<Pen>/tip",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "47c2a644-3ebc-4dae-a106-589b7ca75b59",
"path": "<Touchscreen>/touch*/press",
"interactions": "",
"processors": "",
"groups": "Touch",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "bb9e6b34-44bf-4381-ac63-5aa15d19f677",
"path": "<XRController>/trigger",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "38c99815-14ea-4617-8627-164d27641299",
"path": "<Mouse>/scroll",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "ScrollWheel",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "24066f69-da47-44f3-a07e-0015fb02eb2e",
"path": "<Mouse>/middleButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "MiddleClick",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "4c191405-5738-4d4b-a523-c6a301dbf754",
"path": "<Mouse>/rightButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "RightClick",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "7236c0d9-6ca3-47cf-a6ee-a97f5b59ea77",
"path": "<XRController>/devicePosition",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "TrackedDevicePosition",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "23e01e3a-f935-4948-8d8b-9bcac77714fb",
"path": "<XRController>/deviceRotation",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "TrackedDeviceOrientation",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": [
{
"name": "Keyboard&Mouse",
"bindingGroup": "Keyboard&Mouse",
"devices": [
{
"devicePath": "<Keyboard>",
"isOptional": false,
"isOR": false
},
{
"devicePath": "<Mouse>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Gamepad",
"bindingGroup": "Gamepad",
"devices": [
{
"devicePath": "<Gamepad>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Touch",
"bindingGroup": "Touch",
"devices": [
{
"devicePath": "<Touchscreen>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Joystick",
"bindingGroup": "Joystick",
"devices": [
{
"devicePath": "<Joystick>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "XR",
"bindingGroup": "XR",
"devices": [
{
"devicePath": "<XRController>",
"isOptional": false,
"isOR": false
}
]
}
]
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: ca9f5fa95ffab41fb9a615ab714db018
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 0
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace: UnityEngine.InputSystem

View File

@@ -0,0 +1,119 @@
using System;
using System.Diagnostics;
using UnityEngine.InputSystem.Controls;
////TODO: API to get the control and device from the internal context
////TODO: ToString()
namespace UnityEngine.InputSystem
{
/// <summary>
/// Wraps around values provided by input actions.
/// </summary>
/// <remarks>
/// This is a wrapper around <see cref="InputAction.CallbackContext"/> chiefly for use
/// with GameObject messages (i.e. <see cref="GameObject.SendMessage(string,object)"/>). It exists
/// so that action callback data can be represented as an object, can be reused, and shields
/// the receiver from having to know about action callback specifics.
/// </remarks>
/// <seealso cref="InputAction"/>
[DebuggerDisplay("Value = {Get()}")]
public class InputValue
{
/// <summary>
/// Read the current value as an object.
/// </summary>
/// <remarks>
/// This method allocates GC memory and will thus create garbage. If used during gameplay,
/// it will lead to GC spikes.
/// </remarks>
/// <returns>The current value in the form of a boxed object.</returns>
public object Get()
{
return m_Context.Value.ReadValueAsObject();
}
////TODO: add automatic conversions
/// <summary>
/// Read the current value of the action.
/// </summary>
/// <returns>The current value from the action cast to the specified type.</returns>
/// <typeparam name="TValue">Type of value to read. This must correspond to the
/// <see cref="InputControl.valueType"/> of the action or, if it is a composite, by the
/// <see cref="InputBindingComposite.valueType"/>.
/// The type depends on what type of controls the action is bound to.
/// Common types are <c>float</c> and <see cref="UnityEngine.Vector2"/></typeparam>
/// <exception cref="InvalidOperationException">The given type <typeparamref name="TValue"/>
/// does not match the value type expected by the control or binding composite.</exception>
/// <remarks>
/// The following example shows how to read a value from a <see cref="PlayerInput"/> message.
/// The given <c>InputValue</c> is only valid for the duration of the callback. Storing the <c>InputValue</c> references somewhere and calling Get&lt;T&gt;() later does not work correctly.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem;
/// [RequireComponent(typeof(PlayerInput))]
/// public class MyPlayerLogic : MonoBehaviour
/// {
/// private Vector2 m_Move;
///
/// // 'Move' input action has been triggered.
/// public void OnMove(InputValue value)
/// {
/// // Read value from control. The type depends on what type of controls the action is bound to.
/// m_Move = value.Get&lt;Vector2&gt;();
/// }
///
/// public void Update()
/// {
/// // Update transform from m_Move
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="InputAction.CallbackContext.ReadValue{TValue}"/>
public TValue Get<TValue>()
where TValue : struct
{
if (!m_Context.HasValue)
throw new InvalidOperationException($"Values can only be retrieved while in message callbacks");
return m_Context.Value.ReadValue<TValue>();
}
////TODO: proper message if value type isn't right
/// <summary>
/// Check if the action button is pressed.
/// </summary>
/// <remarks>
/// True if the button is activated over the button threshold. False otherwise
/// The following example check if a button is pressed when receiving a <see cref="PlayerInput"/> message.
/// The given <c>InputValue</c> is only valid for the duration of the callback. Storing the <c>InputValue</c> references somewhere and calling Get&lt;T&gt;() later does not work correctly.
/// </remarks>
/// <example>
/// <code>
/// [RequireComponent(typeof(PlayerInput))]
/// public class MyPlayerLogic : MonoBehaviour
/// {
/// // 'Fire' input action has been triggered.
/// public void OnFire(InputValue value)
/// {
/// if (value.isPressed)
/// FireWeapon();
/// }
///
/// public void FireWeapon()
/// {
/// // Weapon firing code
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="ButtonControl.pressPointOrDefault"/>
public bool isPressed => Get<float>() >= ButtonControl.s_GlobalDefaultButtonPressPoint;
internal InputAction.CallbackContext? m_Context;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b24da9e286b594ace8ec4dfd4a374780
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62899f850307741f2a39c98a8b639597
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -100
icon: {fileID: 2800000, guid: 40265a896a0f341bb956e90c59025a29, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,653 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
#if UNITY_INPUT_SYSTEM_ENABLE_UI
using UnityEngine.InputSystem.UI;
using UnityEngine.InputSystem.UI.Editor;
#endif
////TODO: detect if new input system isn't enabled and provide UI to enable it
#pragma warning disable 0414
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A custom inspector for the <see cref="PlayerInput"/> component.
/// </summary>
[CustomEditor(typeof(PlayerInput))]
internal class PlayerInputEditor : UnityEditor.Editor
{
public const string kDefaultInputActionsAssetPath =
"Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions";
public void OnEnable()
{
InputActionImporter.onImport += Refresh;
InputUser.onChange += OnUserChange;
// Look up properties.
m_ActionsProperty = serializedObject.FindProperty(nameof(PlayerInput.m_Actions));
m_DefaultControlSchemeProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DefaultControlScheme));
m_NeverAutoSwitchControlSchemesProperty = serializedObject.FindProperty(nameof(PlayerInput.m_NeverAutoSwitchControlSchemes));
m_DefaultActionMapProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DefaultActionMap));
m_NotificationBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior));
m_CameraProperty = serializedObject.FindProperty(nameof(PlayerInput.m_Camera));
m_ActionEventsProperty = serializedObject.FindProperty(nameof(PlayerInput.m_ActionEvents));
m_DeviceLostEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DeviceLostEvent));
m_DeviceRegainedEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DeviceRegainedEvent));
m_ControlsChangedEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_ControlsChangedEvent));
#if UNITY_INPUT_SYSTEM_ENABLE_UI
m_UIInputModuleProperty = serializedObject.FindProperty(nameof(PlayerInput.m_UIInputModule));
#endif
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.PlayerInput).Send();
new PlayerInputEditorAnalytic(this).Send();
}
public void OnDestroy()
{
InputActionImporter.onImport -= Refresh;
InputUser.onChange -= OnUserChange;
}
private void Refresh()
{
////FIXME: doesn't seem like we're picking up the results of the latest import
m_ActionAssetInitialized = false;
Repaint();
}
private void OnUserChange(InputUser user, InputUserChange change, InputDevice device)
{
Repaint();
}
public override void OnInspectorGUI()
{
EditorGUI.BeginChangeCheck();
// Action config section.
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_ActionsProperty);
var actionsWereChanged = false;
// Check for if we're using project-wide actions to raise a warning message.
if (m_ActionsProperty.objectReferenceValue != null)
{
InputActionAsset actions = m_ActionsProperty.objectReferenceValue as InputActionAsset;
if (actions == InputSystem.actions)
{
EditorGUILayout.HelpBox("Project-wide actions asset is not recommended to be used with Player " +
"Input because it is a singleton reference and all actions maps are enabled by default.\r\n" +
"You should manually disable all action maps on Start() and " +
"manually enable the default action map.",
MessageType.Warning);
}
}
var assetChanged = CheckIfActionAssetChanged();
// initialize the editor component if the asset has changed or if it has not been initialized yet
if (EditorGUI.EndChangeCheck() || !m_ActionAssetInitialized || assetChanged || m_ActionAssetInstanceID == 0)
{
InitializeEditorComponent(assetChanged);
actionsWereChanged = true;
}
++EditorGUI.indentLevel;
if (m_ControlSchemeOptions != null && m_ControlSchemeOptions.Length > 1) // Don't show if <Any> is the only option.
{
// Default control scheme picker.
Color currentBg = GUI.backgroundColor;
// if the invalid DefaultControlSchemeName is selected set the popup draw the BG color in red
if (m_InvalidDefaultControlSchemeName != null && m_SelectedDefaultControlScheme == 1)
GUI.backgroundColor = Color.red;
var rect = EditorGUILayout.GetControlRect();
var label = EditorGUI.BeginProperty(rect, m_DefaultControlSchemeText, m_DefaultControlSchemeProperty);
var selected = EditorGUI.Popup(rect, label, m_SelectedDefaultControlScheme, m_ControlSchemeOptions);
EditorGUI.EndProperty();
if (selected != m_SelectedDefaultControlScheme)
{
if (selected == 0)
{
m_DefaultControlSchemeProperty.stringValue = null;
}
// if there is an invalid default scheme name it will be at rank 1.
// we use m_InvalidDefaultControlSchemeName to prevent usage of the string with "name<Not Found>"
else if (m_InvalidDefaultControlSchemeName != null && selected == 1)
{
m_DefaultControlSchemeProperty.stringValue = m_InvalidDefaultControlSchemeName;
}
else
{
m_DefaultControlSchemeProperty.stringValue = m_ControlSchemeOptions[selected].text;
}
m_SelectedDefaultControlScheme = selected;
}
// Restore the initial color
GUI.backgroundColor = currentBg;
rect = EditorGUILayout.GetControlRect();
label = EditorGUI.BeginProperty(rect, m_AutoSwitchText, m_NeverAutoSwitchControlSchemesProperty);
var neverAutoSwitchValueOld = m_NeverAutoSwitchControlSchemesProperty.boolValue;
var neverAutoSwitchValueNew = !EditorGUI.Toggle(rect, label, !neverAutoSwitchValueOld);
EditorGUI.EndProperty();
if (neverAutoSwitchValueOld != neverAutoSwitchValueNew)
{
m_NeverAutoSwitchControlSchemesProperty.boolValue = neverAutoSwitchValueNew;
serializedObject.ApplyModifiedProperties();
}
}
if (m_ActionMapOptions != null && m_ActionMapOptions.Length > 0)
{
// Default action map picker.
var rect = EditorGUILayout.GetControlRect();
var label = EditorGUI.BeginProperty(rect, m_DefaultActionMapText, m_DefaultActionMapProperty);
var selected = EditorGUI.Popup(rect, label, m_SelectedDefaultActionMap,
m_ActionMapOptions);
EditorGUI.EndProperty();
if (selected != m_SelectedDefaultActionMap)
{
if (selected == 0)
{
m_DefaultActionMapProperty.stringValue = null;
}
else
{
// Use ID rather than name.
var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue;
var actionMap = asset.FindActionMap(m_ActionMapOptions[selected].text);
if (actionMap != null)
m_DefaultActionMapProperty.stringValue = actionMap.id.ToString();
}
m_SelectedDefaultActionMap = selected;
}
}
--EditorGUI.indentLevel;
DoHelpCreateAssetUI();
#if UNITY_INPUT_SYSTEM_ENABLE_UI
// UI config section.
if (m_UIPropertyText == null)
m_UIPropertyText = EditorGUIUtility.TrTextContent("UI Input Module", m_UIInputModuleProperty.GetTooltip());
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_UIInputModuleProperty, m_UIPropertyText);
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
if (m_UIInputModuleProperty.objectReferenceValue != null)
{
var uiModule = m_UIInputModuleProperty.objectReferenceValue as InputSystemUIInputModule;
if (m_ActionsProperty.objectReferenceValue != null && uiModule.actionsAsset != m_ActionsProperty.objectReferenceValue)
{
EditorGUILayout.HelpBox("The referenced InputSystemUIInputModule is configured using different input actions than this PlayerInput. They should match if you want to synchronize PlayerInput actions to the UI input.", MessageType.Warning);
if (GUILayout.Button(m_FixInputModuleText))
InputSystemUIInputModuleEditor.ReassignActions(uiModule, m_ActionsProperty.objectReferenceValue as InputActionAsset);
}
}
#endif
// Camera section.
if (m_CameraPropertyText == null)
m_CameraPropertyText = EditorGUIUtility.TrTextContent("Camera", m_CameraProperty.GetTooltip());
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_CameraProperty, m_CameraPropertyText);
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
// Notifications/event section.
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_NotificationBehaviorProperty, m_NotificationBehaviorText);
if (EditorGUI.EndChangeCheck() || actionsWereChanged || !m_NotificationBehaviorInitialized)
OnNotificationBehaviorChange();
switch ((PlayerNotifications)m_NotificationBehaviorProperty.intValue)
{
case PlayerNotifications.SendMessages:
case PlayerNotifications.BroadcastMessages:
Debug.Assert(m_SendMessagesHelpText != null);
EditorGUILayout.HelpBox(m_SendMessagesHelpText);
break;
case PlayerNotifications.InvokeUnityEvents:
m_EventsGroupUnfolded = EditorGUILayout.Foldout(m_EventsGroupUnfolded, m_EventsGroupText, toggleOnLabelClick: true);
if (m_EventsGroupUnfolded)
{
// Action events. Group by action map.
if (m_ActionNames != null)
{
using (new EditorGUI.IndentLevelScope())
{
for (var n = 0; n < m_NumActionMaps; ++n)
{
// Skip action maps that have no names (case 1317735).
if (m_ActionMapNames[n] == null)
continue;
m_ActionMapEventsUnfolded[n] = EditorGUILayout.Foldout(m_ActionMapEventsUnfolded[n],
m_ActionMapNames[n], toggleOnLabelClick: true);
using (new EditorGUI.IndentLevelScope())
{
if (m_ActionMapEventsUnfolded[n])
{
for (var i = 0; i < m_ActionNames.Length; ++i)
{
if (m_ActionMapIndices[i] != n)
continue;
EditorGUILayout.PropertyField(m_ActionEventsProperty.GetArrayElementAtIndex(i), m_ActionNames[i]);
}
}
}
}
}
}
// Misc events.
EditorGUILayout.PropertyField(m_DeviceLostEventProperty);
EditorGUILayout.PropertyField(m_DeviceRegainedEventProperty);
EditorGUILayout.PropertyField(m_ControlsChangedEventProperty);
}
break;
}
// Miscellaneous buttons.
DoUtilityButtonsUI();
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
// Debug UI.
if (EditorApplication.isPlaying)
DoDebugUI();
}
// This checks changes that are not captured by BeginChangeCheck/EndChangeCheck.
// One such case is when the user triggers a "Reset" on the component.
bool CheckIfActionAssetChanged()
{
if (m_ActionsProperty.objectReferenceValue != null)
{
var assetInstanceID = m_ActionsProperty.objectReferenceValue.GetInstanceID();
// if the m_ActionAssetInstanceID is 0 the PlayerInputEditor has not been initialized yet, but the asset did not change
bool result = assetInstanceID != m_ActionAssetInstanceID && m_ActionAssetInstanceID != 0;
m_ActionAssetInstanceID = (int)assetInstanceID;
return result;
}
m_ActionAssetInstanceID = -1;
return false;
}
private void DoHelpCreateAssetUI()
{
if (m_ActionsProperty.objectReferenceValue != null)
{
// All good. We already have an asset.
return;
}
EditorGUILayout.HelpBox("There are no input actions associated with this input component yet. Click the button below to create "
+ "a new set of input actions or drag an existing input actions asset into the field above.", MessageType.Info);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button(m_CreateActionsText, EditorStyles.miniButton, GUILayout.MaxWidth(120)))
{
// Request save file location.
var defaultFileName = Application.productName;
var fileName = EditorUtility.SaveFilePanel("Create Input Actions Asset", "Assets", defaultFileName,
InputActionAsset.Extension);
////TODO: take current Supported Devices into account when creating this
// Create and import asset and open editor.
if (!string.IsNullOrEmpty(fileName))
{
if (!fileName.StartsWith(Application.dataPath))
{
Debug.LogError($"Path must be located in Assets/ folder (got: '{fileName}')");
EditorGUILayout.EndHorizontal();
return;
}
if (!fileName.EndsWith("." + InputActionAsset.Extension))
fileName += "." + InputActionAsset.Extension;
// Load default actions and update all GUIDs.
var defaultActionsText = File.ReadAllText(kDefaultInputActionsAssetPath);
var newActions = InputActionAsset.FromJson(defaultActionsText);
foreach (var map in newActions.actionMaps)
{
map.m_Id = Guid.NewGuid().ToString();
foreach (var action in map.actions)
action.m_Id = Guid.NewGuid().ToString();
}
newActions.name = Path.GetFileNameWithoutExtension(fileName);
var newActionsText = newActions.ToJson();
// Write it out and tell the asset DB to pick it up.
File.WriteAllText(fileName, newActionsText);
// Import the new asset
var relativePath = "Assets/" + fileName.Substring(Application.dataPath.Length + 1);
AssetDatabase.ImportAsset(relativePath, ImportAssetOptions.ForceSynchronousImport);
// Load imported object.
var importedObject = AssetDatabase.LoadAssetAtPath<InputActionAsset>(relativePath);
// Set it on the PlayerInput component.
m_ActionsProperty.objectReferenceValue = importedObject;
serializedObject.ApplyModifiedProperties();
// Open the asset.
AssetDatabase.OpenAsset(importedObject);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Separator();
}
private void DoUtilityButtonsUI()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button(m_OpenSettingsText, EditorStyles.miniButton))
InputSettingsProvider.Open();
if (GUILayout.Button(m_OpenDebuggerText, EditorStyles.miniButton))
InputDebuggerWindow.CreateOrShow();
EditorGUILayout.EndHorizontal();
}
private void DoDebugUI()
{
var playerInput = (PlayerInput)target;
if (!playerInput.user.valid)
return;
////TODO: show actions when they happen
var user = playerInput.user.index.ToString();
var controlScheme = playerInput.user.controlScheme?.name;
var devices = string.Join(", ", playerInput.user.pairedDevices);
EditorGUILayout.Space();
EditorGUILayout.LabelField(m_DebugText, EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.LabelField("User", user);
EditorGUILayout.LabelField("Control Scheme", controlScheme);
EditorGUILayout.LabelField("Devices", devices);
EditorGUI.EndDisabledGroup();
}
private void OnNotificationBehaviorChange()
{
Debug.Assert(m_ActionAssetInitialized);
serializedObject.ApplyModifiedProperties();
var notificationBehavior = (PlayerNotifications)m_NotificationBehaviorProperty.intValue;
switch (notificationBehavior)
{
// Create text that lists all the messages sent by the component.
case PlayerNotifications.BroadcastMessages:
case PlayerNotifications.SendMessages:
{
var builder = new StringBuilder();
builder.Append("Will ");
if (notificationBehavior == PlayerNotifications.BroadcastMessages)
builder.Append("BroadcastMessage()");
else
builder.Append("SendMessage()");
builder.Append(" to GameObject: ");
builder.Append(PlayerInput.DeviceLostMessage);
builder.Append(", ");
builder.Append(PlayerInput.DeviceRegainedMessage);
builder.Append(", ");
builder.Append(PlayerInput.ControlsChangedMessage);
var playerInput = (PlayerInput)target;
var asset = playerInput.m_Actions;
if (asset != null)
{
foreach (var action in asset)
{
builder.Append(", On");
builder.Append(CSharpCodeHelpers.MakeTypeName(action.name));
}
}
m_SendMessagesHelpText = new GUIContent(builder.ToString());
break;
}
case PlayerNotifications.InvokeUnityEvents:
{
var playerInput = (PlayerInput)target;
bool areEventsDirty = (playerInput.m_DeviceLostEvent == null) || (playerInput.m_DeviceRegainedEvent == null) || (playerInput.m_ControlsChangedEvent == null);
playerInput.m_DeviceLostEvent ??= new PlayerInput.DeviceLostEvent();
playerInput.m_DeviceRegainedEvent ??= new PlayerInput.DeviceRegainedEvent();
playerInput.m_ControlsChangedEvent ??= new PlayerInput.ControlsChangedEvent();
if (areEventsDirty)
{
serializedObject.Update();
// Force action refresh.
m_ActionAssetInitialized = false;
Refresh();
}
break;
}
}
m_NotificationBehaviorInitialized = true;
}
private void InitializeEditorComponent(bool assetChanged)
{
serializedObject.ApplyModifiedProperties();
m_ActionAssetInitialized = true;
var playerInput = (PlayerInput)target;
var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue;
if (assetChanged)
m_SelectedDefaultActionMap = -1;
if (asset == null)
{
m_ControlSchemeOptions = null;
m_ActionMapOptions = null;
m_ActionNames = null;
m_SelectedDefaultControlScheme = -1;
m_InvalidDefaultControlSchemeName = null;
return;
}
// If we're sending Unity events, read out the event list.
if ((PlayerNotifications)m_NotificationBehaviorProperty.intValue ==
PlayerNotifications.InvokeUnityEvents)
{
////FIXME: this should preserve the same order that we have in the asset
var newActionNames = new List<GUIContent>();
var newActionEvents = new List<PlayerInput.ActionEvent>();
var newActionMapIndices = new List<int>();
m_NumActionMaps = 0;
m_ActionMapNames = null;
void AddEntry(InputAction action, PlayerInput.ActionEvent actionEvent)
{
newActionNames.Add(new GUIContent(action.name));
newActionEvents.Add(actionEvent);
var actionMapIndex = asset.actionMaps.IndexOfReference(action.actionMap);
newActionMapIndices.Add(actionMapIndex);
if (actionMapIndex >= m_NumActionMaps)
m_NumActionMaps = actionMapIndex + 1;
ArrayHelpers.PutAtIfNotSet(ref m_ActionMapNames, actionMapIndex,
() => new GUIContent(action.actionMap.name));
}
// Bring over any action events that we already have and that are still in the asset.
var oldActionEvents = playerInput.m_ActionEvents;
if (oldActionEvents != null)
{
foreach (var entry in oldActionEvents)
{
var guid = entry.actionId;
var action = asset.FindAction(guid);
if (action != null)
AddEntry(action, entry);
}
}
// Add any new actions.
foreach (var action in asset)
{
// Skip if it was already in there.
if (oldActionEvents != null && oldActionEvents.Any(x => x.actionId == action.id.ToString()))
continue;
////FIXME: adds bindings to the name
AddEntry(action, new PlayerInput.ActionEvent(action.id, action.ToString()));
}
m_ActionNames = newActionNames.ToArray();
m_ActionMapIndices = newActionMapIndices.ToArray();
Array.Resize(ref m_ActionMapEventsUnfolded, m_NumActionMaps);
playerInput.m_ActionEvents = newActionEvents.ToArray();
}
// Read out control schemes.
var selectedDefaultControlScheme = playerInput.defaultControlScheme;
m_InvalidDefaultControlSchemeName = null;
m_SelectedDefaultControlScheme = 0;
////TODO: sort alphabetically and ensure that the order is the same in the schemes editor
var controlSchemesNames = asset.controlSchemes.Select(cs => cs.name).ToList();
// try to find the selected Default Control Scheme
if (!string.IsNullOrEmpty(selectedDefaultControlScheme))
{
// +1 since <Any> will be the first in the list
m_SelectedDefaultControlScheme = 1 + controlSchemesNames.FindIndex(name => string.Compare(name, selectedDefaultControlScheme,
StringComparison.InvariantCultureIgnoreCase) == 0);
// if not found, will insert the invalid name next to <Any>
if (m_SelectedDefaultControlScheme == 0)
{
m_InvalidDefaultControlSchemeName = selectedDefaultControlScheme;
m_SelectedDefaultControlScheme = 1;
controlSchemesNames.Insert(0, $"{selectedDefaultControlScheme}{L10n.Tr("<Not Found>")}");
}
}
else
{
playerInput.defaultControlScheme = null;
}
m_ControlSchemeOptions = new GUIContent[controlSchemesNames.Count + 1];
m_ControlSchemeOptions[0] = new GUIContent(EditorGUIUtility.TrTextContent("<Any>"));
for (var i = 0; i < controlSchemesNames.Count; ++i)
{
m_ControlSchemeOptions[i + 1] = new GUIContent(controlSchemesNames[i]);
}
// Read out action maps.
var selectedDefaultActionMap = !string.IsNullOrEmpty(playerInput.defaultActionMap)
? asset.FindActionMap(playerInput.defaultActionMap)
: null;
m_SelectedDefaultActionMap = (asset.actionMaps.Count > 0 && m_SelectedDefaultActionMap == -1) ? 1 : 0;
var actionMaps = asset.actionMaps;
m_ActionMapOptions = new GUIContent[actionMaps.Count + 1];
m_ActionMapOptions[0] = new GUIContent(EditorGUIUtility.TrTextContent("<None>"));
////TODO: sort alphabetically
for (var i = 0; i < actionMaps.Count; ++i)
{
var actionMap = actionMaps[i];
m_ActionMapOptions[i + 1] = new GUIContent(actionMap.name);
if (selectedDefaultActionMap != null && actionMap == selectedDefaultActionMap)
m_SelectedDefaultActionMap = i + 1;
}
if (m_SelectedDefaultActionMap <= 0)
playerInput.defaultActionMap = null;
else
playerInput.defaultActionMap = m_ActionMapOptions[m_SelectedDefaultActionMap].text;
serializedObject.Update();
}
[SerializeField] private bool m_EventsGroupUnfolded;
[SerializeField] private bool[] m_ActionMapEventsUnfolded;
[NonSerialized] private readonly GUIContent m_CreateActionsText = EditorGUIUtility.TrTextContent("Create Actions...");
[NonSerialized] private readonly GUIContent m_FixInputModuleText = EditorGUIUtility.TrTextContent("Fix UI Input Module");
[NonSerialized] private readonly GUIContent m_OpenSettingsText = EditorGUIUtility.TrTextContent("Open Input Settings");
[NonSerialized] private readonly GUIContent m_OpenDebuggerText = EditorGUIUtility.TrTextContent("Open Input Debugger");
[NonSerialized] private readonly GUIContent m_EventsGroupText =
EditorGUIUtility.TrTextContent("Events", "UnityEvents triggered by the PlayerInput component");
[NonSerialized] private readonly GUIContent m_NotificationBehaviorText =
EditorGUIUtility.TrTextContent("Behavior",
"Determine how notifications should be sent when an input-related event associated with the player happens.");
[NonSerialized] private readonly GUIContent m_DefaultControlSchemeText =
EditorGUIUtility.TrTextContent("Default Scheme", "Which control scheme to try by default. If not set, PlayerInput "
+ "will simply go through all control schemes in the action asset and try one after the other. If set, PlayerInput will try "
+ "the given scheme first but if using that fails (e.g. when not required devices are missing) will fall back to trying the other "
+ "control schemes in order.");
[NonSerialized] private readonly GUIContent m_DefaultActionMapText =
EditorGUIUtility.TrTextContent("Default Map", "Action map to enable by default. If not set, no actions will be enabled by default.");
[NonSerialized] private readonly GUIContent m_AutoSwitchText =
EditorGUIUtility.TrTextContent("Auto-Switch",
"By default, when there is only a single PlayerInput, the player "
+ "is allowed to freely switch between control schemes simply by starting to use a different device. By toggling this property off, this "
+ "behavior is disabled and even with a single player, the player will stay locked onto the explicitly selected control scheme. Note "
+ "that you can still change control schemes explicitly through the PlayerInput API.\n\nWhen there are multiple PlayerInputs in the game, auto-switching is disabled automatically regardless of the value of this property.");
[NonSerialized] private readonly GUIContent m_DebugText = EditorGUIUtility.TrTextContent("Debug");
[NonSerialized] private GUIContent m_UIPropertyText;
[NonSerialized] private GUIContent m_CameraPropertyText;
[NonSerialized] private GUIContent m_SendMessagesHelpText;
[NonSerialized] private GUIContent[] m_ActionNames;
[NonSerialized] private GUIContent[] m_ActionMapNames;
[NonSerialized] private int[] m_ActionMapIndices;
[NonSerialized] private int m_NumActionMaps;
[NonSerialized] private int m_SelectedDefaultControlScheme;
[NonSerialized] private string m_InvalidDefaultControlSchemeName;
[NonSerialized] private GUIContent[] m_ControlSchemeOptions;
[NonSerialized] private int m_SelectedDefaultActionMap;
[NonSerialized] private GUIContent[] m_ActionMapOptions;
[NonSerialized] private SerializedProperty m_ActionsProperty;
[NonSerialized] private SerializedProperty m_DefaultControlSchemeProperty;
[NonSerialized] private SerializedProperty m_DefaultActionMapProperty;
[NonSerialized] private SerializedProperty m_NeverAutoSwitchControlSchemesProperty;
[NonSerialized] private SerializedProperty m_NotificationBehaviorProperty;
#if UNITY_INPUT_SYSTEM_ENABLE_UI
[NonSerialized] private SerializedProperty m_UIInputModuleProperty;
#endif
[NonSerialized] private SerializedProperty m_ActionEventsProperty;
[NonSerialized] private SerializedProperty m_CameraProperty;
[NonSerialized] private SerializedProperty m_DeviceLostEventProperty;
[NonSerialized] private SerializedProperty m_DeviceRegainedEventProperty;
[NonSerialized] private SerializedProperty m_ControlsChangedEventProperty;
[NonSerialized] private bool m_NotificationBehaviorInitialized;
[NonSerialized] private bool m_ActionAssetInitialized;
[NonSerialized] private int m_ActionAssetInstanceID;
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78dcfd26eada049b0840e276bd029712
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,813 @@
using System;
using UnityEngine.Events;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
////REVIEW: should we automatically pool/retain up to maxPlayerCount player instances?
////REVIEW: the join/leave messages should probably give a *GameObject* rather than the PlayerInput component (which can be gotten to via a simple GetComponent(InChildren) call)
////TODO: add support for reacting to players missing devices
namespace UnityEngine.InputSystem
{
/// <summary>
/// Manages joining and leaving of players.
/// </summary>
/// <remarks>
/// This is a singleton component. Only one instance should be active in a game at any one time. To retrieve the
/// current instance, use the <see cref="instance"/> property.
///
/// PlayerInputManager provides the implementation of specific player joining mechanisms (<see cref="joinBehavior"/>).
/// It also automatically assigns <see cref="splitScreen">split-screen areas</see>. The input system does not require
/// the PlayerInputManager to have multiple <see cref="PlayerInput"/> components. However, you can always implement
/// your own custom logic instead and simply instantiate multiple [GameObjects](xref:UnityEngine.GameObject) with
/// <see cref="PlayerInput"/> yourself.
///
/// When you use PlayerInputManager, the join behavior you define controls pairing devices to players. This means
/// that <see cref="PlayerInput"/> automatically pairs the device from which the player joined. If control schemes
/// are present in the PlayerInput's set of <see cref="PlayerInput.actions"/>, the input system selects the first compatible
/// device for pairing. If additional devices are required, the input system selects them from the pool of currently
/// unpaired devices.
/// </remarks>
[AddComponentMenu("Input/Player Input Manager")]
[HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
public class PlayerInputManager : MonoBehaviour
{
/// <summary>
/// Name of the message that is sent when a player joins the game.
/// </summary>
public const string PlayerJoinedMessage = "OnPlayerJoined";
public const string PlayerLeftMessage = "OnPlayerLeft";
/// <summary>
/// If enabled, each player will automatically be assigned a portion of the available screen area.
/// </summary>
/// <remarks>
/// For this to work, each <see cref="PlayerInput"/> component must have an associated <see cref="Camera"/>
/// object through <see cref="PlayerInput.camera"/>.
///
/// Note that as player join, the screen may be increasingly subdivided and players may see their
/// previous screen area getting resized.
/// </remarks>
public bool splitScreen
{
get => m_SplitScreen;
set
{
if (m_SplitScreen == value)
return;
m_SplitScreen = value;
if (!m_SplitScreen)
{
// Reset rects on all player cameras.
foreach (var player in PlayerInput.all)
{
var camera = player.camera;
if (camera != null)
camera.rect = new Rect(0, 0, 1, 1);
}
}
else
{
UpdateSplitScreen();
}
}
}
////REVIEW: we probably need support for filling unused screen areas automatically
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines whether subdividing the screen is allowed to
/// produce screen areas that have an aspect ratio different from the screen resolution.
/// </summary>
/// <remarks>
/// By default, when <see cref="splitScreen"/> is enabled, the manager will add or remove screen subdivisions in
/// steps of two. This means that when, for example, the second player is added, the screen will be subdivided into
/// a left and a right screen area; the left one allocated to the first player and the right one allocated to the
/// second player.
///
/// This behavior makes optimal use of screen real estate but will result in screen areas that have aspect ratios
/// different from the screen resolution. If this is not acceptable, this property can be set to true to enforce
/// split-screen to only create screen areas that have the same aspect ratio of the screen.
///
/// This results in the screen being subdivided more aggressively. When, for example, a second player is added,
/// the screen will immediately be divided into a four-way split-screen setup with the lower two screen areas
/// not being used.
///
/// This property is irrelevant if <see cref="fixedNumberOfSplitScreens"/> is used.
/// </remarks>
public bool maintainAspectRatioInSplitScreen => m_MaintainAspectRatioInSplitScreen;
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines how many screen divisions there will be.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default this is set to -1 which means the screen will automatically be divided to best fit the
/// current number of players i.e. the highest player index in <see cref="PlayerInput"/>
/// </remarks>
public int fixedNumberOfSplitScreens => m_FixedNumberOfSplitScreens;
/// <summary>
/// The normalized screen rectangle available for allocating player split-screens into.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default it is set to <c>(0,0,1,1)</c>, i.e. the entire screen area will be used for player screens.
/// If, for example, part of the screen should display a UI/information shared by all players, this
/// property can be used to cut off the area and not have it used by PlayerInputManager.
/// </remarks>
public Rect splitScreenArea => m_SplitScreenRect;
/// <summary>
/// The current number of active players.
/// </summary>
/// <remarks>
/// This count corresponds to all <see cref="PlayerInput"/> instances that are currently enabled.
/// </remarks>
public int playerCount => PlayerInput.s_AllActivePlayersCount;
////FIXME: this needs to be settable
/// <summary>
/// Maximum number of players allowed concurrently in the game.
/// </summary>
/// <remarks>
/// If this limit is reached, joining is turned off automatically.
///
/// By default this is set to -1. Any negative value deactivates the player limit and allows
/// arbitrary many players to join.
/// </remarks>
public int maxPlayerCount => m_MaxPlayerCount;
/// <summary>
/// Whether new players can currently join.
/// </summary>
/// <remarks>
/// While this is true, new players can join via the mechanism determined by <see cref="joinBehavior"/>.
/// </remarks>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="DisableJoining"/>
public bool joiningEnabled => m_AllowJoining;
/// <summary>
/// Determines the mechanism by which players can join when joining is enabled (<see cref="joiningEnabled"/>).
/// </summary>
/// <remarks>
/// </remarks>
public PlayerJoinBehavior joinBehavior
{
get => m_JoinBehavior;
set
{
if (m_JoinBehavior == value)
return;
var joiningEnabled = m_AllowJoining;
if (joiningEnabled)
DisableJoining();
m_JoinBehavior = value;
if (joiningEnabled)
EnableJoining();
}
}
/// <summary>
/// The input action that a player must trigger to join the game.
/// </summary>
/// <remarks>
/// If the join action is a reference to an existing input action, it will be cloned when the PlayerInputManager
/// is enabled. This avoids the situation where the join action can become disabled after the first user joins which
/// can happen when the join action is the same as a player in-game action. When a player joins, input bindings from
/// devices other than the device they joined with are disabled. If the join action had a binding for keyboard and one
/// for gamepad for example, and the first player joined using the keyboard, the expectation is that the next player
/// could still join by pressing the gamepad join button. Without the cloning behavior, the gamepad input would have
/// been disabled.
///
/// For more details about joining behavior, see <see cref="PlayerInput"/>.
/// </remarks>
public InputActionProperty joinAction
{
get => m_JoinAction;
set
{
if (m_JoinAction == value)
return;
////REVIEW: should we suppress notifications for temporary disables?
var joinEnabled = m_AllowJoining && m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered;
if (joinEnabled)
DisableJoining();
m_JoinAction = value;
if (joinEnabled)
EnableJoining();
}
}
public PlayerNotifications notificationBehavior
{
get => m_NotificationBehavior;
set => m_NotificationBehavior = value;
}
public PlayerJoinedEvent playerJoinedEvent
{
get
{
if (m_PlayerJoinedEvent == null)
m_PlayerJoinedEvent = new PlayerJoinedEvent();
return m_PlayerJoinedEvent;
}
}
public PlayerLeftEvent playerLeftEvent
{
get
{
if (m_PlayerLeftEvent == null)
m_PlayerLeftEvent = new PlayerLeftEvent();
return m_PlayerLeftEvent;
}
}
public event Action<PlayerInput> onPlayerJoined
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerJoinedCallbacks.AddCallback(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerJoinedCallbacks.RemoveCallback(value);
}
}
public event Action<PlayerInput> onPlayerLeft
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerLeftCallbacks.AddCallback(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerLeftCallbacks.RemoveCallback(value);
}
}
/// <summary>
/// Reference to the prefab that the manager will instantiate when players join.
/// </summary>
/// <value>Prefab to instantiate for new players.</value>
public GameObject playerPrefab
{
get => m_PlayerPrefab;
set => m_PlayerPrefab = value;
}
/// <summary>
/// Singleton instance of the manager.
/// </summary>
/// <value>Singleton instance or null.</value>
public static PlayerInputManager instance { get; private set; }
/// <summary>
/// Allow players to join the game based on <see cref="joinBehavior"/>.
/// </summary>
/// <seealso cref="DisableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void EnableJoining()
{
switch (m_JoinBehavior)
{
case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
ValidateInputActionAsset();
if (!m_UnpairedDeviceUsedDelegateHooked)
{
if (m_UnpairedDeviceUsedDelegate == null)
m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed;
InputUser.onUnpairedDeviceUsed += m_UnpairedDeviceUsedDelegate;
m_UnpairedDeviceUsedDelegateHooked = true;
++InputUser.listenForUnpairedDeviceActivity;
}
break;
case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
// Hook into join action if we have one.
if (m_JoinAction.action != null)
{
if (!m_JoinActionDelegateHooked)
{
if (m_JoinActionDelegate == null)
m_JoinActionDelegate = JoinPlayerFromActionIfNotAlreadyJoined;
m_JoinAction.action.performed += m_JoinActionDelegate;
m_JoinActionDelegateHooked = true;
}
m_JoinAction.action.Enable();
}
else
{
Debug.LogError(
$"No join action configured on PlayerInputManager but join behavior is set to {nameof(PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)}",
this);
}
break;
}
m_AllowJoining = true;
}
/// <summary>
/// Inhibit players from joining the game.
/// </summary>
/// <remarks>
/// Note that this method might disable the action, depending on how the player
/// joined initially. Specifically, if the initial joining was triggered using
/// the <see cref="PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered"/> behavior,
/// this method also disables the join action.
/// </remarks>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void DisableJoining()
{
switch (m_JoinBehavior)
{
case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
if (m_UnpairedDeviceUsedDelegateHooked)
{
InputUser.onUnpairedDeviceUsed -= m_UnpairedDeviceUsedDelegate;
m_UnpairedDeviceUsedDelegateHooked = false;
--InputUser.listenForUnpairedDeviceActivity;
}
break;
case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
if (m_JoinActionDelegateHooked)
{
var joinAction = m_JoinAction.action;
if (joinAction != null)
m_JoinAction.action.performed -= m_JoinActionDelegate;
m_JoinActionDelegateHooked = false;
}
m_JoinAction.action?.Disable();
break;
}
m_AllowJoining = false;
}
////TODO
/// <summary>
/// Join a new player based on input on a UI element.
/// </summary>
/// <remarks>
/// This should be called directly from a UI callback such as <see cref="Button.onClick"/>. The device
/// that the player joins with is taken from the device that was used to interact with the UI element.
/// </remarks>
internal void JoinPlayerFromUI()
{
if (!CheckIfPlayerCanJoin())
return;
//find used device; InputSystemUIInputModule should probably make that available
throw new NotImplementedException();
}
/// <summary>
/// Join a new player based on input received through an <see cref="InputAction"/>.
/// </summary>
/// <param name="context"></param>
/// <remarks>
/// </remarks>
public void JoinPlayerFromAction(InputAction.CallbackContext context)
{
if (!CheckIfPlayerCanJoin())
return;
var device = context.control.device;
JoinPlayer(pairWithDevice: device);
}
public void JoinPlayerFromActionIfNotAlreadyJoined(InputAction.CallbackContext context)
{
if (!CheckIfPlayerCanJoin())
return;
var device = context.control.device;
if (PlayerInput.FindFirstPairedToDevice(device) != null)
return;
JoinPlayer(pairWithDevice: device);
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevice"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevice">Device to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
///
/// To pair multiple devices, use <see cref="JoinPlayer(int,int,string,InputDevice[])"/>.
/// </remarks>
public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, InputDevice pairWithDevice = null)
{
if (!CheckIfPlayerCanJoin(playerIndex))
return null;
PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
controlScheme: controlScheme, pairWithDevice: pairWithDevice);
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevices"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevices">Devices to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
/// </remarks>
public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, params InputDevice[] pairWithDevices)
{
if (!CheckIfPlayerCanJoin(playerIndex))
return null;
PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
controlScheme: controlScheme, pairWithDevices: pairWithDevices);
}
[SerializeField] internal PlayerNotifications m_NotificationBehavior;
[Tooltip("Set a limit for the maximum number of players who are able to join.")]
[SerializeField] internal int m_MaxPlayerCount = -1;
[SerializeField] internal bool m_AllowJoining = true;
[SerializeField] internal PlayerJoinBehavior m_JoinBehavior;
[SerializeField] internal PlayerJoinedEvent m_PlayerJoinedEvent;
[SerializeField] internal PlayerLeftEvent m_PlayerLeftEvent;
[SerializeField] internal InputActionProperty m_JoinAction;
[SerializeField] internal GameObject m_PlayerPrefab;
[SerializeField] internal bool m_SplitScreen;
[SerializeField] internal bool m_MaintainAspectRatioInSplitScreen;
[Tooltip("Explicitly set a fixed number of screens or otherwise allow the screen to be divided automatically to best fit the number of players.")]
[SerializeField] internal int m_FixedNumberOfSplitScreens = -1;
[SerializeField] internal Rect m_SplitScreenRect = new Rect(0, 0, 1, 1);
[NonSerialized] private bool m_JoinActionDelegateHooked;
[NonSerialized] private bool m_UnpairedDeviceUsedDelegateHooked;
[NonSerialized] private Action<InputAction.CallbackContext> m_JoinActionDelegate;
[NonSerialized] private Action<InputControl, InputEventPtr> m_UnpairedDeviceUsedDelegate;
[NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerJoinedCallbacks;
[NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerLeftCallbacks;
internal static string[] messages => new[]
{
PlayerJoinedMessage,
PlayerLeftMessage,
};
private bool CheckIfPlayerCanJoin(int playerIndex = -1)
{
if (m_PlayerPrefab == null)
{
Debug.LogError("playerPrefab must be set in order to be able to join new players", this);
return false;
}
if (m_MaxPlayerCount >= 0 && playerCount >= m_MaxPlayerCount)
{
Debug.LogWarning("Maximum number of supported players reached: " + maxPlayerCount, this);
return false;
}
// If we have a player index, make sure it's unique.
if (playerIndex != -1)
{
for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
if (PlayerInput.s_AllActivePlayers[i].playerIndex == playerIndex)
{
Debug.LogError(
$"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}",
PlayerInput.s_AllActivePlayers[i]);
return false;
}
}
return true;
}
private void OnUnpairedDeviceUsed(InputControl control, InputEventPtr eventPtr)
{
if (!m_AllowJoining)
return;
if (m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
{
// Make sure it's a button that was actuated.
if (!(control is ButtonControl))
return;
// Make sure it's a device that is usable by the player's actions. We don't want
// to join a player who's then stranded and has no way to actually interact with the game.
if (!IsDeviceUsableWithPlayerActions(control.device))
return;
////REVIEW: should we log a warning or error when the actions for the player do not have control schemes?
JoinPlayer(pairWithDevice: control.device);
}
}
private void OnEnable()
{
if (instance == null)
{
instance = this;
}
else
{
Debug.LogWarning("Multiple PlayerInputManagers in the game. There should only be one PlayerInputManager", this);
return;
}
// if the join action is a reference, clone it so we don't run into problems with the action being disabled by
// PlayerInput when devices are assigned to individual players
if (joinAction.reference != null && joinAction.action?.actionMap?.asset != null)
{
var inputActionAsset = Instantiate(joinAction.action.actionMap.asset);
var inputActionReference = InputActionReference.Create(inputActionAsset.FindAction(joinAction.action.name));
joinAction = new InputActionProperty(inputActionReference);
}
// Join all players already in the game.
for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
NotifyPlayerJoined(PlayerInput.s_AllActivePlayers[i]);
if (m_AllowJoining)
EnableJoining();
}
private void OnDisable()
{
if (instance == this)
instance = null;
if (m_AllowJoining)
DisableJoining();
}
/// <summary>
/// If split-screen is enabled, then for each player in the game, adjust the player's <see cref="Camera.rect"/>
/// to fit the player's split screen area according to the number of players currently in the game and the
/// current split-screen configuration.
/// </summary>
private void UpdateSplitScreen()
{
// Nothing to do if split-screen is not enabled.
if (!m_SplitScreen)
return;
// Determine number of split-screens to create based on highest player index we have.
var minSplitScreenCount = 0;
foreach (var player in PlayerInput.all)
{
if (player.playerIndex >= minSplitScreenCount)
minSplitScreenCount = player.playerIndex + 1;
}
// Adjust to fixed number if we have it.
if (m_FixedNumberOfSplitScreens > 0)
{
if (m_FixedNumberOfSplitScreens < minSplitScreenCount)
Debug.LogWarning(
$"Highest playerIndex of {minSplitScreenCount} exceeds fixed number of split-screens of {m_FixedNumberOfSplitScreens}",
this);
minSplitScreenCount = m_FixedNumberOfSplitScreens;
}
// Determine divisions along X and Y. Usually, we have a square grid of split-screens so all we need to
// do is make it large enough to fit all players.
var numDivisionsX = Mathf.CeilToInt(Mathf.Sqrt(minSplitScreenCount));
var numDivisionsY = numDivisionsX;
if (!m_MaintainAspectRatioInSplitScreen && numDivisionsX * (numDivisionsX - 1) >= minSplitScreenCount)
{
// We're allowed to produce split-screens with aspect ratios different from the screen meaning
// that we always add one more column before finally adding an entirely new row.
numDivisionsY -= 1;
}
// Assign split-screen area to each player.
foreach (var player in PlayerInput.all)
{
// Make sure the player's splitScreenIndex isn't out of range.
var splitScreenIndex = player.splitScreenIndex;
if (splitScreenIndex >= numDivisionsX * numDivisionsY)
{
Debug.LogError(
$"Split-screen index of {splitScreenIndex} on player is out of range (have {numDivisionsX * numDivisionsY} screens); resetting to playerIndex",
player);
player.m_SplitScreenIndex = player.playerIndex;
}
// Make sure we have a camera.
var camera = player.camera;
if (camera == null)
{
Debug.LogError(
"Player has no camera associated with it. Cannot set up split-screen. Point PlayerInput.camera to camera for player.",
player);
continue;
}
// Assign split-screen area based on m_SplitScreenRect.
var column = splitScreenIndex % numDivisionsX;
var row = splitScreenIndex / numDivisionsX;
var rect = new Rect
{
width = m_SplitScreenRect.width / numDivisionsX,
height = m_SplitScreenRect.height / numDivisionsY
};
rect.x = m_SplitScreenRect.x + column * rect.width;
// Y is bottom-to-top but we fill from top down.
rect.y = m_SplitScreenRect.y + m_SplitScreenRect.height - (row + 1) * rect.height;
camera.rect = rect;
}
}
private bool IsDeviceUsableWithPlayerActions(InputDevice device)
{
Debug.Assert(device != null);
if (m_PlayerPrefab == null)
return true;
var playerInput = m_PlayerPrefab.GetComponentInChildren<PlayerInput>();
if (playerInput == null)
return true;
var actions = playerInput.actions;
if (actions == null)
return true;
// If the asset has control schemes, see if there's one that works with the device plus
// whatever unpaired devices we have left.
if (actions.controlSchemes.Count > 0)
{
using (var unpairedDevices = InputUser.GetUnpairedInputDevices())
{
if (InputControlScheme.FindControlSchemeForDevices(unpairedDevices, actions.controlSchemes,
mustIncludeDevice: device) == null)
return false;
}
return true;
}
// Otherwise just check whether any of the maps has bindings usable with the device.
foreach (var actionMap in actions.actionMaps)
if (actionMap.IsUsableWithDevice(device))
return true;
return false;
}
private void ValidateInputActionAsset()
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (m_PlayerPrefab == null || m_PlayerPrefab.GetComponentInChildren<PlayerInput>() == null)
return;
var actions = m_PlayerPrefab.GetComponentInChildren<PlayerInput>().actions;
if (actions == null)
return;
var isValid = true;
foreach (var controlScheme in actions.controlSchemes)
{
if (controlScheme.deviceRequirements.Count > 0)
break;
isValid = false;
}
if (isValid) return;
var assetInfo = actions.name;
#if UNITY_EDITOR
assetInfo = AssetDatabase.GetAssetPath(actions);
#endif
Debug.LogWarning($"The input action asset '{assetInfo}' in the player prefab assigned to PlayerInputManager has " +
"no control schemes with required devices. The JoinPlayersWhenButtonIsPressed join behavior " +
"will not work unless the expected input devices are listed as requirements in the input " +
"action asset.", m_PlayerPrefab);
#endif
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is enabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerJoined(PlayerInput player)
{
Debug.Assert(player != null);
UpdateSplitScreen();
switch (m_NotificationBehavior)
{
case PlayerNotifications.SendMessages:
SendMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.BroadcastMessages:
BroadcastMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.InvokeUnityEvents:
m_PlayerJoinedEvent?.Invoke(player);
break;
case PlayerNotifications.InvokeCSharpEvents:
DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerJoinedCallbacks, player, "onPlayerJoined");
break;
}
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is disabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerLeft(PlayerInput player)
{
Debug.Assert(player != null);
UpdateSplitScreen();
switch (m_NotificationBehavior)
{
case PlayerNotifications.SendMessages:
SendMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.BroadcastMessages:
BroadcastMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.InvokeUnityEvents:
m_PlayerLeftEvent?.Invoke(player);
break;
case PlayerNotifications.InvokeCSharpEvents:
DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerLeftCallbacks, player, "onPlayerLeft");
break;
}
}
[Serializable]
public class PlayerJoinedEvent : UnityEvent<PlayerInput>
{
}
[Serializable]
public class PlayerLeftEvent : UnityEvent<PlayerInput>
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 621567455fd1c4ceb811cc8a00b6a1a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 40265a896a0f341bb956e90c59025a29, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,269 @@
#if UNITY_EDITOR
using System;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Users;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// Custom inspector for <see cref="PlayerInputManager"/>.
/// </summary>
[CustomEditor(typeof(PlayerInputManager))]
internal class PlayerInputManagerEditor : UnityEditor.Editor
{
public void OnEnable()
{
InputUser.onChange += OnUserChange;
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.PlayerInputManager).Send();
new PlayerInputManagerEditorAnalytic(this).Send();
}
public void OnDestroy()
{
InputUser.onChange -= OnUserChange;
}
private void OnUserChange(InputUser user, InputUserChange change, InputDevice device)
{
Repaint();
}
public override void OnInspectorGUI()
{
////TODO: cache properties
EditorGUI.BeginChangeCheck();
DoNotificationSectionUI();
EditorGUILayout.Space();
DoJoinSectionUI();
EditorGUILayout.Space();
DoSplitScreenSectionUI();
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
if (EditorApplication.isPlaying)
DoDebugUI();
}
private void DoNotificationSectionUI()
{
var notificationBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_NotificationBehavior));
EditorGUILayout.PropertyField(notificationBehaviorProperty);
switch ((PlayerNotifications)notificationBehaviorProperty.intValue)
{
case PlayerNotifications.SendMessages:
if (m_SendMessagesHelpText == null)
m_SendMessagesHelpText = EditorGUIUtility.TrTextContent(
$"Will SendMessage() to GameObject: " + string.Join(",", PlayerInputManager.messages));
EditorGUILayout.HelpBox(m_SendMessagesHelpText);
break;
case PlayerNotifications.BroadcastMessages:
if (m_BroadcastMessagesHelpText == null)
m_BroadcastMessagesHelpText = EditorGUIUtility.TrTextContent(
$"Will BroadcastMessage() to GameObject: " + string.Join(",", PlayerInputManager.messages));
EditorGUILayout.HelpBox(m_BroadcastMessagesHelpText);
break;
case PlayerNotifications.InvokeUnityEvents:
m_EventsExpanded = EditorGUILayout.Foldout(m_EventsExpanded, m_EventsLabel, toggleOnLabelClick: true);
if (m_EventsExpanded)
{
var playerJoinedEventProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerJoinedEvent));
var playerLeftEventProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerLeftEvent));
EditorGUILayout.PropertyField(playerJoinedEventProperty);
EditorGUILayout.PropertyField(playerLeftEventProperty);
}
break;
}
}
private void DoJoinSectionUI()
{
EditorGUILayout.LabelField(m_JoiningGroupLabel, EditorStyles.boldLabel);
// Join behavior
var joinBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_JoinBehavior));
EditorGUILayout.PropertyField(joinBehaviorProperty);
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue != PlayerJoinBehavior.JoinPlayersManually)
{
++EditorGUI.indentLevel;
// Join action.
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue ==
PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)
{
var joinActionProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_JoinAction));
EditorGUILayout.PropertyField(joinActionProperty);
}
// Player prefab.
var playerPrefabProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerPrefab));
EditorGUILayout.PropertyField(playerPrefabProperty);
ValidatePlayerPrefab(joinBehaviorProperty, playerPrefabProperty);
--EditorGUI.indentLevel;
}
// Enabled-by-default.
var allowJoiningProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_AllowJoining));
if (m_AllowingJoiningLabel == null)
m_AllowingJoiningLabel = new GUIContent("Joining Enabled By Default", allowJoiningProperty.GetTooltip());
EditorGUILayout.PropertyField(allowJoiningProperty, m_AllowingJoiningLabel);
// Max player count.
var maxPlayerCountProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_MaxPlayerCount));
if (m_EnableMaxPlayerCountLabel == null)
m_EnableMaxPlayerCountLabel = EditorGUIUtility.TrTextContent("Limit Number of Players", maxPlayerCountProperty.GetTooltip());
if (maxPlayerCountProperty.intValue > 0)
m_MaxPlayerCountEnabled = true;
m_MaxPlayerCountEnabled = EditorGUILayout.Toggle(m_EnableMaxPlayerCountLabel, m_MaxPlayerCountEnabled);
if (m_MaxPlayerCountEnabled)
{
++EditorGUI.indentLevel;
if (maxPlayerCountProperty.intValue < 0)
maxPlayerCountProperty.intValue = 1;
EditorGUILayout.PropertyField(maxPlayerCountProperty);
--EditorGUI.indentLevel;
}
else
maxPlayerCountProperty.intValue = -1;
}
private static void ValidatePlayerPrefab(SerializedProperty joinBehaviorProperty,
SerializedProperty playerPrefabProperty)
{
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue != PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
return;
if (playerPrefabProperty.objectReferenceValue == null)
return;
var playerInput = ((GameObject)playerPrefabProperty.objectReferenceValue)
.GetComponentInChildren<PlayerInput>();
if (playerInput == null)
{
EditorGUILayout.HelpBox("No PlayerInput component found in player prefab.", MessageType.Info);
return;
}
if (playerInput.actions == null)
{
EditorGUILayout.HelpBox("PlayerInput component has no input action asset assigned.", MessageType.Info);
return;
}
if (playerInput.actions.controlSchemes.Any(c => c.deviceRequirements.Count > 0) == false)
EditorGUILayout.HelpBox("Join Players When Button Is Pressed behavior will not work when the Input Action Asset " +
"assigned to the PlayerInput component has no required devices in any control scheme.",
MessageType.Info);
}
private void DoSplitScreenSectionUI()
{
EditorGUILayout.LabelField(m_SplitScreenGroupLabel, EditorStyles.boldLabel);
// Split-screen toggle.
var splitScreenProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_SplitScreen));
if (m_SplitScreenLabel == null)
m_SplitScreenLabel = new GUIContent("Enable Split-Screen", splitScreenProperty.GetTooltip());
EditorGUILayout.PropertyField(splitScreenProperty, m_SplitScreenLabel);
if (!splitScreenProperty.boolValue)
return;
++EditorGUI.indentLevel;
// Maintain-aspect-ratio toggle.
var maintainAspectRatioProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_MaintainAspectRatioInSplitScreen));
if (m_MaintainAspectRatioLabel == null)
m_MaintainAspectRatioLabel =
new GUIContent("Maintain Aspect Ratio", maintainAspectRatioProperty.GetTooltip());
EditorGUILayout.PropertyField(maintainAspectRatioProperty, m_MaintainAspectRatioLabel);
// Fixed-number toggle.
var fixedNumberProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_FixedNumberOfSplitScreens));
if (m_EnableFixedNumberOfSplitScreensLabel == null)
m_EnableFixedNumberOfSplitScreensLabel = EditorGUIUtility.TrTextContent("Set Fixed Number", fixedNumberProperty.GetTooltip());
if (fixedNumberProperty.intValue > 0)
m_FixedNumberOfSplitScreensEnabled = true;
m_FixedNumberOfSplitScreensEnabled = EditorGUILayout.Toggle(m_EnableFixedNumberOfSplitScreensLabel,
m_FixedNumberOfSplitScreensEnabled);
if (m_FixedNumberOfSplitScreensEnabled)
{
++EditorGUI.indentLevel;
if (fixedNumberProperty.intValue < 0)
fixedNumberProperty.intValue = 4;
if (m_FixedNumberOfSplitScreensLabel == null)
m_FixedNumberOfSplitScreensLabel = EditorGUIUtility.TrTextContent("Number of Screens",
fixedNumberProperty.tooltip);
EditorGUILayout.PropertyField(fixedNumberProperty, m_FixedNumberOfSplitScreensLabel);
--EditorGUI.indentLevel;
}
else
{
fixedNumberProperty.intValue = -1;
}
// Split-screen area.
var splitScreenAreaProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_SplitScreenRect));
if (m_SplitScreenAreaLabel == null)
m_SplitScreenAreaLabel = new GUIContent("Screen Rectangle", splitScreenAreaProperty.GetTooltip());
EditorGUILayout.PropertyField(splitScreenAreaProperty, m_SplitScreenAreaLabel);
--EditorGUI.indentLevel;
}
private void DoDebugUI()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField(m_DebugLabel, EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
var players = PlayerInput.all;
if (players.Count == 0)
{
EditorGUILayout.LabelField("No Players");
}
else
{
foreach (var player in players)
{
var str = player.gameObject.name;
if (player.splitScreenIndex != -1)
str += $" (Screen #{player.splitScreenIndex})";
EditorGUILayout.LabelField("Player #" + player.playerIndex, str);
}
}
EditorGUI.EndDisabledGroup();
}
[SerializeField] private bool m_EventsExpanded;
[SerializeField] private bool m_MaxPlayerCountEnabled;
[SerializeField] private bool m_FixedNumberOfSplitScreensEnabled;
[NonSerialized] private readonly GUIContent m_JoiningGroupLabel = EditorGUIUtility.TrTextContent("Joining");
[NonSerialized] private readonly GUIContent m_SplitScreenGroupLabel = EditorGUIUtility.TrTextContent("Split-Screen");
[NonSerialized] private readonly GUIContent m_EventsLabel = EditorGUIUtility.TrTextContent("Events");
[NonSerialized] private readonly GUIContent m_DebugLabel = EditorGUIUtility.TrTextContent("Debug");
[NonSerialized] private GUIContent m_SendMessagesHelpText;
[NonSerialized] private GUIContent m_BroadcastMessagesHelpText;
[NonSerialized] private GUIContent m_AllowingJoiningLabel;
[NonSerialized] private GUIContent m_SplitScreenLabel;
[NonSerialized] private GUIContent m_MaintainAspectRatioLabel;
[NonSerialized] private GUIContent m_SplitScreenAreaLabel;
[NonSerialized] private GUIContent m_FixedNumberOfSplitScreensLabel;
[NonSerialized] private GUIContent m_EnableMaxPlayerCountLabel;
[NonSerialized] private GUIContent m_EnableFixedNumberOfSplitScreensLabel;
}
}
#endif // UNITY_EDITOR

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6247927155f224aa5a61c15611fcd34a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Determines how <see cref="PlayerInputManager"/> joins new players.
/// </summary>
/// <remarks>
/// </remarks>
/// <seealso cref="PlayerInputManager"/>
/// <seealso cref="PlayerInputManager.joinBehavior"/>
public enum PlayerJoinBehavior
{
/// <summary>
/// Listen for button presses on devices that are not paired to any player. If they occur
/// and joining is allowed, join a new player using the device the button was pressed on.
/// </summary>
JoinPlayersWhenButtonIsPressed,
/// <summary>
/// Listen for button presses on devices that are not paired to any player. If the control
/// they triggered matches a specific action and joining is allowed, join a new player using
/// the device the button was pressed on.
/// </summary>
JoinPlayersWhenJoinActionIsTriggered,
/// <summary>
/// Don't join players automatically. Call <see cref="PlayerInputManager.JoinPlayerFromUI"/>
/// or <see cref="PlayerInputManager.JoinPlayerFromAction"/> explicitly in order to join new
/// players. Alternatively, just create GameObjects with <see cref="PlayerInput"/>
/// components directly and they will be joined automatically.
/// </summary>
/// <remarks>
/// This behavior also allows implementing more sophisticated device pairing mechanisms when multiple devices
/// are involved. While initial engagement required by <see cref="JoinPlayersWhenButtonIsPressed"/> or
/// <see cref="JoinPlayersWhenJoinActionIsTriggered"/> allows pairing a single device reliably to a player,
/// additional devices that may be required by a control scheme will still get paired automatically out of the
/// pool of available devices.
/// </remarks>
JoinPlayersManually,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86b230bdbb8ce487882861a17e6d2bde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Determines how the triggering of an action or other input-related events are relayed to other GameObjects.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1717:Only FlagsAttribute enums should have plural names")]
public enum PlayerNotifications
{
////TODO: add a "None" behavior; for actions, users may want to poll (or use the generated interfaces)
/// <summary>
/// Use <see cref="GameObject.SendMessage(string,object)"/> to send a message to the <see cref="GameObject"/>
/// that <see cref="PlayerInput"/> belongs to.
///
/// The message name will be the name of the action (e.g. "Jump"; it will not include the action map name),
/// and the object will be the <see cref="PlayerInput"/> on which the action was triggered.
///
/// If the notification is for an action that was triggered, <see cref="SendMessageOptions"/> will be
/// <see cref="SendMessageOptions.RequireReceiver"/> (i.e. an error will be logged if there is no corresponding
/// method). Otherwise it will be <see cref="SendMessageOptions.DontRequireReceiver"/>.
/// </summary>
SendMessages,
/// <summary>
/// Like <see cref="SendMessages"/> but instead of using <see cref="GameObject.SendMessage(string,object)"/>,
/// use <see cref="GameObject.BroadcastMessage(string,object)"/>.
/// </summary>
BroadcastMessages,
/// <summary>
/// Have a separate <a href="https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html">UnityEvent</a> for each notification.
/// Allows wiring up target methods to invoke such that the connection is persisted in Unity serialized data.
///
/// See <see cref="PlayerInput.actionEvents"/> and related callbacks such as <see cref="PlayerInput.controlsChangedEvent"/>.
/// </summary>
InvokeUnityEvents,
////TODO: Kill
/// <summary>
/// Use plain C# callbacks.
/// </summary>
InvokeCSharpEvents
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 326532c3044e949b696215102693a1a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a3e712d7ab2114ca4960246b0d5e6215
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
#if (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
namespace UnityEngine.InputSystem.Steam
{
/// <summary>
/// This is a wrapper around the Steamworks SDK controller API.
/// </summary>
/// <seealso href="https://partner.steamgames.com/doc/api/ISteamController"/>
public interface ISteamControllerAPI
{
void RunFrame();
int GetConnectedControllers(SteamHandle<SteamController>[] outHandles);
SteamHandle<InputActionMap> GetActionSetHandle(string actionSetName);
SteamHandle<InputAction> GetDigitalActionHandle(string actionName);
SteamHandle<InputAction> GetAnalogActionHandle(string actionName);
void ActivateActionSet(SteamHandle<SteamController> controllerHandle, SteamHandle<InputActionMap> actionSetHandle);
SteamHandle<InputActionMap> GetCurrentActionSet(SteamHandle<SteamController> controllerHandle);
void ActivateActionSetLayer(SteamHandle<SteamController> controllerHandle,
SteamHandle<InputActionMap> actionSetLayerHandle);
void DeactivateActionSetLayer(SteamHandle<SteamController> controllerHandle,
SteamHandle<InputActionMap> actionSetLayerHandle);
void DeactivateAllActionSetLayers(SteamHandle<SteamController> controllerHandle);
int GetActiveActionSetLayers(SteamHandle<SteamController> controllerHandle,
out SteamHandle<InputActionMap> handlesOut);
SteamAnalogActionData GetAnalogActionData(SteamHandle<SteamController> controllerHandle,
SteamHandle<InputAction> analogActionHandle);
SteamDigitalActionData GetDigitalActionData(SteamHandle<SteamController> controllerHandle,
SteamHandle<InputAction> digitalActionHandle);
}
public struct SteamDigitalActionData
{
public bool active { get; set; }
public bool pressed { get; set; }
}
public struct SteamAnalogActionData
{
public bool active { get; set; }
public Vector2 position { get; set; }
}
}
#endif // (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 425bde5d5c63e4c5d8d95b068a911236
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
#if (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Steam.Editor;
#endif
////TODO: support action set layers
namespace UnityEngine.InputSystem.Steam
{
/// <summary>
/// Base class for controllers made available through the Steam controller API.
/// </summary>
/// <remarks>
/// Unlike other controllers, the Steam controller is somewhat of an amorphous input
/// device which gains specific shape only in combination with an "In-Game Action File"
/// in VDF format installed alongside the application. These files specify the actions
/// that are supported by the application and are internally bound to specific controls
/// on a controller inside the Steam runtime. The bindings are set up in the Steam client
/// through its own binding UI.
///
/// Note that as the Steam controller API supports PS4 and Xbox controllers as well,
/// the actual hardware device behind a SteamController instance may not be a
/// Steam Controller. The <see cref="steamControllerType"/> property can be used what kind
/// of controller the Steam runtime is talking to internally.
///
/// This class is abstract. Specific Steam controller interfaces can either be implemented
/// manually based on this class or generated automatically from Steam IGA files using
/// <see cref="SteamIGAConverter"/>. This can be done in the editor by right-clicking
/// a .VDF file containing the actions and then selecting "Steam >> Generate Unity Input Device...".
/// The result is a newly generated device layout that will automatically register itself
/// with the input system and will represent a Steam controller with the specific action
/// sets and actions found in the .VDF file. The Steam handles for sets and actions will
/// automatically be exposed from the device and controls will be set up that correspond
/// to each action defined in the .VDF file.
///
/// Devices based on SteamController can be used in one of two ways.
///
/// The first method is by manually managing active action sets on a controller. This is done by
/// calling the various APIs (such as <see cref="ActivateSteamActionSet"/>) that correspond
/// to the methods in the <see cref="ISteamControllerAPI">Steam controller API</see>. The
/// controller handle is implicit in this case and corresponds to the <see cref="steamControllerHandle"/>
/// of the controller the methods are called on.
///
/// The second method is by using
///
///
///
///
///
/// By default, Steam controllers will automatically activate action set layers in
/// response to action maps being enabled and disabled. The correlation between Unity
/// actions and action maps and Steam actions and action sets happens entirely by name.
/// E.g. a Unity action map called "gameplay" will be looked up as a Steam action set
/// using the same name "gameplay".
/// </remarks>
public abstract class SteamController : InputDevice
{
internal const string kSteamInterface = "Steam";
/// <summary>
/// Handle in the <see cref="ISteamControllerAPI">Steam API</see> for the controller.
/// </summary>
public SteamHandle<SteamController> steamControllerHandle { get; internal set; }
/*
* TODO
public SteamControllerType steamControllerType
{
get { throw new NotImplementedException(); }
}*/
/// <summary>
/// The list of Steam action sets supported by this controller.
/// </summary>
/// <remarks>
/// Steam action sets are implicitly supplied to the Steam runtime rather than explicitly configured
/// by the application. ...
/// </remarks>
public abstract ReadOnlyArray<SteamActionSetInfo> steamActionSets { get; }
/// <summary>
/// Determine whether the controller automatically activates and deactivates action set
/// layers in response to <see cref="InputActionMap">input action map</see> being enabled
/// and disabled.
/// </summary>
/// <remarks>
/// This is on by default.
///
/// When on, if an <see cref="InputActionMap">action map</see> has bindings to a SteamController
/// and is enabled or disabled, the SteamController will automatically enable or disable
/// the correspondingly named Steam action set.
/// </remarks>
public bool autoActivateSets { get; set; }
protected SteamController()
{
autoActivateSets = true;
}
public void ActivateSteamActionSet(SteamHandle<InputActionMap> actionSet)
{
SteamSupport.GetAPIAndRequireItToBeSet().ActivateActionSet(steamControllerHandle, actionSet);
}
public SteamHandle<InputActionMap> currentSteamActionSet
{
get { return SteamSupport.GetAPIAndRequireItToBeSet().GetCurrentActionSet(steamControllerHandle); }
}
protected abstract void ResolveSteamActions(ISteamControllerAPI api);
protected abstract void Update(ISteamControllerAPI api);
// These methods avoid having an 'internal' modifier that every override needs to carry along.
internal void InvokeResolveSteamActions()
{
ResolveSteamActions(SteamSupport.GetAPIAndRequireItToBeSet());
}
internal void InvokeUpdate()
{
Update(SteamSupport.GetAPIAndRequireItToBeSet());
}
public struct SteamActionSetInfo
{
public string name { get; set; }
public SteamHandle<InputActionMap> handle { get; set; }
}
}
}
#endif // (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab9baa68d9998419ba6b1bd76b11b630
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
namespace UnityEngine.InputSystem.Steam
{
/*
TODO
public enum SteamControllerType
{
Unknown,
SteamController,
Xbox360,
XboxOne,
GenericXInput,
PS4
}
*/
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ffe30fd9a2d64031a59c21418fb2067
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
#if (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
using System;
namespace UnityEngine.InputSystem.Steam
{
/// <summary>
/// A handle for a Steam controller API object typed <typeparamref name="TObject"/>.
/// </summary>
/// <typeparam name="TObject">A type used to type the Steam handle. The type itself isn't used other than
/// for providing type safety to the Steam handle.</typeparam>
public struct SteamHandle<TObject> : IEquatable<SteamHandle<TObject>>
{
private ulong m_Handle;
public SteamHandle(ulong handle)
{
m_Handle = handle;
}
public override string ToString()
{
return string.Format("Steam({0}): {1}", typeof(TObject).Name, m_Handle);
}
public bool Equals(SteamHandle<TObject> other)
{
return m_Handle == other.m_Handle;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
return obj is SteamHandle<TObject> && Equals((SteamHandle<TObject>)obj);
}
public override int GetHashCode()
{
return m_Handle.GetHashCode();
}
public static bool operator==(SteamHandle<TObject> a, SteamHandle<TObject> b)
{
return a.m_Handle == b.m_Handle;
}
public static bool operator!=(SteamHandle<TObject> a, SteamHandle<TObject> b)
{
return !(a == b);
}
public static explicit operator ulong(SteamHandle<TObject> handle)
{
return handle.m_Handle;
}
}
}
#endif // (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f971d46c202b4b7a90865bf5600683f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,782 @@
#if UNITY_EDITOR && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Editor;
using UnityEngine.InputSystem.Utilities;
////TODO: motion data support
////TODO: haptics support
////TODO: ensure that no two actions have the same name even between maps
////TODO: also need to build a layout based on SteamController that has controls representing the current set of actions
//// (might need this in the runtime)
////TODO: localization support (allow loading existing VDF file and preserving localization strings)
////TODO: allow having actions that are ignored by Steam VDF export
////TODO: support for getting displayNames/glyphs from Steam
////TODO: polling in background
namespace UnityEngine.InputSystem.Steam.Editor
{
/// <summary>
/// Converts input actions to and from Steam IGA file format.
/// </summary>
/// <remarks>
/// The idea behind this converter is to enable users to use Unity's action editor to set up actions
/// for their game and the be able, when targeting desktops through Steam, to convert the game's actions
/// to a Steam VDF file that allows using the Steam Controller API with the game.
///
/// The generated VDF file is meant to allow editing by hand in order to add localization strings or
/// apply Steam-specific settings that cannot be inferred from Unity input actions.
/// </remarks>
public static class SteamIGAConverter
{
/// <summary>
/// Generate C# code for an <see cref="InputDevice"/> derived class that exposes the controls
/// for the actions found in the given Steam IGA description.
/// </summary>
/// <param name="vdf"></param>
/// <param name="namespaceAndClassName"></param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")]
public static string GenerateInputDeviceFromSteamIGA(string vdf, string namespaceAndClassName)
{
if (string.IsNullOrEmpty(vdf))
throw new ArgumentNullException("vdf");
if (string.IsNullOrEmpty(namespaceAndClassName))
throw new ArgumentNullException("namespaceAndClassName");
// Parse VDF.
var parsedVdf = ParseVDF(vdf);
var actions = (Dictionary<string, object>)((Dictionary<string, object>)parsedVdf["In Game Actions"])["actions"];
// Determine class and namespace name.
var namespaceName = "";
var className = "";
var indexOfLastDot = namespaceAndClassName.LastIndexOf('.');
if (indexOfLastDot != -1)
{
namespaceName = namespaceAndClassName.Substring(0, indexOfLastDot);
className = namespaceAndClassName.Substring(indexOfLastDot + 1);
}
else
{
className = namespaceAndClassName;
}
var stateStructName = className + "State";
var builder = new StringBuilder();
builder.Append("// THIS FILE HAS BEEN AUTO-GENERATED\n");
builder.Append("#if (UNITY_EDITOR || UNITY_STANDALONE) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT\n");
builder.Append("using UnityEngine;\n");
builder.Append("using UnityEngine.InputSystem;\n");
builder.Append("using UnityEngine.InputSystem.Controls;\n");
builder.Append("using UnityEngine.InputSystem.Layouts;\n");
builder.Append("using UnityEngine.InputSystem.LowLevel;\n");
builder.Append("using UnityEngine.InputSystem.Utilities;\n");
builder.Append("using UnityEngine.InputSystem.Steam;\n");
builder.Append("#if UNITY_EDITOR\n");
builder.Append("using UnityEditor;\n");
builder.Append("#endif\n");
builder.Append("\n");
if (!string.IsNullOrEmpty(namespaceName))
{
builder.Append("namespace ");
builder.Append(namespaceName);
builder.Append("\n{\n");
}
// InitializeOnLoad attribute.
builder.Append("#if UNITY_EDITOR\n");
builder.Append("[InitializeOnLoad]\n");
builder.Append("#endif\n");
// Control layout attribute.
builder.Append("[InputControlLayout(stateType = typeof(");
builder.Append(stateStructName);
builder.Append("))]\n");
// Class declaration.
builder.Append("public class ");
builder.Append(className);
builder.Append(" : SteamController\n");
builder.Append("{\n");
// Device matcher.
builder.Append(" private static InputDeviceMatcher deviceMatcher\n");
builder.Append(" {\n");
builder.Append(" get { return new InputDeviceMatcher().WithInterface(\"Steam\").WithProduct(\"");
builder.Append(className);
builder.Append("\"); }\n");
builder.Append(" }\n");
// Static constructor.
builder.Append('\n');
builder.Append("#if UNITY_EDITOR\n");
builder.Append(" static ");
builder.Append(className);
builder.Append("()\n");
builder.Append(" {\n");
builder.Append(" InputSystem.RegisterLayout<");
builder.Append(className);
builder.Append(">(matches: deviceMatcher);\n");
builder.Append(" }\n");
builder.Append("#endif\n");
// RuntimeInitializeOnLoadMethod.
// NOTE: Not relying on static ctor here. See il2cpp bug 1014293.
builder.Append('\n');
builder.Append(" [RuntimeInitializeOnLoadMethod(loadType: RuntimeInitializeLoadType.BeforeSceneLoad)]\n");
builder.Append(" private static void RuntimeInitializeOnLoad()\n");
builder.Append(" {\n");
builder.Append(" InputSystem.RegisterLayout<");
builder.Append(className);
builder.Append(">(matches: deviceMatcher);\n");
builder.Append(" }\n");
// Control properties.
builder.Append('\n');
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
var entryProperties = (Dictionary<string, object>)entry.Value;
var isStick = entryProperties.ContainsKey("input_mode") && (string)entryProperties["input_mode"] == "joystick_move";
builder.Append(" [InputControl]\n");
builder.Append(
$" public {(isStick ? "StickControl" : "Vector2Control")} {CSharpCodeHelpers.MakeIdentifier(entry.Key)} {{ get; protected set; }}\n");
}
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
foreach (var entry in buttons)
{
builder.Append(" [InputControl]\n");
builder.Append(
$" public ButtonControl {CSharpCodeHelpers.MakeIdentifier(entry.Key)} {{ get; protected set; }}\n");
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append(" [InputControl]\n");
builder.Append(
$" public AxisControl {CSharpCodeHelpers.MakeIdentifier(entry.Key)} {{ get; protected set; }}\n");
}
}
// FinishSetup method.
builder.Append('\n');
builder.Append(" protected override void FinishSetup()\n");
builder.Append(" {\n");
builder.Append(" base.FinishSetup();\n");
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
var entryProperties = (Dictionary<string, object>)entry.Value;
var isStick = entryProperties.ContainsKey("input_mode") && (string)entryProperties["input_mode"] == "joystick_move";
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)} = GetChildControl<{(isStick ? "StickControl" : "Vector2Control")}>(\"{entry.Key}\");\n");
}
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
foreach (var entry in buttons)
{
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)} = GetChildControl<ButtonControl>(\"{entry.Key}\");\n");
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)} = GetChildControl<AxisControl>(\"{entry.Key}\");\n");
}
}
builder.Append(" }\n");
// ResolveSteamActions method.
builder.Append('\n');
builder.Append(" protected override void ResolveSteamActions(ISteamControllerAPI api)\n");
builder.Append(" {\n");
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// Set handle.
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(setEntry.Key)}SetHandle = api.GetActionSetHandle(\"{setEntry.Key}\");\n");
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle = api.GetAnalogActionHandle(\"{entry.Key}\");\n");
}
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
foreach (var entry in buttons)
{
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle = api.GetDigitalActionHandle(\"{entry.Key}\");\n");
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append(
$" {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle = api.GetAnalogActionHandle(\"{entry.Key}\");\n");
}
}
builder.Append(" }\n");
// Handle cache fields.
builder.Append('\n');
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// Set handle.
builder.Append(
$" public SteamHandle<InputActionMap> {CSharpCodeHelpers.MakeIdentifier(setEntry.Key)}SetHandle {{ get; private set; }}\n");
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
builder.Append(
$" public SteamHandle<InputAction> {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle {{ get; private set; }}\n");
}
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
foreach (var entry in buttons)
{
builder.Append(
$" public SteamHandle<InputAction> {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle {{ get; private set; }}\n");
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append(
$" public SteamHandle<InputAction> {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle {{ get; private set; }}\n");
}
}
// steamActionSets property.
builder.Append('\n');
builder.Append(" private SteamActionSetInfo[] m_ActionSets;\n");
builder.Append(" public override ReadOnlyArray<SteamActionSetInfo> steamActionSets\n");
builder.Append(" {\n");
builder.Append(" get\n");
builder.Append(" {\n");
builder.Append(" if (m_ActionSets == null)\n");
builder.Append(" m_ActionSets = new[]\n");
builder.Append(" {\n");
foreach (var setEntry in actions)
{
builder.Append(string.Format(
" new SteamActionSetInfo {{ name = \"{0}\", handle = {1}SetHandle }},\n",
setEntry.Key,
CSharpCodeHelpers.MakeIdentifier(setEntry.Key)));
}
builder.Append(" };\n");
builder.Append(" return new ReadOnlyArray<SteamActionSetInfo>(m_ActionSets);\n");
builder.Append(" }\n");
builder.Append(" }\n");
// Update method.
builder.Append('\n');
builder.Append(" protected override unsafe void Update(ISteamControllerAPI api)\n");
builder.Append(" {\n");
builder.Append($" {stateStructName} state;\n");
var currentButtonBit = 0;
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
builder.Append(string.Format(" state.{0} = api.GetAnalogActionData(steamControllerHandle, {0}Handle).position;\n",
CSharpCodeHelpers.MakeIdentifier(entry.Key)));
}
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
foreach (var entry in buttons)
{
builder.Append(
$" if (api.GetDigitalActionData(steamControllerHandle, {CSharpCodeHelpers.MakeIdentifier(entry.Key)}Handle).pressed)\n");
builder.Append($" state.buttons[{currentButtonBit / 8}] |= {currentButtonBit % 8};\n");
++currentButtonBit;
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append(string.Format(" state.{0} = api.GetAnalogActionData(steamControllerHandle, {0}Handle).position.x;\n",
CSharpCodeHelpers.MakeIdentifier(entry.Key)));
}
}
builder.Append(" InputSystem.QueueStateEvent(this, state);\n");
builder.Append(" }\n");
builder.Append("}\n");
if (!string.IsNullOrEmpty(namespaceName))
builder.Append("}\n");
// State struct.
builder.Append("public unsafe struct ");
builder.Append(stateStructName);
builder.Append(" : IInputStateTypeInfo\n");
builder.Append("{\n");
builder.Append(" public FourCC format\n");
builder.Append(" {\n");
builder.Append(" get {\n");
////TODO: handle class names that are shorter than 4 characters
////TODO: uppercase characters
builder.Append(
$" return new FourCC('{className[0]}', '{className[1]}', '{className[2]}', '{className[3]}');\n");
builder.Append(" }\n");
builder.Append(" }\n");
builder.Append("\n");
var totalButtonCount = 0;
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// Buttons.
var buttons = (Dictionary<string, object>)setEntryProperties["Button"];
var buttonCount = buttons.Count;
if (buttonCount > 0)
{
foreach (var entry in buttons)
{
builder.Append(
$" [InputControl(name = \"{entry.Key}\", layout = \"Button\", bit = {totalButtonCount})]\n");
++totalButtonCount;
}
}
}
if (totalButtonCount > 0)
{
var byteCount = (totalButtonCount + 7) / 8;
builder.Append(" public fixed byte buttons[");
builder.Append(byteCount.ToString());
builder.Append("];\n");
}
foreach (var setEntry in actions)
{
var setEntryProperties = (Dictionary<string, object>)setEntry.Value;
// StickPadGyros.
var stickPadGyros = (Dictionary<string, object>)setEntryProperties["StickPadGyro"];
foreach (var entry in stickPadGyros)
{
var entryProperties = (Dictionary<string, object>)entry.Value;
var isStick = entryProperties.ContainsKey("input_mode") && (string)entryProperties["input_mode"] == "joystick_move";
builder.Append(
$" [InputControl(name = \"{entry.Key}\", layout = \"{(isStick ? "Stick" : "Vector2")}\")]\n");
builder.Append($" public Vector2 {CSharpCodeHelpers.MakeIdentifier(entry.Key)};\n");
}
// AnalogTriggers.
var analogTriggers = (Dictionary<string, object>)setEntryProperties["AnalogTrigger"];
foreach (var entry in analogTriggers)
{
builder.Append($" [InputControl(name = \"{entry.Key}\", layout = \"Axis\")]\n");
builder.Append($" public float {CSharpCodeHelpers.MakeIdentifier(entry.Key)};\n");
}
}
builder.Append("}\n");
builder.Append("#endif\n");
return builder.ToString();
}
/// <summary>
/// Convert an .inputactions asset to Steam VDF format.
/// </summary>
/// <param name="asset"></param>
/// <param name="locale"></param>
/// <returns>A string in Steam VDF format describing "In Game Actions" corresponding to the actions in
/// <paramref name="asset"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="asset"/> is null.</exception>
public static string ConvertInputActionsToSteamIGA(InputActionAsset asset, string locale = "english")
{
if (asset == null)
throw new ArgumentNullException("asset");
return ConvertInputActionsToSteamIGA(asset.actionMaps, locale: locale);
}
public static string ConvertInputActionsToSteamIGA(IEnumerable<InputActionMap> actionMaps, string locale = "english")
{
if (actionMaps == null)
throw new ArgumentNullException("actionMaps");
var localizationStrings = new Dictionary<string, string>();
var builder = new StringBuilder();
builder.Append("\"In Game Actions\"\n");
builder.Append("{\n");
// Add actions.
builder.Append("\t\"actions\"\n");
builder.Append("\t{\n");
// Add each action map.
foreach (var actionMap in actionMaps)
{
var actionMapName = actionMap.name;
var actionMapIdentifier = CSharpCodeHelpers.MakeIdentifier(actionMapName);
builder.Append("\t\t\"");
builder.Append(actionMapName);
builder.Append("\"\n");
builder.Append("\t\t{\n");
// Title.
builder.Append("\t\t\t\"title\"\t\"#Set_");
builder.Append(actionMapIdentifier);
builder.Append("\"\n");
localizationStrings["Set_" + actionMapIdentifier] = actionMapName;
// StickPadGyro actions.
builder.Append("\t\t\t\"StickPadGyro\"\n");
builder.Append("\t\t\t{\n");
foreach (var action in actionMap.actions.Where(x => GetSteamControllerInputType(x) == "StickPadGyro"))
ConvertInputActionToVDF(action, builder, localizationStrings);
builder.Append("\t\t\t}\n");
// AnalogTrigger actions.
builder.Append("\t\t\t\"AnalogTrigger\"\n");
builder.Append("\t\t\t{\n");
foreach (var action in actionMap.actions.Where(x => GetSteamControllerInputType(x) == "AnalogTrigger"))
ConvertInputActionToVDF(action, builder, localizationStrings);
builder.Append("\t\t\t}\n");
// Button actions.
builder.Append("\t\t\t\"Button\"\n");
builder.Append("\t\t\t{\n");
foreach (var action in actionMap.actions.Where(x => GetSteamControllerInputType(x) == "Button"))
ConvertInputActionToVDF(action, builder, localizationStrings);
builder.Append("\t\t\t}\n");
builder.Append("\t\t}\n");
}
builder.Append("\t}\n");
// Add localizations.
builder.Append("\t\"localization\"\n");
builder.Append("\t{\n");
builder.Append("\t\t\"");
builder.Append(locale);
builder.Append("\"\n");
builder.Append("\t\t{\n");
foreach (var entry in localizationStrings)
{
builder.Append("\t\t\t\"");
builder.Append(entry.Key);
builder.Append("\"\t\"");
builder.Append(entry.Value);
builder.Append("\"\n");
}
builder.Append("\t\t}\n");
builder.Append("\t}\n");
builder.Append("}\n");
return builder.ToString();
}
private static void ConvertInputActionToVDF(InputAction action, StringBuilder builder, Dictionary<string, string> localizationStrings)
{
builder.Append("\t\t\t\t\"");
builder.Append(action.name);
var mapIdentifier = CSharpCodeHelpers.MakeIdentifier(action.actionMap.name);
var actionIdentifier = CSharpCodeHelpers.MakeIdentifier(action.name);
var titleId = "Action_" + mapIdentifier + "_" + actionIdentifier;
localizationStrings[titleId] = action.name;
// StickPadGyros are objects. Everything else is just strings.
var inputType = GetSteamControllerInputType(action);
if (inputType == "StickPadGyro")
{
builder.Append("\"\n");
builder.Append("\t\t\t\t{\n");
// Title.
builder.Append("\t\t\t\t\t\"title\"\t\"#");
builder.Append(titleId);
builder.Append("\"\n");
// Decide on "input_mode". Assume "absolute_mouse" by default and take
// anything built on StickControl as "joystick_move".
var inputMode = "absolute_mouse";
var controlType = EditorInputControlLayoutCache.TryGetLayout(action.expectedControlType).type;
if (typeof(StickControl).IsAssignableFrom(controlType))
inputMode = "joystick_move";
builder.Append("\t\t\t\t\t\"input_mode\"\t\"");
builder.Append(inputMode);
builder.Append("\"\n");
builder.Append("\t\t\t\t}\n");
}
else
{
builder.Append("\"\t\"");
builder.Append(titleId);
builder.Append("\"\n");
}
}
public static string GetSteamControllerInputType(InputAction action)
{
if (action == null)
throw new ArgumentNullException("action");
// Make sure we have an expected control layout.
var expectedControlLayout = action.expectedControlType;
if (string.IsNullOrEmpty(expectedControlLayout))
throw new ArgumentException($"Cannot determine Steam input type for action '{action}' that has no associated expected control layout",
nameof(action));
// Try to fetch the layout.
var layout = EditorInputControlLayoutCache.TryGetLayout(expectedControlLayout);
if (layout == null)
throw new ArgumentException($"Cannot determine Steam input type for action '{action}'; cannot find layout '{expectedControlLayout}'", nameof(action));
// Map our supported control types.
var controlType = layout.type;
if (typeof(ButtonControl).IsAssignableFrom(controlType))
return "Button";
if (typeof(InputControl<float>).IsAssignableFrom(controlType))
return "AnalogTrigger";
if (typeof(Vector2Control).IsAssignableFrom(controlType))
return "StickPadGyro";
// Everything else throws.
throw new ArgumentException($"Cannot determine Steam input type for action '{action}'; layout '{expectedControlLayout}' with control type '{ controlType.Name}' has no known representation in the Steam controller API", nameof(action));
}
public static Dictionary<string, object> ParseVDF(string vdf)
{
var parser = new VDFParser(vdf);
return parser.Parse();
}
private struct VDFParser
{
public string vdf;
public int length;
public int position;
public VDFParser(string vdf)
{
this.vdf = vdf;
length = vdf.Length;
position = 0;
}
public Dictionary<string, object> Parse()
{
var result = new Dictionary<string, object>();
ParseKeyValuePair(result);
SkipWhitespace();
if (position < length)
throw new InvalidOperationException($"Parse error at {position} in '{vdf}'; not expecting any more input");
return result;
}
private bool ParseKeyValuePair(Dictionary<string, object> result)
{
var key = ParseString();
if (key.isEmpty)
return false;
SkipWhitespace();
if (position == length)
throw new InvalidOperationException($"Expecting value or object at position {position} in '{vdf}'");
var nextChar = vdf[position];
if (nextChar == '"')
{
var value = ParseString();
result[key.ToString()] = value.ToString();
}
else if (nextChar == '{')
{
var value = ParseObject();
result[key.ToString()] = value;
}
else
{
throw new InvalidOperationException($"Expecting value or object at position {position} in '{vdf}'");
}
return true;
}
private Substring ParseString()
{
SkipWhitespace();
if (position == length || vdf[position] != '"')
return new Substring();
++position;
var startPos = position;
while (position < length && vdf[position] != '"')
++position;
var endPos = position;
if (position < length)
++position;
return new Substring(vdf, startPos, endPos - startPos);
}
private Dictionary<string, object> ParseObject()
{
SkipWhitespace();
if (position == length || vdf[position] != '{')
return null;
var result = new Dictionary<string, object>();
++position;
while (position < length)
{
if (!ParseKeyValuePair(result))
break;
}
SkipWhitespace();
if (position == length || vdf[position] != '}')
throw new InvalidOperationException($"Expecting '}}' at position {position} in '{vdf}'");
++position;
return result;
}
private void SkipWhitespace()
{
while (position < length && char.IsWhiteSpace(vdf[position]))
++position;
}
}
[MenuItem("Assets/Steam/Export to Steam In-Game Actions File...", true)]
private static bool IsExportContextMenuItemEnabled()
{
return Selection.activeObject is InputActionAsset;
}
[MenuItem("Assets/Steam/Export to Steam In-Game Actions File...")]
private static void ExportContextMenuItem()
{
var selectedAsset = (InputActionAsset)Selection.activeObject;
// Determine default .vdf file name.
var defaultVDFName = "";
var directory = "";
var assetPath = AssetDatabase.GetAssetPath(selectedAsset);
if (!string.IsNullOrEmpty(assetPath))
{
defaultVDFName = Path.GetFileNameWithoutExtension(assetPath) + ".vdf";
directory = Path.GetDirectoryName(assetPath);
}
// Ask for save location.
var fileName = EditorUtility.SaveFilePanel("Export Steam In-Game Actions File", directory, defaultVDFName, "vdf");
if (!string.IsNullOrEmpty(fileName))
{
var text = ConvertInputActionsToSteamIGA(selectedAsset);
File.WriteAllText(fileName, text);
AssetDatabase.Refresh();
}
}
[MenuItem("Assets/Steam/Generate Unity Input Device...", true)]
private static bool IsGenerateContextMenuItemEnabled()
{
// VDF files have no associated importer and so come in as DefaultAssets.
if (!(Selection.activeObject is DefaultAsset))
return false;
var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if (!string.IsNullOrEmpty(assetPath) && Path.GetExtension(assetPath) == ".vdf")
return true;
return false;
}
////TODO: support setting class and namespace name
[MenuItem("Assets/Steam/Generate Unity Input Device...")]
private static void GenerateContextMenuItem()
{
var selectedAsset = Selection.activeObject;
var assetPath = AssetDatabase.GetAssetPath(selectedAsset);
if (string.IsNullOrEmpty(assetPath))
{
Debug.LogError("Cannot determine source asset path");
return;
}
var defaultClassName = Path.GetFileNameWithoutExtension(assetPath);
var defaultFileName = defaultClassName + ".cs";
var defaultDirectory = Path.GetDirectoryName(assetPath);
// Ask for save location.
var fileName = EditorUtility.SaveFilePanel("Generate C# Input Device Class", defaultDirectory, defaultFileName, "cs");
if (string.IsNullOrEmpty(fileName))
return;
// Load VDF file text.
var vdf = File.ReadAllText(assetPath);
// Generate and write output.
var className = Path.GetFileNameWithoutExtension(fileName);
var text = GenerateInputDeviceFromSteamIGA(vdf, className);
File.WriteAllText(fileName, text);
AssetDatabase.Refresh();
}
}
}
#endif // UNITY_EDITOR && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb4d45fbf0aa84484b251242fdb21800
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,219 @@
#if (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT
using System;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Steam
{
/// <summary>
/// Adds support for Steam controllers.
/// </summary>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class SteamSupport
{
/// <summary>
/// Wrapper around the Steam controller API.
/// </summary>
/// <remarks>
/// This must be set by user code for Steam controller support to become functional.
/// </remarks>
public static ISteamControllerAPI api
{
get { return s_API; }
set
{
s_API = value;
InstallHooks(s_API != null);
}
}
internal static ISteamControllerAPI GetAPIAndRequireItToBeSet()
{
if (s_API == null)
throw new InvalidOperationException("ISteamControllerAPI implementation has not been set on SteamSupport");
return s_API;
}
internal static SteamHandle<SteamController>[] s_ConnectedControllers;
internal static SteamController[] s_InputDevices;
internal static int s_InputDeviceCount;
internal static bool s_HooksInstalled;
internal static ISteamControllerAPI s_API;
private const int STEAM_CONTROLLER_MAX_COUNT = 16;
/// <summary>
/// Enable support for the Steam controller API.
/// </summary>
public static void Initialize()
{
// We use this as a base layout.
InputSystem.RegisterLayout<SteamController>();
if (api != null)
InstallHooks(true);
}
private static void InstallHooks(bool state)
{
Debug.Assert(api != null);
if (state && !s_HooksInstalled)
{
InputSystem.onBeforeUpdate += OnUpdate;
InputSystem.onActionChange += OnActionChange;
}
else if (!state && s_HooksInstalled)
{
InputSystem.onBeforeUpdate -= OnUpdate;
InputSystem.onActionChange -= OnActionChange;
}
}
private static void OnActionChange(object mapOrAction, InputActionChange change)
{
// We only care about action map activations. Steam has no support for enabling or disabling
// individual actions and also has no support disabling sets once enabled (can only switch
// to different set).
if (change != InputActionChange.ActionMapEnabled)
return;
// See if the map has any bindings to SteamControllers.
// NOTE: We only support a single SteamController on any action map here. The first SteamController
// we find is the one we're doing all the work on.
var actionMap = (InputActionMap)mapOrAction;
foreach (var action in actionMap.actions)
{
foreach (var control in action.controls)
{
var steamController = control.device as SteamController;
if (steamController == null)
continue;
// Yes, there's active bindings to a SteamController on the map. Look through the Steam action
// sets on the controller for a name match on the action map. If we have one, sync the enable/
// disable status of the set.
var actionMapName = actionMap.name;
foreach (var set in steamController.steamActionSets)
{
if (string.Compare(set.name, actionMapName, StringComparison.InvariantCultureIgnoreCase) != 0)
continue;
// Nothing to do if the Steam controller has auto-syncing disabled.
if (!steamController.autoActivateSets)
return;
// Sync status.
steamController.ActivateSteamActionSet(set.handle);
// Done.
return;
}
}
}
}
private static void OnUpdate()
{
if (api == null)
return;
// Update controller state.
api.RunFrame();
// Check if we have any new controllers have appeared.
if (s_ConnectedControllers == null)
s_ConnectedControllers = new SteamHandle<SteamController>[STEAM_CONTROLLER_MAX_COUNT];
var numConnectedControllers = api.GetConnectedControllers(s_ConnectedControllers);
for (var i = 0; i < numConnectedControllers; ++i)
{
var handle = s_ConnectedControllers[i];
// See if we already have a device for this one.
if (s_InputDevices != null)
{
SteamController existingDevice = null;
for (var n = 0; n < s_InputDeviceCount; ++n)
{
if (s_InputDevices[n].steamControllerHandle == handle)
{
existingDevice = s_InputDevices[n];
break;
}
}
// Yes, we do.
if (existingDevice != null)
continue;
}
////FIXME: this should not create garbage
// No, so create a new device.
var controllerLayouts = InputSystem.ListLayoutsBasedOn("SteamController");
foreach (var layout in controllerLayouts)
{
// Rather than directly creating a device with the layout, let it go through
// the usual matching process.
var device = InputSystem.AddDevice(new InputDeviceDescription
{
interfaceName = SteamController.kSteamInterface,
product = layout
});
// Make sure it's a SteamController we got.
var steamDevice = device as SteamController;
if (steamDevice == null)
{
Debug.LogError(string.Format(
"InputDevice created from layout '{0}' based on the 'SteamController' layout is not a SteamController",
device.layout));
continue;
}
// Resolve the controller's actions.
steamDevice.InvokeResolveSteamActions();
// Assign it the Steam controller handle.
steamDevice.steamControllerHandle = handle;
ArrayHelpers.AppendWithCapacity(ref s_InputDevices, ref s_InputDeviceCount, steamDevice);
}
}
// Update all controllers we have.
for (var i = 0; i < s_InputDeviceCount; ++i)
{
var device = s_InputDevices[i];
var handle = device.steamControllerHandle;
// Check if the device still exists.
var stillExists = false;
for (var n = 0; n < numConnectedControllers; ++n)
if (s_ConnectedControllers[n] == handle)
{
stillExists = true;
break;
}
// If not, remove it.
if (!stillExists)
{
ArrayHelpers.EraseAtByMovingTail(s_InputDevices, ref s_InputDeviceCount, i);
////REVIEW: should this rather queue a device removal event?
InputSystem.RemoveDevice(device);
--i;
continue;
}
////TODO: support polling Steam controllers on an async polling thread adhering to InputSystem.pollingFrequency
// Otherwise, update it.
device.InvokeUpdate();
}
}
}
}
#endif // (UNITY_STANDALONE || UNITY_EDITOR) && UNITY_ENABLE_STEAM_CONTROLLER_SUPPORT

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7bef150b341c4d0dbab3db81ccf2ea81
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More