12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145 |
- 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;
- ////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)"/>
- [Serializable]
- 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 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;
- }
- /// <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);
- }
- [SerializeField] internal string m_Name;
- [SerializeField] internal string m_BindingGroup;
- [SerializeField] 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>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "Conflicts with UnityEngine.Networking.Match, which is deprecated and will go away.")]
- 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>
- [Serializable]
- 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;
- }
- }
- [SerializeField] internal string m_ControlPath;
- [SerializeField] internal Flags m_Flags;
- [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>
- [Serializable]
- internal struct SchemeJson
- {
- public string name;
- public string bindingGroup;
- public DeviceJson[] devices;
- [Serializable]
- 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;
- }
- }
- }
- }
|