b486678290
Library -Artifacts
3797 lines
176 KiB
C#
3797 lines
176 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Unity.Collections;
|
|
using UnityEngine.InputSystem.Composites;
|
|
using UnityEngine.InputSystem.Controls;
|
|
using Unity.Collections.LowLevel.Unsafe;
|
|
using UnityEngine.Profiling;
|
|
using UnityEngine.InputSystem.LowLevel;
|
|
using UnityEngine.InputSystem.Processors;
|
|
using UnityEngine.InputSystem.Interactions;
|
|
using UnityEngine.InputSystem.Utilities;
|
|
using UnityEngine.InputSystem.Layouts;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEngine.InputSystem.Editor;
|
|
#endif
|
|
|
|
////TODO: make diagnostics available in dev players and give it a public API to enable them
|
|
|
|
////TODO: work towards InputManager having no direct knowledge of actions
|
|
|
|
////TODO: allow pushing events into the system any which way; decouple from the buffer in NativeInputSystem being the only source
|
|
|
|
////REVIEW: change the event properties over to using IObservable?
|
|
|
|
////REVIEW: instead of RegisterInteraction and RegisterProcessor, have a generic RegisterInterface (or something)?
|
|
|
|
////REVIEW: can we do away with the 'previous == previous frame' and simply buffer flip on every value write?
|
|
|
|
////REVIEW: should we force keeping mouse/pen/keyboard/touch around in editor even if not in list of supported devices?
|
|
|
|
////REVIEW: do we want to filter out state events that result in no state change?
|
|
|
|
#pragma warning disable CS0649
|
|
namespace UnityEngine.InputSystem
|
|
{
|
|
using DeviceChangeListener = Action<InputDevice, InputDeviceChange>;
|
|
using DeviceStateChangeListener = Action<InputDevice, InputEventPtr>;
|
|
using LayoutChangeListener = Action<string, InputControlLayoutChange>;
|
|
using EventListener = Action<InputEventPtr, InputDevice>;
|
|
using UpdateListener = Action;
|
|
|
|
/// <summary>
|
|
/// Hub of the input system.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Not exposed. Use <see cref="InputSystem"/> as the public entry point to the system.
|
|
///
|
|
/// Manages devices, layouts, and event processing.
|
|
/// </remarks>
|
|
internal partial class InputManager
|
|
{
|
|
public ReadOnlyArray<InputDevice> devices => new ReadOnlyArray<InputDevice>(m_Devices, 0, m_DevicesCount);
|
|
|
|
public TypeTable processors => m_Processors;
|
|
public TypeTable interactions => m_Interactions;
|
|
public TypeTable composites => m_Composites;
|
|
|
|
public InputMetrics metrics
|
|
{
|
|
get
|
|
{
|
|
var result = m_Metrics;
|
|
|
|
result.currentNumDevices = m_DevicesCount;
|
|
result.currentStateSizeInBytes = (int)m_StateBuffers.totalSize;
|
|
|
|
// Count controls.
|
|
result.currentControlCount = m_DevicesCount;
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
result.currentControlCount += m_Devices[i].allControls.Count;
|
|
|
|
// Count layouts.
|
|
result.currentLayoutCount = m_Layouts.layoutTypes.Count;
|
|
result.currentLayoutCount += m_Layouts.layoutStrings.Count;
|
|
result.currentLayoutCount += m_Layouts.layoutBuilders.Count;
|
|
result.currentLayoutCount += m_Layouts.layoutOverrides.Count;
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public InputSettings settings
|
|
{
|
|
get
|
|
{
|
|
Debug.Assert(m_Settings != null);
|
|
return m_Settings;
|
|
}
|
|
set
|
|
{
|
|
if (value == null)
|
|
throw new ArgumentNullException(nameof(value));
|
|
|
|
if (m_Settings == value)
|
|
return;
|
|
|
|
m_Settings = value;
|
|
ApplySettings();
|
|
}
|
|
}
|
|
|
|
public InputUpdateType updateMask
|
|
{
|
|
get => m_UpdateMask;
|
|
set
|
|
{
|
|
// In editor, we don't allow disabling editor updates.
|
|
#if UNITY_EDITOR
|
|
value |= InputUpdateType.Editor;
|
|
#endif
|
|
|
|
if (m_UpdateMask == value)
|
|
return;
|
|
|
|
m_UpdateMask = value;
|
|
|
|
// Recreate state buffers.
|
|
if (m_DevicesCount > 0)
|
|
ReallocateStateBuffers();
|
|
}
|
|
}
|
|
|
|
public InputUpdateType defaultUpdateType
|
|
{
|
|
get
|
|
{
|
|
if (m_CurrentUpdate != default)
|
|
return m_CurrentUpdate;
|
|
|
|
#if UNITY_EDITOR
|
|
if (!m_RunPlayerUpdatesInEditMode && (!gameIsPlaying || !gameHasFocus))
|
|
return InputUpdateType.Editor;
|
|
#endif
|
|
|
|
return m_UpdateMask.GetUpdateTypeForPlayer();
|
|
}
|
|
}
|
|
|
|
public float pollingFrequency
|
|
{
|
|
get => m_PollingFrequency;
|
|
set
|
|
{
|
|
////REVIEW: allow setting to zero to turn off polling altogether?
|
|
if (value <= 0)
|
|
throw new ArgumentException("Polling frequency must be greater than zero", "value");
|
|
|
|
m_PollingFrequency = value;
|
|
if (m_Runtime != null)
|
|
m_Runtime.pollingFrequency = value;
|
|
}
|
|
}
|
|
|
|
public event DeviceChangeListener onDeviceChange
|
|
{
|
|
add => m_DeviceChangeListeners.AddCallback(value);
|
|
remove => m_DeviceChangeListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public event DeviceStateChangeListener onDeviceStateChange
|
|
{
|
|
add => m_DeviceStateChangeListeners.AddCallback(value);
|
|
remove => m_DeviceStateChangeListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public event InputDeviceCommandDelegate onDeviceCommand
|
|
{
|
|
add => m_DeviceCommandCallbacks.AddCallback(value);
|
|
remove => m_DeviceCommandCallbacks.RemoveCallback(value);
|
|
}
|
|
|
|
////REVIEW: would be great to have a way to sort out precedence between two callbacks
|
|
public event InputDeviceFindControlLayoutDelegate onFindControlLayoutForDevice
|
|
{
|
|
add
|
|
{
|
|
m_DeviceFindLayoutCallbacks.AddCallback(value);
|
|
|
|
// Having a new callback on this event can change the set of devices we recognize.
|
|
// See if there's anything in the list of available devices that we can now turn
|
|
// into an InputDevice whereas we couldn't before.
|
|
//
|
|
// NOTE: A callback could also impact already existing devices and theoretically alter
|
|
// what layout we would have used for those. We do *NOT* retroactively apply
|
|
// those changes.
|
|
AddAvailableDevicesThatAreNowRecognized();
|
|
}
|
|
remove => m_DeviceFindLayoutCallbacks.RemoveCallback(value);
|
|
}
|
|
|
|
public event LayoutChangeListener onLayoutChange
|
|
{
|
|
add => m_LayoutChangeListeners.AddCallback(value);
|
|
remove => m_LayoutChangeListeners.RemoveCallback(value);
|
|
}
|
|
|
|
////TODO: add InputEventBuffer struct that uses NativeArray underneath
|
|
////TODO: make InputEventTrace use NativeArray
|
|
////TODO: introduce an alternative that consumes events in bulk
|
|
public event EventListener onEvent
|
|
{
|
|
add => m_EventListeners.AddCallback(value);
|
|
remove => m_EventListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public event UpdateListener onBeforeUpdate
|
|
{
|
|
add
|
|
{
|
|
InstallBeforeUpdateHookIfNecessary();
|
|
m_BeforeUpdateListeners.AddCallback(value);
|
|
}
|
|
remove => m_BeforeUpdateListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public event UpdateListener onAfterUpdate
|
|
{
|
|
add => m_AfterUpdateListeners.AddCallback(value);
|
|
remove => m_AfterUpdateListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public event Action onSettingsChange
|
|
{
|
|
add => m_SettingsChangedListeners.AddCallback(value);
|
|
remove => m_SettingsChangedListeners.RemoveCallback(value);
|
|
}
|
|
|
|
public bool isProcessingEvents => m_InputEventStream.isOpen;
|
|
|
|
#if UNITY_EDITOR
|
|
private bool m_RunPlayerUpdatesInEditMode;
|
|
|
|
/// <summary>
|
|
/// If true, consider the editor to be in "perpetual play mode". Meaning, we ignore editor
|
|
/// updates and just go and continuously process Dynamic/Fixed/BeforeRender regardless of
|
|
/// whether we're in play mode or not.
|
|
///
|
|
/// In this mode, we also ignore game view focus.
|
|
/// </summary>
|
|
public bool runPlayerUpdatesInEditMode
|
|
{
|
|
get => m_RunPlayerUpdatesInEditMode;
|
|
set => m_RunPlayerUpdatesInEditMode = value;
|
|
}
|
|
#endif
|
|
|
|
private bool gameIsPlaying =>
|
|
#if UNITY_EDITOR
|
|
(m_Runtime.isInPlayMode && !m_Runtime.isPaused) || m_RunPlayerUpdatesInEditMode;
|
|
#else
|
|
true;
|
|
#endif
|
|
|
|
private bool gameHasFocus =>
|
|
#if UNITY_EDITOR
|
|
m_RunPlayerUpdatesInEditMode || m_HasFocus || gameShouldGetInputRegardlessOfFocus;
|
|
#else
|
|
m_HasFocus || gameShouldGetInputRegardlessOfFocus;
|
|
#endif
|
|
|
|
private bool gameShouldGetInputRegardlessOfFocus =>
|
|
m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus
|
|
#if UNITY_EDITOR
|
|
&& m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView
|
|
#endif
|
|
;
|
|
|
|
////TODO: when registering a layout that exists as a layout of a different type (type vs string vs constructor),
|
|
//// remove the existing registration
|
|
|
|
// Add a layout constructed from a type.
|
|
// If a layout with the same name already exists, the new layout
|
|
// takes its place.
|
|
public void RegisterControlLayout(string name, Type type)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
throw new ArgumentNullException(nameof(name));
|
|
if (type == null)
|
|
throw new ArgumentNullException(nameof(type));
|
|
|
|
// Note that since InputDevice derives from InputControl, isDeviceLayout implies
|
|
// isControlLayout to be true as well.
|
|
var isDeviceLayout = typeof(InputDevice).IsAssignableFrom(type);
|
|
var isControlLayout = typeof(InputControl).IsAssignableFrom(type);
|
|
|
|
if (!isDeviceLayout && !isControlLayout)
|
|
throw new ArgumentException($"Types used as layouts have to be InputControls or InputDevices; '{type.Name}' is a '{type.BaseType.Name}'",
|
|
nameof(type));
|
|
|
|
var internedName = new InternedString(name);
|
|
var isReplacement = m_Layouts.HasLayout(internedName);
|
|
|
|
// All we do is enter the type into a map. We don't construct an InputControlLayout
|
|
// from it until we actually need it in an InputDeviceBuilder to create a device.
|
|
// This not only avoids us creating a bunch of objects on the managed heap but
|
|
// also avoids us laboriously constructing a XRController layout, for example,
|
|
// in a game that never uses XR.
|
|
m_Layouts.layoutTypes[internedName] = type;
|
|
|
|
////TODO: make this independent of initialization order
|
|
////TODO: re-scan base type information after domain reloads
|
|
|
|
// Walk class hierarchy all the way up to InputControl to see
|
|
// if there's another type that's been registered as a layout.
|
|
// If so, make it a base layout for this one.
|
|
string baseLayout = null;
|
|
for (var baseType = type.BaseType; baseLayout == null && baseType != typeof(InputControl);
|
|
baseType = baseType.BaseType)
|
|
{
|
|
foreach (var entry in m_Layouts.layoutTypes)
|
|
if (entry.Value == baseType)
|
|
{
|
|
baseLayout = entry.Key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
PerformLayoutPostRegistration(internedName, new InlinedArray<InternedString>(new InternedString(baseLayout)),
|
|
isReplacement, isKnownToBeDeviceLayout: isDeviceLayout);
|
|
}
|
|
|
|
public void RegisterControlLayout(string json, string name = null, bool isOverride = false)
|
|
{
|
|
if (string.IsNullOrEmpty(json))
|
|
throw new ArgumentNullException(nameof(json));
|
|
|
|
////REVIEW: as long as no one has instantiated the layout, the base layout information is kinda pointless
|
|
|
|
// Parse out name, device description, and base layout.
|
|
InputControlLayout.ParseHeaderFieldsFromJson(json, out var nameFromJson, out var baseLayouts,
|
|
out var deviceMatcher);
|
|
|
|
// Decide whether to take name from JSON or from code.
|
|
var internedLayoutName = new InternedString(name);
|
|
if (internedLayoutName.IsEmpty())
|
|
{
|
|
internedLayoutName = nameFromJson;
|
|
|
|
// Make sure we have a name.
|
|
if (internedLayoutName.IsEmpty())
|
|
throw new ArgumentException("Layout name has not been given and is not set in JSON layout",
|
|
nameof(name));
|
|
}
|
|
|
|
// If it's an override, it must have a layout the overrides apply to.
|
|
if (isOverride && baseLayouts.length == 0)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Layout override '{internedLayoutName}' must have 'extend' property mentioning layout to which to apply the overrides",
|
|
nameof(json));
|
|
}
|
|
|
|
// Add it to our records.
|
|
var isReplacement = m_Layouts.HasLayout(internedLayoutName);
|
|
if (isReplacement && isOverride)
|
|
{ // Do not allow a layout override to replace a "base layout" by name, but allow layout overrides
|
|
// to replace an existing layout override.
|
|
// This is required to guarantee that its a hierarchy (directed graph) rather
|
|
// than a cyclic graph.
|
|
|
|
var isReplacingOverride = m_Layouts.layoutOverrideNames.Contains(internedLayoutName);
|
|
if (!isReplacingOverride)
|
|
{
|
|
throw new ArgumentException($"Failed to register layout override '{internedLayoutName}'" +
|
|
$"since a layout named '{internedLayoutName}' already exist. Layout overrides must " +
|
|
$"have unique names with respect to existing layouts.");
|
|
}
|
|
}
|
|
|
|
m_Layouts.layoutStrings[internedLayoutName] = json;
|
|
if (isOverride)
|
|
{
|
|
m_Layouts.layoutOverrideNames.Add(internedLayoutName);
|
|
for (var i = 0; i < baseLayouts.length; ++i)
|
|
{
|
|
var baseLayoutName = baseLayouts[i];
|
|
m_Layouts.layoutOverrides.TryGetValue(baseLayoutName, out var overrideList);
|
|
if (!isReplacement)
|
|
ArrayHelpers.Append(ref overrideList, internedLayoutName);
|
|
|
|
|
|
m_Layouts.layoutOverrides[baseLayoutName] = overrideList;
|
|
}
|
|
}
|
|
|
|
PerformLayoutPostRegistration(internedLayoutName, baseLayouts,
|
|
isReplacement: isReplacement, isOverride: isOverride);
|
|
|
|
// If the layout contained a device matcher, register it.
|
|
if (!deviceMatcher.empty)
|
|
RegisterControlLayoutMatcher(internedLayoutName, deviceMatcher);
|
|
}
|
|
|
|
public void RegisterControlLayoutBuilder(Func<InputControlLayout> method, string name,
|
|
string baseLayout = null)
|
|
{
|
|
if (method == null)
|
|
throw new ArgumentNullException(nameof(method));
|
|
if (string.IsNullOrEmpty(name))
|
|
throw new ArgumentNullException(nameof(name));
|
|
|
|
var internedLayoutName = new InternedString(name);
|
|
var internedBaseLayoutName = new InternedString(baseLayout);
|
|
var isReplacement = m_Layouts.HasLayout(internedLayoutName);
|
|
|
|
m_Layouts.layoutBuilders[internedLayoutName] = method;
|
|
|
|
PerformLayoutPostRegistration(internedLayoutName, new InlinedArray<InternedString>(internedBaseLayoutName),
|
|
isReplacement);
|
|
}
|
|
|
|
private void PerformLayoutPostRegistration(InternedString layoutName, InlinedArray<InternedString> baseLayouts,
|
|
bool isReplacement, bool isKnownToBeDeviceLayout = false, bool isOverride = false)
|
|
{
|
|
++m_LayoutRegistrationVersion;
|
|
|
|
// Force-clear layout cache. Don't clear reference count so that
|
|
// the cache gets cleared out properly when released in case someone
|
|
// is using it ATM.
|
|
InputControlLayout.s_CacheInstance.Clear();
|
|
|
|
// For layouts that aren't overrides, add the name of the base
|
|
// layout to the lookup table.
|
|
if (!isOverride && baseLayouts.length > 0)
|
|
{
|
|
if (baseLayouts.length > 1)
|
|
throw new NotSupportedException(
|
|
$"Layout '{layoutName}' has multiple base layouts; this is only supported on layout overrides");
|
|
|
|
var baseLayoutName = baseLayouts[0];
|
|
if (!baseLayoutName.IsEmpty())
|
|
m_Layouts.baseLayoutTable[layoutName] = baseLayoutName;
|
|
}
|
|
|
|
// Nuke any precompiled layouts that are invalidated by the layout registration.
|
|
m_Layouts.precompiledLayouts.Remove(layoutName);
|
|
if (m_Layouts.precompiledLayouts.Count > 0)
|
|
{
|
|
foreach (var layout in m_Layouts.precompiledLayouts.Keys.ToArray())
|
|
{
|
|
var metadata = m_Layouts.precompiledLayouts[layout].metadata;
|
|
|
|
// If it's an override, we remove any precompiled layouts to which overrides are applied.
|
|
if (isOverride)
|
|
{
|
|
for (var i = 0; i < baseLayouts.length; ++i)
|
|
if (layout == baseLayouts[i] ||
|
|
StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(metadata,
|
|
baseLayouts[i], ';'))
|
|
m_Layouts.precompiledLayouts.Remove(layout);
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, we remove any precompile layouts that use the layout we just changed.
|
|
if (StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(metadata,
|
|
layoutName, ';'))
|
|
m_Layouts.precompiledLayouts.Remove(layout);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recreate any devices using the layout. If it's an override, recreate devices using any of the base layouts.
|
|
if (isOverride)
|
|
{
|
|
for (var i = 0; i < baseLayouts.length; ++i)
|
|
RecreateDevicesUsingLayout(baseLayouts[i], isKnownToBeDeviceLayout: isKnownToBeDeviceLayout);
|
|
}
|
|
else
|
|
{
|
|
RecreateDevicesUsingLayout(layoutName, isKnownToBeDeviceLayout: isKnownToBeDeviceLayout);
|
|
}
|
|
|
|
// In the editor, layouts may become available successively after a domain reload so
|
|
// we may end up retaining device information all the way until we run the first full
|
|
// player update. For every layout we register, we check here whether we have a saved
|
|
// device state using a layout with the same name but not having a device description
|
|
// (the latter is important as in that case, we should go through the normal matching
|
|
// process and not just rely on the name of the layout). If so, we try here to recreate
|
|
// the device with the just registered layout.
|
|
#if UNITY_EDITOR
|
|
for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i)
|
|
{
|
|
ref var deviceState = ref m_SavedDeviceStates[i];
|
|
if (layoutName != deviceState.layout || !deviceState.description.empty)
|
|
continue;
|
|
|
|
if (RestoreDeviceFromSavedState(ref deviceState, layoutName))
|
|
{
|
|
ArrayHelpers.EraseAt(ref m_SavedDeviceStates, i);
|
|
--i;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Let listeners know.
|
|
var change = isReplacement ? InputControlLayoutChange.Replaced : InputControlLayoutChange.Added;
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_LayoutChangeListeners, layoutName.ToString(), change, "InputSystem.onLayoutChange");
|
|
}
|
|
|
|
public void RegisterPrecompiledLayout<TDevice>(string metadata)
|
|
where TDevice : InputDevice, new()
|
|
{
|
|
if (metadata == null)
|
|
throw new ArgumentNullException(nameof(metadata));
|
|
|
|
var deviceType = typeof(TDevice).BaseType;
|
|
var layoutName = FindOrRegisterDeviceLayoutForType(deviceType);
|
|
|
|
m_Layouts.precompiledLayouts[layoutName] = new InputControlLayout.Collection.PrecompiledLayout
|
|
{
|
|
factoryMethod = () => new TDevice(),
|
|
metadata = metadata
|
|
};
|
|
}
|
|
|
|
private void RecreateDevicesUsingLayout(InternedString layout, bool isKnownToBeDeviceLayout = false)
|
|
{
|
|
if (m_DevicesCount == 0)
|
|
return;
|
|
|
|
List<InputDevice> devicesUsingLayout = null;
|
|
|
|
// Find all devices using the layout.
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
|
|
bool usesLayout;
|
|
if (isKnownToBeDeviceLayout)
|
|
usesLayout = IsControlUsingLayout(device, layout);
|
|
else
|
|
usesLayout = IsControlOrChildUsingLayoutRecursive(device, layout);
|
|
|
|
if (usesLayout)
|
|
{
|
|
if (devicesUsingLayout == null)
|
|
devicesUsingLayout = new List<InputDevice>();
|
|
devicesUsingLayout.Add(device);
|
|
}
|
|
}
|
|
|
|
// If there's none, we're good.
|
|
if (devicesUsingLayout == null)
|
|
return;
|
|
|
|
// Remove and re-add the matching devices.
|
|
using (InputDeviceBuilder.Ref())
|
|
{
|
|
for (var i = 0; i < devicesUsingLayout.Count; ++i)
|
|
{
|
|
var device = devicesUsingLayout[i];
|
|
RecreateDevice(device, device.m_Layout);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsControlOrChildUsingLayoutRecursive(InputControl control, InternedString layout)
|
|
{
|
|
// Check control itself.
|
|
if (IsControlUsingLayout(control, layout))
|
|
return true;
|
|
|
|
// Check children.
|
|
var children = control.children;
|
|
for (var i = 0; i < children.Count; ++i)
|
|
if (IsControlOrChildUsingLayoutRecursive(children[i], layout))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsControlUsingLayout(InputControl control, InternedString layout)
|
|
{
|
|
// Check direct match.
|
|
if (control.layout == layout)
|
|
return true;
|
|
|
|
// Check base layout chain.
|
|
var baseLayout = control.m_Layout;
|
|
while (m_Layouts.baseLayoutTable.TryGetValue(baseLayout, out baseLayout))
|
|
if (baseLayout == layout)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
public void RegisterControlLayoutMatcher(string layoutName, InputDeviceMatcher matcher)
|
|
{
|
|
if (string.IsNullOrEmpty(layoutName))
|
|
throw new ArgumentNullException(nameof(layoutName));
|
|
if (matcher.empty)
|
|
throw new ArgumentException("Matcher cannot be empty", nameof(matcher));
|
|
|
|
// Add to table.
|
|
var internedLayoutName = new InternedString(layoutName);
|
|
m_Layouts.AddMatcher(internedLayoutName, matcher);
|
|
|
|
// Recreate any device that we match better than its current layout.
|
|
RecreateDevicesUsingLayoutWithInferiorMatch(matcher);
|
|
|
|
// See if we can make sense of any device we couldn't make sense of before.
|
|
AddAvailableDevicesMatchingDescription(matcher, internedLayoutName);
|
|
}
|
|
|
|
public void RegisterControlLayoutMatcher(Type type, InputDeviceMatcher matcher)
|
|
{
|
|
if (type == null)
|
|
throw new ArgumentNullException(nameof(type));
|
|
if (matcher.empty)
|
|
throw new ArgumentException("Matcher cannot be empty", nameof(matcher));
|
|
|
|
var layoutName = m_Layouts.TryFindLayoutForType(type);
|
|
if (layoutName.IsEmpty())
|
|
throw new ArgumentException(
|
|
$"Type '{type.Name}' has not been registered as a control layout", nameof(type));
|
|
|
|
RegisterControlLayoutMatcher(layoutName, matcher);
|
|
}
|
|
|
|
private void RecreateDevicesUsingLayoutWithInferiorMatch(InputDeviceMatcher deviceMatcher)
|
|
{
|
|
if (m_DevicesCount == 0)
|
|
return;
|
|
|
|
using (InputDeviceBuilder.Ref())
|
|
{
|
|
var deviceCount = m_DevicesCount;
|
|
for (var i = 0; i < deviceCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
var deviceDescription = device.description;
|
|
|
|
if (deviceDescription.empty || !(deviceMatcher.MatchPercentage(deviceDescription) > 0))
|
|
continue;
|
|
|
|
var layoutName = TryFindMatchingControlLayout(ref deviceDescription, device.deviceId);
|
|
if (layoutName != device.m_Layout)
|
|
{
|
|
device.m_Description = deviceDescription;
|
|
|
|
RecreateDevice(device, layoutName);
|
|
|
|
// We're removing devices in the middle of the array and appending
|
|
// them at the end. Adjust our index and device count to make sure
|
|
// we're not iterating all the way into already processed devices.
|
|
|
|
--i;
|
|
--deviceCount;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RecreateDevice(InputDevice oldDevice, InternedString newLayout)
|
|
{
|
|
// Remove.
|
|
RemoveDevice(oldDevice, keepOnListOfAvailableDevices: true);
|
|
|
|
// Re-setup device.
|
|
var newDevice = InputDevice.Build<InputDevice>(newLayout, oldDevice.m_Variants,
|
|
deviceDescription: oldDevice.m_Description);
|
|
|
|
// Preserve device properties that should not be changed by the re-creation
|
|
// of a device.
|
|
newDevice.m_DeviceId = oldDevice.m_DeviceId;
|
|
newDevice.m_Description = oldDevice.m_Description;
|
|
if (oldDevice.native)
|
|
newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.Native;
|
|
if (oldDevice.remote)
|
|
newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.Remote;
|
|
if (!oldDevice.enabled)
|
|
{
|
|
newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.DisabledStateHasBeenQueriedFromRuntime;
|
|
newDevice.m_DeviceFlags |= InputDevice.DeviceFlags.DisabledInFrontend;
|
|
}
|
|
|
|
// Re-add.
|
|
AddDevice(newDevice);
|
|
}
|
|
|
|
private void AddAvailableDevicesMatchingDescription(InputDeviceMatcher matcher, InternedString layout)
|
|
{
|
|
#if UNITY_EDITOR
|
|
// If we still have some devices saved from the last domain reload, see
|
|
// if they are matched by the given matcher. If so, turn them into devices.
|
|
for (var i = 0; i < m_SavedDeviceStates.LengthSafe(); ++i)
|
|
{
|
|
ref var deviceState = ref m_SavedDeviceStates[i];
|
|
if (matcher.MatchPercentage(deviceState.description) > 0)
|
|
{
|
|
RestoreDeviceFromSavedState(ref deviceState, layout);
|
|
ArrayHelpers.EraseAt(ref m_SavedDeviceStates, i);
|
|
--i;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// See if the new description to layout mapping allows us to make
|
|
// sense of a device we couldn't make sense of so far.
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
// Ignore if it's a device that has been explicitly removed.
|
|
if (m_AvailableDevices[i].isRemoved)
|
|
continue;
|
|
|
|
var deviceId = m_AvailableDevices[i].deviceId;
|
|
if (TryGetDeviceById(deviceId) != null)
|
|
continue;
|
|
|
|
if (matcher.MatchPercentage(m_AvailableDevices[i].description) > 0f)
|
|
{
|
|
// Try to create InputDevice instance.
|
|
try
|
|
{
|
|
AddDevice(layout, deviceId, deviceDescription: m_AvailableDevices[i].description,
|
|
deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(
|
|
$"Layout '{layout}' matches existing device '{m_AvailableDevices[i].description}' but failed to instantiate: {exception}");
|
|
Debug.LogException(exception);
|
|
continue;
|
|
}
|
|
|
|
// Re-enable device.
|
|
var command = EnableDeviceCommand.Create();
|
|
m_Runtime.DeviceCommand(deviceId, ref command);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RemoveControlLayout(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
throw new ArgumentNullException(nameof(name));
|
|
|
|
var internedName = new InternedString(name);
|
|
|
|
// Remove all devices using the layout.
|
|
for (var i = 0; i < m_DevicesCount;)
|
|
{
|
|
var device = m_Devices[i];
|
|
if (IsControlOrChildUsingLayoutRecursive(device, internedName))
|
|
{
|
|
RemoveDevice(device, keepOnListOfAvailableDevices: true);
|
|
}
|
|
else
|
|
{
|
|
++i;
|
|
}
|
|
}
|
|
|
|
// Remove layout record.
|
|
m_Layouts.layoutTypes.Remove(internedName);
|
|
m_Layouts.layoutStrings.Remove(internedName);
|
|
m_Layouts.layoutBuilders.Remove(internedName);
|
|
m_Layouts.baseLayoutTable.Remove(internedName);
|
|
++m_LayoutRegistrationVersion;
|
|
|
|
////TODO: check all layout inheritance chain for whether they are based on the layout and if so
|
|
//// remove those layouts, too
|
|
|
|
// Let listeners know.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_LayoutChangeListeners, name, InputControlLayoutChange.Removed, "InputSystem.onLayoutChange");
|
|
}
|
|
|
|
public InputControlLayout TryLoadControlLayout(Type type)
|
|
{
|
|
if (type == null)
|
|
throw new ArgumentNullException(nameof(type));
|
|
if (!typeof(InputControl).IsAssignableFrom(type))
|
|
throw new ArgumentException($"Type '{type.Name}' is not an InputControl", nameof(type));
|
|
|
|
// Find the layout name that the given type was registered with.
|
|
var layoutName = m_Layouts.TryFindLayoutForType(type);
|
|
if (layoutName.IsEmpty())
|
|
throw new ArgumentException(
|
|
$"Type '{type.Name}' has not been registered as a control layout", nameof(type));
|
|
|
|
return m_Layouts.TryLoadLayout(layoutName);
|
|
}
|
|
|
|
public InputControlLayout TryLoadControlLayout(InternedString name)
|
|
{
|
|
return m_Layouts.TryLoadLayout(name);
|
|
}
|
|
|
|
////FIXME: allowing the description to be modified as part of this is surprising; find a better way
|
|
public InternedString TryFindMatchingControlLayout(ref InputDeviceDescription deviceDescription, int deviceId = InputDevice.InvalidDeviceId)
|
|
{
|
|
Profiler.BeginSample("InputSystem.TryFindMatchingControlLayout");
|
|
////TODO: this will want to take overrides into account
|
|
|
|
// See if we can match by description.
|
|
var layoutName = m_Layouts.TryFindMatchingLayout(deviceDescription);
|
|
if (layoutName.IsEmpty())
|
|
{
|
|
// No, so try to match by device class. If we have a "Gamepad" layout,
|
|
// for example, a device that classifies itself as a "Gamepad" will match
|
|
// that layout.
|
|
//
|
|
// NOTE: Have to make sure here that we get a device layout and not a
|
|
// control layout.
|
|
if (!string.IsNullOrEmpty(deviceDescription.deviceClass))
|
|
{
|
|
var deviceClassLowerCase = new InternedString(deviceDescription.deviceClass);
|
|
var type = m_Layouts.GetControlTypeForLayout(deviceClassLowerCase);
|
|
if (type != null && typeof(InputDevice).IsAssignableFrom(type))
|
|
layoutName = new InternedString(deviceDescription.deviceClass);
|
|
}
|
|
}
|
|
|
|
////REVIEW: listeners registering new layouts from in here may potentially lead to the creation of devices; should we disallow that?
|
|
////REVIEW: if a callback picks a layout, should we re-run through the list of callbacks? or should we just remove haveOverridenLayoutName?
|
|
// Give listeners a shot to select/create a layout.
|
|
if (m_DeviceFindLayoutCallbacks.length > 0)
|
|
{
|
|
// First time we get here, put our delegate for executing device commands
|
|
// in place. We wrap the call to IInputRuntime.DeviceCommand so that we don't
|
|
// need to expose the runtime to the onFindLayoutForDevice callbacks.
|
|
if (m_DeviceFindExecuteCommandDelegate == null)
|
|
m_DeviceFindExecuteCommandDelegate =
|
|
(ref InputDeviceCommand commandRef) =>
|
|
{
|
|
if (m_DeviceFindExecuteCommandDeviceId == InputDevice.InvalidDeviceId)
|
|
return InputDeviceCommand.GenericFailure;
|
|
return m_Runtime.DeviceCommand(m_DeviceFindExecuteCommandDeviceId, ref commandRef);
|
|
};
|
|
m_DeviceFindExecuteCommandDeviceId = deviceId;
|
|
|
|
var haveOverriddenLayoutName = false;
|
|
m_DeviceFindLayoutCallbacks.LockForChanges();
|
|
for (var i = 0; i < m_DeviceFindLayoutCallbacks.length; ++i)
|
|
{
|
|
try
|
|
{
|
|
var newLayout = m_DeviceFindLayoutCallbacks[i](ref deviceDescription, layoutName, m_DeviceFindExecuteCommandDelegate);
|
|
if (!string.IsNullOrEmpty(newLayout) && !haveOverriddenLayoutName)
|
|
{
|
|
layoutName = new InternedString(newLayout);
|
|
haveOverriddenLayoutName = true;
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError($"{exception.GetType().Name} while executing 'InputSystem.onFindLayoutForDevice' callbacks");
|
|
Debug.LogException(exception);
|
|
}
|
|
}
|
|
m_DeviceFindLayoutCallbacks.UnlockForChanges();
|
|
}
|
|
|
|
Profiler.EndSample();
|
|
return layoutName;
|
|
}
|
|
|
|
private InternedString FindOrRegisterDeviceLayoutForType(Type type)
|
|
{
|
|
var layoutName = m_Layouts.TryFindLayoutForType(type);
|
|
if (layoutName.IsEmpty())
|
|
{
|
|
// Automatically register the given type as a layout.
|
|
if (layoutName.IsEmpty())
|
|
{
|
|
layoutName = new InternedString(type.Name);
|
|
RegisterControlLayout(type.Name, type);
|
|
}
|
|
}
|
|
|
|
return layoutName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if the given device layout is supported by the game according to <see cref="InputSettings.supportedDevices"/>.
|
|
/// </summary>
|
|
/// <param name="layoutName">Name of the device layout.</param>
|
|
/// <returns>True if a device with the given layout should be created for the game, false otherwise.</returns>
|
|
private bool IsDeviceLayoutMarkedAsSupportedInSettings(InternedString layoutName)
|
|
{
|
|
// In the editor, "Supported Devices" can be overridden by a user setting. This causes
|
|
// all available devices to be added regardless of what "Supported Devices" says. This
|
|
// is useful to ensure that things like keyboard, mouse, and pen keep working in the editor
|
|
// even if not supported as devices in the game.
|
|
#if UNITY_EDITOR
|
|
if (InputEditorUserSettings.addDevicesNotSupportedByProject)
|
|
return true;
|
|
#endif
|
|
|
|
var supportedDevices = m_Settings.supportedDevices;
|
|
if (supportedDevices.Count == 0)
|
|
{
|
|
// If supportedDevices is empty, all device layouts are considered supported.
|
|
return true;
|
|
}
|
|
|
|
for (var n = 0; n < supportedDevices.Count; ++n)
|
|
{
|
|
var supportedLayout = new InternedString(supportedDevices[n]);
|
|
if (layoutName == supportedLayout || m_Layouts.IsBasedOn(supportedLayout, layoutName))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public IEnumerable<string> ListControlLayouts(string basedOn = null)
|
|
{
|
|
////FIXME: this may add a name twice
|
|
|
|
if (!string.IsNullOrEmpty(basedOn))
|
|
{
|
|
var internedBasedOn = new InternedString(basedOn);
|
|
foreach (var entry in m_Layouts.layoutTypes)
|
|
if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
|
|
yield return entry.Key;
|
|
foreach (var entry in m_Layouts.layoutStrings)
|
|
if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
|
|
yield return entry.Key;
|
|
foreach (var entry in m_Layouts.layoutBuilders)
|
|
if (m_Layouts.IsBasedOn(internedBasedOn, entry.Key))
|
|
yield return entry.Key;
|
|
}
|
|
else
|
|
{
|
|
foreach (var entry in m_Layouts.layoutTypes)
|
|
yield return entry.Key;
|
|
foreach (var entry in m_Layouts.layoutStrings)
|
|
yield return entry.Key;
|
|
foreach (var entry in m_Layouts.layoutBuilders)
|
|
yield return entry.Key;
|
|
}
|
|
}
|
|
|
|
// Adds all controls that match the given path spec to the given list.
|
|
// Returns number of controls added to the list.
|
|
// NOTE: Does not create garbage.
|
|
|
|
/// <summary>
|
|
/// Adds to the given list all controls that match the given <see cref="InputControlPath">path spec</see>
|
|
/// and are assignable to the given type.
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <param name="controls"></param>
|
|
/// <typeparam name="TControl"></typeparam>
|
|
/// <returns></returns>
|
|
public int GetControls<TControl>(string path, ref InputControlList<TControl> controls)
|
|
where TControl : InputControl
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return 0;
|
|
if (m_DevicesCount == 0)
|
|
return 0;
|
|
|
|
var deviceCount = m_DevicesCount;
|
|
var numMatches = 0;
|
|
for (var i = 0; i < deviceCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
numMatches += InputControlPath.TryFindControls(device, path, 0, ref controls);
|
|
}
|
|
|
|
return numMatches;
|
|
}
|
|
|
|
public void SetDeviceUsage(InputDevice device, InternedString usage)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (device.usages.Count == 1 && device.usages[0] == usage)
|
|
return;
|
|
if (device.usages.Count == 0 && usage.IsEmpty())
|
|
return;
|
|
|
|
device.ClearDeviceUsages();
|
|
if (!usage.IsEmpty())
|
|
device.AddDeviceUsage(usage);
|
|
NotifyUsageChanged(device);
|
|
}
|
|
|
|
public void AddDeviceUsage(InputDevice device, InternedString usage)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (usage.IsEmpty())
|
|
throw new ArgumentException("Usage string cannot be empty", nameof(usage));
|
|
if (device.usages.Contains(usage))
|
|
return;
|
|
|
|
device.AddDeviceUsage(usage);
|
|
NotifyUsageChanged(device);
|
|
}
|
|
|
|
public void RemoveDeviceUsage(InputDevice device, InternedString usage)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (usage.IsEmpty())
|
|
throw new ArgumentException("Usage string cannot be empty", nameof(usage));
|
|
if (!device.usages.Contains(usage))
|
|
return;
|
|
|
|
device.RemoveDeviceUsage(usage);
|
|
NotifyUsageChanged(device);
|
|
}
|
|
|
|
private void NotifyUsageChanged(InputDevice device)
|
|
{
|
|
InputActionState.OnDeviceChange(device, InputDeviceChange.UsageChanged);
|
|
|
|
// Notify listeners.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.UsageChanged, "InputSystem.onDeviceChange");
|
|
|
|
////REVIEW: This was for the XRController leftHand and rightHand getters but these do lookups dynamically now; remove?
|
|
// Usage may affect current device so update.
|
|
device.MakeCurrent();
|
|
}
|
|
|
|
////TODO: make sure that no device or control with a '/' in the name can creep into the system
|
|
|
|
public InputDevice AddDevice(Type type, string name = null)
|
|
{
|
|
if (type == null)
|
|
throw new ArgumentNullException(nameof(type));
|
|
|
|
// Find the layout name that the given type was registered with.
|
|
var layoutName = FindOrRegisterDeviceLayoutForType(type);
|
|
Debug.Assert(!layoutName.IsEmpty(), name);
|
|
|
|
// Note that since we go through the normal by-name lookup here, this will
|
|
// still work if the layout from the type was override with a string layout.
|
|
return AddDevice(layoutName, name);
|
|
}
|
|
|
|
// Creates a device from the given layout and adds it to the system.
|
|
// NOTE: Creates garbage.
|
|
public InputDevice AddDevice(string layout, string name = null, InternedString variants = new InternedString())
|
|
{
|
|
if (string.IsNullOrEmpty(layout))
|
|
throw new ArgumentNullException(nameof(layout));
|
|
|
|
var device = InputDevice.Build<InputDevice>(layout, variants);
|
|
|
|
if (!string.IsNullOrEmpty(name))
|
|
device.m_Name = new InternedString(name);
|
|
|
|
AddDevice(device);
|
|
|
|
return device;
|
|
}
|
|
|
|
// Add device with a forced ID. Used when creating devices reported to us by native.
|
|
private InputDevice AddDevice(InternedString layout, int deviceId,
|
|
string deviceName = null,
|
|
InputDeviceDescription deviceDescription = new InputDeviceDescription(),
|
|
InputDevice.DeviceFlags deviceFlags = 0,
|
|
InternedString variants = default)
|
|
{
|
|
var device = InputDevice.Build<InputDevice>(new InternedString(layout),
|
|
deviceDescription: deviceDescription,
|
|
layoutVariants: variants);
|
|
|
|
device.m_DeviceId = deviceId;
|
|
device.m_Description = deviceDescription;
|
|
device.m_DeviceFlags |= deviceFlags;
|
|
if (!string.IsNullOrEmpty(deviceName))
|
|
device.m_Name = new InternedString(deviceName);
|
|
|
|
// Default display name to product name.
|
|
if (!string.IsNullOrEmpty(deviceDescription.product))
|
|
device.m_DisplayName = deviceDescription.product;
|
|
|
|
AddDevice(device);
|
|
|
|
return device;
|
|
}
|
|
|
|
public void AddDevice(InputDevice device)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (string.IsNullOrEmpty(device.layout))
|
|
throw new InvalidOperationException("Device has no associated layout");
|
|
|
|
// Ignore if the same device gets added multiple times.
|
|
if (ArrayHelpers.Contains(m_Devices, device))
|
|
return;
|
|
|
|
MakeDeviceNameUnique(device);
|
|
AssignUniqueDeviceId(device);
|
|
|
|
// Add to list.
|
|
device.m_DeviceIndex = ArrayHelpers.AppendWithCapacity(ref m_Devices, ref m_DevicesCount, device);
|
|
|
|
////REVIEW: Not sure a full-blown dictionary is the right way here. Alternatives are to keep
|
|
//// a sparse array that directly indexes using the linearly increasing IDs (though that
|
|
//// may get large over time). Or to just do a linear search through m_Devices (but
|
|
//// that may end up tapping a bunch of memory locations in the heap to find the right
|
|
//// device; could be improved by sorting m_Devices by ID and picking a good starting
|
|
//// point based on the ID we have instead of searching from [0] always).
|
|
m_DevicesById[device.deviceId] = device;
|
|
|
|
// Let InputStateBuffers know this device doesn't have any associated state yet.
|
|
device.m_StateBlock.byteOffset = InputStateBlock.InvalidOffset;
|
|
|
|
// Update state buffers.
|
|
ReallocateStateBuffers();
|
|
InitializeDeviceState(device);
|
|
|
|
// Update metrics.
|
|
m_Metrics.maxNumDevices = Mathf.Max(m_DevicesCount, m_Metrics.maxNumDevices);
|
|
m_Metrics.maxStateSizeInBytes = Mathf.Max((int)m_StateBuffers.totalSize, m_Metrics.maxStateSizeInBytes);
|
|
|
|
// Make sure that if the device ID is listed in m_AvailableDevices, the device
|
|
// is no longer marked as removed.
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
if (m_AvailableDevices[i].deviceId == device.deviceId)
|
|
m_AvailableDevices[i].isRemoved = false;
|
|
}
|
|
|
|
// If we're running in the background, find out whether the device can run in
|
|
// the background. If not, disable it.
|
|
var isPlaying = true;
|
|
#if UNITY_EDITOR
|
|
isPlaying = m_Runtime.isInPlayMode;
|
|
#endif
|
|
if (isPlaying && !gameHasFocus
|
|
&& m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.IgnoreFocus
|
|
&& m_Runtime.runInBackground
|
|
&& device.QueryEnabledStateFromRuntime()
|
|
&& !ShouldRunDeviceInBackground(device))
|
|
{
|
|
EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
|
|
}
|
|
|
|
////REVIEW: we may want to suppress this during the initial device discovery phase
|
|
// Let actions re-resolve their paths.
|
|
InputActionState.OnDeviceChange(device, InputDeviceChange.Added);
|
|
|
|
// If the device wants automatic callbacks before input updates,
|
|
// put it on the list.
|
|
if (device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver)
|
|
onBeforeUpdate += beforeUpdateCallbackReceiver.OnUpdate;
|
|
|
|
// If the device has state callbacks, make a note of it.
|
|
if (device is IInputStateCallbackReceiver)
|
|
{
|
|
InstallBeforeUpdateHookIfNecessary();
|
|
device.m_DeviceFlags |= InputDevice.DeviceFlags.HasStateCallbacks;
|
|
m_HaveDevicesWithStateCallbackReceivers = true;
|
|
}
|
|
|
|
// If the device has event merger, make a note of it.
|
|
if (device is IEventMerger)
|
|
device.hasEventMerger = true;
|
|
|
|
// If the device has event preprocessor, make a note of it.
|
|
if (device is IEventPreProcessor)
|
|
device.hasEventPreProcessor = true;
|
|
|
|
// If the device wants before-render updates, enable them if they
|
|
// aren't already.
|
|
if (device.updateBeforeRender)
|
|
updateMask |= InputUpdateType.BeforeRender;
|
|
|
|
// Notify device.
|
|
device.NotifyAdded();
|
|
|
|
////REVIEW: is this really a good thing to do? just plugging in a device shouldn't make
|
|
//// it current, no?
|
|
// Make the device current.
|
|
// BEWARE: if this will not happen for whatever reason, you will break Android sensors,
|
|
// as they rely on .current for enabling native backend, see https://fogbugz.unity3d.com/f/cases/1371204/
|
|
device.MakeCurrent();
|
|
|
|
// Notify listeners.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Added, "InputSystem.onDeviceChange");
|
|
|
|
// Request device to send us an initial state update.
|
|
if (device.enabled)
|
|
device.RequestSync();
|
|
}
|
|
|
|
////TODO: this path should really put the device on the list of available devices
|
|
////TODO: this path should discover disconnected devices
|
|
public InputDevice AddDevice(InputDeviceDescription description)
|
|
{
|
|
////REVIEW: is throwing here really such a useful thing?
|
|
return AddDevice(description, throwIfNoLayoutFound: true);
|
|
}
|
|
|
|
public InputDevice AddDevice(InputDeviceDescription description, bool throwIfNoLayoutFound,
|
|
string deviceName = null, int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0)
|
|
{
|
|
Profiler.BeginSample("InputSystem.AddDevice");
|
|
// Look for matching layout.
|
|
var layout = TryFindMatchingControlLayout(ref description, deviceId);
|
|
|
|
// If no layout was found, bail out.
|
|
if (layout.IsEmpty())
|
|
{
|
|
if (throwIfNoLayoutFound)
|
|
throw new ArgumentException($"Cannot find layout matching device description '{description}'", nameof(description));
|
|
|
|
// If it's a device coming from the runtime, disable it.
|
|
if (deviceId != InputDevice.InvalidDeviceId)
|
|
{
|
|
var command = DisableDeviceCommand.Create();
|
|
m_Runtime.DeviceCommand(deviceId, ref command);
|
|
}
|
|
|
|
Profiler.EndSample();
|
|
return null;
|
|
}
|
|
|
|
var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags);
|
|
device.m_Description = description;
|
|
Profiler.EndSample();
|
|
return device;
|
|
}
|
|
|
|
public InputDevice AddDevice(InputDeviceDescription description, InternedString layout, string deviceName = null,
|
|
int deviceId = InputDevice.InvalidDeviceId, InputDevice.DeviceFlags deviceFlags = 0)
|
|
{
|
|
try
|
|
{
|
|
Profiler.BeginSample("InputSystem.AddDevice");
|
|
|
|
var device = AddDevice(layout, deviceId, deviceName, description, deviceFlags);
|
|
device.m_Description = description;
|
|
return device;
|
|
}
|
|
finally
|
|
{
|
|
Profiler.EndSample();
|
|
}
|
|
}
|
|
|
|
public void RemoveDevice(InputDevice device, bool keepOnListOfAvailableDevices = false)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
|
|
// If device has not been added, ignore.
|
|
if (device.m_DeviceIndex == InputDevice.kInvalidDeviceIndex)
|
|
return;
|
|
|
|
// Remove state monitors while device index is still valid.
|
|
RemoveStateChangeMonitors(device);
|
|
|
|
// Remove from device array.
|
|
var deviceIndex = device.m_DeviceIndex;
|
|
var deviceId = device.deviceId;
|
|
if (deviceIndex < m_StateChangeMonitors.LengthSafe())
|
|
{
|
|
// m_StateChangeMonitors mirrors layout of m_Devices *but* may be shorter.
|
|
var count = m_StateChangeMonitors.Length;
|
|
ArrayHelpers.EraseAtWithCapacity(m_StateChangeMonitors, ref count, deviceIndex);
|
|
}
|
|
ArrayHelpers.EraseAtWithCapacity(m_Devices, ref m_DevicesCount, deviceIndex);
|
|
|
|
m_DevicesById.Remove(deviceId);
|
|
|
|
if (m_Devices != null)
|
|
{
|
|
// Remove from state buffers.
|
|
ReallocateStateBuffers();
|
|
}
|
|
else
|
|
{
|
|
// No more devices. Kill state buffers.
|
|
m_StateBuffers.FreeAll();
|
|
}
|
|
|
|
////TODO: When we remove a native device like this, make sure we tell the backend to disable it (and re-enable it when re-add it)
|
|
|
|
// Update device indices. Do this after reallocating state buffers as that call requires
|
|
// the old indices to still be in place.
|
|
for (var i = deviceIndex; i < m_DevicesCount; ++i)
|
|
--m_Devices[i].m_DeviceIndex; // Indices have shifted down by one.
|
|
device.m_DeviceIndex = InputDevice.kInvalidDeviceIndex;
|
|
|
|
// Update list of available devices.
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
if (m_AvailableDevices[i].deviceId == deviceId)
|
|
{
|
|
if (keepOnListOfAvailableDevices)
|
|
m_AvailableDevices[i].isRemoved = true;
|
|
else
|
|
ArrayHelpers.EraseAtWithCapacity(m_AvailableDevices, ref m_AvailableDeviceCount, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Unbake offset into global state buffers.
|
|
device.BakeOffsetIntoStateBlockRecursive((uint)-device.m_StateBlock.byteOffset);
|
|
|
|
// Force enabled actions to remove controls from the device.
|
|
// We've already set the device index to be invalid so we any attempts
|
|
// by actions to uninstall state monitors will get ignored.
|
|
InputActionState.OnDeviceChange(device, InputDeviceChange.Removed);
|
|
|
|
// Kill before update callback, if applicable.
|
|
if (device is IInputUpdateCallbackReceiver beforeUpdateCallbackReceiver)
|
|
onBeforeUpdate -= beforeUpdateCallbackReceiver.OnUpdate;
|
|
|
|
// Disable before-render updates if this was the last device
|
|
// that requires them.
|
|
if (device.updateBeforeRender)
|
|
{
|
|
var haveDeviceRequiringBeforeRender = false;
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
if (m_Devices[i].updateBeforeRender)
|
|
{
|
|
haveDeviceRequiringBeforeRender = true;
|
|
break;
|
|
}
|
|
|
|
if (!haveDeviceRequiringBeforeRender)
|
|
updateMask &= ~InputUpdateType.BeforeRender;
|
|
}
|
|
|
|
// Let device know.
|
|
device.NotifyRemoved();
|
|
|
|
// Let listeners know.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Removed, "InputSystem.onDeviceChange");
|
|
|
|
// Try setting next device of same type as current
|
|
InputSystem.GetDevice(device.GetType())?.MakeCurrent();
|
|
}
|
|
|
|
public void FlushDisconnectedDevices()
|
|
{
|
|
m_DisconnectedDevices.Clear(m_DisconnectedDevicesCount);
|
|
m_DisconnectedDevicesCount = 0;
|
|
}
|
|
|
|
public unsafe void ResetDevice(InputDevice device, bool alsoResetDontResetControls = false, bool? issueResetCommand = null)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
if (!device.added)
|
|
throw new InvalidOperationException($"Device '{device}' has not been added to the system");
|
|
|
|
var isHardReset = alsoResetDontResetControls || !device.hasDontResetControls;
|
|
|
|
// Trigger reset notification.
|
|
var change = isHardReset ? InputDeviceChange.HardReset : InputDeviceChange.SoftReset;
|
|
InputActionState.OnDeviceChange(device, change);
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, change, "onDeviceChange");
|
|
|
|
// If the device implements its own reset, let it handle it.
|
|
if (!alsoResetDontResetControls && device is ICustomDeviceReset customReset)
|
|
{
|
|
customReset.Reset();
|
|
}
|
|
else
|
|
{
|
|
var defaultStatePtr = device.defaultStatePtr;
|
|
var deviceStateBlockSize = device.stateBlock.alignedSizeInBytes;
|
|
|
|
// Allocate temp memory to hold one state event.
|
|
////REVIEW: the need for an event here is sufficiently obscure to warrant scrutiny; likely, there's a better way
|
|
//// to tell synthetic input (or input sources in general) apart
|
|
// NOTE: We wrap the reset in an artificial state event so that it appears to the rest of the system
|
|
// like any other input. If we don't do that but rather just call UpdateState() with a null event
|
|
// pointer, the change will be considered an internal state change and will get ignored by some
|
|
// pieces of code (such as EnhancedTouch which filters out internal state changes of Touchscreen
|
|
// by ignoring any change that is not coming from an input event).
|
|
using (var tempBuffer =
|
|
new NativeArray<byte>(InputEvent.kBaseEventSize + sizeof(int) + (int)deviceStateBlockSize, Allocator.Temp))
|
|
{
|
|
var stateEventPtr = (StateEvent*)tempBuffer.GetUnsafePtr();
|
|
var statePtr = stateEventPtr->state;
|
|
var currentTime = m_Runtime.currentTime;
|
|
|
|
// Set up the state event.
|
|
ref var stateBlock = ref device.m_StateBlock;
|
|
stateEventPtr->baseEvent.type = StateEvent.Type;
|
|
stateEventPtr->baseEvent.sizeInBytes = InputEvent.kBaseEventSize + sizeof(int) + deviceStateBlockSize;
|
|
stateEventPtr->baseEvent.time = currentTime;
|
|
stateEventPtr->baseEvent.deviceId = device.deviceId;
|
|
stateEventPtr->baseEvent.eventId = -1;
|
|
stateEventPtr->stateFormat = device.m_StateBlock.format;
|
|
|
|
// Decide whether we perform a soft reset or a hard reset.
|
|
if (isHardReset)
|
|
{
|
|
// Perform a hard reset where we wipe the entire device and set a full
|
|
// reset request to the backend.
|
|
UnsafeUtility.MemCpy(statePtr,
|
|
(byte*)defaultStatePtr + stateBlock.byteOffset,
|
|
deviceStateBlockSize);
|
|
}
|
|
else
|
|
{
|
|
// Perform a soft reset where we exclude any dontReset control (which is automatically
|
|
// toggled on for noisy controls) and do *NOT* send a reset request to the backend.
|
|
|
|
var currentStatePtr = device.currentStatePtr;
|
|
var resetMaskPtr = m_StateBuffers.resetMaskBuffer;
|
|
|
|
// To preserve values from dontReset controls, we need to first copy their current values.
|
|
UnsafeUtility.MemCpy(statePtr,
|
|
(byte*)currentStatePtr + stateBlock.byteOffset,
|
|
deviceStateBlockSize);
|
|
|
|
// And then we copy over default values masked by dontReset bits.
|
|
MemoryHelpers.MemCpyMasked(statePtr,
|
|
(byte*)defaultStatePtr + stateBlock.byteOffset,
|
|
(int)deviceStateBlockSize,
|
|
(byte*)resetMaskPtr + stateBlock.byteOffset);
|
|
}
|
|
|
|
UpdateState(device, defaultUpdateType, statePtr, 0, deviceStateBlockSize, currentTime,
|
|
new InputEventPtr((InputEvent*)stateEventPtr));
|
|
}
|
|
}
|
|
|
|
// In the editor, we don't want to issue RequestResetCommand to devices based on focus of the game view
|
|
// as this would also reset device state for the editor. And we don't need the reset commands in this case
|
|
// as -- unlike in the player --, Unity keeps running and we will keep seeing OS messages for these devices.
|
|
// So, in the editor, we generally suppress reset commands.
|
|
//
|
|
// The only exception is when the editor itself loses focus. We issue sync requests to all devices when
|
|
// coming back into focus. But for any device that doesn't support syncs, we actually do want to have a
|
|
// reset command reach the background.
|
|
//
|
|
// Finally, in the player, we also avoid reset commands when disabling a device as these are pointless.
|
|
// We sync/reset when enabling a device in the backend.
|
|
var doIssueResetCommand = isHardReset;
|
|
if (issueResetCommand != null)
|
|
doIssueResetCommand = issueResetCommand.Value;
|
|
#if UNITY_EDITOR
|
|
else if (m_Settings.editorInputBehaviorInPlayMode != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView)
|
|
doIssueResetCommand = false;
|
|
#endif
|
|
|
|
if (doIssueResetCommand)
|
|
device.RequestReset();
|
|
}
|
|
|
|
public InputDevice TryGetDevice(string nameOrLayout)
|
|
{
|
|
if (string.IsNullOrEmpty(nameOrLayout))
|
|
throw new ArgumentException("Name is null or empty.", nameof(nameOrLayout));
|
|
|
|
if (m_DevicesCount == 0)
|
|
return null;
|
|
|
|
var nameOrLayoutLowerCase = nameOrLayout.ToLower();
|
|
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
if (device.m_Name.ToLower() == nameOrLayoutLowerCase ||
|
|
device.m_Layout.ToLower() == nameOrLayoutLowerCase)
|
|
return device;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public InputDevice GetDevice(string nameOrLayout)
|
|
{
|
|
var device = TryGetDevice(nameOrLayout);
|
|
if (device == null)
|
|
throw new ArgumentException($"Cannot find device with name or layout '{nameOrLayout}'", nameof(nameOrLayout));
|
|
|
|
return device;
|
|
}
|
|
|
|
public InputDevice TryGetDevice(Type layoutType)
|
|
{
|
|
var layoutName = m_Layouts.TryFindLayoutForType(layoutType);
|
|
if (layoutName.IsEmpty())
|
|
return null;
|
|
|
|
return TryGetDevice(layoutName);
|
|
}
|
|
|
|
public InputDevice TryGetDeviceById(int id)
|
|
{
|
|
if (m_DevicesById.TryGetValue(id, out var result))
|
|
return result;
|
|
return null;
|
|
}
|
|
|
|
// Adds any device that's been reported to the system but could not be matched to
|
|
// a layout to the given list.
|
|
public int GetUnsupportedDevices(List<InputDeviceDescription> descriptions)
|
|
{
|
|
if (descriptions == null)
|
|
throw new ArgumentNullException(nameof(descriptions));
|
|
|
|
var numFound = 0;
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
if (TryGetDeviceById(m_AvailableDevices[i].deviceId) != null)
|
|
continue;
|
|
|
|
descriptions.Add(m_AvailableDevices[i].description);
|
|
++numFound;
|
|
}
|
|
|
|
return numFound;
|
|
}
|
|
|
|
// We have three different levels of disabling a device.
|
|
internal enum DeviceDisableScope
|
|
{
|
|
Everywhere, // Device is disabled globally and explicitly. Should neither send nor receive events.
|
|
InFrontendOnly, // Device is only disabled on managed side but not in backend. Should keep sending events but should not receive them (useful for redirecting their data).
|
|
TemporaryWhilePlayerIsInBackground, // Device has been disabled automatically and temporarily by system while application is running in the background.
|
|
}
|
|
|
|
public void EnableOrDisableDevice(InputDevice device, bool enable, DeviceDisableScope scope = default)
|
|
{
|
|
if (device == null)
|
|
throw new ArgumentNullException(nameof(device));
|
|
|
|
// Synchronize the enable/disabled state of the device.
|
|
if (enable)
|
|
{
|
|
////REVIEW: Do we really want to allow overriding disabledWhileInBackground like it currently does?
|
|
|
|
// Enable device.
|
|
switch (scope)
|
|
{
|
|
case DeviceDisableScope.Everywhere:
|
|
device.disabledWhileInBackground = false;
|
|
if (!device.disabledInFrontend && !device.disabledInRuntime)
|
|
return;
|
|
if (device.disabledInRuntime)
|
|
{
|
|
device.ExecuteEnableCommand();
|
|
device.disabledInRuntime = false;
|
|
}
|
|
if (device.disabledInFrontend)
|
|
{
|
|
if (!device.RequestSync())
|
|
ResetDevice(device);
|
|
device.disabledInFrontend = false;
|
|
}
|
|
break;
|
|
|
|
case DeviceDisableScope.InFrontendOnly:
|
|
device.disabledWhileInBackground = false;
|
|
if (!device.disabledInFrontend && device.disabledInRuntime)
|
|
return;
|
|
if (!device.disabledInRuntime)
|
|
{
|
|
device.ExecuteDisableCommand();
|
|
device.disabledInRuntime = true;
|
|
}
|
|
if (device.disabledInFrontend)
|
|
{
|
|
if (!device.RequestSync())
|
|
ResetDevice(device);
|
|
device.disabledInFrontend = false;
|
|
}
|
|
break;
|
|
|
|
case DeviceDisableScope.TemporaryWhilePlayerIsInBackground:
|
|
if (device.disabledWhileInBackground)
|
|
{
|
|
if (device.disabledInRuntime)
|
|
{
|
|
device.ExecuteEnableCommand();
|
|
device.disabledInRuntime = false;
|
|
}
|
|
if (!device.RequestSync())
|
|
ResetDevice(device);
|
|
device.disabledWhileInBackground = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Disable device.
|
|
switch (scope)
|
|
{
|
|
case DeviceDisableScope.Everywhere:
|
|
device.disabledWhileInBackground = false;
|
|
if (device.disabledInFrontend && device.disabledInRuntime)
|
|
return;
|
|
if (!device.disabledInRuntime)
|
|
{
|
|
device.ExecuteDisableCommand();
|
|
device.disabledInRuntime = true;
|
|
}
|
|
if (!device.disabledInFrontend)
|
|
{
|
|
// When disabling a device, also issuing a reset in the backend is pointless.
|
|
ResetDevice(device, issueResetCommand: false);
|
|
device.disabledInFrontend = true;
|
|
}
|
|
break;
|
|
|
|
case DeviceDisableScope.InFrontendOnly:
|
|
device.disabledWhileInBackground = false;
|
|
if (!device.disabledInRuntime && device.disabledInFrontend)
|
|
return;
|
|
if (device.disabledInRuntime)
|
|
{
|
|
device.ExecuteEnableCommand();
|
|
device.disabledInRuntime = false;
|
|
}
|
|
if (!device.disabledInFrontend)
|
|
{
|
|
// When disabling a device, also issuing a reset in the backend is pointless.
|
|
ResetDevice(device, issueResetCommand: false);
|
|
device.disabledInFrontend = true;
|
|
}
|
|
break;
|
|
|
|
case DeviceDisableScope.TemporaryWhilePlayerIsInBackground:
|
|
// Won't flag a device as DisabledWhileInBackground if it is explicitly disabled in
|
|
// the frontend.
|
|
if (device.disabledInFrontend || device.disabledWhileInBackground)
|
|
return;
|
|
device.disabledWhileInBackground = true;
|
|
ResetDevice(device, issueResetCommand: false);
|
|
#if UNITY_EDITOR
|
|
if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView)
|
|
#endif
|
|
{
|
|
device.ExecuteDisableCommand();
|
|
device.disabledInRuntime = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Let listeners know.
|
|
var deviceChange = enable ? InputDeviceChange.Enabled : InputDeviceChange.Disabled;
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, deviceChange, "InputSystem.onDeviceChange");
|
|
}
|
|
|
|
private unsafe void QueueEvent(InputEvent* eventPtr)
|
|
{
|
|
// If we're currently in OnUpdate(), the m_InputEventStream will be open. In that case,
|
|
// append events directly to that buffer and do *NOT* go into native.
|
|
if (m_InputEventStream.isOpen)
|
|
{
|
|
m_InputEventStream.Write(eventPtr);
|
|
return;
|
|
}
|
|
|
|
// Don't bother keeping the data on the managed side. Just stuff the raw data directly
|
|
// into the native buffers. This also means this method is thread-safe.
|
|
m_Runtime.QueueEvent(eventPtr);
|
|
}
|
|
|
|
public unsafe void QueueEvent(InputEventPtr ptr)
|
|
{
|
|
QueueEvent(ptr.data);
|
|
}
|
|
|
|
public unsafe void QueueEvent<TEvent>(ref TEvent inputEvent)
|
|
where TEvent : struct, IInputEventTypeInfo
|
|
{
|
|
QueueEvent((InputEvent*)UnsafeUtility.AddressOf(ref inputEvent));
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
Update(defaultUpdateType);
|
|
}
|
|
|
|
public void Update(InputUpdateType updateType)
|
|
{
|
|
m_Runtime.Update(updateType);
|
|
}
|
|
|
|
internal void Initialize(IInputRuntime runtime, InputSettings settings)
|
|
{
|
|
Debug.Assert(settings != null);
|
|
|
|
m_Settings = settings;
|
|
|
|
InitializeData();
|
|
InstallRuntime(runtime);
|
|
InstallGlobals();
|
|
|
|
ApplySettings();
|
|
}
|
|
|
|
internal void Destroy()
|
|
{
|
|
// There isn't really much of a point in removing devices but we still
|
|
// want to clear out any global state they may be keeping. So just tell
|
|
// the devices that they got removed without actually removing them.
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
m_Devices[i].NotifyRemoved();
|
|
|
|
// Free all state memory.
|
|
m_StateBuffers.FreeAll();
|
|
|
|
// Uninstall globals.
|
|
UninstallGlobals();
|
|
|
|
// Destroy settings if they are temporary.
|
|
if (m_Settings != null && m_Settings.hideFlags == HideFlags.HideAndDontSave)
|
|
Object.DestroyImmediate(m_Settings);
|
|
}
|
|
|
|
internal void InitializeData()
|
|
{
|
|
m_Layouts.Allocate();
|
|
m_Processors.Initialize();
|
|
m_Interactions.Initialize();
|
|
m_Composites.Initialize();
|
|
m_DevicesById = new Dictionary<int, InputDevice>();
|
|
|
|
// Determine our default set of enabled update types. By
|
|
// default we enable both fixed and dynamic update because
|
|
// we don't know which one the user is going to use. The user
|
|
// can manually turn off one of them to optimize operation.
|
|
m_UpdateMask = InputUpdateType.Dynamic | InputUpdateType.Fixed;
|
|
m_HasFocus = Application.isFocused;
|
|
#if UNITY_EDITOR
|
|
m_EditorIsActive = true;
|
|
m_UpdateMask |= InputUpdateType.Editor;
|
|
#endif
|
|
|
|
// Default polling frequency is 60 Hz.
|
|
m_PollingFrequency = 60;
|
|
|
|
// Register layouts.
|
|
// NOTE: Base layouts must be registered before their derived layouts
|
|
// for the detection of base layouts to work.
|
|
RegisterControlLayout("Axis", typeof(AxisControl)); // Controls.
|
|
RegisterControlLayout("Button", typeof(ButtonControl));
|
|
RegisterControlLayout("DiscreteButton", typeof(DiscreteButtonControl));
|
|
RegisterControlLayout("Key", typeof(KeyControl));
|
|
RegisterControlLayout("Analog", typeof(AxisControl));
|
|
RegisterControlLayout("Integer", typeof(IntegerControl));
|
|
RegisterControlLayout("Digital", typeof(IntegerControl));
|
|
RegisterControlLayout("Double", typeof(DoubleControl));
|
|
RegisterControlLayout("Vector2", typeof(Vector2Control));
|
|
RegisterControlLayout("Vector3", typeof(Vector3Control));
|
|
RegisterControlLayout("Delta", typeof(DeltaControl));
|
|
RegisterControlLayout("Quaternion", typeof(QuaternionControl));
|
|
RegisterControlLayout("Stick", typeof(StickControl));
|
|
RegisterControlLayout("Dpad", typeof(DpadControl));
|
|
RegisterControlLayout("DpadAxis", typeof(DpadControl.DpadAxisControl));
|
|
RegisterControlLayout("AnyKey", typeof(AnyKeyControl));
|
|
RegisterControlLayout("Touch", typeof(TouchControl));
|
|
RegisterControlLayout("TouchPhase", typeof(TouchPhaseControl));
|
|
RegisterControlLayout("TouchPress", typeof(TouchPressControl));
|
|
|
|
RegisterControlLayout("Gamepad", typeof(Gamepad)); // Devices.
|
|
RegisterControlLayout("Joystick", typeof(Joystick));
|
|
RegisterControlLayout("Keyboard", typeof(Keyboard));
|
|
RegisterControlLayout("Pointer", typeof(Pointer));
|
|
RegisterControlLayout("Mouse", typeof(Mouse));
|
|
RegisterControlLayout("Pen", typeof(Pen));
|
|
RegisterControlLayout("Touchscreen", typeof(Touchscreen));
|
|
RegisterControlLayout("Sensor", typeof(Sensor));
|
|
RegisterControlLayout("Accelerometer", typeof(Accelerometer));
|
|
RegisterControlLayout("Gyroscope", typeof(Gyroscope));
|
|
RegisterControlLayout("GravitySensor", typeof(GravitySensor));
|
|
RegisterControlLayout("AttitudeSensor", typeof(AttitudeSensor));
|
|
RegisterControlLayout("LinearAccelerationSensor", typeof(LinearAccelerationSensor));
|
|
RegisterControlLayout("MagneticFieldSensor", typeof(MagneticFieldSensor));
|
|
RegisterControlLayout("LightSensor", typeof(LightSensor));
|
|
RegisterControlLayout("PressureSensor", typeof(PressureSensor));
|
|
RegisterControlLayout("HumiditySensor", typeof(HumiditySensor));
|
|
RegisterControlLayout("AmbientTemperatureSensor", typeof(AmbientTemperatureSensor));
|
|
RegisterControlLayout("StepCounter", typeof(StepCounter));
|
|
RegisterControlLayout("TrackedDevice", typeof(TrackedDevice));
|
|
|
|
// Precompiled layouts.
|
|
RegisterPrecompiledLayout<FastKeyboard>(FastKeyboard.metadata);
|
|
RegisterPrecompiledLayout<FastTouchscreen>(FastTouchscreen.metadata);
|
|
RegisterPrecompiledLayout<FastMouse>(FastMouse.metadata);
|
|
|
|
// Register processors.
|
|
processors.AddTypeRegistration("Invert", typeof(InvertProcessor));
|
|
processors.AddTypeRegistration("InvertVector2", typeof(InvertVector2Processor));
|
|
processors.AddTypeRegistration("InvertVector3", typeof(InvertVector3Processor));
|
|
processors.AddTypeRegistration("Clamp", typeof(ClampProcessor));
|
|
processors.AddTypeRegistration("Normalize", typeof(NormalizeProcessor));
|
|
processors.AddTypeRegistration("NormalizeVector2", typeof(NormalizeVector2Processor));
|
|
processors.AddTypeRegistration("NormalizeVector3", typeof(NormalizeVector3Processor));
|
|
processors.AddTypeRegistration("Scale", typeof(ScaleProcessor));
|
|
processors.AddTypeRegistration("ScaleVector2", typeof(ScaleVector2Processor));
|
|
processors.AddTypeRegistration("ScaleVector3", typeof(ScaleVector3Processor));
|
|
processors.AddTypeRegistration("StickDeadzone", typeof(StickDeadzoneProcessor));
|
|
processors.AddTypeRegistration("AxisDeadzone", typeof(AxisDeadzoneProcessor));
|
|
processors.AddTypeRegistration("CompensateDirection", typeof(CompensateDirectionProcessor));
|
|
processors.AddTypeRegistration("CompensateRotation", typeof(CompensateRotationProcessor));
|
|
|
|
#if UNITY_EDITOR
|
|
processors.AddTypeRegistration("AutoWindowSpace", typeof(EditorWindowSpaceProcessor));
|
|
#endif
|
|
|
|
// Register interactions.
|
|
interactions.AddTypeRegistration("Hold", typeof(HoldInteraction));
|
|
interactions.AddTypeRegistration("Tap", typeof(TapInteraction));
|
|
interactions.AddTypeRegistration("SlowTap", typeof(SlowTapInteraction));
|
|
interactions.AddTypeRegistration("MultiTap", typeof(MultiTapInteraction));
|
|
interactions.AddTypeRegistration("Press", typeof(PressInteraction));
|
|
|
|
// Register composites.
|
|
composites.AddTypeRegistration("1DAxis", typeof(AxisComposite));
|
|
composites.AddTypeRegistration("2DVector", typeof(Vector2Composite));
|
|
composites.AddTypeRegistration("3DVector", typeof(Vector3Composite));
|
|
composites.AddTypeRegistration("Axis", typeof(AxisComposite));// Alias for pre-0.2 name.
|
|
composites.AddTypeRegistration("Dpad", typeof(Vector2Composite));// Alias for pre-0.2 name.
|
|
composites.AddTypeRegistration("ButtonWithOneModifier", typeof(ButtonWithOneModifier));
|
|
composites.AddTypeRegistration("ButtonWithTwoModifiers", typeof(ButtonWithTwoModifiers));
|
|
composites.AddTypeRegistration("OneModifier", typeof(OneModifierComposite));
|
|
composites.AddTypeRegistration("TwoModifiers", typeof(TwoModifiersComposite));
|
|
}
|
|
|
|
internal void InstallRuntime(IInputRuntime runtime)
|
|
{
|
|
if (m_Runtime != null)
|
|
{
|
|
m_Runtime.onUpdate = null;
|
|
m_Runtime.onBeforeUpdate = null;
|
|
m_Runtime.onDeviceDiscovered = null;
|
|
m_Runtime.onPlayerFocusChanged = null;
|
|
m_Runtime.onShouldRunUpdate = null;
|
|
#if UNITY_EDITOR
|
|
m_Runtime.onPlayerLoopInitialization = null;
|
|
#endif
|
|
}
|
|
|
|
m_Runtime = runtime;
|
|
m_Runtime.onUpdate = OnUpdate;
|
|
m_Runtime.onDeviceDiscovered = OnNativeDeviceDiscovered;
|
|
m_Runtime.onPlayerFocusChanged = OnFocusChanged;
|
|
m_Runtime.onShouldRunUpdate = ShouldRunUpdate;
|
|
#if UNITY_EDITOR
|
|
m_Runtime.onPlayerLoopInitialization = OnPlayerLoopInitialization;
|
|
#endif
|
|
m_Runtime.pollingFrequency = pollingFrequency;
|
|
m_HasFocus = m_Runtime.isPlayerFocused;
|
|
|
|
// We only hook NativeInputSystem.onBeforeUpdate if necessary.
|
|
if (m_BeforeUpdateListeners.length > 0 || m_HaveDevicesWithStateCallbackReceivers)
|
|
{
|
|
m_Runtime.onBeforeUpdate = OnBeforeUpdate;
|
|
m_NativeBeforeUpdateHooked = true;
|
|
}
|
|
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
InputAnalytics.Initialize(this);
|
|
m_Runtime.onShutdown = () => InputAnalytics.OnShutdown(this);
|
|
#endif
|
|
}
|
|
|
|
internal void InstallGlobals()
|
|
{
|
|
Debug.Assert(m_Runtime != null);
|
|
|
|
InputControlLayout.s_Layouts = m_Layouts;
|
|
InputProcessor.s_Processors = m_Processors;
|
|
InputInteraction.s_Interactions = m_Interactions;
|
|
InputBindingComposite.s_Composites = m_Composites;
|
|
|
|
InputRuntime.s_Instance = m_Runtime;
|
|
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup =
|
|
m_Runtime.currentTimeOffsetToRealtimeSinceStartup;
|
|
|
|
// Reset update state.
|
|
InputUpdate.Restore(new InputUpdate.SerializedState());
|
|
|
|
unsafe
|
|
{
|
|
InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdateType.Dynamic);
|
|
InputStateBuffers.s_DefaultStateBuffer = m_StateBuffers.defaultStateBuffer;
|
|
InputStateBuffers.s_NoiseMaskBuffer = m_StateBuffers.noiseMaskBuffer;
|
|
InputStateBuffers.s_ResetMaskBuffer = m_StateBuffers.resetMaskBuffer;
|
|
}
|
|
}
|
|
|
|
internal void UninstallGlobals()
|
|
{
|
|
if (ReferenceEquals(InputControlLayout.s_Layouts.baseLayoutTable, m_Layouts.baseLayoutTable))
|
|
InputControlLayout.s_Layouts = new InputControlLayout.Collection();
|
|
if (ReferenceEquals(InputProcessor.s_Processors.table, m_Processors.table))
|
|
InputProcessor.s_Processors = new TypeTable();
|
|
if (ReferenceEquals(InputInteraction.s_Interactions.table, m_Interactions.table))
|
|
InputInteraction.s_Interactions = new TypeTable();
|
|
if (ReferenceEquals(InputBindingComposite.s_Composites.table, m_Composites.table))
|
|
InputBindingComposite.s_Composites = new TypeTable();
|
|
|
|
// Clear layout cache.
|
|
InputControlLayout.s_CacheInstance = default;
|
|
InputControlLayout.s_CacheInstanceRef = 0;
|
|
|
|
// Detach from runtime.
|
|
if (m_Runtime != null)
|
|
{
|
|
m_Runtime.onUpdate = null;
|
|
m_Runtime.onDeviceDiscovered = null;
|
|
m_Runtime.onBeforeUpdate = null;
|
|
m_Runtime.onPlayerFocusChanged = null;
|
|
m_Runtime.onShouldRunUpdate = null;
|
|
|
|
if (ReferenceEquals(InputRuntime.s_Instance, m_Runtime))
|
|
InputRuntime.s_Instance = null;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
internal struct AvailableDevice
|
|
{
|
|
public InputDeviceDescription description;
|
|
public int deviceId;
|
|
public bool isNative;
|
|
public bool isRemoved;
|
|
}
|
|
|
|
// Used by EditorInputControlLayoutCache to determine whether its state is outdated.
|
|
internal int m_LayoutRegistrationVersion;
|
|
private float m_PollingFrequency;
|
|
|
|
internal InputControlLayout.Collection m_Layouts;
|
|
private TypeTable m_Processors;
|
|
private TypeTable m_Interactions;
|
|
private TypeTable m_Composites;
|
|
|
|
private int m_DevicesCount;
|
|
private InputDevice[] m_Devices;
|
|
|
|
private Dictionary<int, InputDevice> m_DevicesById;
|
|
internal int m_AvailableDeviceCount;
|
|
internal AvailableDevice[] m_AvailableDevices; // A record of all devices reported to the system (from native or user code).
|
|
|
|
////REVIEW: should these be weak-referenced?
|
|
internal int m_DisconnectedDevicesCount;
|
|
internal InputDevice[] m_DisconnectedDevices;
|
|
|
|
internal InputUpdateType m_UpdateMask; // Which of our update types are enabled.
|
|
private InputUpdateType m_CurrentUpdate;
|
|
internal InputStateBuffers m_StateBuffers;
|
|
|
|
#if UNITY_EDITOR
|
|
// remember time offset to correctly restore it after editor mode is done
|
|
private double latestNonEditorTimeOffsetToRealtimeSinceStartup;
|
|
#endif
|
|
|
|
// We don't use UnityEvents and thus don't persist the callbacks during domain reloads.
|
|
// Restoration of UnityActions is unreliable and it's too easy to end up with double
|
|
// registrations what will lead to all kinds of misbehavior.
|
|
private CallbackArray<DeviceChangeListener> m_DeviceChangeListeners;
|
|
private CallbackArray<DeviceStateChangeListener> m_DeviceStateChangeListeners;
|
|
private CallbackArray<InputDeviceFindControlLayoutDelegate> m_DeviceFindLayoutCallbacks;
|
|
internal CallbackArray<InputDeviceCommandDelegate> m_DeviceCommandCallbacks;
|
|
private CallbackArray<LayoutChangeListener> m_LayoutChangeListeners;
|
|
private CallbackArray<EventListener> m_EventListeners;
|
|
private CallbackArray<UpdateListener> m_BeforeUpdateListeners;
|
|
private CallbackArray<UpdateListener> m_AfterUpdateListeners;
|
|
private CallbackArray<Action> m_SettingsChangedListeners;
|
|
private bool m_NativeBeforeUpdateHooked;
|
|
private bool m_HaveDevicesWithStateCallbackReceivers;
|
|
private bool m_HasFocus;
|
|
private InputEventStream m_InputEventStream;
|
|
|
|
// We want to sync devices when the editor comes back into focus. Unfortunately, there's no
|
|
// callback for this so we have to poll this state.
|
|
#if UNITY_EDITOR
|
|
private bool m_EditorIsActive;
|
|
#endif
|
|
|
|
// We allocate the 'executeDeviceCommand' closure passed to 'onFindLayoutForDevice'
|
|
// only once to avoid creating garbage.
|
|
private InputDeviceExecuteCommandDelegate m_DeviceFindExecuteCommandDelegate;
|
|
private int m_DeviceFindExecuteCommandDeviceId;
|
|
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
private bool m_HaveSentStartupAnalytics;
|
|
#endif
|
|
|
|
internal IInputRuntime m_Runtime;
|
|
internal InputMetrics m_Metrics;
|
|
internal InputSettings m_Settings;
|
|
|
|
#if UNITY_EDITOR
|
|
internal IInputDiagnostics m_Diagnostics;
|
|
#endif
|
|
|
|
////REVIEW: Make it so that device names *always* have a number appended? (i.e. Gamepad1, Gamepad2, etc. instead of Gamepad, Gamepad1, etc)
|
|
|
|
private void MakeDeviceNameUnique(InputDevice device)
|
|
{
|
|
if (m_DevicesCount == 0)
|
|
return;
|
|
|
|
var deviceName = StringHelpers.MakeUniqueName(device.name, m_Devices, x => x != null ? x.name : string.Empty);
|
|
if (deviceName != device.name)
|
|
{
|
|
// If we have changed the name of the device, nuke all path strings in the control
|
|
// hierarchy so that they will get re-recreated when queried.
|
|
ResetControlPathsRecursive(device);
|
|
|
|
// Assign name.
|
|
device.m_Name = new InternedString(deviceName);
|
|
}
|
|
}
|
|
|
|
private static void ResetControlPathsRecursive(InputControl control)
|
|
{
|
|
control.m_Path = null;
|
|
|
|
var children = control.children;
|
|
var childCount = children.Count;
|
|
|
|
for (var i = 0; i < childCount; ++i)
|
|
ResetControlPathsRecursive(children[i]);
|
|
}
|
|
|
|
private void AssignUniqueDeviceId(InputDevice device)
|
|
{
|
|
// If the device already has an ID, make sure it's unique.
|
|
if (device.deviceId != InputDevice.InvalidDeviceId)
|
|
{
|
|
// Safety check to make sure out IDs are really unique.
|
|
// Given they are assigned by the native system they should be fine
|
|
// but let's make sure.
|
|
var existingDeviceWithId = TryGetDeviceById(device.deviceId);
|
|
if (existingDeviceWithId != null)
|
|
throw new InvalidOperationException(
|
|
$"Duplicate device ID {device.deviceId} detected for devices '{device.name}' and '{existingDeviceWithId.name}'");
|
|
}
|
|
else
|
|
{
|
|
device.m_DeviceId = m_Runtime.AllocateDeviceId();
|
|
}
|
|
}
|
|
|
|
// (Re)allocates state buffers and assigns each device that's been added
|
|
// a segment of the buffer. Preserves the current state of devices.
|
|
// NOTE: Installs the buffers globally.
|
|
private unsafe void ReallocateStateBuffers()
|
|
{
|
|
var oldBuffers = m_StateBuffers;
|
|
|
|
// Allocate new buffers.
|
|
var newBuffers = new InputStateBuffers();
|
|
newBuffers.AllocateAll(m_Devices, m_DevicesCount);
|
|
|
|
// Migrate state.
|
|
newBuffers.MigrateAll(m_Devices, m_DevicesCount, oldBuffers);
|
|
|
|
// Install the new buffers.
|
|
oldBuffers.FreeAll();
|
|
m_StateBuffers = newBuffers;
|
|
InputStateBuffers.s_DefaultStateBuffer = newBuffers.defaultStateBuffer;
|
|
InputStateBuffers.s_NoiseMaskBuffer = newBuffers.noiseMaskBuffer;
|
|
InputStateBuffers.s_ResetMaskBuffer = newBuffers.resetMaskBuffer;
|
|
|
|
// Switch to buffers.
|
|
InputStateBuffers.SwitchTo(m_StateBuffers,
|
|
InputUpdate.s_LatestUpdateType != InputUpdateType.None ? InputUpdate.s_LatestUpdateType : defaultUpdateType);
|
|
|
|
////TODO: need to update state change monitors
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize default state for given device.
|
|
/// </summary>
|
|
/// <param name="device">A newly added input device.</param>
|
|
/// <remarks>
|
|
/// For every device, one copy of its state is kept around which is initialized with the default
|
|
/// values for the device. If the device has no control that has an explicitly specified control
|
|
/// value, the buffer simply contains all zeroes.
|
|
///
|
|
/// The default state buffer is initialized once when a device is added to the system and then
|
|
/// migrated by <see cref="InputStateBuffers"/> like other device state and removed when the device
|
|
/// is removed from the system.
|
|
/// </remarks>
|
|
private unsafe void InitializeDefaultState(InputDevice device)
|
|
{
|
|
// Nothing to do if device has a default state of all zeroes.
|
|
if (!device.hasControlsWithDefaultState)
|
|
return;
|
|
|
|
// Otherwise go through each control and write its default value.
|
|
var controls = device.allControls;
|
|
var controlCount = controls.Count;
|
|
var defaultStateBuffer = m_StateBuffers.defaultStateBuffer;
|
|
for (var n = 0; n < controlCount; ++n)
|
|
{
|
|
var control = controls[n];
|
|
if (!control.hasDefaultState)
|
|
continue;
|
|
|
|
control.m_StateBlock.Write(defaultStateBuffer, control.m_DefaultState);
|
|
}
|
|
|
|
// Copy default state to all front and back buffers.
|
|
var stateBlock = device.m_StateBlock;
|
|
var deviceIndex = device.m_DeviceIndex;
|
|
if (m_StateBuffers.m_PlayerStateBuffers.valid)
|
|
{
|
|
stateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
|
|
stateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
if (m_StateBuffers.m_EditorStateBuffers.valid)
|
|
{
|
|
stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
|
|
stateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private unsafe void InitializeDeviceState(InputDevice device)
|
|
{
|
|
Debug.Assert(device != null, "Device must not be null");
|
|
Debug.Assert(device.added, "Device must have been added");
|
|
Debug.Assert(device.stateBlock.byteOffset != InputStateBlock.InvalidOffset, "Device state block offset is invalid");
|
|
Debug.Assert(device.stateBlock.byteOffset + device.stateBlock.alignedSizeInBytes <= m_StateBuffers.sizePerBuffer,
|
|
"Device state block is not contained in state buffer");
|
|
|
|
var controls = device.allControls;
|
|
var controlCount = controls.Count;
|
|
var resetMaskBuffer = m_StateBuffers.resetMaskBuffer;
|
|
|
|
var haveControlsWithDefaultState = device.hasControlsWithDefaultState;
|
|
|
|
// Assume that everything in the device is noise. This way we also catch memory regions
|
|
// that are not actually covered by a control and implicitly mark them as noise (e.g. the
|
|
// report ID in HID input reports).
|
|
//
|
|
// NOTE: Noise is indicated by *unset* bits so we don't have to do anything here to start
|
|
// with all-noise as we expect noise mask memory to be cleared on allocation.
|
|
var noiseMaskBuffer = m_StateBuffers.noiseMaskBuffer;
|
|
|
|
// We first toggle all bits *on* and then toggle bits for noisy and dontReset controls *off* individually.
|
|
// We do this instead of just leaving all bits *off* and then going through controls that aren't noisy/dontReset *on*.
|
|
// If we did the latter, we'd have the problem that a parent control such as TouchControl would toggle on bits for
|
|
// the entirety of its state block and thus cover the state of all its child controls.
|
|
MemoryHelpers.SetBitsInBuffer(noiseMaskBuffer, (int)device.stateBlock.byteOffset, 0, (int)device.stateBlock.sizeInBits, false);
|
|
MemoryHelpers.SetBitsInBuffer(resetMaskBuffer, (int)device.stateBlock.byteOffset, 0, (int)device.stateBlock.sizeInBits, true);
|
|
|
|
// Go through controls.
|
|
var defaultStateBuffer = m_StateBuffers.defaultStateBuffer;
|
|
for (var n = 0; n < controlCount; ++n)
|
|
{
|
|
var control = controls[n];
|
|
|
|
// Don't allow controls that hijack state from other controls to set independent noise or dontReset flags.
|
|
if (control.usesStateFromOtherControl)
|
|
continue;
|
|
|
|
if (!control.noisy || control.dontReset)
|
|
{
|
|
ref var stateBlock = ref control.m_StateBlock;
|
|
|
|
Debug.Assert(stateBlock.byteOffset != InputStateBlock.InvalidOffset, "Byte offset is invalid on control's state block");
|
|
Debug.Assert(stateBlock.bitOffset != InputStateBlock.InvalidOffset, "Bit offset is invalid on control's state block");
|
|
Debug.Assert(stateBlock.sizeInBits != InputStateBlock.InvalidOffset, "Size is invalid on control's state block");
|
|
Debug.Assert(stateBlock.byteOffset >= device.stateBlock.byteOffset, "Control's offset is located below device's offset");
|
|
Debug.Assert(stateBlock.byteOffset + stateBlock.alignedSizeInBytes <=
|
|
device.stateBlock.byteOffset + device.stateBlock.alignedSizeInBytes, "Control state block lies outside of state buffer");
|
|
|
|
// If control isn't noisy, toggle its bits *on* in the noise mask.
|
|
if (!control.noisy)
|
|
MemoryHelpers.SetBitsInBuffer(noiseMaskBuffer, (int)stateBlock.byteOffset, (int)stateBlock.bitOffset,
|
|
(int)stateBlock.sizeInBits, true);
|
|
|
|
// If control shouldn't be reset, toggle its bits *off* in the reset mask.
|
|
if (control.dontReset)
|
|
MemoryHelpers.SetBitsInBuffer(resetMaskBuffer, (int)stateBlock.byteOffset, (int)stateBlock.bitOffset,
|
|
(int)stateBlock.sizeInBits, false);
|
|
}
|
|
|
|
// If control has default state, write it into to the device's default state.
|
|
if (haveControlsWithDefaultState && control.hasDefaultState)
|
|
control.m_StateBlock.Write(defaultStateBuffer, control.m_DefaultState);
|
|
}
|
|
|
|
// Copy default state to all front and back buffers.
|
|
if (haveControlsWithDefaultState)
|
|
{
|
|
ref var deviceStateBlock = ref device.m_StateBlock;
|
|
var deviceIndex = device.m_DeviceIndex;
|
|
if (m_StateBuffers.m_PlayerStateBuffers.valid)
|
|
{
|
|
deviceStateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
|
|
deviceStateBlock.CopyToFrom(m_StateBuffers.m_PlayerStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
if (m_StateBuffers.m_EditorStateBuffers.valid)
|
|
{
|
|
deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetFrontBuffer(deviceIndex), defaultStateBuffer);
|
|
deviceStateBlock.CopyToFrom(m_StateBuffers.m_EditorStateBuffers.GetBackBuffer(deviceIndex), defaultStateBuffer);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private void OnNativeDeviceDiscovered(int deviceId, string deviceDescriptor)
|
|
{
|
|
// Make sure we're not adding to m_AvailableDevices before we restored what we
|
|
// had before a domain reload.
|
|
RestoreDevicesAfterDomainReloadIfNecessary();
|
|
|
|
// See if we have a disconnected device we can revive.
|
|
// NOTE: We do this all the way up here as the first thing before we even parse the JSON descriptor so
|
|
// if we do have a device we can revive, we can do so without allocating any GC memory.
|
|
var device = TryMatchDisconnectedDevice(deviceDescriptor);
|
|
|
|
// Parse description, if need be.
|
|
var description = device?.description ?? InputDeviceDescription.FromJson(deviceDescriptor);
|
|
|
|
// Add it.
|
|
var markAsRemoved = false;
|
|
try
|
|
{
|
|
// If we have a restricted set of supported devices, first check if it's a device
|
|
// we should support.
|
|
if (m_Settings.supportedDevices.Count > 0)
|
|
{
|
|
var layout = device != null ? device.m_Layout : TryFindMatchingControlLayout(ref description, deviceId);
|
|
if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout))
|
|
{
|
|
// Not supported. Ignore device. Still will get added to m_AvailableDevices
|
|
// list in finally clause below. If later the set of supported devices changes
|
|
// so that the device is now supported, ApplySettings() will pull it back out
|
|
// and create the device.
|
|
markAsRemoved = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (device != null)
|
|
{
|
|
// It's a device we pulled from the disconnected list. Update the device with the
|
|
// new ID, re-add it and notify that we've reconnected.
|
|
|
|
device.m_DeviceId = deviceId;
|
|
device.m_DeviceFlags |= InputDevice.DeviceFlags.Native;
|
|
device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledInFrontend;
|
|
device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledWhileInBackground;
|
|
device.m_DeviceFlags &= ~InputDevice.DeviceFlags.DisabledStateHasBeenQueriedFromRuntime;
|
|
|
|
AddDevice(device);
|
|
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners, device, InputDeviceChange.Reconnected,
|
|
"InputSystem.onDeviceChange");
|
|
}
|
|
else
|
|
{
|
|
// Go through normal machinery to try to create a new device.
|
|
AddDevice(description, throwIfNoLayoutFound: false, deviceId: deviceId,
|
|
deviceFlags: InputDevice.DeviceFlags.Native);
|
|
}
|
|
}
|
|
// We're catching exceptions very aggressively here. The reason is that we don't want
|
|
// exceptions thrown as a result of trying to create devices from device discoveries reported
|
|
// by native to break the system as a whole. Instead, we want to make the error visible but then
|
|
// go and work with whatever devices we *did* manage to create successfully.
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError($"Could not create a device for '{description}' (exception: {exception})");
|
|
}
|
|
finally
|
|
{
|
|
// Remember it. Do this *after* the AddDevice() call above so that if there's
|
|
// a listener creating layouts on the fly we won't end up matching this device and
|
|
// create an InputDevice right away (which would then conflict with the one we
|
|
// create in AddDevice).
|
|
ArrayHelpers.AppendWithCapacity(ref m_AvailableDevices, ref m_AvailableDeviceCount,
|
|
new AvailableDevice
|
|
{
|
|
description = description,
|
|
deviceId = deviceId,
|
|
isNative = true,
|
|
isRemoved = markAsRemoved,
|
|
});
|
|
}
|
|
}
|
|
|
|
private InputDevice TryMatchDisconnectedDevice(string deviceDescriptor)
|
|
{
|
|
for (var i = 0; i < m_DisconnectedDevicesCount; ++i)
|
|
{
|
|
var device = m_DisconnectedDevices[i];
|
|
var description = device.description;
|
|
|
|
// We don't parse the full description but rather go property by property in order to not
|
|
// allocate GC memory if we can avoid it.
|
|
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("interface", description.interfaceName, deviceDescriptor))
|
|
continue;
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("product", description.product, deviceDescriptor))
|
|
continue;
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("manufacturer", description.manufacturer, deviceDescriptor))
|
|
continue;
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("type", description.deviceClass, deviceDescriptor))
|
|
continue;
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("capabilities", description.capabilities, deviceDescriptor))
|
|
continue;
|
|
if (!InputDeviceDescription.ComparePropertyToDeviceDescriptor("serial", description.serial, deviceDescriptor))
|
|
continue;
|
|
|
|
ArrayHelpers.EraseAtWithCapacity(m_DisconnectedDevices, ref m_DisconnectedDevicesCount, i);
|
|
return device;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void InstallBeforeUpdateHookIfNecessary()
|
|
{
|
|
if (m_NativeBeforeUpdateHooked || m_Runtime == null)
|
|
return;
|
|
|
|
m_Runtime.onBeforeUpdate = OnBeforeUpdate;
|
|
m_NativeBeforeUpdateHooked = true;
|
|
}
|
|
|
|
private void RestoreDevicesAfterDomainReloadIfNecessary()
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (m_SavedDeviceStates != null)
|
|
RestoreDevicesAfterDomainReload();
|
|
#endif
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void SyncAllDevicesWhenEditorIsActivated()
|
|
{
|
|
var isActive = m_Runtime.isEditorActive;
|
|
if (isActive == m_EditorIsActive)
|
|
return;
|
|
|
|
m_EditorIsActive = isActive;
|
|
if (m_EditorIsActive)
|
|
SyncAllDevices();
|
|
}
|
|
|
|
private void SyncAllDevices()
|
|
{
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
// When the editor comes back into focus, we actually do want resets to happen
|
|
// for devices that don't support syncs as they will likely have missed input while
|
|
// we were in the background.
|
|
if (!m_Devices[i].RequestSync())
|
|
ResetDevice(m_Devices[i], issueResetCommand: true);
|
|
}
|
|
}
|
|
|
|
internal void SyncAllDevicesAfterEnteringPlayMode()
|
|
{
|
|
// Because we ignore all events between exiting edit mode and entering play mode,
|
|
// that includes any potential device resets/syncs/etc,
|
|
// we need to resync all devices after we're in play mode proper.
|
|
////TODO: this is a hacky workaround, implement a proper solution where events from sync/resets are not ignored.
|
|
SyncAllDevices();
|
|
}
|
|
|
|
#endif
|
|
|
|
private void WarnAboutDevicesFailingToRecreateAfterDomainReload()
|
|
{
|
|
// If we still have any saved device states, we have devices that we couldn't figure
|
|
// out how to recreate after a domain reload. Log a warning for each of them and
|
|
// let go of them.
|
|
#if UNITY_EDITOR
|
|
if (m_SavedDeviceStates == null)
|
|
return;
|
|
|
|
for (var i = 0; i < m_SavedDeviceStates.Length; ++i)
|
|
{
|
|
ref var state = ref m_SavedDeviceStates[i];
|
|
Debug.LogWarning($"Could not recreate device '{state.name}' with layout '{state.layout}' after domain reload");
|
|
}
|
|
|
|
// At this point, we throw the device states away and forget about
|
|
// what we had before the domain reload.
|
|
m_SavedDeviceStates = null;
|
|
#endif
|
|
}
|
|
|
|
private void OnBeforeUpdate(InputUpdateType updateType)
|
|
{
|
|
// Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
|
|
RestoreDevicesAfterDomainReloadIfNecessary();
|
|
|
|
if ((updateType & m_UpdateMask) == 0)
|
|
return;
|
|
|
|
InputStateBuffers.SwitchTo(m_StateBuffers, updateType);
|
|
|
|
InputUpdate.OnBeforeUpdate(updateType);
|
|
|
|
// For devices that have state callbacks, tell them we're carrying state over
|
|
// into the next frame.
|
|
if (m_HaveDevicesWithStateCallbackReceivers && updateType != InputUpdateType.BeforeRender) ////REVIEW: before-render handling is probably wrong
|
|
{
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
if (!device.hasStateCallbacks)
|
|
continue;
|
|
|
|
// NOTE: We do *not* perform a buffer flip here as we do not want to change what is the
|
|
// current and what is the previous state when we carry state forward. Rather,
|
|
// OnBeforeUpdate, if it modifies state, it modifies the current state directly.
|
|
// Also, for the same reasons, we do not modify the dynamic/fixed update counts
|
|
// on the device. If an event comes in in the upcoming update, it should lead to
|
|
// a buffer flip.
|
|
|
|
((IInputStateCallbackReceiver)device).OnNextUpdate();
|
|
}
|
|
}
|
|
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_BeforeUpdateListeners, "onBeforeUpdate");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply the settings in <see cref="m_Settings"/>.
|
|
/// </summary>
|
|
internal void ApplySettings()
|
|
{
|
|
// Sync update mask.
|
|
var newUpdateMask = InputUpdateType.Editor;
|
|
if ((m_UpdateMask & InputUpdateType.BeforeRender) != 0)
|
|
{
|
|
// BeforeRender updates are enabled in response to devices needing BeforeRender updates
|
|
// so we always preserve this if set.
|
|
newUpdateMask |= InputUpdateType.BeforeRender;
|
|
}
|
|
if (m_Settings.updateMode == InputSettings.s_OldUnsupportedFixedAndDynamicUpdateSetting)
|
|
m_Settings.updateMode = InputSettings.UpdateMode.ProcessEventsInDynamicUpdate;
|
|
switch (m_Settings.updateMode)
|
|
{
|
|
case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate:
|
|
newUpdateMask |= InputUpdateType.Dynamic;
|
|
break;
|
|
case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
|
|
newUpdateMask |= InputUpdateType.Fixed;
|
|
break;
|
|
case InputSettings.UpdateMode.ProcessEventsManually:
|
|
newUpdateMask |= InputUpdateType.Manual;
|
|
break;
|
|
default:
|
|
throw new NotSupportedException("Invalid input update mode: " + m_Settings.updateMode);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
// In the editor, we force editor updates to be on even if InputEditorUserSettings.lockInputToGameView is
|
|
// on as otherwise we'll end up accumulating events in edit mode without anyone flushing the
|
|
// queue out regularly.
|
|
newUpdateMask |= InputUpdateType.Editor;
|
|
#endif
|
|
updateMask = newUpdateMask;
|
|
|
|
////TODO: optimize this so that we don't repeatedly recreate state if we add/remove multiple devices
|
|
//// (same goes for not resolving actions repeatedly)
|
|
|
|
// Check if there's any native device we aren't using ATM which now fits
|
|
// the set of supported devices.
|
|
AddAvailableDevicesThatAreNowRecognized();
|
|
|
|
// If the settings restrict the set of supported devices, demote any native
|
|
// device we currently have that doesn't fit the requirements.
|
|
if (settings.supportedDevices.Count > 0)
|
|
{
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
var layout = device.m_Layout;
|
|
|
|
// If it's not in m_AvailableDevices, we don't automatically remove it.
|
|
// Whatever has been added directly through AddDevice(), we keep and don't
|
|
// restrict by `supportDevices`.
|
|
var isInAvailableDevices = false;
|
|
for (var n = 0; n < m_AvailableDeviceCount; ++n)
|
|
{
|
|
if (m_AvailableDevices[n].deviceId == device.deviceId)
|
|
{
|
|
isInAvailableDevices = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!isInAvailableDevices)
|
|
continue;
|
|
|
|
// If the device layout isn't supported according to the current settings,
|
|
// remove the device.
|
|
if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout))
|
|
{
|
|
RemoveDevice(device, keepOnListOfAvailableDevices: true);
|
|
--i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply feature flags.
|
|
if (m_Settings.m_FeatureFlags != null)
|
|
{
|
|
#if UNITY_EDITOR
|
|
runPlayerUpdatesInEditMode = m_Settings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode);
|
|
#endif
|
|
|
|
if (m_Settings.IsFeatureEnabled(InputFeatureNames.kUseWindowsGamingInputBackend))
|
|
{
|
|
var command = UseWindowsGamingInputCommand.Create(true);
|
|
if (ExecuteGlobalCommand(ref command) < 0)
|
|
Debug.LogError($"Could not enable Windows.Gaming.Input");
|
|
}
|
|
}
|
|
|
|
// Cache some values.
|
|
Touchscreen.s_TapTime = settings.defaultTapTime;
|
|
Touchscreen.s_TapDelayTime = settings.multiTapDelayTime;
|
|
Touchscreen.s_TapRadiusSquared = settings.tapRadius * settings.tapRadius;
|
|
// Extra clamp here as we can't tell what we're getting from serialized data.
|
|
ButtonControl.s_GlobalDefaultButtonPressPoint = Mathf.Clamp(settings.defaultButtonPressPoint, ButtonControl.kMinButtonPressPoint, float.MaxValue);
|
|
ButtonControl.s_GlobalDefaultButtonReleaseThreshold = settings.buttonReleaseThreshold;
|
|
|
|
// Let listeners know.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_SettingsChangedListeners,
|
|
"InputSystem.onSettingsChange");
|
|
}
|
|
|
|
internal unsafe long ExecuteGlobalCommand<TCommand>(ref TCommand command)
|
|
where TCommand : struct, IInputDeviceCommandInfo
|
|
{
|
|
var ptr = (InputDeviceCommand*)UnsafeUtility.AddressOf(ref command);
|
|
// device id is irrelevant as we route it based on fourcc internally
|
|
return InputRuntime.s_Instance.DeviceCommand(0, ptr);
|
|
}
|
|
|
|
internal void AddAvailableDevicesThatAreNowRecognized()
|
|
{
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
var id = m_AvailableDevices[i].deviceId;
|
|
if (TryGetDeviceById(id) != null)
|
|
continue;
|
|
|
|
var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description, id);
|
|
if (!IsDeviceLayoutMarkedAsSupportedInSettings(layout)) continue;
|
|
|
|
if (layout.IsEmpty())
|
|
{
|
|
// If it's a device coming from the runtime, disable it.
|
|
if (id != InputDevice.InvalidDeviceId)
|
|
{
|
|
var command = DisableDeviceCommand.Create();
|
|
m_Runtime.DeviceCommand(id, ref command);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
AddDevice(m_AvailableDevices[i].description, layout, deviceId: id,
|
|
deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// the user might have changed the layout of one device, but others in the system might still have
|
|
// layouts we can't make sense of. Just quietly swallow exceptions from those so as not to spam
|
|
// the user with information about devices unrelated to what was actually changed.
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool ShouldRunDeviceInBackground(InputDevice device)
|
|
{
|
|
var runDeviceInBackground =
|
|
m_Settings.backgroundBehavior != InputSettings.BackgroundBehavior.ResetAndDisableAllDevices &&
|
|
device.canRunInBackground;
|
|
|
|
// In editor, we may override canRunInBackground depending on the gameViewFocus setting.
|
|
#if UNITY_EDITOR
|
|
if (runDeviceInBackground)
|
|
{
|
|
if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus)
|
|
runDeviceInBackground = false;
|
|
else if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus)
|
|
runDeviceInBackground = !(device is Pointer || device is Keyboard);
|
|
}
|
|
#endif
|
|
|
|
return runDeviceInBackground;
|
|
}
|
|
|
|
internal void OnFocusChanged(bool focus)
|
|
{
|
|
#if UNITY_EDITOR
|
|
SyncAllDevicesWhenEditorIsActivated();
|
|
|
|
if (!m_Runtime.isInPlayMode)
|
|
{
|
|
m_HasFocus = focus;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#if UNITY_EDITOR
|
|
var gameViewFocus = m_Settings.editorInputBehaviorInPlayMode;
|
|
#endif
|
|
|
|
var runInBackground =
|
|
#if UNITY_EDITOR
|
|
// In the editor, the player loop will always be run even if the Game View does not have focus. This
|
|
// amounts to runInBackground being always true in the editor, regardless of what the setting in
|
|
// the Player Settings window is.
|
|
//
|
|
// If, however, "Game View Focus" is set to "Exactly As In Player", we force code here down the same
|
|
// path as in the player.
|
|
gameViewFocus != InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView || m_Runtime.runInBackground;
|
|
#else
|
|
m_Runtime.runInBackground;
|
|
#endif
|
|
|
|
var backgroundBehavior = m_Settings.backgroundBehavior;
|
|
if (backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && runInBackground)
|
|
{
|
|
// If runInBackground is true, no device changes should happen, even when focus is gained. So early out.
|
|
// If runInBackground is false, we still want to sync devices when focus is gained. So we need to continue further.
|
|
m_HasFocus = focus;
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
// Set the current update type while we process the focus changes to make sure we
|
|
// feed into the right buffer. No need to do this in the player as it doesn't have
|
|
// the editor/player confusion.
|
|
m_CurrentUpdate = m_UpdateMask.GetUpdateTypeForPlayer();
|
|
#endif
|
|
|
|
if (!focus)
|
|
{
|
|
// We only react to loss of focus when we will keep running in the background. If not,
|
|
// we'll do nothing and just wait for focus to come back (where we then try to sync all devices).
|
|
if (runInBackground)
|
|
{
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
// Determine whether to run this device in the background.
|
|
var device = m_Devices[i];
|
|
if (!device.enabled || ShouldRunDeviceInBackground(device))
|
|
continue;
|
|
|
|
// Disable the device. This will also soft-reset it.
|
|
EnableOrDisableDevice(device, false, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
|
|
|
|
// In case we invoked a callback that messed with our device array, adjust our index.
|
|
var index = m_Devices.IndexOfReference(device, m_DevicesCount);
|
|
if (index == -1)
|
|
--i;
|
|
else
|
|
i = index;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// On focus gain, reenable and sync devices.
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
|
|
// Re-enable the device if we disabled it on focus loss. This will also issue a sync.
|
|
if (device.disabledWhileInBackground)
|
|
EnableOrDisableDevice(device, true, DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
|
|
// Try to sync. If it fails and we didn't run in the background, perform
|
|
// a reset instead. This is to cope with backends that are unable to sync but
|
|
// may still retain state which now may be outdated because the input device may
|
|
// have changed state while we weren't running. So at least make the backend flush
|
|
// its state (if any).
|
|
else if (device.enabled && !runInBackground && !device.RequestSync())
|
|
ResetDevice(device);
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
m_CurrentUpdate = InputUpdateType.None;
|
|
#endif
|
|
|
|
// We set this *after* the block above as defaultUpdateType is influenced by the setting.
|
|
m_HasFocus = focus;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
internal void LeavePlayMode()
|
|
{
|
|
// Reenable all devices and reset their play mode state.
|
|
m_CurrentUpdate = InputUpdate.GetUpdateTypeForPlayer(m_UpdateMask);
|
|
InputStateBuffers.SwitchTo(m_StateBuffers, m_CurrentUpdate);
|
|
for (var i = 0; i < m_DevicesCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
if (device.disabledWhileInBackground)
|
|
EnableOrDisableDevice(device, true, scope: DeviceDisableScope.TemporaryWhilePlayerIsInBackground);
|
|
ResetDevice(device, alsoResetDontResetControls: true);
|
|
}
|
|
m_CurrentUpdate = default;
|
|
}
|
|
|
|
private void OnPlayerLoopInitialization()
|
|
{
|
|
if (!gameIsPlaying || // if game is not playing
|
|
!InputUpdate.s_LatestUpdateType.IsEditorUpdate() || // or last update was not editor update
|
|
!InputUpdate.s_LatestNonEditorUpdateType.IsPlayerUpdate()) // or update before that was not player update
|
|
return; // then no need to restore anything
|
|
|
|
InputUpdate.RestoreStateAfterEditorUpdate();
|
|
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = latestNonEditorTimeOffsetToRealtimeSinceStartup;
|
|
InputStateBuffers.SwitchTo(m_StateBuffers, InputUpdate.s_LatestUpdateType);
|
|
}
|
|
|
|
#endif
|
|
|
|
internal bool ShouldRunUpdate(InputUpdateType updateType)
|
|
{
|
|
// We perform a "null" update after domain reloads and on startup to get our devices
|
|
// in place before the runtime calls MonoBehaviour callbacks. See InputSystem.RunInitialUpdate().
|
|
if (updateType == InputUpdateType.None)
|
|
return true;
|
|
|
|
var mask = m_UpdateMask;
|
|
|
|
#if UNITY_EDITOR
|
|
// If the player isn't running, the only thing we run is editor updates, except if
|
|
// explicitly overriden via `runUpdatesInEditMode`.
|
|
// NOTE: This means that in edit mode (outside of play mode) we *never* switch to player
|
|
// input state. So, any script anywhere will see input state from the editor. If you
|
|
// have an [ExecuteInEditMode] MonoBehaviour and it polls the gamepad, for example,
|
|
// it will see gamepad inputs going to the editor and respond to them.
|
|
if (!gameIsPlaying && updateType != InputUpdateType.Editor && !runPlayerUpdatesInEditMode)
|
|
return false;
|
|
#endif
|
|
|
|
return (updateType & mask) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process input events.
|
|
/// </summary>
|
|
/// <param name="updateType"></param>
|
|
/// <param name="eventBuffer"></param>
|
|
/// <remarks>
|
|
/// This method is the core workhorse of the input system. It is called from <see cref="UnityEngineInternal.Input.NativeInputSystem"/>.
|
|
/// Usually this happens in response to the player loop running and triggering updates at set points. However,
|
|
/// updates can also be manually triggered through <see cref="InputSystem.Update"/>.
|
|
///
|
|
/// The method receives the event buffer used internally by the runtime to collect events.
|
|
///
|
|
/// Note that update types do *NOT* say what the events we receive are for. The update type only indicates
|
|
/// where in the Unity's application loop we got called from. Where the event data goes depends wholly on
|
|
/// which buffers we activate in the update and write the event data into.
|
|
/// </remarks>
|
|
/// <exception cref="InvalidOperationException">Thrown if OnUpdate is called recursively.</exception>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1809:AvoidExcessiveLocals", Justification = "TODO: Refactor later.")]
|
|
private unsafe void OnUpdate(InputUpdateType updateType, ref InputEventBuffer eventBuffer)
|
|
{
|
|
////TODO: switch from Profiler to CustomSampler API
|
|
// NOTE: This is *not* using try/finally as we've seen unreliability in the EndSample()
|
|
// execution (and we're not sure where it's coming from).
|
|
Profiler.BeginSample("InputUpdate");
|
|
|
|
if (m_InputEventStream.isOpen)
|
|
throw new InvalidOperationException("Already have an event buffer set! Was OnUpdate() called recursively?");
|
|
|
|
// Restore devices before checking update mask. See InputSystem.RunInitialUpdate().
|
|
RestoreDevicesAfterDomainReloadIfNecessary();
|
|
|
|
// In the editor, we issue a sync on all devices when the editor comes back to the foreground.
|
|
#if UNITY_EDITOR
|
|
SyncAllDevicesWhenEditorIsActivated();
|
|
#endif
|
|
|
|
if ((updateType & m_UpdateMask) == 0)
|
|
{
|
|
Profiler.EndSample();
|
|
return;
|
|
}
|
|
|
|
WarnAboutDevicesFailingToRecreateAfterDomainReload();
|
|
|
|
// First update sends out startup analytics.
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
if (!m_HaveSentStartupAnalytics)
|
|
{
|
|
InputAnalytics.OnStartup(this);
|
|
m_HaveSentStartupAnalytics = true;
|
|
}
|
|
#endif
|
|
|
|
// Update metrics.
|
|
++m_Metrics.totalUpdateCount;
|
|
|
|
#if UNITY_EDITOR
|
|
// If current update is editor update and previous update was non-editor,
|
|
// store the time offset so we can restore it right after editor update is complete
|
|
if (((updateType & InputUpdateType.Editor) == InputUpdateType.Editor) && (m_CurrentUpdate & InputUpdateType.Editor) == 0)
|
|
latestNonEditorTimeOffsetToRealtimeSinceStartup =
|
|
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup;
|
|
#endif
|
|
|
|
// Store current time offset.
|
|
InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = m_Runtime.currentTimeOffsetToRealtimeSinceStartup;
|
|
|
|
InputStateBuffers.SwitchTo(m_StateBuffers, updateType);
|
|
|
|
m_CurrentUpdate = updateType;
|
|
InputUpdate.OnUpdate(updateType);
|
|
|
|
var shouldProcessActionTimeouts = updateType.IsPlayerUpdate() && gameIsPlaying;
|
|
|
|
// See if we're supposed to only take events up to a certain time.
|
|
// NOTE: We do not require the events in the queue to be sorted. Instead, we will walk over
|
|
// all events in the buffer each time. Note that if there are multiple events for the same
|
|
// device, it depends on the producer of these events to queue them in correct order.
|
|
// Otherwise, once an event with a newer timestamp has been processed, events coming later
|
|
// in the buffer and having older timestamps will get rejected.
|
|
|
|
var currentTime = updateType == InputUpdateType.Fixed ? m_Runtime.currentTimeForFixedUpdate : m_Runtime.currentTime;
|
|
var timesliceEvents = (updateType == InputUpdateType.Fixed || updateType == InputUpdateType.BeforeRender) &&
|
|
InputSystem.settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate;
|
|
|
|
// Figure out if we can just flush the buffer and early out.
|
|
var canFlushBuffer =
|
|
false
|
|
#if UNITY_EDITOR
|
|
// If out of focus and runInBackground is off and ExactlyAsInPlayer is on, discard input.
|
|
|| (!gameHasFocus && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView &&
|
|
(!m_Runtime.runInBackground ||
|
|
m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices))
|
|
#else
|
|
|| (!gameHasFocus && !m_Runtime.runInBackground)
|
|
#endif
|
|
;
|
|
var canEarlyOut =
|
|
// Early out if there's no events to process.
|
|
eventBuffer.eventCount == 0
|
|
|| canFlushBuffer ||
|
|
// If we're in the background and not supposed to process events in this update (but somehow
|
|
// still ended up here), we're done.
|
|
((!gameHasFocus || gameShouldGetInputRegardlessOfFocus) &&
|
|
((m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.ResetAndDisableAllDevices && updateType != InputUpdateType.Editor)
|
|
#if UNITY_EDITOR
|
|
|| (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus && updateType != InputUpdateType.Editor)
|
|
|| (m_Settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus && m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView && updateType == InputUpdateType.Editor)
|
|
#endif
|
|
)
|
|
#if UNITY_EDITOR
|
|
// When the game is playing and has focus, we never process input in editor updates. All we
|
|
// do is just switch to editor state buffers and then exit.
|
|
|| (gameIsPlaying && gameHasFocus && updateType == InputUpdateType.Editor)
|
|
#endif
|
|
);
|
|
|
|
if (canEarlyOut)
|
|
{
|
|
// Normally, we process action timeouts after first processing all events. If we have no
|
|
// events, we still need to check timeouts.
|
|
if (shouldProcessActionTimeouts)
|
|
ProcessStateChangeMonitorTimeouts();
|
|
|
|
Profiler.EndSample();
|
|
InvokeAfterUpdateCallback(updateType);
|
|
if (canFlushBuffer)
|
|
eventBuffer.Reset();
|
|
m_CurrentUpdate = default;
|
|
return;
|
|
}
|
|
|
|
var processingStartTime = Stopwatch.GetTimestamp();
|
|
var totalEventLag = 0.0;
|
|
|
|
#if UNITY_EDITOR
|
|
var isPlaying = gameIsPlaying;
|
|
#endif
|
|
|
|
try
|
|
{
|
|
m_InputEventStream = new InputEventStream(ref eventBuffer, m_Settings.maxQueuedEventsPerUpdate);
|
|
var totalEventBytesProcessed = 0U;
|
|
|
|
InputEvent* skipEventMergingFor = null;
|
|
|
|
// Handle events.
|
|
while (m_InputEventStream.remainingEventCount > 0)
|
|
{
|
|
if (m_Settings.maxEventBytesPerUpdate > 0 &&
|
|
totalEventBytesProcessed >= m_Settings.maxEventBytesPerUpdate)
|
|
{
|
|
Debug.LogError(
|
|
"Exceeded budget for maximum input event throughput per InputSystem.Update(). Discarding remaining events. "
|
|
+ "Increase InputSystem.settings.maxEventBytesPerUpdate or set it to 0 to remove the limit.");
|
|
break;
|
|
}
|
|
|
|
InputDevice device = null;
|
|
var currentEventReadPtr = m_InputEventStream.currentEventPtr;
|
|
|
|
Debug.Assert(!currentEventReadPtr->handled, "Event in buffer is already marked as handled");
|
|
|
|
// In before render updates, we only take state events and only those for devices
|
|
// that have before render updates enabled.
|
|
if (updateType == InputUpdateType.BeforeRender)
|
|
{
|
|
while (m_InputEventStream.remainingEventCount > 0)
|
|
{
|
|
Debug.Assert(!currentEventReadPtr->handled,
|
|
"Iterated to event in buffer that is already marked as handled");
|
|
|
|
device = TryGetDeviceById(currentEventReadPtr->deviceId);
|
|
if (device != null && device.updateBeforeRender &&
|
|
(currentEventReadPtr->type == StateEvent.Type ||
|
|
currentEventReadPtr->type == DeltaStateEvent.Type))
|
|
break;
|
|
|
|
currentEventReadPtr = m_InputEventStream.Advance(leaveEventInBuffer: true);
|
|
}
|
|
}
|
|
|
|
if (m_InputEventStream.remainingEventCount == 0)
|
|
break;
|
|
|
|
var currentEventTimeInternal = currentEventReadPtr->internalTime;
|
|
var currentEventType = currentEventReadPtr->type;
|
|
|
|
// In the editor, we discard all input events that occur in-between exiting edit mode and having
|
|
// entered play mode as otherwise we'll spill a bunch of UI events that have occurred while the
|
|
// UI was sort of neither in this mode nor in that mode. This would usually lead to the game receiving
|
|
// an accumulation of spurious inputs right in one of its first updates.
|
|
//
|
|
// NOTE: There's a chance the solution here will prove inadequate on the long run. We may do things
|
|
// here such as throwing partial touches away and then letting the rest of a touch go through.
|
|
// Could be that ultimately we need to issue a full reset of all devices at the beginning of
|
|
// play mode in the editor.
|
|
#if UNITY_EDITOR
|
|
if ((currentEventType == StateEvent.Type ||
|
|
currentEventType == DeltaStateEvent.Type) &&
|
|
(updateType & InputUpdateType.Editor) == 0 &&
|
|
InputSystem.s_SystemObject.exitEditModeTime > 0 &&
|
|
currentEventTimeInternal >= InputSystem.s_SystemObject.exitEditModeTime &&
|
|
(currentEventTimeInternal < InputSystem.s_SystemObject.enterPlayModeTime ||
|
|
InputSystem.s_SystemObject.enterPlayModeTime == 0))
|
|
{
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
// If we're timeslicing, check if the event time is within limits.
|
|
if (timesliceEvents && currentEventTimeInternal >= currentTime)
|
|
{
|
|
m_InputEventStream.Advance(true);
|
|
continue;
|
|
}
|
|
|
|
// If we can't find the device, ignore the event.
|
|
if (device == null)
|
|
device = TryGetDeviceById(currentEventReadPtr->deviceId);
|
|
if (device == null)
|
|
{
|
|
#if UNITY_EDITOR
|
|
////TODO: see if this is a device we haven't created and if so, just ignore
|
|
m_Diagnostics?.OnCannotFindDeviceForEvent(new InputEventPtr(currentEventReadPtr));
|
|
#endif
|
|
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
|
|
// In the editor, we may need to bump events from editor updates into player updates
|
|
// and vice versa.
|
|
#if UNITY_EDITOR
|
|
if (isPlaying && !gameHasFocus)
|
|
{
|
|
if (m_Settings.editorInputBehaviorInPlayMode == InputSettings.EditorInputBehaviorInPlayMode
|
|
.PointersAndKeyboardsRespectGameViewFocus &&
|
|
m_Settings.backgroundBehavior !=
|
|
InputSettings.BackgroundBehavior.ResetAndDisableAllDevices)
|
|
{
|
|
var isPointerOrKeyboard = device is Pointer || device is Keyboard;
|
|
if (updateType != InputUpdateType.Editor)
|
|
{
|
|
// Let everything but pointer and keyboard input through.
|
|
// If the event is from a pointer or keyboard, leave it in the buffer so it can be dealt with
|
|
// in a subsequent editor update. Otherwise, take it out.
|
|
if (isPointerOrKeyboard)
|
|
{
|
|
m_InputEventStream.Advance(true);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Let only pointer and keyboard input through.
|
|
if (!isPointerOrKeyboard)
|
|
{
|
|
m_InputEventStream.Advance(true);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// If device is disabled, we let the event through only in certain cases.
|
|
// Removal and configuration change events should always be processed.
|
|
if (!device.enabled &&
|
|
currentEventType != DeviceRemoveEvent.Type &&
|
|
currentEventType != DeviceConfigurationEvent.Type &&
|
|
(device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime |
|
|
InputDevice.DeviceFlags.DisabledWhileInBackground)) != 0)
|
|
{
|
|
#if UNITY_EDITOR
|
|
// If the device is disabled in the backend, getting events for them
|
|
// is something that indicates a problem in the backend so diagnose.
|
|
if ((device.m_DeviceFlags & InputDevice.DeviceFlags.DisabledInRuntime) != 0)
|
|
m_Diagnostics?.OnEventForDisabledDevice(currentEventReadPtr, device);
|
|
#endif
|
|
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
|
|
// Check if the device wants to merge successive events.
|
|
if (!settings.disableRedundantEventsMerging && device.hasEventMerger && currentEventReadPtr != skipEventMergingFor)
|
|
{
|
|
// NOTE: This relies on events in the buffer being consecutive for the same device. This is not
|
|
// necessarily the case for events coming in from the background event queue where parallel
|
|
// producers may create interleaved input sequences. This will be fixed once we have the
|
|
// new buffering scheme for input events working in the native runtime.
|
|
|
|
var nextEvent = m_InputEventStream.Peek();
|
|
// If there is next event after current one.
|
|
if ((nextEvent != null)
|
|
// And if next event is for the same device.
|
|
&& (currentEventReadPtr->deviceId == nextEvent->deviceId)
|
|
// And if next event is in the same timeslicing slot.
|
|
&& (timesliceEvents ? (nextEvent->internalTime < currentTime) : true)
|
|
)
|
|
{
|
|
// Then try to merge current event into next event.
|
|
if (((IEventMerger)device).MergeForward(currentEventReadPtr, nextEvent))
|
|
{
|
|
// And if succeeded, skip current event, as it was merged into next event.
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
|
|
// If we can't merge current event with next one for any reason, we assume the next event
|
|
// carries crucial entropy (button changed state, phase changed, counter changed, etc).
|
|
// Hence semantic meaning for current event is "can't merge current with next because next is different".
|
|
// But semantic meaning for next event is "next event carries important information and should be preserved",
|
|
// from that point of view next event should not be merged with current nor with _next after next_ event.
|
|
//
|
|
// For example, given such stream of events:
|
|
// Mouse Mouse Mouse Mouse Mouse Mouse Mouse
|
|
// Event no1 Event no2 Event no3 Event no4 Event no5 Event no6 Event no7
|
|
// Time 1 Time 2 Time 3 Time 4 Time 5 Time 6 Time 7
|
|
// Pos(10,20) Pos(12,21) Pos(13,23) Pos(14,24) Pos(16,25) Pos(17,27) Pos(18,28)
|
|
// Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1) Delta(2,1) Delta(1,2) Delta(1,1)
|
|
// BtnLeft(0) BtnLeft(0) BtnLeft(0) BtnLeft(1) BtnLeft(1) BtnLeft(1) BtnLeft(1)
|
|
//
|
|
// if we then merge without skipping next event here:
|
|
// Mouse Mouse
|
|
// Event no3 Event no7
|
|
// Time 3 Time 7
|
|
// Pos(13,23) Pos(18,28)
|
|
// Delta(4,4) Delta(5,5)
|
|
// BtnLeft(0) BtnLeft(1)
|
|
//
|
|
// As you can see, the event no4 containing mouse button press was lost,
|
|
// and with it we lose the important information of timestamp of mouse button press.
|
|
//
|
|
// With skipping merging next event we will get:
|
|
// Mouse Mouse Mouse
|
|
// Time 3 Time 4 Time 7
|
|
// Event no3 Event no4 Event no7
|
|
// Pos(13,23) Pos(14,24) Pos(18,28)
|
|
// Delta(3,3) Delta(1,1) Delta(4,4)
|
|
// BtnLeft(0) BtnLeft(1) BtnLeft(1)
|
|
//
|
|
// And no4 is preserved, with the exact timestamp of button press.
|
|
skipEventMergingFor = nextEvent;
|
|
}
|
|
}
|
|
|
|
// Give the device a chance to do something with data before we propagate it to event listeners.
|
|
if (device.hasEventPreProcessor)
|
|
{
|
|
#if UNITY_EDITOR
|
|
var eventSizeBeforePreProcessor = currentEventReadPtr->sizeInBytes;
|
|
#endif
|
|
var shouldProcess = ((IEventPreProcessor)device).PreProcessEvent(currentEventReadPtr);
|
|
#if UNITY_EDITOR
|
|
if (currentEventReadPtr->sizeInBytes > eventSizeBeforePreProcessor)
|
|
throw new AccessViolationException($"'{device}'.PreProcessEvent tries to grow an event from {eventSizeBeforePreProcessor} bytes to {currentEventReadPtr->sizeInBytes} bytes, this will potentially corrupt events after the current event and/or cause out-of-bounds memory access.");
|
|
#endif
|
|
if (!shouldProcess)
|
|
{
|
|
// Skip event if PreProcessEvent considers it to be irrelevant.
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Give listeners a shot at the event.
|
|
// NOTE: We call listeners also for events where the device is disabled. This is crucial for code
|
|
// such as TouchSimulation that disables the originating devices and then uses its events to
|
|
// create simulated events from.
|
|
if (m_EventListeners.length > 0)
|
|
{
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners,
|
|
new InputEventPtr(currentEventReadPtr), device, "InputSystem.onEvent");
|
|
|
|
// If a listener marks the event as handled, we don't process it further.
|
|
if (currentEventReadPtr->handled)
|
|
{
|
|
m_InputEventStream.Advance(false);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Update metrics.
|
|
if (currentEventTimeInternal <= currentTime)
|
|
totalEventLag += currentTime - currentEventTimeInternal;
|
|
++m_Metrics.totalEventCount;
|
|
m_Metrics.totalEventBytes += (int)currentEventReadPtr->sizeInBytes;
|
|
|
|
// Process.
|
|
switch (currentEventType)
|
|
{
|
|
case StateEvent.Type:
|
|
case DeltaStateEvent.Type:
|
|
|
|
var eventPtr = new InputEventPtr(currentEventReadPtr);
|
|
|
|
// Ignore the event if the last state update we received for the device was
|
|
// newer than this state event is. We don't allow devices to go back in time.
|
|
//
|
|
// NOTE: We make an exception here for devices that implement IInputStateCallbackReceiver (such
|
|
// as Touchscreen). For devices that dynamically incorporate state it can be hard ensuring
|
|
// a global ordering of events as there may be multiple substreams (e.g. each individual touch)
|
|
// that are generated in the backend and would require considerable work to ensure monotonically
|
|
// increasing timestamps across all such streams.
|
|
var deviceIsStateCallbackReceiver = device.hasStateCallbacks;
|
|
if (currentEventTimeInternal < device.m_LastUpdateTimeInternal &&
|
|
!(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat))
|
|
{
|
|
#if UNITY_EDITOR
|
|
m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device);
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
// Update the state of the device from the event. If the device is an IInputStateCallbackReceiver,
|
|
// let the device handle the event. If not, we do it ourselves.
|
|
var haveChangedStateOtherThanNoise = true;
|
|
if (deviceIsStateCallbackReceiver)
|
|
{
|
|
// NOTE: We leave it to the device to make sure the event has the right format. This allows the
|
|
// device to handle multiple different incoming formats.
|
|
((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr);
|
|
}
|
|
else
|
|
{
|
|
// If the state format doesn't match, ignore the event.
|
|
if (device.stateBlock.format != eventPtr.stateFormat)
|
|
{
|
|
#if UNITY_EDITOR
|
|
m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device);
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
haveChangedStateOtherThanNoise = UpdateState(device, eventPtr, updateType);
|
|
}
|
|
|
|
totalEventBytesProcessed += eventPtr.sizeInBytes;
|
|
|
|
// Update timestamp on device.
|
|
// NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps.
|
|
// Only events should. If running play mode updates in editor, we want to defer to the play mode
|
|
// callbacks to set the last update time to avoid dropping events only processed by the editor state.
|
|
if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime
|
|
#if UNITY_EDITOR
|
|
&& !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode)
|
|
#endif
|
|
)
|
|
device.m_LastUpdateTimeInternal = eventPtr.internalTime;
|
|
|
|
// Make device current. Again, only do this when receiving events.
|
|
if (haveChangedStateOtherThanNoise)
|
|
device.MakeCurrent();
|
|
|
|
break;
|
|
|
|
case TextEvent.Type:
|
|
{
|
|
var textEventPtr = (TextEvent*)currentEventReadPtr;
|
|
if (device is ITextInputReceiver textInputReceiver)
|
|
{
|
|
var utf32Char = textEventPtr->character;
|
|
if (utf32Char >= 0x10000)
|
|
{
|
|
// Send surrogate pair.
|
|
utf32Char -= 0x10000;
|
|
var highSurrogate = 0xD800 + ((utf32Char >> 10) & 0x3FF);
|
|
var lowSurrogate = 0xDC00 + (utf32Char & 0x3FF);
|
|
|
|
textInputReceiver.OnTextInput((char)highSurrogate);
|
|
textInputReceiver.OnTextInput((char)lowSurrogate);
|
|
}
|
|
else
|
|
{
|
|
// Send single, plain character.
|
|
textInputReceiver.OnTextInput((char)utf32Char);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case IMECompositionEvent.Type:
|
|
{
|
|
var imeEventPtr = (IMECompositionEvent*)currentEventReadPtr;
|
|
var textInputReceiver = device as ITextInputReceiver;
|
|
textInputReceiver?.OnIMECompositionChanged(imeEventPtr->compositionString);
|
|
break;
|
|
}
|
|
|
|
case DeviceRemoveEvent.Type:
|
|
{
|
|
RemoveDevice(device, keepOnListOfAvailableDevices: false);
|
|
|
|
// If it's a native device with a description, put it on the list of disconnected
|
|
// devices.
|
|
if (device.native && !device.description.empty)
|
|
{
|
|
ArrayHelpers.AppendWithCapacity(ref m_DisconnectedDevices,
|
|
ref m_DisconnectedDevicesCount, device);
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners,
|
|
device, InputDeviceChange.Disconnected, "InputSystem.onDeviceChange");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case DeviceConfigurationEvent.Type:
|
|
device.NotifyConfigurationChanged();
|
|
InputActionState.OnDeviceChange(device, InputDeviceChange.ConfigurationChanged);
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceChangeListeners,
|
|
device, InputDeviceChange.ConfigurationChanged, "InputSystem.onDeviceChange");
|
|
break;
|
|
|
|
case DeviceResetEvent.Type:
|
|
ResetDevice(device,
|
|
alsoResetDontResetControls: ((DeviceResetEvent*)currentEventReadPtr)->hardReset);
|
|
break;
|
|
}
|
|
|
|
m_InputEventStream.Advance(leaveEventInBuffer: false);
|
|
}
|
|
|
|
m_Metrics.totalEventProcessingTime +=
|
|
((double)(Stopwatch.GetTimestamp() - processingStartTime)) / Stopwatch.Frequency;
|
|
m_Metrics.totalEventLagTime += totalEventLag;
|
|
|
|
m_InputEventStream.Close(ref eventBuffer);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// We need to restore m_InputEventStream to a sound state
|
|
// to avoid failing recursive OnUpdate check next frame.
|
|
m_InputEventStream.CleanUpAfterException();
|
|
throw;
|
|
}
|
|
|
|
if (shouldProcessActionTimeouts)
|
|
ProcessStateChangeMonitorTimeouts();
|
|
|
|
Profiler.EndSample();
|
|
|
|
////FIXME: need to ensure that if someone calls QueueEvent() from an onAfterUpdate callback, we don't end up with a
|
|
//// mess in the event buffer
|
|
//// same goes for events that someone may queue from a change monitor callback
|
|
InvokeAfterUpdateCallback(updateType);
|
|
m_CurrentUpdate = default;
|
|
}
|
|
|
|
private void InvokeAfterUpdateCallback(InputUpdateType updateType)
|
|
{
|
|
// don't invoke the after update callback if this is an editor update and the game is playing. We
|
|
// skip event processing when playing in the editor and the game has focus, which means that any
|
|
// handlers for this delegate that query input state during this update will get no values.
|
|
if (updateType == InputUpdateType.Editor && gameIsPlaying)
|
|
return;
|
|
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_AfterUpdateListeners,
|
|
"InputSystem.onAfterUpdate");
|
|
}
|
|
|
|
internal unsafe bool UpdateState(InputDevice device, InputEvent* eventPtr, InputUpdateType updateType)
|
|
{
|
|
Debug.Assert(eventPtr != null, "Received NULL event ptr");
|
|
|
|
var stateBlockOfDevice = device.m_StateBlock;
|
|
var stateBlockSizeOfDevice = stateBlockOfDevice.sizeInBits / 8; // Always byte-aligned; avoid calling alignedSizeInBytes.
|
|
var offsetInDeviceStateToCopyTo = 0u;
|
|
uint sizeOfStateToCopy;
|
|
uint receivedStateSize;
|
|
byte* ptrToReceivedState;
|
|
FourCC receivedStateFormat;
|
|
|
|
// Grab state data from event and decide where to copy to and how much to copy.
|
|
if (eventPtr->type == StateEvent.Type)
|
|
{
|
|
var stateEventPtr = (StateEvent*)eventPtr;
|
|
receivedStateFormat = stateEventPtr->stateFormat;
|
|
receivedStateSize = stateEventPtr->stateSizeInBytes;
|
|
ptrToReceivedState = (byte*)stateEventPtr->state;
|
|
|
|
// Ignore extra state at end of event.
|
|
sizeOfStateToCopy = receivedStateSize;
|
|
if (sizeOfStateToCopy > stateBlockSizeOfDevice)
|
|
sizeOfStateToCopy = stateBlockSizeOfDevice;
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(eventPtr->type == DeltaStateEvent.Type, "Given event must either be a StateEvent or a DeltaStateEvent");
|
|
|
|
var deltaEventPtr = (DeltaStateEvent*)eventPtr;
|
|
receivedStateFormat = deltaEventPtr->stateFormat;
|
|
receivedStateSize = deltaEventPtr->deltaStateSizeInBytes;
|
|
ptrToReceivedState = (byte*)deltaEventPtr->deltaState;
|
|
offsetInDeviceStateToCopyTo = deltaEventPtr->stateOffset;
|
|
|
|
// Ignore extra state at end of event.
|
|
sizeOfStateToCopy = receivedStateSize;
|
|
if (offsetInDeviceStateToCopyTo + sizeOfStateToCopy > stateBlockSizeOfDevice)
|
|
{
|
|
if (offsetInDeviceStateToCopyTo >= stateBlockSizeOfDevice)
|
|
return false; // Entire delta state is out of range.
|
|
|
|
sizeOfStateToCopy = stateBlockSizeOfDevice - offsetInDeviceStateToCopyTo;
|
|
}
|
|
}
|
|
|
|
Debug.Assert(device.m_StateBlock.format == receivedStateFormat, "Received state format does not match format of device");
|
|
|
|
// Write state.
|
|
return UpdateState(device, updateType, ptrToReceivedState, offsetInDeviceStateToCopyTo,
|
|
sizeOfStateToCopy, eventPtr->internalTime, eventPtr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method is the workhorse for updating input state in the system. It runs all the logic of incorporating
|
|
/// new state into devices and triggering whatever change monitors are attached to the state memory that gets
|
|
/// touched.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method can be invoked from outside the event processing loop and the given data does not have to come
|
|
/// from an event.
|
|
///
|
|
/// This method does NOT respect <see cref="IInputStateCallbackReceiver"/>. This means that the device will
|
|
/// NOT get a shot at intervening in the state write.
|
|
/// </remarks>
|
|
/// <param name="device">Device to update state on. <paramref name="stateOffsetInDevice"/> is relative to device's
|
|
/// starting offset in memory.</param>
|
|
/// <param name="eventPtr">Pointer to state event from which the state change was initiated. Null if the state
|
|
/// change is not coming from an event.</param>
|
|
internal unsafe bool UpdateState(InputDevice device, InputUpdateType updateType,
|
|
void* statePtr, uint stateOffsetInDevice, uint stateSize, double internalTime, InputEventPtr eventPtr = default)
|
|
{
|
|
var deviceIndex = device.m_DeviceIndex;
|
|
ref var stateBlockOfDevice = ref device.m_StateBlock;
|
|
|
|
////TODO: limit stateSize and StateOffset by the device's state memory
|
|
|
|
var deviceBuffer = (byte*)InputStateBuffers.GetFrontBufferForDevice(deviceIndex);
|
|
|
|
// If state monitors need to be re-sorted, do it now.
|
|
// NOTE: This must happen with the monitors in non-signalled state!
|
|
SortStateChangeMonitorsIfNecessary(deviceIndex);
|
|
|
|
// Before we update state, let change monitors compare the old and the new state.
|
|
// We do this instead of first updating the front buffer and then comparing to the
|
|
// back buffer as that would require a buffer flip for each state change in order
|
|
// for the monitors to work reliably. By comparing the *event* data to the current
|
|
// state, we can have multiple state events in the same frame yet still get reliable
|
|
// change notifications.
|
|
var haveSignalledMonitors =
|
|
ProcessStateChangeMonitors(deviceIndex, statePtr,
|
|
deviceBuffer + stateBlockOfDevice.byteOffset,
|
|
stateSize, stateOffsetInDevice);
|
|
|
|
var deviceStateOffset = device.m_StateBlock.byteOffset + stateOffsetInDevice;
|
|
var deviceStatePtr = deviceBuffer + deviceStateOffset;
|
|
|
|
////REVIEW: Should we do this only for events but not for InputState.Change()?
|
|
// If noise filtering on .current is turned on and the device may have noise,
|
|
// determine if the event carries signal or not.
|
|
var noiseMask = device.noisy
|
|
? (byte*)InputStateBuffers.s_NoiseMaskBuffer + deviceStateOffset
|
|
: null;
|
|
// Compare the current state of the device to the newly received state but overlay
|
|
// the comparison by the noise mask.
|
|
var makeDeviceCurrent = !MemoryHelpers.MemCmpBitRegion(deviceStatePtr, statePtr,
|
|
0, stateSize * 8, mask: noiseMask);
|
|
|
|
// Buffer flip.
|
|
var flipped = FlipBuffersForDeviceIfNecessary(device, updateType);
|
|
|
|
// Now write the state.
|
|
#if UNITY_EDITOR
|
|
if (updateType == InputUpdateType.Editor)
|
|
{
|
|
WriteStateChange(m_StateBuffers.m_EditorStateBuffers, deviceIndex, ref stateBlockOfDevice, stateOffsetInDevice,
|
|
statePtr, stateSize, flipped);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
WriteStateChange(m_StateBuffers.m_PlayerStateBuffers, deviceIndex, ref stateBlockOfDevice,
|
|
stateOffsetInDevice, statePtr, stateSize, flipped);
|
|
}
|
|
|
|
// Notify listeners.
|
|
DelegateHelpers.InvokeCallbacksSafe(ref m_DeviceStateChangeListeners,
|
|
device, eventPtr, "InputSystem.onDeviceStateChange");
|
|
|
|
// Now that we've committed the new state to memory, if any of the change
|
|
// monitors fired, let the associated actions know.
|
|
if (haveSignalledMonitors)
|
|
FireStateChangeNotifications(deviceIndex, internalTime, eventPtr);
|
|
|
|
return makeDeviceCurrent;
|
|
}
|
|
|
|
private static unsafe void WriteStateChange(InputStateBuffers.DoubleBuffers buffers, int deviceIndex,
|
|
ref InputStateBlock deviceStateBlock, uint stateOffsetInDevice, void* statePtr, uint stateSizeInBytes, bool flippedBuffers)
|
|
{
|
|
var frontBuffer = buffers.GetFrontBuffer(deviceIndex);
|
|
Debug.Assert(frontBuffer != null);
|
|
|
|
// If we're updating less than the full state, we need to preserve the parts we are not updating.
|
|
// Instead of trying to optimize here and only copy what we really need, we just go and copy the
|
|
// entire state of the device over.
|
|
//
|
|
// NOTE: This copying must only happen once, right after a buffer flip. Otherwise we may copy old,
|
|
// stale input state from the back buffer over state that has already been updated with newer
|
|
// data.
|
|
var deviceStateSize = deviceStateBlock.sizeInBits / 8; // Always byte-aligned; avoid calling alignedSizeInBytes.
|
|
if (flippedBuffers && deviceStateSize != stateSizeInBytes)
|
|
{
|
|
var backBuffer = buffers.GetBackBuffer(deviceIndex);
|
|
Debug.Assert(backBuffer != null);
|
|
|
|
UnsafeUtility.MemCpy(
|
|
(byte*)frontBuffer + deviceStateBlock.byteOffset,
|
|
(byte*)backBuffer + deviceStateBlock.byteOffset,
|
|
deviceStateSize);
|
|
}
|
|
|
|
UnsafeUtility.MemCpy((byte*)frontBuffer + deviceStateBlock.byteOffset + stateOffsetInDevice, statePtr,
|
|
stateSizeInBytes);
|
|
}
|
|
|
|
// Flip front and back buffer for device, if necessary. May flip buffers for more than just
|
|
// the given update type.
|
|
// Returns true if there was a buffer flip.
|
|
private bool FlipBuffersForDeviceIfNecessary(InputDevice device, InputUpdateType updateType)
|
|
{
|
|
if (updateType == InputUpdateType.BeforeRender)
|
|
{
|
|
////REVIEW: I think this is wrong; if we haven't flipped in the current dynamic or fixed update, we should do so now
|
|
// We never flip buffers for before render. Instead, we already write
|
|
// into the front buffer.
|
|
return false;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
////REVIEW: should this use the editor update ticks as quasi-frame-boundaries?
|
|
// Updates go to the editor only if the game isn't playing or does not have focus.
|
|
// Otherwise we fall through to the logic that flips for the *next* dynamic and
|
|
// fixed updates.
|
|
if (updateType == InputUpdateType.Editor)
|
|
{
|
|
////REVIEW: This isn't right. The editor does have update ticks which constitute the equivalent of player frames.
|
|
// The editor doesn't really have a concept of frame-to-frame operation the
|
|
// same way the player does. So we simply flip buffers on a device whenever
|
|
// a new state event for it comes in.
|
|
m_StateBuffers.m_EditorStateBuffers.SwapBuffers(device.m_DeviceIndex);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// Flip buffers if we haven't already for this frame.
|
|
if (device.m_CurrentUpdateStepCount != InputUpdate.s_UpdateStepCount)
|
|
{
|
|
m_StateBuffers.m_PlayerStateBuffers.SwapBuffers(device.m_DeviceIndex);
|
|
device.m_CurrentUpdateStepCount = InputUpdate.s_UpdateStepCount;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Domain reload survival logic. Also used for pushing and popping input system
|
|
// state for testing.
|
|
|
|
// Stuff everything that we want to survive a domain reload into
|
|
// a m_SerializedState.
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
[Serializable]
|
|
internal struct DeviceState
|
|
{
|
|
// Preserving InputDevices is somewhat tricky business. Serializing
|
|
// them in full would involve pretty nasty work. We have the restriction,
|
|
// however, that everything needs to be created from layouts (it partly
|
|
// exists for the sake of reload survivability), so we should be able to
|
|
// just go and recreate the device from the layout. This also has the
|
|
// advantage that if the layout changes between reloads, the change
|
|
// automatically takes effect.
|
|
public string name;
|
|
public string layout;
|
|
public string variants;
|
|
public string[] usages;
|
|
public int deviceId;
|
|
public int participantId;
|
|
public InputDevice.DeviceFlags flags;
|
|
public InputDeviceDescription description;
|
|
|
|
public void Restore(InputDevice device)
|
|
{
|
|
var usageCount = usages.LengthSafe();
|
|
for (var i = 0; i < usageCount; ++i)
|
|
device.AddDeviceUsage(new InternedString(usages[i]));
|
|
device.m_ParticipantId = participantId;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// State we take across domain reloads.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Most of the state we re-recreate in-between reloads and do not store
|
|
/// in this structure. In particular, we do not preserve anything from
|
|
/// the various RegisterXXX().
|
|
///
|
|
/// WARNING
|
|
///
|
|
/// Making changes to serialized data format will likely to break upgrading projects from older versions.
|
|
/// That is until you restart the editor, then we recreate everything from clean state.
|
|
/// </remarks>
|
|
[Serializable]
|
|
internal struct SerializedState
|
|
{
|
|
public int layoutRegistrationVersion;
|
|
public float pollingFrequency;
|
|
public DeviceState[] devices;
|
|
public AvailableDevice[] availableDevices;
|
|
public InputStateBuffers buffers;
|
|
public InputUpdate.SerializedState updateState;
|
|
public InputUpdateType updateMask;
|
|
public InputMetrics metrics;
|
|
public InputSettings settings;
|
|
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
public bool haveSentStartupAnalytics;
|
|
#endif
|
|
}
|
|
|
|
internal SerializedState SaveState()
|
|
{
|
|
// Devices.
|
|
var deviceCount = m_DevicesCount;
|
|
var deviceArray = new DeviceState[deviceCount];
|
|
for (var i = 0; i < deviceCount; ++i)
|
|
{
|
|
var device = m_Devices[i];
|
|
string[] usages = null;
|
|
if (device.usages.Count > 0)
|
|
usages = device.usages.Select(x => x.ToString()).ToArray();
|
|
|
|
var deviceState = new DeviceState
|
|
{
|
|
name = device.name,
|
|
layout = device.layout,
|
|
variants = device.variants,
|
|
deviceId = device.deviceId,
|
|
participantId = device.m_ParticipantId,
|
|
usages = usages,
|
|
description = device.m_Description,
|
|
flags = device.m_DeviceFlags
|
|
};
|
|
deviceArray[i] = deviceState;
|
|
}
|
|
|
|
return new SerializedState
|
|
{
|
|
layoutRegistrationVersion = m_LayoutRegistrationVersion,
|
|
pollingFrequency = m_PollingFrequency,
|
|
devices = deviceArray,
|
|
availableDevices = m_AvailableDevices?.Take(m_AvailableDeviceCount).ToArray(),
|
|
buffers = m_StateBuffers,
|
|
updateState = InputUpdate.Save(),
|
|
updateMask = m_UpdateMask,
|
|
metrics = m_Metrics,
|
|
settings = m_Settings,
|
|
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
haveSentStartupAnalytics = m_HaveSentStartupAnalytics,
|
|
#endif
|
|
};
|
|
}
|
|
|
|
internal void RestoreStateWithoutDevices(SerializedState state)
|
|
{
|
|
m_StateBuffers = state.buffers;
|
|
m_LayoutRegistrationVersion = state.layoutRegistrationVersion + 1;
|
|
updateMask = state.updateMask;
|
|
m_Metrics = state.metrics;
|
|
m_PollingFrequency = state.pollingFrequency;
|
|
|
|
if (m_Settings != null)
|
|
Object.DestroyImmediate(m_Settings);
|
|
m_Settings = state.settings;
|
|
|
|
#if UNITY_ANALYTICS || UNITY_EDITOR
|
|
m_HaveSentStartupAnalytics = state.haveSentStartupAnalytics;
|
|
#endif
|
|
|
|
////REVIEW: instead of accessing globals here, we could move this to when we re-create devices
|
|
|
|
// Update state.
|
|
InputUpdate.Restore(state.updateState);
|
|
}
|
|
|
|
// If these are set, we clear them out on the first input update.
|
|
internal DeviceState[] m_SavedDeviceStates;
|
|
internal AvailableDevice[] m_SavedAvailableDevices;
|
|
|
|
/// <summary>
|
|
/// Recreate devices based on the devices we had before a domain reload.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that device indices may change between domain reloads.
|
|
///
|
|
/// We recreate devices using the layout information as it exists now as opposed to
|
|
/// as it existed before the domain reload. This means we'll be picking up any changes that
|
|
/// have happened to layouts as part of the reload (including layouts having been removed
|
|
/// entirely).
|
|
/// </remarks>
|
|
internal void RestoreDevicesAfterDomainReload()
|
|
{
|
|
Profiler.BeginSample("InputManager.RestoreDevicesAfterDomainReload");
|
|
|
|
using (InputDeviceBuilder.Ref())
|
|
{
|
|
DeviceState[] retainedDeviceStates = null;
|
|
var deviceStates = m_SavedDeviceStates;
|
|
var deviceCount = m_SavedDeviceStates.LengthSafe();
|
|
m_SavedDeviceStates = null; // Prevent layout matcher registering themselves on the fly from picking anything off this list.
|
|
for (var i = 0; i < deviceCount; ++i)
|
|
{
|
|
ref var deviceState = ref deviceStates[i];
|
|
|
|
var device = TryGetDeviceById(deviceState.deviceId);
|
|
if (device != null)
|
|
continue;
|
|
|
|
var layout = TryFindMatchingControlLayout(ref deviceState.description,
|
|
deviceState.deviceId);
|
|
if (layout.IsEmpty())
|
|
{
|
|
var previousLayout = new InternedString(deviceState.layout);
|
|
if (m_Layouts.HasLayout(previousLayout))
|
|
layout = previousLayout;
|
|
}
|
|
if (layout.IsEmpty() || !RestoreDeviceFromSavedState(ref deviceState, layout))
|
|
ArrayHelpers.Append(ref retainedDeviceStates, deviceState);
|
|
}
|
|
|
|
// See if we can make sense of an available device now that we couldn't make sense of
|
|
// before. This can be the case if there's new layout information that wasn't available
|
|
// before.
|
|
if (m_SavedAvailableDevices != null)
|
|
{
|
|
m_AvailableDevices = m_SavedAvailableDevices;
|
|
m_AvailableDeviceCount = m_SavedAvailableDevices.LengthSafe();
|
|
for (var i = 0; i < m_AvailableDeviceCount; ++i)
|
|
{
|
|
var device = TryGetDeviceById(m_AvailableDevices[i].deviceId);
|
|
if (device != null)
|
|
continue;
|
|
|
|
if (m_AvailableDevices[i].isRemoved)
|
|
continue;
|
|
|
|
var layout = TryFindMatchingControlLayout(ref m_AvailableDevices[i].description,
|
|
m_AvailableDevices[i].deviceId);
|
|
if (!layout.IsEmpty())
|
|
{
|
|
try
|
|
{
|
|
AddDevice(layout, m_AvailableDevices[i].deviceId,
|
|
deviceDescription: m_AvailableDevices[i].description,
|
|
deviceFlags: m_AvailableDevices[i].isNative ? InputDevice.DeviceFlags.Native : 0);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Just ignore. Simply means we still can't really turn the device into something useful.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Done. Discard saved arrays.
|
|
m_SavedDeviceStates = retainedDeviceStates;
|
|
m_SavedAvailableDevices = null;
|
|
}
|
|
|
|
Profiler.EndSample();
|
|
}
|
|
|
|
// We have two general types of devices we need to care about when recreating devices
|
|
// after domain reloads:
|
|
//
|
|
// A) device with InputDeviceDescription
|
|
// B) device created directly from specific layout
|
|
//
|
|
// A) should go through the normal matching process whereas B) should get recreated with
|
|
// layout of same name (if still available).
|
|
//
|
|
// So we kick device recreation off from two points:
|
|
//
|
|
// 1) From RegisterControlLayoutMatcher to catch A)
|
|
// 2) From RegisterControlLayout to catch B)
|
|
//
|
|
// Additionally, we have the complication that a layout a device was using was something
|
|
// dynamically registered from onFindLayoutForDevice. We don't do anything special about that.
|
|
// The first full input update will flush out the list of saved device states and at that
|
|
// point, any onFindLayoutForDevice hooks simply have to be in place. If they are, devices
|
|
// will get recreated appropriately.
|
|
//
|
|
// It would be much simpler to recreate all devices as the first thing in the first full input
|
|
// update but that would mean that devices would become available only very late. They would
|
|
// not, for example, be available when MonoBehaviour.Start methods are invoked.
|
|
|
|
private bool RestoreDeviceFromSavedState(ref DeviceState deviceState, InternedString layout)
|
|
{
|
|
// We assign the same device IDs here to newly created devices that they had
|
|
// before the domain reload. This is safe as device ID allocation is under the
|
|
// control of the runtime and not expected to be affected by a domain reload.
|
|
|
|
InputDevice device;
|
|
try
|
|
{
|
|
device = AddDevice(layout,
|
|
deviceDescription: deviceState.description,
|
|
deviceId: deviceState.deviceId,
|
|
deviceName: deviceState.name,
|
|
deviceFlags: deviceState.flags,
|
|
variants: new InternedString(deviceState.variants));
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(
|
|
$"Could not recreate input device '{deviceState.description}' with layout '{deviceState.layout}' and variants '{deviceState.variants}' after domain reload");
|
|
Debug.LogException(exception);
|
|
return true; // Don't try again.
|
|
}
|
|
|
|
deviceState.Restore(device);
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif // UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
}
|
|
}
|