Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Actions /InputControlScheme.cs
| using System; | |
| using System.Collections; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Text; | |
| using Unity.Collections; | |
| using UnityEngine.InputSystem.Layouts; | |
| using UnityEngine.InputSystem.Utilities; | |
| using UnityEditor; | |
| ////TODO: introduce the concept of a "variation" | |
| //// - a variation is just a variant of a control scheme, not a full control scheme by itself | |
| //// - an individual variation can be toggled on and off independently | |
| //// - while a control is is active, all its variations that are toggled on are also active | |
| //// - assignment to variations works the same as assignment to control schemes | |
| //// use case: left/right stick toggles, left/right bumper toggles, etc | |
| ////TODO: introduce concept of precedence where one control scheme will be preferred over another that is also a match | |
| //// (might be its enough to represent this simply through ordering by giving the user control over the ordering through the UI) | |
| ////REVIEW: allow associating control schemes with platforms, too? | |
| namespace UnityEngine.InputSystem | |
| { | |
| /// <summary> | |
| /// A named set of zero or more device requirements along with an associated binding group. | |
| /// </summary> | |
| /// <remarks> | |
| /// Control schemes provide an additional layer on top of binding groups. While binding | |
| /// groups allow differentiating sets of bindings (e.g. a "Keyboard&Mouse" group versus | |
| /// a "Gamepad" group), control schemes impose a set of devices requirements that must be | |
| /// met in order for a specific set of bindings to be usable. | |
| /// | |
| /// Note that control schemes can only be defined at the <see cref="InputActionAsset"/> level. | |
| /// </remarks> | |
| /// <seealso cref="InputActionAsset.controlSchemes"/> | |
| /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> | |
| [] | |
| public struct InputControlScheme : IEquatable<InputControlScheme> | |
| { | |
| /// <summary> | |
| /// Name of the control scheme. Not <c>null</c> or empty except if InputControlScheme | |
| /// instance is invalid (i.e. default-initialized). | |
| /// </summary> | |
| /// <value>Name of the scheme.</value> | |
| /// <remarks> | |
| /// May be empty or null except if the control scheme is part of an <see cref="InputActionAsset"/>. | |
| /// </remarks> | |
| /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> | |
| public string name => m_Name; | |
| /// <summary> | |
| /// Binding group that is associated with the control scheme. Not <c>null</c> or empty | |
| /// except if InputControlScheme is invalid (i.e. default-initialized). | |
| /// </summary> | |
| /// <value>Binding group for the scheme.</value> | |
| /// <remarks> | |
| /// All bindings in this group are considered to be part of the control scheme. | |
| /// </remarks> | |
| /// <seealso cref="InputBinding.groups"/> | |
| public string bindingGroup | |
| { | |
| get => m_BindingGroup; | |
| set => m_BindingGroup = value; | |
| } | |
| /// <summary> | |
| /// Devices used by the control scheme. | |
| /// </summary> | |
| /// <value>Device requirements of the scheme.</value> | |
| /// <remarks> | |
| /// No two entries will be allowed to match the same control or device at runtime in order for the requirements | |
| /// of the control scheme to be considered satisfied. If, for example, one entry requires a "<Gamepad>" and | |
| /// another entry requires a "<Gamepad>", then at runtime two gamepads will be required even though a single | |
| /// one will match both requirements individually. However, if, for example, one entry requires "<Gamepad>/leftStick" | |
| /// and another requires "<Gamepad>, the same device can match both requirements as each one resolves to | |
| /// a different control. | |
| /// | |
| /// It it allowed to define control schemes without device requirements, i.e. for which this | |
| /// property will be an empty array. Note, however, that features such as automatic control scheme | |
| /// switching in <see cref="PlayerInput"/> will not work with such control schemes. | |
| /// </remarks> | |
| public ReadOnlyArray<DeviceRequirement> deviceRequirements => | |
| new ReadOnlyArray<DeviceRequirement>(m_DeviceRequirements); | |
| /// <summary> | |
| /// Initialize the control scheme with the given name, device requirements, | |
| /// and binding group. | |
| /// </summary> | |
| /// <param name="name">Name to use for the scheme. Required.</param> | |
| /// <param name="devices">List of device requirements.</param> | |
| /// <param name="bindingGroup">Name to use for the binding group (see <see cref="InputBinding.groups"/>) | |
| /// associated with the control scheme. If this is <c>null</c> or empty, <paramref name="name"/> is | |
| /// used instead (with <see cref="InputBinding.Separator"/> characters stripped from the name).</param> | |
| /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> | |
| public InputControlScheme(string name, IEnumerable<DeviceRequirement> devices = null, string bindingGroup = null) | |
| : this() | |
| { | |
| if (string.IsNullOrEmpty(name)) | |
| throw new ArgumentNullException(nameof(name)); | |
| SetNameAndBindingGroup(name, bindingGroup); | |
| m_DeviceRequirements = null; | |
| if (devices != null) | |
| { | |
| m_DeviceRequirements = devices.ToArray(); | |
| if (m_DeviceRequirements.Length == 0) | |
| m_DeviceRequirements = null; | |
| } | |
| } | |
| internal InputControlScheme(SerializedProperty sp) | |
| { | |
| var requirements = new List<DeviceRequirement>(); | |
| var deviceRequirementsArray = sp.FindPropertyRelative(nameof(m_DeviceRequirements)); | |
| if (deviceRequirementsArray == null) | |
| throw new ArgumentException("The serialized property does not contain an InputControlScheme object."); | |
| foreach (SerializedProperty deviceRequirement in deviceRequirementsArray) | |
| { | |
| requirements.Add(new DeviceRequirement | |
| { | |
| controlPath = deviceRequirement.FindPropertyRelative(nameof(DeviceRequirement.m_ControlPath)).stringValue, | |
| m_Flags = (DeviceRequirement.Flags)deviceRequirement.FindPropertyRelative(nameof(DeviceRequirement.m_Flags)).enumValueFlag | |
| }); | |
| } | |
| m_Name = sp.FindPropertyRelative(nameof(m_Name)).stringValue; | |
| m_DeviceRequirements = requirements.ToArray(); | |
| m_BindingGroup = sp.FindPropertyRelative(nameof(m_BindingGroup)).stringValue; | |
| } | |
| internal void SetNameAndBindingGroup(string name, string bindingGroup = null) | |
| { | |
| m_Name = name; | |
| if (!string.IsNullOrEmpty(bindingGroup)) | |
| m_BindingGroup = bindingGroup; | |
| else | |
| m_BindingGroup = name.Contains(InputBinding.Separator) | |
| ? name.Replace(InputBinding.kSeparatorString, "") | |
| : name; | |
| } | |
| /// <summary> | |
| /// Given a list of devices and a list of control schemes, find the most suitable control | |
| /// scheme to use with the devices. | |
| /// </summary> | |
| /// <param name="devices">A list of devices. If the list is empty, only schemes with | |
| /// empty <see cref="deviceRequirements"/> lists will get matched.</param> | |
| /// <param name="schemes">A list of control schemes.</param> | |
| /// <param name="mustIncludeDevice">If not <c>null</c>, a successful match has to include the given device.</param> | |
| /// <param name="allowUnsuccesfulMatch">If true, then allow returning a match that has unsatisfied requirements but still | |
| /// matched at least some requirement. If there are several unsuccessful matches, the returned scheme is still the highest | |
| /// scoring one among those.</param> | |
| /// <typeparam name="TDevices">Collection type to use for the list of devices.</typeparam> | |
| /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam> | |
| /// <returns>The control scheme that best matched the given devices or <c>null</c> if no | |
| /// scheme was found suitable.</returns> | |
| /// <exception cref="ArgumentNullException"><paramref name="devices"/> is <c>null</c> -or- | |
| /// <paramref name="schemes"/> is <c>null</c>.</exception> | |
| /// <remarks> | |
| /// Any successful match (see <see cref="MatchResult.isSuccessfulMatch"/>) will be considered. | |
| /// The one that matches the most amount of devices (see <see cref="MatchResult.devices"/>) | |
| /// will be returned. If more than one schemes matches equally well, the first one encountered | |
| /// in the list is returned. | |
| /// | |
| /// Note that schemes are not required to match all devices available in the list. The result | |
| /// will simply be the scheme that matched the most devices of what was devices. Use <see | |
| /// cref="PickDevicesFrom{TDevices}"/> to find the devices that a control scheme selects. | |
| /// | |
| /// This method is parameterized over <typeparamref name="TDevices"/> and <typeparamref name="TSchemes"/> | |
| /// to allow avoiding GC heap allocations from boxing of structs such as <see cref="ReadOnlyArray{TValue}"/>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// // Create an .inputactions asset. | |
| /// var asset = ScriptableObject.CreateInstance<InputActionAsset>(); | |
| /// | |
| /// // Add some control schemes to the asset. | |
| /// asset.AddControlScheme("KeyboardMouse") | |
| /// .WithRequiredDevice<Keyboard>() | |
| /// .WithRequiredDevice<Mouse>()); | |
| /// asset.AddControlScheme("Gamepad") | |
| /// .WithRequiredDevice<Gamepad>()); | |
| /// asset.AddControlScheme("DualGamepad") | |
| /// .WithRequiredDevice<Gamepad>()) | |
| /// .WithOptionalGamepad<Gamepad>()); | |
| /// | |
| /// // Add some devices that we can test with. | |
| /// var keyboard = InputSystem.AddDevice<Keyboard>(); | |
| /// var mouse = InputSystem.AddDevice<Mouse>(); | |
| /// var gamepad1 = InputSystem.AddDevice<Gamepad>(); | |
| /// var gamepad2 = InputSystem.AddDevice<Gamepad>(); | |
| /// | |
| /// // Matching with just a keyboard won't match any scheme. | |
| /// InputControlScheme.FindControlSchemeForDevices( | |
| /// new InputDevice[] { keyboard }, asset.controlSchemes); | |
| /// | |
| /// // Matching with a keyboard and mouse with match the "KeyboardMouse" scheme. | |
| /// InputControlScheme.FindControlSchemeForDevices( | |
| /// new InputDevice[] { keyboard, mouse }, asset.controlSchemes); | |
| /// | |
| /// // Matching with a single gamepad will match the "Gamepad" scheme. | |
| /// // Note that since the second gamepad is optional in "DualGamepad" could | |
| /// // match the same set of devices but it doesn't match any better than | |
| /// // "Gamepad" and that one comes first in the list. | |
| /// InputControlScheme.FindControlSchemeForDevices( | |
| /// new InputDevice[] { gamepad1 }, asset.controlSchemes); | |
| /// | |
| /// // Matching with two gamepads will match the "DualGamepad" scheme. | |
| /// // Note that "Gamepad" will match this device list as well. If "DualGamepad" | |
| /// // didn't exist, "Gamepad" would be the result here. However, "DualGamepad" | |
| /// // matches the list better than "Gamepad" so that's what gets returned here. | |
| /// InputControlScheme.FindControlSchemeForDevices( | |
| /// new InputDevice[] { gamepad1, gamepad2 }, asset.controlSchemes); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| public static InputControlScheme? FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes, InputDevice mustIncludeDevice = null, bool allowUnsuccesfulMatch = false) | |
| where TDevices : IReadOnlyList<InputDevice> | |
| where TSchemes : IEnumerable<InputControlScheme> | |
| { | |
| if (devices == null) | |
| throw new ArgumentNullException(nameof(devices)); | |
| if (schemes == null) | |
| throw new ArgumentNullException(nameof(schemes)); | |
| if (!FindControlSchemeForDevices(devices, schemes, out var controlScheme, out var matchResult, mustIncludeDevice, allowUnsuccesfulMatch)) | |
| return null; | |
| matchResult.Dispose(); | |
| return controlScheme; | |
| } | |
| public static bool FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes, | |
| out InputControlScheme controlScheme, out MatchResult matchResult, InputDevice mustIncludeDevice = null, bool allowUnsuccessfulMatch = false) | |
| where TDevices : IReadOnlyList<InputDevice> | |
| where TSchemes : IEnumerable<InputControlScheme> | |
| { | |
| if (devices == null) | |
| throw new ArgumentNullException(nameof(devices)); | |
| if (schemes == null) | |
| throw new ArgumentNullException(nameof(schemes)); | |
| MatchResult? bestResult = null; | |
| InputControlScheme? bestScheme = null; | |
| foreach (var scheme in schemes) | |
| { | |
| var result = scheme.PickDevicesFrom(devices, favorDevice: mustIncludeDevice); | |
| // Ignore if scheme doesn't fit devices. | |
| if (!result.isSuccessfulMatch && (!allowUnsuccessfulMatch || result.score <= 0)) | |
| { | |
| result.Dispose(); | |
| continue; | |
| } | |
| // Ignore if we have a device we specifically want to be part of the result and | |
| // the current match doesn't have it. | |
| if (mustIncludeDevice != null && !result.devices.Contains(mustIncludeDevice)) | |
| { | |
| result.Dispose(); | |
| continue; | |
| } | |
| // Ignore if it does fit but we already have a better fit. | |
| if (bestResult != null && bestResult.Value.score >= result.score) | |
| { | |
| result.Dispose(); | |
| continue; | |
| } | |
| bestResult?.Dispose(); | |
| bestResult = result; | |
| bestScheme = scheme; | |
| } | |
| matchResult = bestResult ?? default; | |
| controlScheme = bestScheme ?? default; | |
| return bestResult.HasValue; | |
| } | |
| ////FIXME: docs are wrong now | |
| /// <summary> | |
| /// Return the first control schemes from the given list that supports the given | |
| /// device (see <see cref="SupportsDevice"/>). | |
| /// </summary> | |
| /// <param name="device">An input device.</param> | |
| /// <param name="schemes">A list of control schemes. Can be empty.</param> | |
| /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam> | |
| /// <returns>The first schemes from <paramref name="schemes"/> that supports <paramref name="device"/> | |
| /// or <c>null</c> if none of the schemes is usable with the device.</returns> | |
| /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c> -or- | |
| /// <paramref name="schemes"/> is <c>null</c>.</exception> | |
| public static InputControlScheme? FindControlSchemeForDevice<TSchemes>(InputDevice device, TSchemes schemes) | |
| where TSchemes : IEnumerable<InputControlScheme> | |
| { | |
| if (schemes == null) | |
| throw new ArgumentNullException(nameof(schemes)); | |
| if (device == null) | |
| throw new ArgumentNullException(nameof(device)); | |
| return FindControlSchemeForDevices(new OneOrMore<InputDevice, ReadOnlyArray<InputDevice>>(device), schemes); | |
| } | |
| /// <summary> | |
| /// Whether the control scheme has a requirement in <see cref="deviceRequirements"/> that | |
| /// targets the given device. | |
| /// </summary> | |
| /// <param name="device">An input device.</param> | |
| /// <returns>True if the control scheme has a device requirement matching the device.</returns> | |
| /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> | |
| /// <remarks> | |
| /// Note that both optional (see <see cref="DeviceRequirement.isOptional"/>) and non-optional | |
| /// device requirements are taken into account. | |
| /// | |
| /// </remarks> | |
| public bool SupportsDevice(InputDevice device) | |
| { | |
| if (device == null) | |
| throw new ArgumentNullException(nameof(device)); | |
| ////REVIEW: does this need to take AND and OR into account? | |
| for (var i = 0; i < m_DeviceRequirements.Length; ++i) | |
| { | |
| var control = InputControlPath.TryFindControl(device, m_DeviceRequirements[i].controlPath); | |
| if (control != null) | |
| return true; | |
| } | |
| return false; | |
| } | |
| ////REVIEW: have mode where instead of matching only the first device that matches a requirement, we match as many | |
| //// as we can get? (could be useful for single-player) | |
| /// <summary> | |
| /// Based on a list of devices, make a selection that matches the <see cref="deviceRequirements">requirements</see> | |
| /// imposed by the control scheme. | |
| /// </summary> | |
| /// <param name="devices">A list of devices to choose from.</param> | |
| /// <param name="favorDevice">If not null, the device will be favored over other devices in <paramref name="devices"/>. | |
| /// Note that the device must be present in the list also.</param> | |
| /// <returns>A <see cref="MatchResult"/> structure containing the result of the pick. Note that this structure | |
| /// must be manually <see cref="MatchResult.Dispose">disposed</see> or unmanaged memory will be leaked.</returns> | |
| /// <remarks> | |
| /// Does not allocate managed memory. | |
| /// </remarks> | |
| public MatchResult PickDevicesFrom<TDevices>(TDevices devices, InputDevice favorDevice = null) | |
| where TDevices : IReadOnlyList<InputDevice> | |
| { | |
| // Empty device requirements match anything while not really picking anything. | |
| if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0) | |
| { | |
| return new MatchResult | |
| { | |
| m_Result = MatchResult.Result.AllSatisfied, | |
| // Prevent zero score on successful match but make less than one which would | |
| // result from having a single requirement. | |
| m_Score = 0.5f, | |
| }; | |
| } | |
| // Go through each requirement and match it. | |
| // NOTE: Even if `devices` is empty, we don't know yet whether we have a NoMatch. | |
| // All our devices may be optional. | |
| var haveAllRequired = true; | |
| var haveAllOptional = true; | |
| var requirementCount = m_DeviceRequirements.Length; | |
| var score = 0f; | |
| var controls = new InputControlList<InputControl>(Allocator.Persistent, requirementCount); | |
| try | |
| { | |
| var orChainIsSatisfied = false; | |
| var orChainHasRequiredDevices = false; | |
| for (var i = 0; i < requirementCount; ++i) | |
| { | |
| var isOR = m_DeviceRequirements[i].isOR; | |
| var isOptional = m_DeviceRequirements[i].isOptional; | |
| // If this is an OR requirement and we already have a match in this OR chain, | |
| // skip this requirement. | |
| if (isOR && orChainIsSatisfied) | |
| { | |
| // Skill need to add an entry for this requirement. | |
| controls.Add(null); | |
| continue; | |
| } | |
| // Null and empty paths shouldn't make it into the list but make double | |
| // sure here. Simply ignore entries that don't have a path. | |
| var path = m_DeviceRequirements[i].controlPath; | |
| if (string.IsNullOrEmpty(path)) | |
| { | |
| score += 1; | |
| controls.Add(null); | |
| continue; | |
| } | |
| // Find the first matching control among the devices we have. | |
| InputControl match = null; | |
| for (var n = 0; n < devices.Count; ++n) | |
| { | |
| var device = devices[n]; | |
| // If we should favor a device, we swap it in at index #0 regardless | |
| // of where in the list the device occurs (it MUST, however, occur in the list). | |
| if (favorDevice != null) | |
| { | |
| if (n == 0) | |
| device = favorDevice; | |
| else if (device == favorDevice) | |
| device = devices[0]; | |
| } | |
| // See if we have a match. | |
| var matchedControl = InputControlPath.TryFindControl(device, path); | |
| if (matchedControl == null) | |
| continue; // No. | |
| // We have a match but if we've already matched the same control through another requirement, | |
| // we can't use the match. | |
| if (controls.Contains(matchedControl)) | |
| continue; | |
| match = matchedControl; | |
| // Compute score for match. | |
| var deviceLayoutOfControlPath = new InternedString(InputControlPath.TryGetDeviceLayout(path)); | |
| if (deviceLayoutOfControlPath.IsEmpty()) | |
| { | |
| // Generic match adds 1 to score. | |
| score += 1; | |
| } | |
| else | |
| { | |
| var deviceLayoutOfControl = matchedControl.device.m_Layout; | |
| if (InputControlLayout.s_Layouts.ComputeDistanceInInheritanceHierarchy(deviceLayoutOfControlPath, | |
| deviceLayoutOfControl, out var distance)) | |
| { | |
| score += 1 + 1f / (Math.Abs(distance) + 1); | |
| } | |
| else | |
| { | |
| // Shouldn't really get here as for the control to be a match for the path, the device layouts | |
| // would be expected to be related to each other. But just add 1 for a generic match and go on. | |
| score += 1; | |
| } | |
| } | |
| break; | |
| } | |
| // Check requirements in AND and OR chains. We look ahead here to find out whether | |
| // the next requirement is starting an OR chain. As the OR combines with the previous | |
| // requirement in the list, this affects our current requirement. | |
| var nextIsOR = i + 1 < requirementCount && m_DeviceRequirements[i + 1].isOR; | |
| if (nextIsOR) | |
| { | |
| // Shouldn't get here if the chain is already satisfied. Should be handled | |
| // at beginning of loop and we shouldn't even be looking at finding controls | |
| // in that case. | |
| Debug.Assert(!orChainIsSatisfied); | |
| // It's an OR with the next requirement. Depends on the outcome of other matches whether | |
| // we're good or not. | |
| if (match != null) | |
| { | |
| // First match in this chain. | |
| orChainIsSatisfied = true; | |
| } | |
| else | |
| { | |
| // Chain not satisfied yet. | |
| if (!isOptional) | |
| orChainHasRequiredDevices = true; | |
| } | |
| } | |
| else if (isOR && i == requirementCount - 1) | |
| { | |
| // It's an OR at the very end of the requirements list. Terminate | |
| // the OR chain. | |
| if (match == null) | |
| { | |
| if (orChainHasRequiredDevices) | |
| haveAllRequired = false; | |
| else | |
| haveAllOptional = false; | |
| } | |
| } | |
| else | |
| { | |
| // It's an AND. | |
| if (match == null) | |
| { | |
| if (isOptional) | |
| haveAllOptional = false; | |
| else | |
| haveAllRequired = false; | |
| } | |
| // Terminate ongoing OR chain. | |
| if (i > 0 && m_DeviceRequirements[i - 1].isOR) | |
| { | |
| if (!orChainIsSatisfied) | |
| { | |
| if (orChainHasRequiredDevices) | |
| haveAllRequired = false; | |
| else | |
| haveAllOptional = false; | |
| } | |
| orChainIsSatisfied = false; | |
| } | |
| } | |
| // Add match to list. Maybe null. | |
| controls.Add(match); | |
| } | |
| // We should have matched each of our requirements. | |
| Debug.Assert(controls.Count == requirementCount); | |
| } | |
| catch (Exception) | |
| { | |
| controls.Dispose(); | |
| throw; | |
| } | |
| return new MatchResult | |
| { | |
| m_Result = !haveAllRequired | |
| ? MatchResult.Result.MissingRequired | |
| : !haveAllOptional | |
| ? MatchResult.Result.MissingOptional | |
| : MatchResult.Result.AllSatisfied, | |
| m_Controls = controls, | |
| m_Requirements = m_DeviceRequirements, | |
| m_Score = score, | |
| }; | |
| } | |
| public bool Equals(InputControlScheme other) | |
| { | |
| if (!(string.Equals(m_Name, other.m_Name, StringComparison.InvariantCultureIgnoreCase) && | |
| string.Equals(m_BindingGroup, other.m_BindingGroup, StringComparison.InvariantCultureIgnoreCase))) | |
| return false; | |
| // Compare device requirements. | |
| if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0) | |
| return other.m_DeviceRequirements == null || other.m_DeviceRequirements.Length == 0; | |
| if (other.m_DeviceRequirements == null || m_DeviceRequirements.Length != other.m_DeviceRequirements.Length) | |
| return false; | |
| var deviceCount = m_DeviceRequirements.Length; | |
| for (var i = 0; i < deviceCount; ++i) | |
| { | |
| var device = m_DeviceRequirements[i]; | |
| var haveMatch = false; | |
| for (var n = 0; n < deviceCount; ++n) | |
| { | |
| if (other.m_DeviceRequirements[n] == device) | |
| { | |
| haveMatch = true; | |
| break; | |
| } | |
| } | |
| if (!haveMatch) | |
| return false; | |
| } | |
| return true; | |
| } | |
| public override bool Equals(object obj) | |
| { | |
| if (ReferenceEquals(null, obj)) | |
| return false; | |
| return obj is InputControlScheme && Equals((InputControlScheme)obj); | |
| } | |
| public override int GetHashCode() | |
| { | |
| unchecked | |
| { | |
| var hashCode = (m_Name != null ? m_Name.GetHashCode() : 0); | |
| hashCode = (hashCode * 397) ^ (m_BindingGroup != null ? m_BindingGroup.GetHashCode() : 0); | |
| hashCode = (hashCode * 397) ^ (m_DeviceRequirements != null ? m_DeviceRequirements.GetHashCode() : 0); | |
| return hashCode; | |
| } | |
| } | |
| public override string ToString() | |
| { | |
| if (string.IsNullOrEmpty(m_Name)) | |
| return base.ToString(); | |
| if (m_DeviceRequirements == null) | |
| return m_Name; | |
| var builder = new StringBuilder(); | |
| builder.Append(m_Name); | |
| builder.Append('('); | |
| var isFirst = true; | |
| foreach (var device in m_DeviceRequirements) | |
| { | |
| if (!isFirst) | |
| builder.Append(','); | |
| builder.Append(device.controlPath); | |
| isFirst = false; | |
| } | |
| builder.Append(')'); | |
| return builder.ToString(); | |
| } | |
| public static bool operator==(InputControlScheme left, InputControlScheme right) | |
| { | |
| return left.Equals(right); | |
| } | |
| public static bool operator!=(InputControlScheme left, InputControlScheme right) | |
| { | |
| return !left.Equals(right); | |
| } | |
| [] internal string m_Name; | |
| [] internal string m_BindingGroup; | |
| [] internal DeviceRequirement[] m_DeviceRequirements; | |
| /// <summary> | |
| /// The result of matching a list of <see cref="InputDevice">devices</see> against a list of | |
| /// <see cref="DeviceRequirement">requirements</see> in an <see cref="InputControlScheme"/>. | |
| /// </summary> | |
| /// <remarks> | |
| /// This struct uses <see cref="InputControlList{TControl}"/> which allocates unmanaged memory | |
| /// and thus must be disposed in order to not leak unmanaged heap memory. | |
| /// </remarks> | |
| /// <seealso cref="InputControlScheme.PickDevicesFrom{TDevices}"/> | |
| public struct MatchResult : IEnumerable<MatchResult.Match>, IDisposable | |
| { | |
| /// <summary> | |
| /// Overall, relative measure for how well the control scheme matches. | |
| /// </summary> | |
| /// <value>Scoring value for the control scheme match.</value> | |
| /// <remarks> | |
| /// Two control schemes may, for example, both support gamepads but one may be tailored to a specific | |
| /// gamepad whereas the other one is a generic gamepad control scheme. To differentiate the two, we need | |
| /// to know not only that a control schemes but how well it matches relative to other schemes. This is | |
| /// what the score value is used for. | |
| /// | |
| /// Scores are computed primarily based on layouts referenced from device requirements. To start with, each | |
| /// matching device requirement (whether optional or mandatory) will add 1 to the score. This the base | |
| /// score of a match. Then, for each requirement a delta is computed from the device layout referenced by | |
| /// the requirement to the device layout used by the matching control. For example, if the requirement is | |
| /// <c>"<Gamepad></c> and the matching control uses the <see cref="DualShock.DualShock4GamepadHID"/> | |
| /// layout, the delta is 2 as the latter layout is derived from <see cref="Gamepad"/> via the intermediate | |
| /// <see cref="DualShock.DualShockGamepad"/> layout, i.e. two steps in the inheritance hierarchy. The | |
| /// <em>inverse</em> of the delta plus one, i.e. <c>1/(delta+1)</c> is then added to the score. This means | |
| /// that an exact match will add an additional 1 to the score and less exact matches will add progressively | |
| /// smaller values to the score (proportional to the distance of the actual layout to the one used in the | |
| /// requirement). | |
| /// | |
| /// What this leads to is that, for example, a control scheme with a <c>"<Gamepad>"</c> requirement | |
| /// will match a <see cref="DualShock.DualShock4GamepadHID"/> with a <em>lower</em> score than a control | |
| /// scheme with a <c>"<DualShockGamepad>"</c> requirement as the <see cref="Gamepad"/> layout is | |
| /// further removed (i.e. smaller inverse delta) from <see cref="DualShock.DualShock4GamepadHID"/> than | |
| /// <see cref="DualShock.DualShockGamepad"/>. | |
| /// </remarks> | |
| public float score => m_Score; | |
| /// <summary> | |
| /// Whether the device requirements got successfully matched. | |
| /// </summary> | |
| /// <value>True if the scheme's device requirements were satisfied.</value> | |
| public bool isSuccessfulMatch => m_Result != Result.MissingRequired; | |
| /// <summary> | |
| /// Whether there are missing required devices. | |
| /// </summary> | |
| /// <value>True if there are missing, non-optional devices.</value> | |
| /// <seealso cref="DeviceRequirement.isOptional"/> | |
| public bool hasMissingRequiredDevices => m_Result == Result.MissingRequired; | |
| /// <summary> | |
| /// Whether there are missing optional devices. This does not prevent | |
| /// a successful match. | |
| /// </summary> | |
| /// <value>True if there are missing optional devices.</value> | |
| /// <seealso cref="DeviceRequirement.isOptional"/> | |
| public bool hasMissingOptionalDevices => m_Result == Result.MissingOptional; | |
| /// <summary> | |
| /// The devices that got picked from the available devices. | |
| /// </summary> | |
| public InputControlList<InputDevice> devices | |
| { | |
| get | |
| { | |
| // Lazily construct the device list. If we have missing required | |
| // devices, though, always return an empty list. The user can still see | |
| // the individual matches on each of the requirement entries but we | |
| // consider the device picking itself failed. | |
| if (m_Devices.Count == 0 && !hasMissingRequiredDevices) | |
| { | |
| var controlCount = m_Controls.Count; | |
| if (controlCount != 0) | |
| { | |
| m_Devices.Capacity = controlCount; | |
| for (var i = 0; i < controlCount; ++i) | |
| { | |
| var control = m_Controls[i]; | |
| if (control == null) | |
| continue; | |
| var device = control.device; | |
| if (m_Devices.Contains(device)) | |
| continue; // Duplicate match of same device. | |
| m_Devices.Add(device); | |
| } | |
| } | |
| } | |
| return m_Devices; | |
| } | |
| } | |
| public Match this[int index] | |
| { | |
| get | |
| { | |
| if (index < 0 || m_Requirements == null || index >= m_Requirements.Length) | |
| throw new ArgumentOutOfRangeException("index"); | |
| return new Match | |
| { | |
| m_RequirementIndex = index, | |
| m_Requirements = m_Requirements, | |
| m_Controls = m_Controls, | |
| }; | |
| } | |
| } | |
| /// <summary> | |
| /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme. | |
| /// </summary> | |
| /// <returns>An enumerate going over each individual match.</returns> | |
| public IEnumerator<Match> GetEnumerator() | |
| { | |
| return new Enumerator | |
| { | |
| m_Index = -1, | |
| m_Requirements = m_Requirements, | |
| m_Controls = m_Controls, | |
| }; | |
| } | |
| /// <summary> | |
| /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme. | |
| /// </summary> | |
| /// <returns>An enumerate going over each individual match.</returns> | |
| IEnumerator IEnumerable.GetEnumerator() | |
| { | |
| return GetEnumerator(); | |
| } | |
| /// <summary> | |
| /// Discard the list of devices. | |
| /// </summary> | |
| public void Dispose() | |
| { | |
| m_Controls.Dispose(); | |
| m_Devices.Dispose(); | |
| } | |
| internal Result m_Result; | |
| internal float m_Score; | |
| internal InputControlList<InputDevice> m_Devices; | |
| internal InputControlList<InputControl> m_Controls; | |
| internal DeviceRequirement[] m_Requirements; | |
| internal enum Result | |
| { | |
| AllSatisfied, | |
| MissingRequired, | |
| MissingOptional, | |
| } | |
| ////REVIEW: would be great to not have to repeatedly copy InputControlLists around | |
| /// <summary> | |
| /// A single matched <see cref="DeviceRequirement"/>. | |
| /// </summary> | |
| /// <remarks> | |
| /// Links the control that was matched with the respective device requirement. | |
| /// </remarks> | |
| [] | |
| public struct Match | |
| { | |
| /// <summary> | |
| /// The control that was match from the requirement's <see cref="DeviceRequirement.controlPath"/> | |
| /// </summary> | |
| /// <remarks> | |
| /// This is the same as <see cref="device"/> if the <see cref="DeviceRequirement.controlPath">control | |
| /// path</see> matches the device directly rather than matching a control on the device. | |
| /// | |
| /// Note that while a control path can match arbitrary many controls, only the first matched control | |
| /// will be returned here. To get all controls that were matched by a specific requirement, a | |
| /// manual query must be performed using <see cref="InputControlPath"/>. | |
| /// | |
| /// If the match failed, this will be null. | |
| /// </remarks> | |
| public InputControl control => m_Controls[m_RequirementIndex]; | |
| /// <summary> | |
| /// The device that got matched. | |
| /// </summary> | |
| /// <remarks> | |
| /// If a specific control on the device was matched, this will be <see cref="InputControl.device"/> or | |
| /// <see cref="control"/>. If a device was matched directly, this will be the same as <see cref="control"/>. | |
| /// </remarks> | |
| public InputDevice device | |
| { | |
| get | |
| { | |
| var control = this.control; | |
| return control?.device; | |
| } | |
| } | |
| /// <summary> | |
| /// Index of the requirement in <see cref="InputControlScheme.deviceRequirements"/>. | |
| /// </summary> | |
| public int requirementIndex => m_RequirementIndex; | |
| /// <summary> | |
| /// The device requirement that got matched. | |
| /// </summary> | |
| public DeviceRequirement requirement => m_Requirements[m_RequirementIndex]; | |
| public bool isOptional => requirement.isOptional; | |
| internal int m_RequirementIndex; | |
| internal DeviceRequirement[] m_Requirements; | |
| internal InputControlList<InputControl> m_Controls; | |
| } | |
| private struct Enumerator : IEnumerator<Match> | |
| { | |
| public bool MoveNext() | |
| { | |
| ++m_Index; | |
| return m_Requirements != null && m_Index < m_Requirements.Length; | |
| } | |
| public void Reset() | |
| { | |
| m_Index = -1; | |
| } | |
| public Match Current | |
| { | |
| get | |
| { | |
| if (m_Requirements == null || m_Index < 0 || m_Index >= m_Requirements.Length) | |
| throw new InvalidOperationException("Enumerator is not valid"); | |
| return new Match | |
| { | |
| m_RequirementIndex = m_Index, | |
| m_Requirements = m_Requirements, | |
| m_Controls = m_Controls, | |
| }; | |
| } | |
| } | |
| object IEnumerator.Current => Current; | |
| public void Dispose() | |
| { | |
| } | |
| internal int m_Index; | |
| internal DeviceRequirement[] m_Requirements; | |
| internal InputControlList<InputControl> m_Controls; | |
| } | |
| } | |
| /// <summary> | |
| /// | |
| /// </summary> | |
| /// <remarks> | |
| /// Note that device requirements may require specific controls to be present rather than only requiring | |
| /// the presence of a certain type of device. For example, a requirement with a <see cref="controlPath"/> | |
| /// of "*/{PrimaryAction}" will be satisfied by any device that has a control marked as <see cref="CommonUsages.PrimaryAction"/>. | |
| /// | |
| /// Requirements are ordered in a list and can combine with their previous requirement in either <see cref="isAND"> | |
| /// AND</see> or in <see cref="isOR">OR</see> fashion. The default is for requirements to combine with AND. | |
| /// | |
| /// Note that it is not possible to express nested constraints like <c>(a AND b) OR (c AND d)</c>. Also note that | |
| /// operator precedence is the opposite of C#, meaning that OR has *higher* precedence than AND. This means | |
| /// that <c>a OR b AND c OR d</c> reads as <c>(a OR b) AND (c OR d)</c> (in C# it would read as <c>a OR | |
| /// (b AND c) OR d</c>. | |
| /// | |
| /// More complex expressions can often be expressed differently. For example, <c>(a AND b) OR (c AND d)</c> | |
| /// can be expressed as <c>a OR c AND b OR d</c>. | |
| /// </remarks> | |
| [] | |
| public struct DeviceRequirement : IEquatable<DeviceRequirement> | |
| { | |
| /// <summary> | |
| /// <see cref="InputControlPath">Control path</see> that is matched against a device to determine | |
| /// whether it qualifies for the control scheme. | |
| /// </summary> | |
| /// <remarks> | |
| /// </remarks> | |
| /// <example> | |
| /// <code> | |
| /// // A left-hand XR controller. | |
| /// "<XRController>{LeftHand}" | |
| /// | |
| /// // A gamepad. | |
| /// "<Gamepad>" | |
| /// </code> | |
| /// </example> | |
| public string controlPath | |
| { | |
| get => m_ControlPath; | |
| set => m_ControlPath = value; | |
| } | |
| /// <summary> | |
| /// If true, a device with the given <see cref="controlPath">device path</see> is employed by the | |
| /// control scheme if one is available. If none is available, the control scheme is still | |
| /// functional. | |
| /// </summary> | |
| public bool isOptional | |
| { | |
| get => (m_Flags & Flags.Optional) != 0; | |
| set | |
| { | |
| if (value) | |
| m_Flags |= Flags.Optional; | |
| else | |
| m_Flags &= ~Flags.Optional; | |
| } | |
| } | |
| /// <summary> | |
| /// Whether the requirement combines with the previous requirement (if any) as a boolean AND. | |
| /// </summary> | |
| /// <remarks> | |
| /// This is the default. For example, to require both a left hand and a right XR controller, | |
| /// the first requirement would be for "<XRController>{LeftHand}" and the second | |
| /// requirement would be for ">XRController>{RightHand}" and would return true for this | |
| /// property. | |
| /// </remarks> | |
| /// <seealso cref="isOR"/> | |
| public bool isAND | |
| { | |
| get => !isOR; | |
| set => isOR = !value; | |
| } | |
| /// <summary> | |
| /// Whether the requirement combines with the previous requirement (if any) as a boolean OR. | |
| /// </summary> | |
| /// <remarks> | |
| /// This allows designing control schemes that flexibly work with combinations of devices such that | |
| /// if one specific device isn't present, another device can substitute for it. | |
| /// | |
| /// For example, to design a mouse+keyboard control scheme that can alternatively work with a pen | |
| /// instead of a mouse, the first requirement could be for "<Keyboard>", the second one | |
| /// could be for "<Mouse>" and the third one could be for "<Pen>" and return true | |
| /// for this property. Both the mouse and the pen would be marked as required (i.e. not <see cref="isOptional"/>) | |
| /// but the device requirements are satisfied even if either device is present. | |
| /// | |
| /// Note that if both a pen and a mouse are present at the same time, still only one device is | |
| /// picked. In this case, the mouse "wins" as it comes first in the list of requirements. | |
| /// </remarks> | |
| public bool isOR | |
| { | |
| get => (m_Flags & Flags.Or) != 0; | |
| set | |
| { | |
| if (value) | |
| m_Flags |= Flags.Or; | |
| else | |
| m_Flags &= ~Flags.Or; | |
| } | |
| } | |
| [] internal string m_ControlPath; | |
| [] internal Flags m_Flags; | |
| [] | |
| internal enum Flags | |
| { | |
| None = 0, | |
| Optional = 1 << 0, | |
| Or = 1 << 1, | |
| } | |
| public override string ToString() | |
| { | |
| if (!string.IsNullOrEmpty(controlPath)) | |
| { | |
| if (isOptional) | |
| return controlPath + " (Optional)"; | |
| return controlPath + " (Required)"; | |
| } | |
| return base.ToString(); | |
| } | |
| public bool Equals(DeviceRequirement other) | |
| { | |
| return string.Equals(m_ControlPath, other.m_ControlPath) && m_Flags == other.m_Flags && | |
| string.Equals(controlPath, other.controlPath) && isOptional == other.isOptional; | |
| } | |
| public override bool Equals(object obj) | |
| { | |
| if (ReferenceEquals(null, obj)) | |
| return false; | |
| return obj is DeviceRequirement && Equals((DeviceRequirement)obj); | |
| } | |
| public override int GetHashCode() | |
| { | |
| unchecked | |
| { | |
| var hashCode = (m_ControlPath != null ? m_ControlPath.GetHashCode() : 0); | |
| hashCode = (hashCode * 397) ^ m_Flags.GetHashCode(); | |
| hashCode = (hashCode * 397) ^ (controlPath != null ? controlPath.GetHashCode() : 0); | |
| hashCode = (hashCode * 397) ^ isOptional.GetHashCode(); | |
| return hashCode; | |
| } | |
| } | |
| public static bool operator==(DeviceRequirement left, DeviceRequirement right) | |
| { | |
| return left.Equals(right); | |
| } | |
| public static bool operator!=(DeviceRequirement left, DeviceRequirement right) | |
| { | |
| return !left.Equals(right); | |
| } | |
| } | |
| /// <summary> | |
| /// JSON-serialized form of a control scheme. | |
| /// </summary> | |
| [] | |
| internal struct SchemeJson | |
| { | |
| public string name; | |
| public string bindingGroup; | |
| public DeviceJson[] devices; | |
| [] | |
| public struct DeviceJson | |
| { | |
| public string devicePath; | |
| public bool isOptional; | |
| public bool isOR; | |
| public DeviceRequirement ToDeviceEntry() | |
| { | |
| return new DeviceRequirement | |
| { | |
| controlPath = devicePath, | |
| isOptional = isOptional, | |
| isOR = isOR, | |
| }; | |
| } | |
| public static DeviceJson From(DeviceRequirement requirement) | |
| { | |
| return new DeviceJson | |
| { | |
| devicePath = requirement.controlPath, | |
| isOptional = requirement.isOptional, | |
| isOR = requirement.isOR, | |
| }; | |
| } | |
| } | |
| public InputControlScheme ToScheme() | |
| { | |
| DeviceRequirement[] deviceRequirements = null; | |
| if (devices != null && devices.Length > 0) | |
| { | |
| var count = devices.Length; | |
| deviceRequirements = new DeviceRequirement[count]; | |
| for (var i = 0; i < count; ++i) | |
| deviceRequirements[i] = devices[i].ToDeviceEntry(); | |
| } | |
| return new InputControlScheme | |
| { | |
| m_Name = string.IsNullOrEmpty(name) ? null : name, | |
| m_BindingGroup = string.IsNullOrEmpty(bindingGroup) ? null : bindingGroup, | |
| m_DeviceRequirements = deviceRequirements, | |
| }; | |
| } | |
| public static SchemeJson ToJson(InputControlScheme scheme) | |
| { | |
| DeviceJson[] devices = null; | |
| if (scheme.m_DeviceRequirements != null && scheme.m_DeviceRequirements.Length > 0) | |
| { | |
| var count = scheme.m_DeviceRequirements.Length; | |
| devices = new DeviceJson[count]; | |
| for (var i = 0; i < count; ++i) | |
| devices[i] = DeviceJson.From(scheme.m_DeviceRequirements[i]); | |
| } | |
| return new SchemeJson | |
| { | |
| name = scheme.m_Name, | |
| bindingGroup = scheme.m_BindingGroup, | |
| devices = devices, | |
| }; | |
| } | |
| public static SchemeJson[] ToJson(InputControlScheme[] schemes) | |
| { | |
| if (schemes == null || schemes.Length == 0) | |
| return null; | |
| var count = schemes.Length; | |
| var result = new SchemeJson[count]; | |
| for (var i = 0; i < count; ++i) | |
| result[i] = ToJson(schemes[i]); | |
| return result; | |
| } | |
| public static InputControlScheme[] ToSchemes(SchemeJson[] schemes) | |
| { | |
| if (schemes == null || schemes.Length == 0) | |
| return null; | |
| var count = schemes.Length; | |
| var result = new InputControlScheme[count]; | |
| for (var i = 0; i < count; ++i) | |
| result[i] = schemes[i].ToScheme(); | |
| return result; | |
| } | |
| } | |
| } | |
| } | |