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; #if UNITY_EDITOR using UnityEditor; #endif ////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 { /// /// A named set of zero or more device requirements along with an associated binding group. /// /// /// 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 level. /// /// /// [Serializable] public struct InputControlScheme : IEquatable { /// /// Name of the control scheme. Not null or empty except if InputControlScheme /// instance is invalid (i.e. default-initialized). /// /// Name of the scheme. /// /// May be empty or null except if the control scheme is part of an . /// /// public string name => m_Name; /// /// Binding group that is associated with the control scheme. Not null or empty /// except if InputControlScheme is invalid (i.e. default-initialized). /// /// Binding group for the scheme. /// /// All bindings in this group are considered to be part of the control scheme. /// /// public string bindingGroup { get => m_BindingGroup; set => m_BindingGroup = value; } /// /// Devices used by the control scheme. /// /// Device requirements of the scheme. /// /// 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 will not work with such control schemes. /// public ReadOnlyArray deviceRequirements => new ReadOnlyArray(m_DeviceRequirements); /// /// Initialize the control scheme with the given name, device requirements, /// and binding group. /// /// Name to use for the scheme. Required. /// List of device requirements. /// Name to use for the binding group (see ) /// associated with the control scheme. If this is null or empty, is /// used instead (with characters stripped from the name). /// is null or empty. public InputControlScheme(string name, IEnumerable 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; } } #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS internal InputControlScheme(SerializedProperty sp) { var requirements = new List(); 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; } #endif 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; } /// /// Given a list of devices and a list of control schemes, find the most suitable control /// scheme to use with the devices. /// /// A list of devices. If the list is empty, only schemes with /// empty lists will get matched. /// A list of control schemes. /// If not null, a successful match has to include the given device. /// 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. /// Collection type to use for the list of devices. /// Collection type to use for the list of schemes. /// The control scheme that best matched the given devices or null if no /// scheme was found suitable. /// is null -or- /// is null. /// /// Any successful match (see ) will be considered. /// The one that matches the most amount of devices (see ) /// 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 to find the devices that a control scheme selects. /// /// This method is parameterized over and /// to allow avoiding GC heap allocations from boxing of structs such as . /// /// /// /// // 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); /// /// /// public static InputControlScheme? FindControlSchemeForDevices(TDevices devices, TSchemes schemes, InputDevice mustIncludeDevice = null, bool allowUnsuccesfulMatch = false) where TDevices : IReadOnlyList where TSchemes : IEnumerable { 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 devices, TSchemes schemes, out InputControlScheme controlScheme, out MatchResult matchResult, InputDevice mustIncludeDevice = null, bool allowUnsuccessfulMatch = false) where TDevices : IReadOnlyList where TSchemes : IEnumerable { 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 /// /// Return the first control schemes from the given list that supports the given /// device (see ). /// /// An input device. /// A list of control schemes. Can be empty. /// Collection type to use for the list of schemes. /// The first schemes from that supports /// or null if none of the schemes is usable with the device. /// is null -or- /// is null. public static InputControlScheme? FindControlSchemeForDevice(InputDevice device, TSchemes schemes) where TSchemes : IEnumerable { if (schemes == null) throw new ArgumentNullException(nameof(schemes)); if (device == null) throw new ArgumentNullException(nameof(device)); return FindControlSchemeForDevices(new OneOrMore>(device), schemes); } /// /// Whether the control scheme has a requirement in that /// targets the given device. /// /// An input device. /// True if the control scheme has a device requirement matching the device. /// is null. /// /// Note that both optional (see ) and non-optional /// device requirements are taken into account. /// /// 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) /// /// Based on a list of devices, make a selection that matches the requirements /// imposed by the control scheme. /// /// A list of devices to choose from. /// If not null, the device will be favored over other devices in . /// Note that the device must be present in the list also. /// A structure containing the result of the pick. Note that this structure /// must be manually disposed or unmanaged memory will be leaked. /// /// Does not allocate managed memory. /// public MatchResult PickDevicesFrom(TDevices devices, InputDevice favorDevice = null) where TDevices : IReadOnlyList { // 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(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; /// /// The result of matching a list of devices against a list of /// requirements in an . /// /// /// This struct uses which allocates unmanaged memory /// and thus must be disposed in order to not leak unmanaged heap memory. /// /// public struct MatchResult : IEnumerable, IDisposable { /// /// Overall, relative measure for how well the control scheme matches. /// /// Scoring value for the control scheme match. /// /// 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 /// "<Gamepad> and the matching control uses the /// layout, the delta is 2 as the latter layout is derived from via the intermediate /// layout, i.e. two steps in the inheritance hierarchy. The /// inverse of the delta plus one, i.e. 1/(delta+1) 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 "<Gamepad>" requirement /// will match a with a lower score than a control /// scheme with a "<DualShockGamepad>" requirement as the layout is /// further removed (i.e. smaller inverse delta) from than /// . /// public float score => m_Score; /// /// Whether the device requirements got successfully matched. /// /// True if the scheme's device requirements were satisfied. public bool isSuccessfulMatch => m_Result != Result.MissingRequired; /// /// Whether there are missing required devices. /// /// True if there are missing, non-optional devices. /// public bool hasMissingRequiredDevices => m_Result == Result.MissingRequired; /// /// Whether there are missing optional devices. This does not prevent /// a successful match. /// /// True if there are missing optional devices. /// public bool hasMissingOptionalDevices => m_Result == Result.MissingOptional; /// /// The devices that got picked from the available devices. /// public InputControlList 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, }; } } /// /// Enumerate the match for each individual in the control scheme. /// /// An enumerate going over each individual match. public IEnumerator GetEnumerator() { return new Enumerator { m_Index = -1, m_Requirements = m_Requirements, m_Controls = m_Controls, }; } /// /// Enumerate the match for each individual in the control scheme. /// /// An enumerate going over each individual match. IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// /// Discard the list of devices. /// public void Dispose() { m_Controls.Dispose(); m_Devices.Dispose(); } internal Result m_Result; internal float m_Score; internal InputControlList m_Devices; internal InputControlList m_Controls; internal DeviceRequirement[] m_Requirements; internal enum Result { AllSatisfied, MissingRequired, MissingOptional, } ////REVIEW: would be great to not have to repeatedly copy InputControlLists around /// /// A single matched . /// /// /// Links the control that was matched with the respective device requirement. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "Conflicts with UnityEngine.Networking.Match, which is deprecated and will go away.")] public struct Match { /// /// The control that was match from the requirement's /// /// /// This is the same as if the control /// path 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 . /// /// If the match failed, this will be null. /// public InputControl control => m_Controls[m_RequirementIndex]; /// /// The device that got matched. /// /// /// If a specific control on the device was matched, this will be or /// . If a device was matched directly, this will be the same as . /// public InputDevice device { get { var control = this.control; return control?.device; } } /// /// Index of the requirement in . /// public int requirementIndex => m_RequirementIndex; /// /// The device requirement that got matched. /// public DeviceRequirement requirement => m_Requirements[m_RequirementIndex]; public bool isOptional => requirement.isOptional; internal int m_RequirementIndex; internal DeviceRequirement[] m_Requirements; internal InputControlList m_Controls; } private struct Enumerator : IEnumerator { 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 m_Controls; } } /// /// /// /// /// 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 /// of "*/{PrimaryAction}" will be satisfied by any device that has a control marked as . /// /// Requirements are ordered in a list and can combine with their previous requirement in either /// AND or in OR fashion. The default is for requirements to combine with AND. /// /// Note that it is not possible to express nested constraints like (a AND b) OR (c AND d). Also note that /// operator precedence is the opposite of C#, meaning that OR has *higher* precedence than AND. This means /// that a OR b AND c OR d reads as (a OR b) AND (c OR d) (in C# it would read as a OR /// (b AND c) OR d. /// /// More complex expressions can often be expressed differently. For example, (a AND b) OR (c AND d) /// can be expressed as a OR c AND b OR d. /// [Serializable] public struct DeviceRequirement : IEquatable { /// /// Control path that is matched against a device to determine /// whether it qualifies for the control scheme. /// /// /// /// /// /// // A left-hand XR controller. /// "<XRController>{LeftHand}" /// /// // A gamepad. /// "<Gamepad>" /// /// public string controlPath { get => m_ControlPath; set => m_ControlPath = value; } /// /// If true, a device with the given device path is employed by the /// control scheme if one is available. If none is available, the control scheme is still /// functional. /// public bool isOptional { get => (m_Flags & Flags.Optional) != 0; set { if (value) m_Flags |= Flags.Optional; else m_Flags &= ~Flags.Optional; } } /// /// Whether the requirement combines with the previous requirement (if any) as a boolean AND. /// /// /// 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. /// /// public bool isAND { get => !isOR; set => isOR = !value; } /// /// Whether the requirement combines with the previous requirement (if any) as a boolean OR. /// /// /// 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 ) /// 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. /// 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); } } /// /// JSON-serialized form of a control scheme. /// [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; } } } }