UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5769f689f15dce4a9f92aff27659c54
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15fa4fb6e47739e43b36a70fce024b6f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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, it’s 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
|
||||
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ede17f8b7e9dfcf42b560545a8102eeb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 830105192d35a17469c56711468d1e8f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c8442e8094ce974598eacc0302ddabb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eefa91e7a89647338245ee3818141d1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df88ce51eaa14f2f8975023f7f45d2d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7225419e3d446c5982afd5ba9cce319
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa26399d579948dd90c6ff298cdb0570
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e235e01f7e6408f87e899ac373c85d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef4b66ea93dd243a783c32ab1da39fc4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 < 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()");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf42e49077e442fd8b9dc29c328d345
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71f3c0e630f0148b496f565843b5b272
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6261dd2a83757498188548c57e2492bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cc0c6a7fa1c14a518fec64497f4937f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e31057ef324a04478bb4f7d469b3cdc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a71161af1440e4b1fbbefefb3aaffce0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1441
Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HID.cs
Normal file
1441
Packages/com.unity.inputsystem/InputSystem/Plugins/HID/HID.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8806f46699a44c39a6a66c5d07772871
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae42a3986cf84fe2aa1df4aa984dd806
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a736049645e8c4781aedf679c2ee6be6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f0b0cfb7c484e43823c1a038c97a5f3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96500a17645c67442a0ccdca007e6d28
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine.Scripting;
|
||||
|
||||
[assembly: InternalsVisibleTo("UnityEngine.InputForUIVisualizer")]
|
||||
[assembly: InternalsVisibleTo("Unity.InputSystem.Tests")]
|
||||
[assembly: AlwaysLinkAssembly]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65c861c7ef9bee2479c728694e87ea45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fe35221cd5f4ca9b9df947d34e7e1f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Unity.InputSystem.ForUI",
|
||||
"references": [
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09dcc9c0126b6b34ab2a877b8f2277f5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6965b068653ebe45986cb10b38854d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c597fa0f94a3d54b9c0e85f56e5f821
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3da49be2d645b418c8ee308bb1a011c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ab54adba6d259742a9022c514b04149
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea65eddf43398654fa32b724723c71a4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9817b64a59aab947a96a4cf6247d018
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec5b5d14e5b51f44abf18ab8f288cdb5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e49b0a8b53e45445979b3fa9604a35c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d54531977ecb194c95e2d3aa7a5d72a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>"<Gamepad>/buttonSouth"</c>
|
||||
/// and another uses <c>"<Gamepad>/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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92ae37f8da4c25141a0c43e136275e46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9d677f1681015749b15c436eec6d880
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74405005f3013c2498103a79211ba93b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cccd07fcea7f491793d4e7a5adc17a2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc69a9a42102dea4dafad4147e3ed98e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<T>() 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<Vector2>();
|
||||
/// }
|
||||
///
|
||||
/// 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<T>() 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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78dcfd26eada049b0840e276bd029712
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6247927155f224aa5a61c15611fcd34a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86b230bdbb8ce487882861a17e6d2bde
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 326532c3044e949b696215102693a1a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3e712d7ab2114ca4960246b0d5e6215
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 425bde5d5c63e4c5d8d95b068a911236
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab9baa68d9998419ba6b1bd76b11b630
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace UnityEngine.InputSystem.Steam
|
||||
{
|
||||
/*
|
||||
TODO
|
||||
public enum SteamControllerType
|
||||
{
|
||||
Unknown,
|
||||
SteamController,
|
||||
Xbox360,
|
||||
XboxOne,
|
||||
GenericXInput,
|
||||
PS4
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ffe30fd9a2d64031a59c21418fb2067
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f971d46c202b4b7a90865bf5600683f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb4d45fbf0aa84484b251242fdb21800
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user