From 7ee77c1856676e525747c080573be4a6e247dd80 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Thu, 16 Oct 2025 20:47:57 -0400 Subject: [PATCH 01/12] Rename top class to ISW; First draft of new API with default multiplayer support implemented --- .../CustomEditors/OfflineInputDataEditor.cs | 16 +- .../InputWrapperDebuggerWindow.cs | 22 +- Editor/Scripts/Helper.cs | 2 - Editor/Scripts/InputScriptGenerator.cs | 2 - .../InputManagerContentBuilder.cs | 66 +---- .../InputPlayerContentBuilder.cs | 10 - .../InputUserChangeInfoContentBuilder.cs | 27 -- .../InputUserChangeInfoContentBuilder.cs.meta | 11 - .../PlayerIDContentBuilder.cs | 25 -- .../PlayerIDContentBuilder.cs.meta | 11 - Runtime/Scripts/Actions/ActionReference.cs | 33 ++- Runtime/Scripts/Actions/ActionWrapper.cs | 12 +- Runtime/Scripts/Bindings/BindingChanger.cs | 8 +- Runtime/Scripts/Bindings/BindingInfo.cs | 2 +- Runtime/Scripts/Bindings/BindingSaveLoad.cs | 5 +- .../Scripts/Components/InputActionUpdater.cs | 10 +- .../CustomSetups/CustomSetupsRegisterer.cs | 8 +- Runtime/Scripts/Data/OfflineInputData.cs | 8 - Runtime/Scripts/Data/RuntimeInputData.cs | 18 +- Runtime/Scripts/Enums/PlayerID.cs | 12 - Runtime/Scripts/Enums/PlayerID.cs.meta | 11 - ...nPress.cs => ISW.WaitForAnyButtonPress.cs} | 4 +- ...meta => ISW.WaitForAnyButtonPress.cs.meta} | 0 Runtime/Scripts/{Input.cs => ISW.cs} | 246 +++++++++--------- .../Scripts/{Input.cs.meta => ISW.cs.meta} | 0 Runtime/Scripts/InputPlayer.cs | 24 +- Runtime/Scripts/InputPlayerCollection.cs | 139 +++++++--- Runtime/Scripts/InputUserChangeInfo.cs | 13 +- .../Utilities/Extensions/ArrayExtensions.cs | 14 - .../Extensions/StringExtensions.cs.meta | 2 +- 30 files changed, 324 insertions(+), 437 deletions(-) delete mode 100644 Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs delete mode 100644 Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs.meta delete mode 100644 Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs delete mode 100644 Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta delete mode 100644 Runtime/Scripts/Enums/PlayerID.cs delete mode 100644 Runtime/Scripts/Enums/PlayerID.cs.meta rename Runtime/Scripts/{Input.WaitForAnyButtonPress.cs => ISW.WaitForAnyButtonPress.cs} (95%) rename Runtime/Scripts/{Input.WaitForAnyButtonPress.cs.meta => ISW.WaitForAnyButtonPress.cs.meta} (100%) rename Runtime/Scripts/{Input.cs => ISW.cs} (71%) rename Runtime/Scripts/{Input.cs.meta => ISW.cs.meta} (100%) diff --git a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs index c4694b7..221fb55 100644 --- a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs +++ b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs @@ -17,8 +17,6 @@ internal class OfflineInputDataEditor : UnityEditor.Editor private SerializedProperty initializationMode; - private SerializedProperty enableMultiplayer; - private SerializedProperty maxPlayers; private SerializedProperty defaultContext; private SerializedProperty inputContexts; @@ -48,8 +46,6 @@ internal class OfflineInputDataEditor : UnityEditor.Editor private void OnEnable() { initializationMode = serializedObject.FindProperty(nameof(initializationMode)); - enableMultiplayer = serializedObject.FindProperty(nameof(enableMultiplayer)); - maxPlayers = serializedObject.FindProperty(nameof(maxPlayers)); defaultContext = serializedObject.FindProperty(nameof(defaultContext)); inputContexts = serializedObject.FindProperty(nameof(inputContexts)); @@ -119,17 +115,7 @@ private void DrawSpecialNote(string text) public override void OnInspectorGUI() { - // TODO (multiplayer): Remove the disabled group when MP support is completed. - DrawHeader("Multiplayer"); - EditorGUI.BeginDisabledGroup(true); - DrawWarning("Multiplayer support is currently incomplete, so it cannot be enabled right now."); - EditorGUILayout.PropertyField(enableMultiplayer); - EditorGUILayout.PropertyField(maxPlayers); - maxPlayers.intValue = Mathf.Clamp(maxPlayers.intValue, 2, OfflineInputData.MAX_PLAYERS_LIMIT); - EditorGUI.EndDisabledGroup(); - - EditorInspectorUtility.DrawHorizontalLine(); - + DrawHeader("Initialization"); EditorGUILayout.PropertyField(initializationMode); EditorInspectorUtility.DrawHorizontalLine(); diff --git a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs index 0dcabc1..127efd3 100644 --- a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs +++ b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs @@ -26,7 +26,8 @@ internal TimestampedObject(T value, string timestamp) } } - private List> mostRecentContexts = new(); + private readonly List> mostRecentContexts = new(); + private int selectedPlayerID = 0; // TODO: Make switchable in the debugger UI private OfflineInputData offlineInputData; private OfflineInputData OfflineInputData @@ -51,23 +52,22 @@ private void OnDisable() private void HandlePlayModeStateChanged(PlayModeStateChange state) { - switch (state) { case PlayModeStateChange.EnteredPlayMode: mostRecentContexts.Clear(); - mostRecentContexts.Add(new TimestampedObject(Input.Context, 0.ToString())); - Input.EDITOR_OnPlayerInputContextChanged += HandlePlayerInputContextChanged; + mostRecentContexts.Add(new TimestampedObject(ISW.EDITOR_GetDefaultContext(), 0.ToString())); + ISW.EDITOR_OnPlayerInputContextChanged += HandlePlayerInputContextChanged; break; case PlayModeStateChange.ExitingPlayMode: - Input.EDITOR_OnPlayerInputContextChanged -= HandlePlayerInputContextChanged; + ISW.EDITOR_OnPlayerInputContextChanged -= HandlePlayerInputContextChanged; break; } } - private void HandlePlayerInputContextChanged(PlayerID playerID, InputContext inputContext) + private void HandlePlayerInputContextChanged(int playerID, InputContext inputContext) { - ISWDebug.Log($"Input Context changed for {playerID}: {inputContext}"); + ISWDebug.Log($"Input Context changed for player {playerID}: {inputContext}"); mostRecentContexts.Add(new TimestampedObject(inputContext, Time.frameCount.ToString())); if (mostRecentContexts.Count > MAX_SHOWN_RECENT_CONTEXTS) { @@ -95,7 +95,7 @@ private void OnGUI() return; } - if (!Input.EDITOR_IsInitialized) + if (!ISW.EDITOR_IsInitialized) { EditorGUILayout.Space(EditorGUIUtility.singleLineHeight); EditorGUILayout.LabelField("Input not yet initialized, waiting...", new GUIStyle(EditorStyles.label) { fontStyle = FontStyle.BoldAndItalic }); @@ -103,8 +103,8 @@ private void OnGUI() } GUILayout.BeginVertical(); - ShowDebugInfoField("Current Control Scheme", Input.CurrentControlScheme.ToString()); - ShowDebugInfoField("Current Context", Input.Context.ToString()); + ShowDebugInfoField("Current Control Scheme", ISW.Player(selectedPlayerID).CurrentControlScheme.ToString()); + ShowDebugInfoField("Current Context", ISW.Player(selectedPlayerID).InputContext.ToString()); ShowIndentedField("Active Maps", ActiveMapLabelFields); ShowIndentedField("Most Recent Contexts", MostRecentContextLabelFields); GUILayout.EndVertical(); @@ -124,7 +124,7 @@ private void ActiveMapLabelFields() { foreach (InputContextInfo inputContextInfo in OfflineInputData.InputContexts) { - if (inputContextInfo.Name.AsEnumMember() != Input.Context.ToString()) + if (inputContextInfo.Name.AsEnumMember() != ISW.Player(selectedPlayerID).InputContext.ToString()) { continue; } diff --git a/Editor/Scripts/Helper.cs b/Editor/Scripts/Helper.cs index 4d977e2..f0e4947 100644 --- a/Editor/Scripts/Helper.cs +++ b/Editor/Scripts/Helper.cs @@ -30,8 +30,6 @@ internal static class Helper internal static string InputPlayerFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string ControlSchemeFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string InputContextFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string PlayerIDFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string InputUserChangeInfoFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string RuntimeInputDataFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string BindingChangerFileSystemPath => EditorScriptGetter.GetSystemFilePath(typeof(BindingChanger)); private static string InputManagerFolderSystemPath => EditorAssetGetter.GetSystemFolderPath(OfflineInputData.MainInputScriptFile); diff --git a/Editor/Scripts/InputScriptGenerator.cs b/Editor/Scripts/InputScriptGenerator.cs index dc598c4..5f79a99 100644 --- a/Editor/Scripts/InputScriptGenerator.cs +++ b/Editor/Scripts/InputScriptGenerator.cs @@ -33,10 +33,8 @@ internal static void GenerateInputScriptCode() ModifyExistingFile(Helper.ControlSchemeFileSystemPath, new ControlSchemeContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputContextFileSystemPath, new InputContextContentBuilder(offlineInputData)); - ModifyExistingFile(Helper.PlayerIDFileSystemPath, new PlayerIDContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputPlayerFileSystemPath, new InputPlayerContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputManagerFileSystemPath, new InputManagerContentBuilder(offlineInputData)); - ModifyExistingFile(Helper.InputUserChangeInfoFileSystemPath, new InputUserChangeInfoContentBuilder(offlineInputData)); ModifyExistingFile(Helper.RuntimeInputDataFileSystemPath, new RuntimeInputDataContentBuilder(offlineInputData)); ModifyExistingFile(Helper.BindingChangerFileSystemPath, new BindingChangerContentBuilder(offlineInputData)); diff --git a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs index 8fab957..5e57882 100644 --- a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs @@ -8,56 +8,19 @@ namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders { internal class InputManagerContentBuilder : ContentBuilder { + private const string DEFAULT_PLAYER_FIELD = "DefaultPlayer"; + internal override void AddContent(InputScriptGeneratorMarkerInfo info) { - void addEmptyLine() => info.NewLines.Add(string.Empty); - switch (info.MarkerName) { case "RuntimeInputDataPath": info.NewLines.Add($" private const string RUNTIME_INPUT_DATA_PATH = \"{OfflineInputData.RUNTIME_INPUT_DATA_PATH}\";"); break; - case "SingleOrMultiPlayerFieldsAndProperties": - if (Data.EnableMultiplayer) - { - info.NewLines.Add(" private static bool allowPlayerJoining;\n" + - " public static bool AllowPlayerJoining\n" + - " {\n" + - " get => allowPlayerJoining;\n" + - " set\n" + - " {\n" + - " if (value == allowPlayerJoining) return;\n" + - " allowPlayerJoining = value;\n" + - " ListenForAnyButtonPress = value ? listenForAnyButtonPress + 1 : listenForAnyButtonPress - 1;\n" + - " }\n" + - " }"); - addEmptyLine(); - info.NewLines.Add($" public static {nameof(InputPlayer)} Player({nameof(PlayerID)} id) => GetPlayer(id);"); - addEmptyLine(); - info.NewLines.Add($" public static IEnumerable<{nameof(InputPlayer)}> Players => playerCollection.Players;"); - addEmptyLine(); - break; - } - info.NewLines.Add(getSinglePlayerEventWrapperString(nameof(InputUserChangeInfo), "OnInputUserChange")); - addEmptyLine(); - info.NewLines.Add(getSinglePlayerEventWrapperString(nameof(ControlScheme), "OnControlSchemeChanged")); - addEmptyLine(); - info.NewLines.Add(getSinglePlayerEventWrapperString("char", "OnKeyboardTextInput")); - addEmptyLine(); + case "SinglePlayerFieldsAndProperties": string[] mapNames = Helper.GetMapNames(Asset).ToArray(); - info.NewLines.AddRange(mapNames.Select(mapName => $" public static {mapName.AsType()}Actions {mapName.AsType()} => Player1.{mapName.AsType()};")); - if (mapNames.Length > 0) addEmptyLine(); - info.NewLines.Add($" public static {nameof(InputContext)} Context"); - info.NewLines.Add(" {"); - info.NewLines.Add($" get => Player1.InputContext;"); - info.NewLines.Add($" set => Player1.InputContext = value;"); - info.NewLines.Add(" }"); - addEmptyLine(); - info.NewLines.Add($" public static {nameof(ControlScheme)} CurrentControlScheme => Player1.CurrentControlScheme;"); - info.NewLines.Add($" public static Vector2 MousePosition => Mouse.current.position.ReadValue();"); - addEmptyLine(); - info.NewLines.Add($" private static {nameof(InputPlayer)} Player1 => GetPlayer({nameof(PlayerID)}.{nameof(PlayerID.Player1)});"); - info.NewLines.Add($" private static bool AllowPlayerJoining => false;"); + info.NewLines.AddRange(mapNames.Select(mapName => $" public static {mapName.AsType()}Actions {mapName.AsType()} => {DEFAULT_PLAYER_FIELD}.{mapName.AsType()};")); + info.NewLines.Add($" public static {nameof(ControlScheme)} CurrentControlScheme => {DEFAULT_PLAYER_FIELD}.CurrentControlScheme;"); break; case "DefaultContextProperty": string defaultContextValue = $"{nameof(InputContext)}.{Data.DefaultContext}"; @@ -81,25 +44,6 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) if (Data.LoadAllBindingOverridesOnInitialize) info.NewLines.Add(" LoadBindingsForAllPlayers();"); break; - case "EnableContextForAllPlayersSignature": - string accessor = Data.EnableMultiplayer ? "public" : "private"; - info.NewLines.Add($" {accessor} static void EnableContextForAllPlayers({nameof(InputContext)} inputContext)"); - break; - case "PlayerGetter": - string playerGetter = Data.EnableMultiplayer - ? $" {nameof(InputPlayer)} player = GetPlayer(playerID);" - : $" {nameof(InputPlayer)} player = Player1;"; - info.NewLines.Add(playerGetter); - break; - } - - string getSinglePlayerEventWrapperString(string parameterName, string eventName) - { - return $" public static event Action<{parameterName}> {eventName}\n" + - " {\n" + - $" add => Player1.{eventName} += value;\n" + - $" remove => Player1.{eventName} -= value;\n" + - " }"; } } diff --git a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs index 6c71e4b..fdf1b39 100644 --- a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs @@ -14,16 +14,6 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) { switch (info.MarkerName) { - case "ControlSchemeEventDefinition": - info.NewLines.Add(Data.EnableMultiplayer - ? $" public event Action<{nameof(InputPlayer)}> OnControlSchemeChanged;" - : $" public event Action<{nameof(ControlScheme)}> OnControlSchemeChanged;"); - break; - case "ControlSchemeEventInvocation": - info.NewLines.Add(Data.EnableMultiplayer - ? " OnControlSchemeChanged?.Invoke(this);" - : " OnControlSchemeChanged?.Invoke(controlScheme);"); - break; case "ActionsProperties": foreach (string mapName in Helper.GetMapNames(Asset)) info.NewLines.Add($" public {mapName.AsProperty()}Actions {mapName.AsProperty()}" + " { get; }"); diff --git a/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs deleted file mode 100644 index 8c79b83..0000000 --- a/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs +++ /dev/null @@ -1,27 +0,0 @@ -using NPTP.InputSystemWrapper.Data; -using NPTP.InputSystemWrapper.Enums; - -namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders -{ - internal class InputUserChangeInfoContentBuilder : ContentBuilder - { - internal override void AddContent(InputScriptGeneratorMarkerInfo info) - { - switch (info.MarkerName) - { - case "PlayerIDProperty": - if (Helper.OfflineInputData.EnableMultiplayer) - info.NewLines.Add($" public {nameof(PlayerID)} {nameof(PlayerID)} " + @"{ get; }"); - break; - case "PlayerIDConstructor": - if (Helper.OfflineInputData.EnableMultiplayer) - info.NewLines.Add($" {nameof(PlayerID)} = inputPlayer.ID;"); - break; - } - } - - internal InputUserChangeInfoContentBuilder(OfflineInputData offlineInputData) : base(offlineInputData) - { - } - } -} \ No newline at end of file diff --git a/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs.meta b/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs.meta deleted file mode 100644 index 1383cdb..0000000 --- a/Editor/Scripts/ScriptContentBuilders/InputUserChangeInfoContentBuilder.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b584baa06262b424b8fb67eef6601429 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs deleted file mode 100644 index e279616..0000000 --- a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using NPTP.InputSystemWrapper.Data; - -namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders -{ - internal class PlayerIDContentBuilder : ContentBuilder - { - internal override void AddContent(InputScriptGeneratorMarkerInfo info) - { - switch (info.MarkerName) - { - case "Members": - int numPlayers = Data.EnableMultiplayer ? Data.MaxPlayers : 1; - for (int i = 0; i < numPlayers; i++) - { - info.NewLines.Add($" Player{i + 1} = {i},"); - } - break; - } - } - - internal PlayerIDContentBuilder(OfflineInputData offlineInputData) : base(offlineInputData) - { - } - } -} \ No newline at end of file diff --git a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta b/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta deleted file mode 100644 index c6ab9c5..0000000 --- a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 421af271854fba843b40295cbb372a6f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Scripts/Actions/ActionReference.cs b/Runtime/Scripts/Actions/ActionReference.cs index e61d575..6e8115e 100644 --- a/Runtime/Scripts/Actions/ActionReference.cs +++ b/Runtime/Scripts/Actions/ActionReference.cs @@ -22,9 +22,12 @@ public partial class ActionReference [SerializeField] private CompositePart compositePart; public CompositePart CompositePart => compositePart; + + [SerializeField] private bool applyToAllPlayers; + internal bool ApplyToAllPlayers => applyToAllPlayers; - // TODO (multiplayer): Ability to tie the reference to a particular player ID (ie change to serialized field with an "AllPlayers" option?) - internal PlayerID PlayerID => PlayerID.Player1; + [SerializeField] private int playerID; + internal int PlayerID => playerID; private ActionWrapper actionWrapper; public ActionWrapper ActionWrapper @@ -32,12 +35,16 @@ public ActionWrapper ActionWrapper get { if (actionWrapper != null) + { return actionWrapper; + } if (reference == null || reference.action == null) + { return null; + } - Input.TryGetActionWrapper(PlayerID, reference.action, out actionWrapper); + ISW.TryGetActionWrapper(PlayerID, reference.action, out actionWrapper); return actionWrapper; } } @@ -45,7 +52,7 @@ public ActionWrapper ActionWrapper public static bool TryConvert(InputActionReference inputActionReference, out ActionReference actionReference) { if (inputActionReference != null && inputActionReference.action != null && - Input.TryConvert(inputActionReference, out ActionWrapper actionWrapper)) + ISW.TryConvert(inputActionReference, out ActionWrapper actionWrapper)) { actionReference = new ActionReference(inputActionReference.action) { actionWrapper = actionWrapper }; return true; @@ -55,9 +62,9 @@ public static bool TryConvert(InputActionReference inputActionReference, out Act return false; } - public static bool TryConvert(InputAction inputAction, out ActionReference actionReference) + public static bool TryConvert(InputAction inputAction, int playerID, out ActionReference actionReference) { - if (inputAction != null && Input.TryGetActionWrapper(PlayerID.Player1, inputAction, out ActionWrapper actionWrapper)) + if (inputAction != null && ISW.TryGetActionWrapper(playerID, inputAction, out ActionWrapper actionWrapper)) { actionReference = new ActionReference(inputAction) { actionWrapper = actionWrapper }; return true; @@ -75,10 +82,9 @@ public bool TryGetCurrentBindingInfo(out IEnumerable bindingInfos) return false; } - if (useCompositePart) - return ActionWrapper.TryGetCurrentBindingInfo(compositePart, out bindingInfos); - else - return ActionWrapper.TryGetCurrentBindingInfo(out bindingInfos); + return useCompositePart + ? ActionWrapper.TryGetCurrentBindingInfo(compositePart, out bindingInfos) + : ActionWrapper.TryGetCurrentBindingInfo(out bindingInfos); } public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) @@ -89,10 +95,9 @@ public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable callback = null) diff --git a/Runtime/Scripts/Actions/ActionWrapper.cs b/Runtime/Scripts/Actions/ActionWrapper.cs index b55527c..15431fb 100644 --- a/Runtime/Scripts/Actions/ActionWrapper.cs +++ b/Runtime/Scripts/Actions/ActionWrapper.cs @@ -27,22 +27,22 @@ public event Action OnEvent public bool IsDown => InputAction.phase == InputActionPhase.Performed; public void StartInteractiveRebind(ControlScheme controlScheme, Action callback = null) => - Input.StartInteractiveRebind(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), callback); + ISW.StartInteractiveRebind(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), callback); public void StartInteractiveRebind(ControlScheme controlScheme, CompositePart compositePart, Action callback = null) => - Input.StartInteractiveRebind(new ActionBindingInfo(this, compositePart, controlScheme), callback); + ISW.StartInteractiveRebind(new ActionBindingInfo(this, compositePart, controlScheme), callback); public bool TryGetCurrentBindingInfo(out IEnumerable bindingInfos) => - Input.TryGetCurrentBindingInfo(this, CompositePart.DontIsolatePart, out bindingInfos); + ISW.TryGetCurrentBindingInfo(this, CompositePart.DontIsolatePart, out bindingInfos); public bool TryGetCurrentBindingInfo(CompositePart compositePart, out IEnumerable bindingInfos) => - Input.TryGetCurrentBindingInfo(this, compositePart, out bindingInfos); + ISW.TryGetCurrentBindingInfo(this, compositePart, out bindingInfos); public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) => - Input.TryGetBindingInfo(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), out bindingInfos); + ISW.TryGetBindingInfo(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), out bindingInfos); public bool TryGetBindingInfo(ControlScheme controlScheme, CompositePart compositePart, out IEnumerable bindingInfos) => - Input.TryGetBindingInfo(new ActionBindingInfo(this, compositePart, controlScheme), out bindingInfos); + ISW.TryGetBindingInfo(new ActionBindingInfo(this, compositePart, controlScheme), out bindingInfos); internal void RegisterCallbacks() { diff --git a/Runtime/Scripts/Bindings/BindingChanger.cs b/Runtime/Scripts/Bindings/BindingChanger.cs index eaf9a08..8f9ee69 100644 --- a/Runtime/Scripts/Bindings/BindingChanger.cs +++ b/Runtime/Scripts/Bindings/BindingChanger.cs @@ -60,7 +60,7 @@ void onComplete(RebindingOperation op) callback?.Invoke(new RebindInfo(actionWrapper, RebindInfo.Status.Completed, bindingInfos)); CleanUpRebindingOperation(ref rebindingOperation); - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } @@ -115,7 +115,7 @@ internal static void ResetBindingToDefaultForControlScheme(ActionBindingInfo act bool compositeCondition(InputBinding binding) => actionBindingInfo.DontUseCompositePart || actionBindingInfo.CompositePart.Matches(binding); if (RemoveDeviceOverridesFromAction(actionBindingInfo.ActionWrapper.InputAction, controlScheme.ToBindingMask(), compositeCondition)) { - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } @@ -129,7 +129,7 @@ internal static void ResetBindingsToDefaultForControlScheme(InputActionAsset ass if (changed) { - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } @@ -140,7 +140,7 @@ internal static void ResetBindingsToDefault(InputActionAsset asset) if (changed) { - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } diff --git a/Runtime/Scripts/Bindings/BindingInfo.cs b/Runtime/Scripts/Bindings/BindingInfo.cs index 24868ac..e161d08 100644 --- a/Runtime/Scripts/Bindings/BindingInfo.cs +++ b/Runtime/Scripts/Bindings/BindingInfo.cs @@ -26,7 +26,7 @@ public string DisplayName get { LocalizedStringRequest localizedStringRequest = new(localizationKey); - Input.BroadcastLocalizedStringRequested(localizedStringRequest); + ISW.BroadcastLocalizedStringRequested(localizedStringRequest); return string.IsNullOrEmpty(localizedStringRequest.localizedString) ? localizationKey : localizedStringRequest.localizedString; diff --git a/Runtime/Scripts/Bindings/BindingSaveLoad.cs b/Runtime/Scripts/Bindings/BindingSaveLoad.cs index 85a8f15..76880ef 100644 --- a/Runtime/Scripts/Bindings/BindingSaveLoad.cs +++ b/Runtime/Scripts/Bindings/BindingSaveLoad.cs @@ -1,5 +1,4 @@ using System.IO; -using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Utilities; using UnityEngine; using UnityEngine.InputSystem; @@ -11,9 +10,9 @@ internal static class BindingSaveLoad private const string FILE_TYPE = "json"; private const string BINDING_FILE_NAME_PREFIX = "InputBindingOverrides_"; - private static string GetBindingFilePathForPlayer(PlayerID playerID) + private static string GetBindingFilePathForPlayer(int playerID) { - return $"{Application.persistentDataPath}{Path.DirectorySeparatorChar}{BINDING_FILE_NAME_PREFIX}{playerID.ToString()}.{FILE_TYPE}"; + return $"{Application.persistentDataPath}{Path.DirectorySeparatorChar}{BINDING_FILE_NAME_PREFIX}PlayerID{playerID}.{FILE_TYPE}"; } internal static void LoadBindingsFromDiskForPlayer(InputPlayer inputPlayer) diff --git a/Runtime/Scripts/Components/InputActionUpdater.cs b/Runtime/Scripts/Components/InputActionUpdater.cs index b8e83ae..b8155fd 100644 --- a/Runtime/Scripts/Components/InputActionUpdater.cs +++ b/Runtime/Scripts/Components/InputActionUpdater.cs @@ -26,18 +26,18 @@ private void Start() private void OnEnable() { - Input.OnInputUserChange += HandleInputUserChange; - Input.OnBindingsChanged += HandleBindingsChanged; + ISW.OnAnyPlayerInputUserChange += HandleAnyPlayerInputUserChange; + ISW.OnBindingsChanged += HandleBindingsChanged; UpdateEvents(); } private void OnDisable() { - Input.OnInputUserChange -= HandleInputUserChange; - Input.OnBindingsChanged -= HandleBindingsChanged; + ISW.OnAnyPlayerInputUserChange -= HandleAnyPlayerInputUserChange; + ISW.OnBindingsChanged -= HandleBindingsChanged; } - private void HandleInputUserChange(InputUserChangeInfo inputUserChangeInfo) + private void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { UpdateEvents(); } diff --git a/Runtime/Scripts/CustomSetups/CustomSetupsRegisterer.cs b/Runtime/Scripts/CustomSetups/CustomSetupsRegisterer.cs index ef19fa9..d282059 100644 --- a/Runtime/Scripts/CustomSetups/CustomSetupsRegisterer.cs +++ b/Runtime/Scripts/CustomSetups/CustomSetupsRegisterer.cs @@ -1,5 +1,4 @@ using NPTP.InputSystemWrapper.Data; -using NPTP.InputSystemWrapper.Utilities.Extensions; #if UNITY_EDITOR using NPTP.InputSystemWrapper.Utilities; @@ -34,9 +33,10 @@ static CustomSetupsRegisterer() internal static void PerformRegistrations(RuntimeInputData runtimeInputData) { - runtimeInputData.CustomLayouts.ForEach(layout => layout.Register()); - runtimeInputData.CustomBindings.ForEach(binding => binding.Register()); - runtimeInputData.CustomInteractions.ForEach(interaction => interaction.Register()); + foreach (CustomSetup customSetup in runtimeInputData.AllCustomSetups) + { + customSetup.Register(); + } } } } diff --git a/Runtime/Scripts/Data/OfflineInputData.cs b/Runtime/Scripts/Data/OfflineInputData.cs index 34714cb..5a33d0d 100644 --- a/Runtime/Scripts/Data/OfflineInputData.cs +++ b/Runtime/Scripts/Data/OfflineInputData.cs @@ -20,7 +20,6 @@ internal class OfflineInputData : ScriptableObject { #if UNITY_EDITOR internal const string RUNTIME_INPUT_DATA_PATH = nameof(RuntimeInputData); - internal const int MAX_PLAYERS_LIMIT = 4; [SerializeField] private TextAsset rootPathIdentifier; internal string AssetsPathToPackage @@ -44,13 +43,6 @@ internal string AssetsPathToPackage [SerializeField] private InitializationMode initializationMode = InitializationMode.BeforeSceneLoad; internal InitializationMode InitializationMode => initializationMode; - [SerializeField] private bool enableMultiplayer; - internal bool EnableMultiplayer => enableMultiplayer; - - // TODO (multiplayer): remove player limits, refactor playerIDs into guid-style structs etc. and use lazy initialization on ID entry/player creation - [SerializeField][Range(2, MAX_PLAYERS_LIMIT)] private int maxPlayers = MAX_PLAYERS_LIMIT; - internal int MaxPlayers => maxPlayers; - [SerializeField] private InputContext defaultContext = 0; internal InputContext DefaultContext => defaultContext; diff --git a/Runtime/Scripts/Data/RuntimeInputData.cs b/Runtime/Scripts/Data/RuntimeInputData.cs index 037aa1d..474d4dd 100644 --- a/Runtime/Scripts/Data/RuntimeInputData.cs +++ b/Runtime/Scripts/Data/RuntimeInputData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.CustomSetups; using NPTP.InputSystemWrapper.Enums; @@ -17,14 +18,21 @@ internal class RuntimeInputData : ScriptableObject internal InputActionAsset InputActionAsset => inputActionAsset; [SerializeField] private CustomLayout[] customLayouts; - internal CustomLayout[] CustomLayouts => customLayouts; - [SerializeField] private CustomBinding[] customBindings; - internal CustomBinding[] CustomBindings => customBindings; - [SerializeField] private CustomInteraction[] customInteractions; - internal CustomInteraction[] CustomInteractions => customInteractions; + public IEnumerable AllCustomSetups + { + get + { + List customSetups = new(); + customSetups.AddRange(customLayouts); + customSetups.AddRange(customBindings); + customSetups.AddRange(customInteractions); + return customSetups; + } + } + // MARKER.ControlSchemeBindingData.Start // MARKER.ControlSchemeBindingData.End diff --git a/Runtime/Scripts/Enums/PlayerID.cs b/Runtime/Scripts/Enums/PlayerID.cs deleted file mode 100644 index a1c4fbf..0000000 --- a/Runtime/Scripts/Enums/PlayerID.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NPTP.InputSystemWrapper.Enums -{ - /// - /// An ID for all possible players which also serves as an integer index. - /// - public enum PlayerID - { - // MARKER.Members.Start - Player1 = 0, - // MARKER.Members.End - } -} diff --git a/Runtime/Scripts/Enums/PlayerID.cs.meta b/Runtime/Scripts/Enums/PlayerID.cs.meta deleted file mode 100644 index d51210b..0000000 --- a/Runtime/Scripts/Enums/PlayerID.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 819aa11fe695a044c88d64e15d85e54c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Scripts/Input.WaitForAnyButtonPress.cs b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs similarity index 95% rename from Runtime/Scripts/Input.WaitForAnyButtonPress.cs rename to Runtime/Scripts/ISW.WaitForAnyButtonPress.cs index afb39b8..fb705ca 100644 --- a/Runtime/Scripts/Input.WaitForAnyButtonPress.cs +++ b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs @@ -3,12 +3,12 @@ namespace NPTP.InputSystemWrapper { - public static partial class Input + public static partial class ISW { /// /// Custom yield instruction for coroutines to make waiting for any button press a lot more syntactically convenient. /// Use like: - /// yield return new Input.WaitForAnyButtonPress(); + /// yield return new ISW.WaitForAnyButtonPress(); /// public class WaitForAnyButtonPress : CustomYieldInstruction { diff --git a/Runtime/Scripts/Input.WaitForAnyButtonPress.cs.meta b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta similarity index 100% rename from Runtime/Scripts/Input.WaitForAnyButtonPress.cs.meta rename to Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta diff --git a/Runtime/Scripts/Input.cs b/Runtime/Scripts/ISW.cs similarity index 71% rename from Runtime/Scripts/Input.cs rename to Runtime/Scripts/ISW.cs index d9d168c..5508720 100644 --- a/Runtime/Scripts/Input.cs +++ b/Runtime/Scripts/ISW.cs @@ -24,11 +24,12 @@ namespace NPTP.InputSystemWrapper { /// /// Main point of usage for all input in the game. + /// ISW stands for "Input System Wrapper". /// - public static partial class Input + public static partial class ISW { #region Fields & Properties - + // MARKER.RuntimeInputDataPath.Start private const string RUNTIME_INPUT_DATA_PATH = "RuntimeInputData"; // MARKER.RuntimeInputDataPath.End @@ -42,54 +43,49 @@ public static partial class Input /// /// Use as a general purpose catch-all for when to update any UI that displays controls. + /// Invoked on InputUserChange, on ControlScheme change, and on bindings changed. /// public static event Action OnControlsUpdated; // TODO (architecture): Shortcoming here. OnInputUserChange doesn't always get called when a binding changes, so we have this as well. // Can we consolidate these events into a higher-level abstraction? Or separate them by desired events (binding change, control scheme change, etc with more granularity) public static event Action OnBindingsChanged; - + public static event Action OnAnyPlayerInputUserChange; + public static event Action OnAnyPlayerControlSchemeChanged; + public static event Action OnAnyPlayerKeyboardTextInput; public static event Action OnAnyButtonPress { add => AddAnyButtonPressListener(value); remove => RemoveAnyButtonPressListener(value); } - - // MARKER.SingleOrMultiPlayerFieldsAndProperties.Start - public static event Action OnInputUserChange - { - add => Player1.OnInputUserChange += value; - remove => Player1.OnInputUserChange -= value; - } - - public static event Action OnControlSchemeChanged - { - add => Player1.OnControlSchemeChanged += value; - remove => Player1.OnControlSchemeChanged -= value; - } - public static event Action OnKeyboardTextInput - { - add => Player1.OnKeyboardTextInput += value; - remove => Player1.OnKeyboardTextInput -= value; - } - - public static InputContext Context + // MARKER.SinglePlayerFieldsAndProperties.Start + // MARKER.SinglePlayerFieldsAndProperties.End + + public static Vector2 MousePosition => Mouse.current.position.ReadValue(); + + private static bool allowPlayerJoining; + public static bool AllowPlayerJoining { - get => Player1.InputContext; - set => Player1.InputContext = value; + get => allowPlayerJoining; + set + { + if (value == allowPlayerJoining) + { + return; + } + + allowPlayerJoining = value; + if (value) OnAnyButtonPress += JoinPlayerByActivatedInputControl; + else OnAnyButtonPress -= JoinPlayerByActivatedInputControl; + } } - - public static ControlScheme CurrentControlScheme => Player1.CurrentControlScheme; - public static Vector2 MousePosition => Mouse.current.position.ReadValue(); - - private static InputPlayer Player1 => GetPlayer(PlayerID.Player1); - private static bool AllowPlayerJoining => false; - // MARKER.SingleOrMultiPlayerFieldsAndProperties.End - + // MARKER.DefaultContextProperty.Start private static InputContext DefaultContext => InputContext.Default; // MARKER.DefaultContextProperty.End + + private static InputPlayer DefaultPlayer => Player(0); private static bool initialized; private static HashSet> anyButtonPressListeners; @@ -97,7 +93,7 @@ public static InputContext Context private static InputPlayerCollection playerCollection; private static RuntimeInputData runtimeInputData; private static RebindingOperation rebindingOperation; - + #endregion #region Setup @@ -111,11 +107,11 @@ private static void Initialize() { return; } - + // Allows input system to work even when domain reload is disabled in editor. if (RuntimeSafeEditorUtility.IsDomainReloadDisabled()) { - ReflectionUtility.ResetStaticClassMembersToDefault(typeof(Input)); + ReflectionUtility.ResetStaticClassMembersToDefault(typeof(ISW)); } SetUpTerminationConditions(); @@ -126,34 +122,33 @@ private static void Initialize() throw new Exception($"{nameof(RuntimeInputData)} is null or its input action asset is null - input will not work!"); } - int maxPlayers = Enum.GetValues(typeof(PlayerID)).Length; - - // TODO (optimization): Could make startup slow. It should probably just be a requirement of using this package that you clear your old input modules & event systems out. + // Clear out anything in the scene that would interfere with the ISW's autonomous operation. ObjectUtility.DestroyObjectsOfType(); // These registrations must occur before players get assigned InputActionAssets, or else issues resolving the bindings will arise. CustomSetupsRegisterer.PerformRegistrations(runtimeInputData); - playerCollection = new InputPlayerCollection(runtimeInputData.InputActionAsset, maxPlayers); + playerCollection = new InputPlayerCollection(runtimeInputData.InputActionAsset); + playerCollection.OnPlayerAdded += HandlePlayerAdded; + playerCollection.OnPlayerRemoved += HandlePlayerRemoved; #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif + + playerCollection.Add(0); + // MARKER.LoadAllBindingsOnInitialization.Start LoadBindingsForAllPlayers(); // MARKER.LoadAllBindingsOnInitialization.End - EnableContextForAllPlayers(DefaultContext); + SetContextForAllPlayers(DefaultContext); anyButtonPressListeners = new HashSet>(); ++InputUser.listenForUnpairedDeviceActivity; InputUser.onChange += HandleInputUserChange; - - // TODO (architecture): Support code gen around this - // MARKER.ControlsUpdatedSubscriptions.Start - // MARKER.ControlsUpdatedSubscriptions.End - OnInputUserChange += ControlsUpdate; - OnBindingsChanged += ControlsUpdate; - OnControlSchemeChanged += ControlsUpdate; + OnAnyPlayerInputUserChange += BroadcastControlsUpdated; + OnBindingsChanged += BroadcastControlsUpdated; + OnAnyPlayerControlSchemeChanged += BroadcastControlsUpdated; initialized = true; } @@ -165,7 +160,10 @@ private static void SetUpTerminationConditions() EditorApplication.playModeStateChanged += handlePlayModeStateChanged; void handlePlayModeStateChanged(PlayModeStateChange playModeStateChange) { - if (playModeStateChange is PlayModeStateChange.ExitingPlayMode) Terminate(); + if (playModeStateChange is PlayModeStateChange.ExitingPlayMode) + { + Terminate(); + } } #else Application.quitting -= Terminate; @@ -179,12 +177,9 @@ private static void Terminate() #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; #endif - // TODO (architecture): Support code gen around this - // MARKER.ControlsUpdatedSubscriptions.Start - // MARKER.ControlsUpdatedSubscriptions.End - OnInputUserChange -= ControlsUpdate; - OnBindingsChanged -= ControlsUpdate; - OnControlSchemeChanged -= ControlsUpdate; + OnAnyPlayerInputUserChange -= BroadcastControlsUpdated; + OnBindingsChanged -= BroadcastControlsUpdated; + OnAnyPlayerControlSchemeChanged -= BroadcastControlsUpdated; playerCollection.TerminateAll(); playerCollection = null; @@ -195,26 +190,37 @@ private static void Terminate() #endregion #region Public Interface + + public static InputPlayer Player(int playerID) + { + return playerCollection[playerID]; + } + + public static void ClearPlayer(int playerID) + { + playerCollection.Remove(playerID); + } + + public static bool ControlSchemeHas(ControlScheme controlScheme, int playerID = 0) where TDevice : InputDevice + { + return Player(playerID).ControlSchemeHas(controlScheme); + } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static bool ControlSchemeHas(ControlScheme controlScheme) where TDevice : InputDevice + public static void SetContextForAllPlayers(InputContext inputContext) { - return Player1.ControlSchemeHas(controlScheme); + playerCollection.SetContextForAll(inputContext); } /// /// Try to get the ActionWrapper for the (deprecated) InputActionReference's action. /// Useful as a transitional tool from normal Unity Input System usage to full ISW integration. /// - // TODO: remove this method in time - public static bool TryConvert(InputActionReference inputActionReference, out ActionWrapper actionWrapper) + // TODO: remove this method eventually + public static bool TryConvert(InputActionReference inputActionReference, int playerID, out ActionWrapper actionWrapper) { if (inputActionReference != null && inputActionReference.action != null) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - + InputPlayer player = playerCollection[playerID]; return player.TryGetMatchingActionWrapper(inputActionReference.action, out actionWrapper); } @@ -222,7 +228,14 @@ public static bool TryConvert(InputActionReference inputActionReference, out Act return false; } - // TODO (multiplayer): MP method signature which takes a PlayerID + /// + /// Single-player overload + /// + public static bool TryConvert(InputActionReference inputActionReference, out ActionWrapper actionWrapper) + { + return TryConvert(inputActionReference, 0, out actionWrapper); + } + public static void ResetBindingForAction(ActionReference actionReference, ControlScheme controlScheme) { if (actionReference == null || actionReference.ActionWrapper == null) @@ -230,48 +243,29 @@ public static void ResetBindingForAction(ActionReference actionReference, Contro return; } - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - + // Note that player ID is contained in the ActionReference. ActionBindingInfo actionBindingInfo = new ActionBindingInfo(actionReference.ActionWrapper, actionReference.CompositePart, controlScheme); BindingChanger.ResetBindingToDefaultForControlScheme(actionBindingInfo, controlScheme); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void ResetAllBindingsForControlScheme(ControlScheme controlScheme) + public static void ResetAllBindingsForControlScheme(ControlScheme controlScheme, int playerID = 0) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingChanger.ResetBindingsToDefaultForControlScheme(player.Asset, controlScheme); + BindingChanger.ResetBindingsToDefaultForControlScheme(Player(playerID).Asset, controlScheme); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void LoadAllBindings() + public static void LoadAllBindings(int playerID = 0) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); + BindingSaveLoad.LoadBindingsFromDiskForPlayer(Player(playerID)); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void SaveAllBindings() + public static void SaveAllBindings(int playerID = 0) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingSaveLoad.SaveBindingsToDiskForPlayer(player); + BindingSaveLoad.SaveBindingsToDiskForPlayer(Player(playerID)); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void ResetAllBindings() + public static void ResetAllBindings(int playerID = 0) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingChanger.ResetBindingsToDefault(player.Asset); + BindingChanger.ResetBindingsToDefault(Player(playerID).Asset); } #endregion @@ -288,9 +282,8 @@ internal static void BroadcastBindingsChanged() OnBindingsChanged?.Invoke(); } - private static void ControlsUpdate(InputUserChangeInfo inputUserChangeInfo) => BroadcastControlsUpdated(); - private static void ControlsUpdate(ControlScheme controlScheme) => BroadcastControlsUpdated(); - private static void ControlsUpdate() => BroadcastControlsUpdated(); + private static void BroadcastControlsUpdated(InputUserChangeInfo inputUserChangeInfo) => BroadcastControlsUpdated(); + private static void BroadcastControlsUpdated(InputPlayer inputPlayer) => BroadcastControlsUpdated(); private static void BroadcastControlsUpdated() { OnControlsUpdated?.Invoke(); @@ -341,25 +334,53 @@ internal static bool TryGetBindingInfo(ActionBindingInfo actionBindingInfo, out return BindingGetter.TryGetBindingInfo(runtimeInputData, actionBindingInfo, out bindingInfos); } - internal static bool TryGetActionWrapper(PlayerID playerID, InputAction inputAction, out ActionWrapper actionWrapper) + internal static bool TryGetActionWrapper(int playerID, InputAction inputAction, out ActionWrapper actionWrapper) { - return GetPlayer(playerID).TryGetMatchingActionWrapper(inputAction, out actionWrapper); + return Player(playerID).TryGetMatchingActionWrapper(inputAction, out actionWrapper); } #endregion #region Private Runtime Functionality + + private static void UpdatePlayerCollectionListeners() + { + foreach (InputPlayer player in playerCollection) + { + player.OnInputUserChange -= HandleAnyPlayerInputUserChange; + player.OnInputUserChange += HandleAnyPlayerInputUserChange; + + player.OnControlSchemeChanged -= HandleAnyPlayerControlSchemeChanged; + player.OnControlSchemeChanged += HandleAnyPlayerControlSchemeChanged; + + player.OnKeyboardTextInput -= HandleAnyPlayerKeyboardTextInput; + player.OnKeyboardTextInput += HandleAnyPlayerKeyboardTextInput; + } + } - // MARKER.EnableContextForAllPlayersSignature.Start - private static void EnableContextForAllPlayers(InputContext inputContext) - // MARKER.EnableContextForAllPlayersSignature.End + private static void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { - playerCollection.EnableContextForAll(inputContext); + OnAnyPlayerInputUserChange?.Invoke(inputUserChangeInfo); + } + + private static void HandleAnyPlayerControlSchemeChanged(InputPlayer inputPlayer) + { + OnAnyPlayerControlSchemeChanged?.Invoke(inputPlayer); } - private static InputPlayer GetPlayer(PlayerID id) + private static void HandleAnyPlayerKeyboardTextInput(char c) { - return playerCollection[id]; + OnAnyPlayerKeyboardTextInput?.Invoke(c); + } + + private static void HandlePlayerAdded(InputPlayer inputPlayer) + { + UpdatePlayerCollectionListeners(); + } + + private static void HandlePlayerRemoved(int playerID) + { + UpdatePlayerCollectionListeners(); } private static void AddAnyButtonPressListener(Action action) @@ -397,14 +418,6 @@ private static void UnregisterAllAnyButtonPressListeners() private static void HandleAnyButtonPressed(InputControl inputControl) { InvokeAnyButtonPressListeners(inputControl); - - // Player joining is always disallowed in SinglePlayer mode. - if (!AllowPlayerJoining) - { - return; - } - - JoinPlayerByActivatedInputControl(inputControl); } private static void InvokeAnyButtonPressListeners(InputControl inputControl) @@ -418,9 +431,9 @@ private static void InvokeAnyButtonPressListeners(InputControl inputControl) private static void LoadBindingsForAllPlayers() { - foreach (PlayerID playerID in Enum.GetValues(typeof(PlayerID))) + foreach (InputPlayer player in playerCollection) { - BindingSaveLoad.LoadBindingsFromDiskForPlayer(GetPlayer(playerID)); + BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); } } @@ -455,10 +468,11 @@ private static void HandleInputUserChange(InputUser inputUser, InputUserChange i #region Editor-Only Debug #if UNITY_EDITOR - internal static event Action EDITOR_OnPlayerInputContextChanged; + internal static event Action EDITOR_OnPlayerInputContextChanged; internal static bool EDITOR_IsInitialized => initialized; - + internal static InputContext EDITOR_GetDefaultContext() => DefaultContext; + private static void EDITOR_HandlePlayerInputContextChanged(InputPlayer inputPlayer) { EDITOR_OnPlayerInputContextChanged?.Invoke(inputPlayer.ID, inputPlayer.InputContext); diff --git a/Runtime/Scripts/Input.cs.meta b/Runtime/Scripts/ISW.cs.meta similarity index 100% rename from Runtime/Scripts/Input.cs.meta rename to Runtime/Scripts/ISW.cs.meta diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/InputPlayer.cs index c29d2d7..00ac4c2 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/InputPlayer.cs @@ -23,7 +23,7 @@ public sealed class InputPlayer /// public event Action OnInputUserChange; - public event Action OnControlSchemeChanged; + public event Action OnControlSchemeChanged; /// /// The input player can be used when enabled, and is ignored when disabled. @@ -72,7 +72,7 @@ public InputContext InputContext } } - public PlayerID ID { get; } + public int ID { get; } public ControlScheme CurrentControlScheme { get; private set; } // MARKER.ActionsProperties.Start @@ -87,6 +87,11 @@ internal InputDevice LastUsedDevice return lastUsedDevice; } } + + internal bool IsMultiplayer + { + set => playerInput.neverAutoSwitchControlSchemes = value; + } internal InputActionAsset Asset { get; } @@ -117,7 +122,7 @@ internal InputDevice LastUsedDevice #region Setup & Teardown - internal InputPlayer(InputActionAsset asset, PlayerID id, bool isMultiplayer, Transform parent) + internal InputPlayer(InputActionAsset asset, int id, bool isMultiplayer, Transform parent) { Asset = InstantiateNewActions(asset); ID = id; @@ -128,7 +133,7 @@ internal InputPlayer(InputActionAsset asset, PlayerID id, bool isMultiplayer, Tr SetUpInputPlayerGameObject(isMultiplayer, parent); PopulateEventSystemActionsPool(); - // Input context gets set by top Input class after this instantiation, which sets up maps & event system actions/overrides, so we don't have to handle that here. + // Input context gets set by top ISW class after this instantiation, which sets up maps & event system actions/overrides, so we don't have to handle that here. } internal void Terminate() @@ -169,12 +174,8 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInput = playerInputGameObject.AddComponent(); playerInput.neverAutoSwitchControlSchemes = isMultiplayer; - - if (isMultiplayer) - playerInputGameObject.AddComponent(); - else - playerInputGameObject.AddComponent(); - + + playerInputGameObject.AddComponent(); uiInputModule = playerInputGameObject.AddComponent(); uiInputModule.actionsAsset = Asset; SetEventSystemOptions(); @@ -335,7 +336,7 @@ internal void HandleInputUserChange(InputUserChange inputUserChange, InputDevice CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); if (previousControlScheme != CurrentControlScheme) { - OnControlSchemeChanged?.Invoke(CurrentControlScheme); + OnControlSchemeChanged?.Invoke(this); } break; } @@ -452,6 +453,7 @@ private void EnableMapsForContext(InputContext context) #region Editor-Only Debug #if UNITY_EDITOR + // ReSharper disable once InconsistentNaming internal event Action EDITOR_OnInputContextChanged; #endif #endregion diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs index ff3d9e0..3561432 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/InputPlayerCollection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using NPTP.InputSystemWrapper.Utilities.Extensions; @@ -11,56 +12,103 @@ namespace NPTP.InputSystemWrapper { /// /// Useful interface layer for dealing with a collection of multiple players. - /// Note we avoid foreach & LINQ usage on the internal array to improve performance. - /// (Our ForEach extension is just a standard array for loop.) /// - internal sealed class InputPlayerCollection + internal sealed class InputPlayerCollection : IEnumerable { - internal IEnumerable Players => players; - internal InputPlayer this[PlayerID id] => players[(int)id]; - internal int Count => players.Length; + internal event Action OnPlayerAdded; + internal event Action OnPlayerRemoved; - private readonly InputPlayer[] players; + private readonly InputActionAsset inputActionAsset; + private readonly Transform inputParent; + private InputPlayer[] players = Array.Empty(); - #region Internal + private IEnumerable Players => players.Where(player => player != null); + private int PlayerCount => Players.Count(); - internal InputPlayerCollection(InputActionAsset asset, int size) + public IEnumerator GetEnumerator() => Players.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal InputPlayer this[int playerID] { - Transform parent = CreateInputParentInScene(); - bool isMultiplayer = size > 1; - - players = new InputPlayer[size]; - for (int i = 0; i < size; i++) + get { - PlayerID id = (PlayerID)i; - InputPlayer newPlayer = new(asset, id, isMultiplayer, parent); - players[i] = newPlayer; + Add(playerID); + return players[playerID]; } + } + + internal InputPlayerCollection(InputActionAsset asset) + { + inputParent = CreateInputParentInScene(); + inputActionAsset = asset; + } - // Loop again as the enabled/disabled handler requires a stable players array, - // and we're changing the value of player.Enabled here. - // Player 1 is always enabled by default when a new InputPlayerCollection is created (game is started). - for (int i = 0; i < players.Length; i++) + #region Internal Methods + + internal void Add(int playerID) + { + if (playerID >= players.Length) { - InputPlayer player = players[i]; - player.OnEnabledOrDisabled += HandlePlayerEnabledOrDisabled; - player.Enabled = player.ID == PlayerID.Player1; + InputPlayer[] extended = new InputPlayer[playerID - 1]; + Array.Copy(players, extended, players.Length); + players = extended; + } + + if (players[playerID] != null) + { + return; + } + + InputPlayer newPlayer = new InputPlayer(inputActionAsset, playerID, true, inputParent); + players[playerID] = newPlayer; + newPlayer.OnEnabledOrDisabled += HandlePlayerEnabledOrDisabled; + newPlayer.Enabled = true; #if UNITY_EDITOR - player.EDITOR_OnInputContextChanged += EDITOR_HandlePlayerInputContextChanged; + newPlayer.EDITOR_OnInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif + + foreach (InputPlayer player in Players) + { + player.IsMultiplayer = true; + } + + OnPlayerAdded?.Invoke(newPlayer); + } + + internal void Remove(int playerID) + { + if (playerID <= 0) + { + Debug.LogError("Cannot terminate the default player."); + return; + } + + if (playerID >= players.Length || players[playerID] == null) + { + return; + } + + players[playerID].Terminate(); + players[playerID] = null; + + if (PlayerCount == 1) + { + players[0].IsMultiplayer = false; } + + OnPlayerRemoved?.Invoke(playerID); } internal void TerminateAll() { - players.ForEach(p => + foreach (InputPlayer player in players) { + player.OnEnabledOrDisabled -= HandlePlayerEnabledOrDisabled; + player.Terminate(); #if UNITY_EDITOR - p.EDITOR_OnInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; + player.EDITOR_OnInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; #endif - p.OnEnabledOrDisabled -= HandlePlayerEnabledOrDisabled; - p.Terminate(); - }); + } players.DefaultAll(); } @@ -93,6 +141,11 @@ internal bool TryGetPlayerPairedWithDevice(InputDevice device, out InputPlayer p { for (int i = 0; i < players.Length; i++) { + if (players[i] == null) + { + continue; + } + if (players[i].IsDevicePaired(device)) { player = players[i]; @@ -109,6 +162,11 @@ internal bool TryGetPlayerAssociatedWithAsset(InputActionAsset asset, out InputP { for (int i = 0; i < players.Length; i++) { + if (players[i] == null) + { + continue; + } + InputPlayer player = players[i]; if (player.Asset == asset) { @@ -125,6 +183,11 @@ internal bool TryPairDeviceToFirstDisabledPlayer(InputDevice device, out InputPl { for (int i = 0; i < players.Length; i++) { + if (players[i] == null) + { + continue; + } + InputPlayer player = players[i]; if (player.Enabled) { @@ -153,14 +216,17 @@ internal void HandleInputUserChange(InputUser inputUser, InputUserChange inputUs } } - internal void EnableContextForAll(InputContext inputContext) + internal void SetContextForAll(InputContext inputContext) { - players.ForEach(p => p.InputContext = inputContext); + foreach (InputPlayer player in players) + { + player.InputContext = inputContext; + } } #endregion - #region Private + #region Private Methods private Transform CreateInputParentInScene() { @@ -181,7 +247,10 @@ private void HandlePlayerEnabledOrDisabled(InputPlayer enabledOrDisabledPlayer) int enabledPlayersCount = players.Count(player => player.Enabled); if (enabledPlayersCount > 1) { - players.ForEach(p => p.EnableAutoSwitching(false)); + foreach (InputPlayer player in players) + { + player.EnableAutoSwitching(false); + } } else if (enabledPlayersCount == 1) { @@ -193,7 +262,7 @@ private void HandlePlayerEnabledOrDisabled(InputPlayer enabledOrDisabledPlayer) #endregion - #region Editor-Only Debug + #region Editor-Only Debug Fields/Properties/Methods #if UNITY_EDITOR internal event Action EDITOR_OnPlayerInputContextChanged; diff --git a/Runtime/Scripts/InputUserChangeInfo.cs b/Runtime/Scripts/InputUserChangeInfo.cs index 01c4450..72290ad 100644 --- a/Runtime/Scripts/InputUserChangeInfo.cs +++ b/Runtime/Scripts/InputUserChangeInfo.cs @@ -5,22 +5,15 @@ namespace NPTP.InputSystemWrapper { public struct InputUserChangeInfo { + public InputPlayer Player { get; } public ControlScheme ControlScheme { get; } public InputUserChange InputUserChange { get; } - // MARKER.PlayerIDProperty.Start - // MARKER.PlayerIDProperty.End - - // We pass in the entire inputPlayer because this code can change when multiplayer is activated - // in the input system wrapper package, and we get more properties from the inputPlayer. - // Feeding it in this way just makes the code auto-generation easier and exist in less places. - public InputUserChangeInfo(InputPlayer inputPlayer, InputUserChange inputUserChange) + internal InputUserChangeInfo(InputPlayer inputPlayer, InputUserChange inputUserChange) { + Player = inputPlayer; ControlScheme = inputPlayer.CurrentControlScheme; InputUserChange = inputUserChange; - - // MARKER.PlayerIDConstructor.Start - // MARKER.PlayerIDConstructor.End } } } diff --git a/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs b/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs index d2e5a24..1fa91ff 100644 --- a/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs +++ b/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs @@ -1,22 +1,8 @@ -using System; namespace NPTP.InputSystemWrapper.Utilities.Extensions { internal static class ArrayExtensions { - internal static void ForEach(this T[] array, Action action) - { - if (array == null || action == null) - { - return; - } - - for (int i = 0; i < array.Length; i++) - { - action.Invoke(array[i]); - } - } - internal static bool IsNullOrEmpty(this T[] array) { return array == null || array.Length == 0; diff --git a/Runtime/Scripts/Utilities/Extensions/StringExtensions.cs.meta b/Runtime/Scripts/Utilities/Extensions/StringExtensions.cs.meta index 7c7f546..c29d459 100644 --- a/Runtime/Scripts/Utilities/Extensions/StringExtensions.cs.meta +++ b/Runtime/Scripts/Utilities/Extensions/StringExtensions.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 3fe0a262c90e89640800cb366c703c1b +guid: c8859d465347bc542a239409ec01418a MonoImporter: externalObjects: {} serializedVersion: 2 From 13b80e321011374884155fb21a2a1128201a4074 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 18 Oct 2025 13:13:46 -0400 Subject: [PATCH 02/12] Multiplayer fixes; Add & remove player, AllowPlayerJoining working for new devices joining in --- .../InputWrapperDebuggerWindow.cs | 17 +- .../PropertyDrawers/ActionReferenceDrawer.cs | 7 +- .../ActionsContentBuilder.cs | 4 +- .../InputPlayerContentBuilder.cs | 2 +- Runtime/Scripts/Actions/ActionReference.cs | 13 +- Runtime/Scripts/Actions/ActionWrapper.cs | 42 ++-- Runtime/Scripts/Actions/ValueActionWrapper.cs | 6 +- Runtime/Scripts/ISW.cs | 80 +++++--- Runtime/Scripts/InputPlayer.cs | 51 ++--- Runtime/Scripts/InputPlayerCollection.cs | 184 ++++++++---------- Runtime/Scripts/Templates/ActionsTemplate.cs | 6 +- .../Utilities/Extensions/ArrayExtensions.cs | 5 + 12 files changed, 229 insertions(+), 188 deletions(-) diff --git a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs index 127efd3..b3de751 100644 --- a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs +++ b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs @@ -101,13 +101,16 @@ private void OnGUI() EditorGUILayout.LabelField("Input not yet initialized, waiting...", new GUIStyle(EditorStyles.label) { fontStyle = FontStyle.BoldAndItalic }); return; } - - GUILayout.BeginVertical(); - ShowDebugInfoField("Current Control Scheme", ISW.Player(selectedPlayerID).CurrentControlScheme.ToString()); - ShowDebugInfoField("Current Context", ISW.Player(selectedPlayerID).InputContext.ToString()); - ShowIndentedField("Active Maps", ActiveMapLabelFields); - ShowIndentedField("Most Recent Contexts", MostRecentContextLabelFields); - GUILayout.EndVertical(); + + if (ISW.EDITOR_TryGetPlayer(selectedPlayerID, out InputPlayer player)) + { + GUILayout.BeginVertical(); + ShowDebugInfoField("Current Control Scheme", player.CurrentControlScheme.ToString()); + ShowDebugInfoField("Current Context", player.InputContext.ToString()); + ShowIndentedField("Active Maps", ActiveMapLabelFields); + ShowIndentedField("Most Recent Contexts", MostRecentContextLabelFields); + GUILayout.EndVertical(); + } } private void ShowIndentedField(string fieldName, Action showAction) diff --git a/Editor/Scripts/PropertyDrawers/ActionReferenceDrawer.cs b/Editor/Scripts/PropertyDrawers/ActionReferenceDrawer.cs index 6d4730e..657ab8d 100644 --- a/Editor/Scripts/PropertyDrawers/ActionReferenceDrawer.cs +++ b/Editor/Scripts/PropertyDrawers/ActionReferenceDrawer.cs @@ -11,12 +11,13 @@ internal class ActionReferenceDrawer : PropertyDrawer private const string REFERENCE = "reference"; private const string USE_COMPOSITE_PART = "useCompositePart"; private const string COMPOSITE_PART = "compositePart"; + private const string PLAYER_ID = "playerID"; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { // The number of lines is dependent on this bool value (showing composite part of action/binding). bool useCompositePart = property.FindPropertyRelative(USE_COMPOSITE_PART).boolValue; - float multiplier = useCompositePart ? 4 : 3; + float multiplier = useCompositePart ? 5 : 4; return multiplier * EditorGUIUtility.singleLineHeight; } @@ -41,6 +42,10 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten currentRect.y += lineHeight; EditorGUI.PropertyField(currentRect, useCompositePart); + SerializedProperty playerID = property.FindPropertyRelative(PLAYER_ID); + currentRect.y += lineHeight; + EditorGUI.PropertyField(currentRect, playerID); + if (useCompositePart.boolValue) { SerializedProperty compositePart = property.FindPropertyRelative(COMPOSITE_PART); diff --git a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs index 481c4c9..083174e 100644 --- a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs @@ -45,14 +45,14 @@ internal static void AddContent(string markerName, InputActionMap map, List table)"); + lines.Add($" internal {className()}(int playerID, {nameof(InputActionAsset)} asset, Dictionary table)"); break; case "ActionMapAssignment": lines.Add($" {actionMapProperty()} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);"); break; case "ActionWrapperAssignments": foreach (string action in getActionNames()) - lines.Add($" {action.AsProperty()} = new ({actionMapProperty()}.FindAction(\"{action}\", throwIfNotFound: true), table);"); + lines.Add($" {action.AsProperty()} = new (playerID, {actionMapProperty()}.FindAction(\"{action}\", throwIfNotFound: true), table);"); break; case "RegisterCallbacks": foreach (string action in getActionNames()) diff --git a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs index fdf1b39..17fb60a 100644 --- a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs @@ -20,7 +20,7 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) break; case "ActionsInstantiation": foreach (string map in Helper.GetMapNames(Asset)) - info.NewLines.Add($" {map.AsProperty()} = new {map.AsType()}Actions(Asset, actionWrapperTable);"); + info.NewLines.Add($" {map.AsProperty()} = new {map.AsType()}Actions(ID, Asset, actionWrapperTable);"); break; case "EventSystemOptions": info.NewLines.Add($" uiInputModule.moveRepeatDelay = {Data.MoveRepeatDelay.ToString(CultureInfo.InvariantCulture)}f;"); diff --git a/Runtime/Scripts/Actions/ActionReference.cs b/Runtime/Scripts/Actions/ActionReference.cs index 6e8115e..3e7f2ed 100644 --- a/Runtime/Scripts/Actions/ActionReference.cs +++ b/Runtime/Scripts/Actions/ActionReference.cs @@ -15,6 +15,15 @@ namespace NPTP.InputSystemWrapper.Actions [Serializable] public partial class ActionReference { + public event Action OnEvent + { + add => ActionWrapper.OnEvent += value; + remove => ActionWrapper.OnEvent -= value; + } + + public bool DownThisFrame => ActionWrapper.DownThisFrame; + public bool IsDown => ActionWrapper.IsDown; + [SerializeField] private InputActionReference reference; [SerializeField] private bool useCompositePart; @@ -30,7 +39,7 @@ public partial class ActionReference internal int PlayerID => playerID; private ActionWrapper actionWrapper; - public ActionWrapper ActionWrapper + internal ActionWrapper ActionWrapper { get { @@ -48,6 +57,8 @@ public ActionWrapper ActionWrapper return actionWrapper; } } + + public string ActionName => ActionWrapper != null ? ActionWrapper.InputAction.name : "Not found"; public static bool TryConvert(InputActionReference inputActionReference, out ActionReference actionReference) { diff --git a/Runtime/Scripts/Actions/ActionWrapper.cs b/Runtime/Scripts/Actions/ActionWrapper.cs index 15431fb..d5e1bf0 100644 --- a/Runtime/Scripts/Actions/ActionWrapper.cs +++ b/Runtime/Scripts/Actions/ActionWrapper.cs @@ -14,6 +14,7 @@ namespace NPTP.InputSystemWrapper.Actions /// public class ActionWrapper { + internal int PlayerID { get; } internal InputAction InputAction { get; } private event Action onEvent; @@ -25,6 +26,27 @@ public event Action OnEvent public bool DownThisFrame => InputAction.WasPerformedThisFrame() && (InputAction.type != InputActionType.PassThrough || !InputAction.WasReleasedThisFrame()); public bool IsDown => InputAction.phase == InputActionPhase.Performed; + + internal ActionWrapper(int playerID, InputAction inputAction, Dictionary table) + { + PlayerID = playerID; + InputAction = inputAction; + table.Add(inputAction.id, this); + } + + internal void RegisterCallbacks() + { + InputAction.started += HandleActionEvent; + InputAction.performed += HandleActionEvent; + InputAction.canceled += HandleActionEvent; + } + + internal void UnregisterCallbacks() + { + InputAction.started -= HandleActionEvent; + InputAction.performed -= HandleActionEvent; + InputAction.canceled -= HandleActionEvent; + } public void StartInteractiveRebind(ControlScheme controlScheme, Action callback = null) => ISW.StartInteractiveRebind(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), callback); @@ -44,26 +66,6 @@ public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) => ISW.TryGetBindingInfo(new ActionBindingInfo(this, compositePart, controlScheme), out bindingInfos); - internal void RegisterCallbacks() - { - InputAction.started += HandleActionEvent; - InputAction.performed += HandleActionEvent; - InputAction.canceled += HandleActionEvent; - } - - internal void UnregisterCallbacks() - { - InputAction.started -= HandleActionEvent; - InputAction.performed -= HandleActionEvent; - InputAction.canceled -= HandleActionEvent; - } - - internal ActionWrapper(InputAction inputAction, Dictionary table) - { - InputAction = inputAction; - table.Add(inputAction.id, this); - } - private void HandleActionEvent(InputAction.CallbackContext context) => onEvent?.Invoke(context); } } diff --git a/Runtime/Scripts/Actions/ValueActionWrapper.cs b/Runtime/Scripts/Actions/ValueActionWrapper.cs index 8dc7c1d..807f07f 100644 --- a/Runtime/Scripts/Actions/ValueActionWrapper.cs +++ b/Runtime/Scripts/Actions/ValueActionWrapper.cs @@ -6,7 +6,7 @@ namespace NPTP.InputSystemWrapper.Actions { public abstract class ValueActionWrapper : ActionWrapper { - protected ValueActionWrapper(InputAction inputAction, Dictionary table) : base(inputAction, table) + protected ValueActionWrapper(int playerID, InputAction inputAction, Dictionary table) : base(playerID, inputAction, table) { } } @@ -15,7 +15,7 @@ public sealed class ValueActionWrapper : ValueActionWrapper where T : struct { public T ReadValue() => InputAction.ReadValue(); - internal ValueActionWrapper(InputAction inputAction, Dictionary table) : base(inputAction, table) + internal ValueActionWrapper(int playerID, InputAction inputAction, Dictionary table) : base(playerID, inputAction, table) { } } @@ -24,7 +24,7 @@ public sealed class AnyValueActionWrapper : ValueActionWrapper { public object ReadValue() => InputAction.ReadValueAsObject(); - internal AnyValueActionWrapper(InputAction inputAction, Dictionary table) : base(inputAction, table) + internal AnyValueActionWrapper(int playerID, InputAction inputAction, Dictionary table) : base(playerID, inputAction, table) { } } diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index 5508720..1a55387 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -47,23 +47,25 @@ public static partial class ISW /// public static event Action OnControlsUpdated; + /// + /// Invoked on any button pressed on any connected device regardless of actions mapped, assets enabled, etc. + /// + public static event Action OnAnyButtonPress + { + add => AddAnyButtonPressListener(value); + remove => RemoveAnyButtonPressListener(value); + } + // TODO (architecture): Shortcoming here. OnInputUserChange doesn't always get called when a binding changes, so we have this as well. // Can we consolidate these events into a higher-level abstraction? Or separate them by desired events (binding change, control scheme change, etc with more granularity) public static event Action OnBindingsChanged; public static event Action OnAnyPlayerInputUserChange; public static event Action OnAnyPlayerControlSchemeChanged; public static event Action OnAnyPlayerKeyboardTextInput; - public static event Action OnAnyButtonPress - { - add => AddAnyButtonPressListener(value); - remove => RemoveAnyButtonPressListener(value); - } - + // MARKER.SinglePlayerFieldsAndProperties.Start // MARKER.SinglePlayerFieldsAndProperties.End - public static Vector2 MousePosition => Mouse.current.position.ReadValue(); - private static bool allowPlayerJoining; public static bool AllowPlayerJoining { @@ -71,21 +73,20 @@ public static bool AllowPlayerJoining set { if (value == allowPlayerJoining) - { return; - } - + allowPlayerJoining = value; if (value) OnAnyButtonPress += JoinPlayerByActivatedInputControl; else OnAnyButtonPress -= JoinPlayerByActivatedInputControl; } } + public static Vector2 MousePosition => Mouse.current.position.ReadValue(); + // MARKER.DefaultContextProperty.Start private static InputContext DefaultContext => InputContext.Default; // MARKER.DefaultContextProperty.End - - private static InputPlayer DefaultPlayer => Player(0); + private static InputPlayer DefaultPlayer => playerCollection.DefaultPlayer; private static bool initialized; private static HashSet> anyButtonPressListeners; @@ -128,15 +129,11 @@ private static void Initialize() // These registrations must occur before players get assigned InputActionAssets, or else issues resolving the bindings will arise. CustomSetupsRegisterer.PerformRegistrations(runtimeInputData); - playerCollection = new InputPlayerCollection(runtimeInputData.InputActionAsset); - playerCollection.OnPlayerAdded += HandlePlayerAdded; - playerCollection.OnPlayerRemoved += HandlePlayerRemoved; + playerCollection = new InputPlayerCollection(runtimeInputData.InputActionAsset, HandlePlayerAdded, HandlePlayerRemoved); #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif - playerCollection.Add(0); - // MARKER.LoadAllBindingsOnInitialization.Start LoadBindingsForAllPlayers(); // MARKER.LoadAllBindingsOnInitialization.End @@ -181,7 +178,7 @@ private static void Terminate() OnBindingsChanged -= BroadcastControlsUpdated; OnAnyPlayerControlSchemeChanged -= BroadcastControlsUpdated; - playerCollection.TerminateAll(); + playerCollection.Terminate(); playerCollection = null; --InputUser.listenForUnpairedDeviceActivity; InputUser.onChange -= HandleInputUserChange; @@ -190,13 +187,14 @@ private static void Terminate() #endregion #region Public Interface - + + public static void AddPlayer(int playerID) => Player(playerID); public static InputPlayer Player(int playerID) { - return playerCollection[playerID]; + return playerCollection.GetOrAdd(playerID); } - public static void ClearPlayer(int playerID) + public static void RemovePlayer(int playerID) { playerCollection.Remove(playerID); } @@ -220,7 +218,7 @@ public static bool TryConvert(InputActionReference inputActionReference, int pla { if (inputActionReference != null && inputActionReference.action != null) { - InputPlayer player = playerCollection[playerID]; + InputPlayer player = playerCollection.GetOrAdd(playerID); return player.TryGetMatchingActionWrapper(inputActionReference.action, out actionWrapper); } @@ -319,7 +317,7 @@ internal static void StartInteractiveRebind(ActionBindingInfo actionBindingInfo, internal static bool TryGetCurrentBindingInfo(ActionWrapper actionWrapper, CompositePart compositePart, out IEnumerable bindingInfos) { - if (!playerCollection.TryGetPlayerAssociatedWithAsset(actionWrapper.InputAction.actionMap.asset, out InputPlayer player)) + if (!playerCollection.TryGetPlayer(actionWrapper.PlayerID, out InputPlayer player)) { bindingInfos = default; return false; @@ -441,22 +439,38 @@ private static void JoinPlayerByActivatedInputControl(InputControl inputControl) { InputDevice device = inputControl.device; - // Mouse + Keyboard is always joined, currently used devices can't be stolen, and we can't join an inactive player if they're all already active. - if (device is Mouse or Keyboard || playerCollection.IsDeviceLastUsedByAnyPlayer(device) || !playerCollection.AnyPlayerDisabled()) + if (device == null) + { + return; + } + + // Mouse + Keyboard is always joined. + if (device is Mouse or Keyboard) + { + return; + } + + // Any devices already in use can't be stolen. + if (playerCollection.IsDeviceLastUsedByAnyPlayer(device)) { return; } - // Allow "stealing" a device paired to, but currently unused by, another player. + // Allow stealing a device paired to, but currently unused by, another player. if (playerCollection.TryGetPlayerPairedWithDevice(device, out InputPlayer pairedPlayer)) { pairedPlayer.UnpairDevice(device); } + // Find a player to pair the device to. if (playerCollection.TryPairDeviceToFirstDisabledPlayer(device, out InputPlayer disabledPlayer)) { disabledPlayer.Enabled = true; + return; } + + // If no disabled players exist, create and pair to a new player. + playerCollection.PairDeviceToNewPlayer(device); } private static void HandleInputUserChange(InputUser inputUser, InputUserChange inputUserChange, InputDevice inputDevice) @@ -473,6 +487,18 @@ private static void HandleInputUserChange(InputUser inputUser, InputUserChange i internal static bool EDITOR_IsInitialized => initialized; internal static InputContext EDITOR_GetDefaultContext() => DefaultContext; + internal static bool EDITOR_TryGetPlayer(int playerID, out InputPlayer inputPlayer) + { + if (playerCollection == null) + { + inputPlayer = default; + return false; + } + + inputPlayer = playerCollection.GetOrAdd(playerID); + return true; + } + private static void EDITOR_HandlePlayerInputContextChanged(InputPlayer inputPlayer) { EDITOR_OnPlayerInputContextChanged?.Invoke(inputPlayer.ID, inputPlayer.InputContext); diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/InputPlayer.cs index 00ac4c2..486332b 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/InputPlayer.cs @@ -40,9 +40,9 @@ public sealed class InputPlayer public bool Enabled { get => enabled; - internal set + set { - if (playerInput == null) + if (playerInput == null || enabled == value) { return; } @@ -73,8 +73,21 @@ public InputContext InputContext } public int ID { get; } - public ControlScheme CurrentControlScheme { get; private set; } - + + private ControlScheme currentControlScheme; + public ControlScheme CurrentControlScheme + { + get => currentControlScheme; + private set + { + if (CurrentControlScheme == value) + return; + + currentControlScheme = value; + OnControlSchemeChanged?.Invoke(this); + } + } + // MARKER.ActionsProperties.Start // MARKER.ActionsProperties.End @@ -90,7 +103,13 @@ internal InputDevice LastUsedDevice internal bool IsMultiplayer { - set => playerInput.neverAutoSwitchControlSchemes = value; + set + { + if (playerInput != null) + { + playerInput.neverAutoSwitchControlSchemes = value; + } + } } internal InputActionAsset Asset { get; } @@ -138,7 +157,7 @@ internal InputPlayer(InputActionAsset asset, int id, bool isMultiplayer, Transfo internal void Terminate() { - Asset.Disable(); + Enabled = false; DisableKeyboardTextInput(); DisableAllMapsAndRemoveCallbacks(); Object.Destroy(playerInputGameObject); @@ -182,10 +201,11 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInput.actions = Asset; playerInput.uiInputModule = uiInputModule; - playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; + playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; // Set this manually because the initial control scheme gets set before we are able to respond to it with event handlers. - CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); + // TODO: Fix for new players where the string reads "Null" !!! + // CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); } private void SetEventSystemOptions() @@ -302,16 +322,6 @@ internal void UnpairDevices() // UpdateLastUsedDevice(); } - internal void EnableAutoSwitching(bool enable) - { - if (playerInput == null) - { - return; - } - - playerInput.neverAutoSwitchControlSchemes = !enable; - } - /// /// Called by the InputPlayerCollection. If we got here, it means we have already checked that the input user /// experiencing a change refers to this player. @@ -332,12 +342,7 @@ internal void HandleInputUserChange(InputUserChange inputUserChange, InputDevice UpdateDevices(inputDevice); break; case InputUserChange.ControlSchemeChanged: - ControlScheme previousControlScheme = CurrentControlScheme; CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); - if (previousControlScheme != CurrentControlScheme) - { - OnControlSchemeChanged?.Invoke(this); - } break; } diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs index 3561432..e070c67 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/InputPlayerCollection.cs @@ -15,48 +15,47 @@ namespace NPTP.InputSystemWrapper /// internal sealed class InputPlayerCollection : IEnumerable { - internal event Action OnPlayerAdded; - internal event Action OnPlayerRemoved; + private const int DEFAULT_PLAYER_PLAYER_ID = 0; private readonly InputActionAsset inputActionAsset; private readonly Transform inputParent; - private InputPlayer[] players = Array.Empty(); - + private readonly Action onPlayerAdded; + private readonly Action onPlayerRemoved; + + internal InputPlayer DefaultPlayer { get; } + private IEnumerable Players => players.Where(player => player != null); private int PlayerCount => Players.Count(); - public IEnumerator GetEnumerator() => Players.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - internal InputPlayer this[int playerID] - { - get - { - Add(playerID); - return players[playerID]; - } - } - - internal InputPlayerCollection(InputActionAsset asset) + private InputPlayer[] players = Array.Empty(); + + internal InputPlayerCollection(InputActionAsset asset, Action playerAddedListener, Action playerRemovedListener) { - inputParent = CreateInputParentInScene(); inputActionAsset = asset; + inputParent = CreateInputParentInScene(); + onPlayerAdded = playerAddedListener; + onPlayerRemoved = playerRemovedListener; + + DefaultPlayer = GetOrAdd(DEFAULT_PLAYER_PLAYER_ID); } + + public IEnumerator GetEnumerator() => Players.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #region Internal Methods - - internal void Add(int playerID) + + internal InputPlayer GetOrAdd(int playerID) { if (playerID >= players.Length) { - InputPlayer[] extended = new InputPlayer[playerID - 1]; + InputPlayer[] extended = new InputPlayer[playerID + 1]; Array.Copy(players, extended, players.Length); players = extended; } if (players[playerID] != null) { - return; + return players[playerID]; } InputPlayer newPlayer = new InputPlayer(inputActionAsset, playerID, true, inputParent); @@ -67,19 +66,15 @@ internal void Add(int playerID) newPlayer.EDITOR_OnInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif - foreach (InputPlayer player in Players) - { - player.IsMultiplayer = true; - } - - OnPlayerAdded?.Invoke(newPlayer); + onPlayerAdded?.Invoke(newPlayer); + return newPlayer; } internal void Remove(int playerID) { - if (playerID <= 0) + if (playerID <= DEFAULT_PLAYER_PLAYER_ID) { - Debug.LogError("Cannot terminate the default player."); + Debug.LogError($"Cannot remove the default player or get a player with ID < {DEFAULT_PLAYER_PLAYER_ID}."); return; } @@ -90,18 +85,13 @@ internal void Remove(int playerID) players[playerID].Terminate(); players[playerID] = null; - - if (PlayerCount == 1) - { - players[0].IsMultiplayer = false; - } - - OnPlayerRemoved?.Invoke(playerID); + + onPlayerRemoved?.Invoke(playerID); } - internal void TerminateAll() + internal void Terminate() { - foreach (InputPlayer player in players) + foreach (InputPlayer player in Players) { player.OnEnabledOrDisabled -= HandlePlayerEnabledOrDisabled; player.Terminate(); @@ -112,83 +102,56 @@ internal void TerminateAll() players.DefaultAll(); } - - internal bool IsDeviceLastUsedByAnyPlayer(InputDevice device) + + public void SetMultiplayer(bool isMultiplayer) { - for (int i = 0; i < players.Length; i++) + foreach (InputPlayer player in Players) { - if (players[i].LastUsedDevice == device) - { - return true; - } + player.IsMultiplayer = isMultiplayer; } + } - return false; + internal bool IsDeviceLastUsedByAnyPlayer(InputDevice device) + { + return Players.Any(player => player.LastUsedDevice == device); } internal bool AnyPlayerDisabled() { - for (int i = 0; i < players.Length; i++) - { - InputPlayer player = players[i]; - if (!player.Enabled) return true; - } - - return false; + return Players.Any(player => !player.Enabled); } - - internal bool TryGetPlayerPairedWithDevice(InputDevice device, out InputPlayer player) + + internal bool TryGetPlayer(int playerID, out InputPlayer player) { - for (int i = 0; i < players.Length; i++) + if (!players.IndexIsValid(playerID) || players[playerID] == null) { - if (players[i] == null) - { - continue; - } - - if (players[i].IsDevicePaired(device)) - { - player = players[i]; - return true; - } + player = default; + return false; } - player = null; - return false; + player = players[playerID]; + return true; } - - // TODO (optimization): ActionWrapper should have a playerID perhaps, or link to player, or something, to optimize this. - internal bool TryGetPlayerAssociatedWithAsset(InputActionAsset asset, out InputPlayer playerAssociatedWithAsset) + + internal bool TryGetPlayerPairedWithDevice(InputDevice device, out InputPlayer pairedPlayer) { - for (int i = 0; i < players.Length; i++) + foreach (var player in Players) { - if (players[i] == null) - { - continue; - } - - InputPlayer player = players[i]; - if (player.Asset == asset) + if (player.IsDevicePaired(device)) { - playerAssociatedWithAsset = player; + pairedPlayer = player; return true; } } - playerAssociatedWithAsset = null; + pairedPlayer = null; return false; } - + internal bool TryPairDeviceToFirstDisabledPlayer(InputDevice device, out InputPlayer pairedPlayer) { - for (int i = 0; i < players.Length; i++) + foreach (var player in Players) { - if (players[i] == null) - { - continue; - } - - InputPlayer player = players[i]; if (player.Enabled) { continue; @@ -203,11 +166,15 @@ internal bool TryPairDeviceToFirstDisabledPlayer(InputDevice device, out InputPl return false; } + internal void PairDeviceToNewPlayer(InputDevice device) + { + AddFirstPossiblePlayerID().PairDevice(device); + } + internal void HandleInputUserChange(InputUser inputUser, InputUserChange inputUserChange, InputDevice inputDevice) { - for (int i = 0; i < players.Length; i++) + foreach (InputPlayer player in Players) { - InputPlayer player = players[i]; if (player.IsUser(inputUser)) { player.HandleInputUserChange(inputUserChange, inputDevice); @@ -218,7 +185,7 @@ internal void HandleInputUserChange(InputUser inputUser, InputUserChange inputUs internal void SetContextForAll(InputContext inputContext) { - foreach (InputPlayer player in players) + foreach (InputPlayer player in Players) { player.InputContext = inputContext; } @@ -227,6 +194,23 @@ internal void SetContextForAll(InputContext inputContext) #endregion #region Private Methods + + /// + /// Add a new player at the first possible player ID. + /// This may be between, or greater than any existing player IDS. + /// + private InputPlayer AddFirstPossiblePlayerID() + { + for (int i = 0; i < players.Length; i++) + { + if (players[i] == null) + { + return GetOrAdd(i); + } + } + + return GetOrAdd(players.Length); + } private Transform CreateInputParentInScene() { @@ -243,23 +227,23 @@ private void HandlePlayerEnabledOrDisabled(InputPlayer enabledOrDisabledPlayer) { enabledOrDisabledPlayer.UnpairDevices(); } + + int enabledPlayersCount = Players.Count(player => player.Enabled); - int enabledPlayersCount = players.Count(player => player.Enabled); if (enabledPlayersCount > 1) { - foreach (InputPlayer player in players) + foreach (InputPlayer player in Players) { - player.EnableAutoSwitching(false); + player.IsMultiplayer = true; } } else if (enabledPlayersCount == 1) { - // If there's only one player active, let them switch between all available devices. - InputPlayer soleEnabledPlayer = players.First(player => player.Enabled); - soleEnabledPlayer.EnableAutoSwitching(true); + InputPlayer soleEnabledPlayer = Players.First(player => player.Enabled); + soleEnabledPlayer.IsMultiplayer = false; } } - + #endregion #region Editor-Only Debug Fields/Properties/Methods diff --git a/Runtime/Scripts/Templates/ActionsTemplate.cs b/Runtime/Scripts/Templates/ActionsTemplate.cs index bd6ffb9..cd25b4e 100644 --- a/Runtime/Scripts/Templates/ActionsTemplate.cs +++ b/Runtime/Scripts/Templates/ActionsTemplate.cs @@ -31,7 +31,7 @@ public class ActionsTemplate private bool enabled; // MARKER.ConstructorSignature.Start - internal ActionsTemplate(InputActionAsset asset, Dictionary table) + internal ActionsTemplate(int playerID, InputActionAsset asset, Dictionary table) // MARKER.ConstructorSignature.End { // MARKER.ActionMapAssignment.Start @@ -39,8 +39,8 @@ internal ActionsTemplate(InputActionAsset asset, Dictionary // MARKER.ActionMapAssignment.End // MARKER.ActionWrapperAssignments.Start - TemplateAction1 = new (ActionMap.FindAction("TemplateAction1", throwIfNotFound: true), table); - TemplateAction2 = new (ActionMap.FindAction("TemplateAction2", throwIfNotFound: true), table); + TemplateAction1 = new (playerID, ActionMap.FindAction("TemplateAction1", throwIfNotFound: true), table); + TemplateAction2 = new (playerID, ActionMap.FindAction("TemplateAction2", throwIfNotFound: true), table); // MARKER.ActionWrapperAssignments.End // MARKER.Ignore.Start throw new System.NotImplementedException($"This template class {nameof(ActionsTemplate)} should never be instantiated!"); diff --git a/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs b/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs index 1fa91ff..5411c75 100644 --- a/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs +++ b/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs @@ -15,5 +15,10 @@ internal static void DefaultAll(this T[] array) array[i] = default; } } + + internal static bool IndexIsValid(this T[] array, int index) + { + return array != null && 0 <= index && index < array.Length; + } } } \ No newline at end of file From 3340047062d57a603e34e2226d64aed30089bac8 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Fri, 24 Oct 2025 18:51:43 -0400 Subject: [PATCH 03/12] ActionEventInfo wrapping input action event context; any button press improvements for MP --- .../ActionsContentBuilder.cs | 2 +- Runtime/Scripts/Actions/ActionEventInfo.cs | 39 ++++++++ .../Scripts/Actions/ActionEventInfo.cs.meta | 11 +++ Runtime/Scripts/Actions/ActionReference.cs | 2 +- Runtime/Scripts/Actions/ActionWrapper.cs | 6 +- Runtime/Scripts/AnyButtonPressListener.cs | 6 ++ .../Scripts/AnyButtonPressListener.cs.meta | 3 + Runtime/Scripts/ISW.WaitForAnyButtonPress.cs | 3 +- Runtime/Scripts/ISW.cs | 93 ++++-------------- Runtime/Scripts/InputPlayer.cs | 36 ++++++- Runtime/Scripts/InputPlayerCollection.cs | 22 +++-- .../AnyButtonPressListenerCollection.cs | 96 +++++++++++++++++++ .../AnyButtonPressListenerCollection.cs.meta | 3 + 13 files changed, 230 insertions(+), 92 deletions(-) create mode 100644 Runtime/Scripts/Actions/ActionEventInfo.cs create mode 100644 Runtime/Scripts/Actions/ActionEventInfo.cs.meta create mode 100644 Runtime/Scripts/AnyButtonPressListener.cs create mode 100644 Runtime/Scripts/AnyButtonPressListener.cs.meta create mode 100644 Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs create mode 100644 Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs.meta diff --git a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs index 083174e..353f203 100644 --- a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs @@ -24,7 +24,7 @@ internal static void AddContent(string markerName, InputActionMap map, List action.PlayerID; + + private readonly ActionWrapper action; + + public ActionEventInfo(ActionWrapper actionWrapper, InputAction.CallbackContext callbackContext) + { + Phase = callbackContext.phase; + action = actionWrapper; + } + + public T ReadValue() where T : struct + { + return action.InputAction.ReadValue(); + } + } + + // TODO: ActionWrapper needs a base class, then inheriting classes with different events, one for ActionEventInfo and another for this ActionEventInfo + public readonly struct ActionEventInfo where T : struct + { + public InputActionPhase Phase { get; } + public int PlayerID => action.PlayerID; + public T Value => action.ReadValue(); + + private readonly ValueActionWrapper action; + + public ActionEventInfo(ValueActionWrapper actionWrapper, InputAction.CallbackContext callbackContext) + { + Phase = callbackContext.phase; + action = actionWrapper; + } + } +} diff --git a/Runtime/Scripts/Actions/ActionEventInfo.cs.meta b/Runtime/Scripts/Actions/ActionEventInfo.cs.meta new file mode 100644 index 0000000..00b0777 --- /dev/null +++ b/Runtime/Scripts/Actions/ActionEventInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8b5708adf5949684fb5112686995a58b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Actions/ActionReference.cs b/Runtime/Scripts/Actions/ActionReference.cs index 3e7f2ed..ae360a4 100644 --- a/Runtime/Scripts/Actions/ActionReference.cs +++ b/Runtime/Scripts/Actions/ActionReference.cs @@ -15,7 +15,7 @@ namespace NPTP.InputSystemWrapper.Actions [Serializable] public partial class ActionReference { - public event Action OnEvent + public event Action OnEvent { add => ActionWrapper.OnEvent += value; remove => ActionWrapper.OnEvent -= value; diff --git a/Runtime/Scripts/Actions/ActionWrapper.cs b/Runtime/Scripts/Actions/ActionWrapper.cs index d5e1bf0..dc11bbf 100644 --- a/Runtime/Scripts/Actions/ActionWrapper.cs +++ b/Runtime/Scripts/Actions/ActionWrapper.cs @@ -17,8 +17,8 @@ public class ActionWrapper internal int PlayerID { get; } internal InputAction InputAction { get; } - private event Action onEvent; - public event Action OnEvent + private event Action onEvent; + public event Action OnEvent { add { onEvent -= value; onEvent += value; } remove => onEvent -= value; @@ -66,6 +66,6 @@ public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) => ISW.TryGetBindingInfo(new ActionBindingInfo(this, compositePart, controlScheme), out bindingInfos); - private void HandleActionEvent(InputAction.CallbackContext context) => onEvent?.Invoke(context); + private void HandleActionEvent(InputAction.CallbackContext context) => onEvent?.Invoke(new ActionEventInfo(this, context)); } } diff --git a/Runtime/Scripts/AnyButtonPressListener.cs b/Runtime/Scripts/AnyButtonPressListener.cs new file mode 100644 index 0000000..97fda82 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPressListener.cs @@ -0,0 +1,6 @@ +using UnityEngine.InputSystem; + +namespace NPTP.InputSystemWrapper +{ + public delegate void AnyButtonPressListener(InputControl inputControl); +} \ No newline at end of file diff --git a/Runtime/Scripts/AnyButtonPressListener.cs.meta b/Runtime/Scripts/AnyButtonPressListener.cs.meta new file mode 100644 index 0000000..97b92f6 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPressListener.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc89431c7d9e4f0db3b7f95aeae576ed +timeCreated: 1761333051 \ No newline at end of file diff --git a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs index fb705ca..7be6824 100644 --- a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs +++ b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs @@ -10,6 +10,7 @@ public static partial class ISW /// Use like: /// yield return new ISW.WaitForAnyButtonPress(); /// + // TODO: Player-specific version specified by playerID int public class WaitForAnyButtonPress : CustomYieldInstruction { public override bool keepWaiting @@ -48,7 +49,7 @@ private bool ListeningForAnyButtonPress private bool anyButtonPressed; - ~WaitForAnyButtonPress() => OnAnyButtonPress -= HandleAnyButtonPress; + ~WaitForAnyButtonPress() => ListeningForAnyButtonPress = false; public WaitForAnyButtonPress() { diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index 1a55387..cd35d93 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using NPTP.InputSystemWrapper.Actions; using NPTP.InputSystemWrapper.Bindings; using UnityEngine; @@ -8,7 +7,6 @@ using UnityEngine.InputSystem; using UnityEngine.InputSystem.UI; using UnityEngine.InputSystem.Users; -using UnityEngine.InputSystem.Utilities; using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Data; using NPTP.InputSystemWrapper.Generated.Actions; @@ -50,10 +48,10 @@ public static partial class ISW /// /// Invoked on any button pressed on any connected device regardless of actions mapped, assets enabled, etc. /// - public static event Action OnAnyButtonPress + public static event AnyButtonPressListener OnAnyButtonPress { - add => AddAnyButtonPressListener(value); - remove => RemoveAnyButtonPressListener(value); + add => anyButtonPressListenerCollection.Add(value); + remove => anyButtonPressListenerCollection.Remove(value); } // TODO (architecture): Shortcoming here. OnInputUserChange doesn't always get called when a binding changes, so we have this as well. @@ -89,11 +87,10 @@ public static bool AllowPlayerJoining private static InputPlayer DefaultPlayer => playerCollection.DefaultPlayer; private static bool initialized; - private static HashSet> anyButtonPressListeners; - private static IDisposable anyButtonPressCaller; private static InputPlayerCollection playerCollection; private static RuntimeInputData runtimeInputData; private static RebindingOperation rebindingOperation; + private static AnyButtonPressListenerCollection anyButtonPressListenerCollection; #endregion @@ -133,6 +130,7 @@ private static void Initialize() #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif + UpdateAfterPlayerCollectionChange(); // MARKER.LoadAllBindingsOnInitialization.Start LoadBindingsForAllPlayers(); @@ -140,7 +138,7 @@ private static void Initialize() SetContextForAllPlayers(DefaultContext); - anyButtonPressListeners = new HashSet>(); + anyButtonPressListenerCollection = new AnyButtonPressListenerCollection(); ++InputUser.listenForUnpairedDeviceActivity; InputUser.onChange += HandleInputUserChange; OnAnyPlayerInputUserChange += BroadcastControlsUpdated; @@ -170,7 +168,7 @@ void handlePlayModeStateChanged(PlayModeStateChange playModeStateChange) private static void Terminate() { - UnregisterAllAnyButtonPressListeners(); + anyButtonPressListenerCollection.Clear(); #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; #endif @@ -341,21 +339,6 @@ internal static bool TryGetActionWrapper(int playerID, InputAction inputAction, #region Private Runtime Functionality - private static void UpdatePlayerCollectionListeners() - { - foreach (InputPlayer player in playerCollection) - { - player.OnInputUserChange -= HandleAnyPlayerInputUserChange; - player.OnInputUserChange += HandleAnyPlayerInputUserChange; - - player.OnControlSchemeChanged -= HandleAnyPlayerControlSchemeChanged; - player.OnControlSchemeChanged += HandleAnyPlayerControlSchemeChanged; - - player.OnKeyboardTextInput -= HandleAnyPlayerKeyboardTextInput; - player.OnKeyboardTextInput += HandleAnyPlayerKeyboardTextInput; - } - } - private static void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { OnAnyPlayerInputUserChange?.Invoke(inputUserChangeInfo); @@ -371,62 +354,24 @@ private static void HandleAnyPlayerKeyboardTextInput(char c) OnAnyPlayerKeyboardTextInput?.Invoke(c); } - private static void HandlePlayerAdded(InputPlayer inputPlayer) - { - UpdatePlayerCollectionListeners(); - } - - private static void HandlePlayerRemoved(int playerID) - { - UpdatePlayerCollectionListeners(); - } - - private static void AddAnyButtonPressListener(Action action) - { - if (action == null || anyButtonPressListeners.Contains(action)) - return; - anyButtonPressListeners.Add(action); - if (anyButtonPressCaller == null) - anyButtonPressCaller = InputSystem.onAnyButtonPress.Call(HandleAnyButtonPressed); - } + private static void HandlePlayerAdded(InputPlayer inputPlayer) => UpdateAfterPlayerCollectionChange(); + private static void HandlePlayerRemoved(int playerID) => UpdateAfterPlayerCollectionChange(); - private static void RemoveAnyButtonPressListener(Action value) + private static void UpdateAfterPlayerCollectionChange() { - if (value == null || !anyButtonPressListeners.Contains(value)) - return; - anyButtonPressListeners.Remove(value); - DisposeAnyButtonPressCallerIfNoListeners(); - } - - private static void DisposeAnyButtonPressCallerIfNoListeners() - { - if (anyButtonPressListeners.Count == 0 && anyButtonPressCaller != null) + foreach (InputPlayer player in playerCollection) { - anyButtonPressCaller.Dispose(); - anyButtonPressCaller = null; + player.OnInputUserChange -= HandleAnyPlayerInputUserChange; + player.OnInputUserChange += HandleAnyPlayerInputUserChange; + + player.OnControlSchemeChanged -= HandleAnyPlayerControlSchemeChanged; + player.OnControlSchemeChanged += HandleAnyPlayerControlSchemeChanged; + + player.OnKeyboardTextInput -= HandleAnyPlayerKeyboardTextInput; + player.OnKeyboardTextInput += HandleAnyPlayerKeyboardTextInput; } } - private static void UnregisterAllAnyButtonPressListeners() - { - anyButtonPressListeners.Clear(); - DisposeAnyButtonPressCallerIfNoListeners(); - } - - private static void HandleAnyButtonPressed(InputControl inputControl) - { - InvokeAnyButtonPressListeners(inputControl); - } - - private static void InvokeAnyButtonPressListeners(InputControl inputControl) - { - // Temp array for invocation instead of enumerating the anyButtonPressListeners hash set, since - // listeners could unsubscribe during invocation which would modify the hashset. - Action[] listeners = anyButtonPressListeners.ToArray(); - for (int i = 0; i < listeners.Length; i++) - listeners[i]?.Invoke(inputControl); - } - private static void LoadBindingsForAllPlayers() { foreach (InputPlayer player in playerCollection) diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/InputPlayer.cs index 486332b..4faca61 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/InputPlayer.cs @@ -4,6 +4,7 @@ using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Generated.Actions; +using NPTP.InputSystemWrapper.Utilities; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; @@ -36,6 +37,16 @@ public sealed class InputPlayer /// public event Action OnKeyboardTextInput; + /// + /// Invoked when any device paired to this player has any button pressed, + /// regardless of which assets/maps/actions are enabled or disabled. + /// + public event AnyButtonPressListener OnAnyButtonPress + { + add => AddAnyButtonPressListener(value); + remove => RemoveAnyButtonPressListener(value); + } + private bool enabled; public bool Enabled { @@ -123,6 +134,7 @@ internal bool IsMultiplayer private PlayerInput playerInput; private InputSystemUIInputModule uiInputModule; private bool keyboardTextInputEnabled; + private SpecificPlayerAnyButtonPressListenerCollection anyButtonPressListenerCollection; // Event System actions private readonly Dictionary eventSystemActionsPool = new(); @@ -158,6 +170,7 @@ internal InputPlayer(InputActionAsset asset, int id, bool isMultiplayer, Transfo internal void Terminate() { Enabled = false; + anyButtonPressListenerCollection?.Clear(); DisableKeyboardTextInput(); DisableAllMapsAndRemoveCallbacks(); Object.Destroy(playerInputGameObject); @@ -201,10 +214,12 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInput.actions = Asset; playerInput.uiInputModule = uiInputModule; - playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; + // TODO: Unity means to add a "None" behavior which we will use once it's available since any events here are unnecessary overhead that we don't use + playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; + + // TODO: Fix for new players where the string reads "Null" // Set this manually because the initial control scheme gets set before we are able to respond to it with event handlers. - // TODO: Fix for new players where the string reads "Null" !!! // CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); } @@ -362,7 +377,22 @@ internal bool TryGetMatchingActionWrapper(InputAction otherAction, out ActionWra #endregion #region Private - + + private void AddAnyButtonPressListener(AnyButtonPressListener listener) + { + anyButtonPressListenerCollection ??= new SpecificPlayerAnyButtonPressListenerCollection(this); + anyButtonPressListenerCollection.Add(listener); + } + + private void RemoveAnyButtonPressListener(AnyButtonPressListener listener) + { + anyButtonPressListenerCollection.Remove(listener); + if (anyButtonPressListenerCollection.Count == 0) + { + anyButtonPressListenerCollection = null; + } + } + private void UpdateDevices(InputDevice changedDevice) { if (changedDevice is Keyboard && keyboardTextInputEnabled) diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs index e070c67..02b919e 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/InputPlayerCollection.cs @@ -16,27 +16,29 @@ namespace NPTP.InputSystemWrapper internal sealed class InputPlayerCollection : IEnumerable { private const int DEFAULT_PLAYER_PLAYER_ID = 0; - - private readonly InputActionAsset inputActionAsset; - private readonly Transform inputParent; - private readonly Action onPlayerAdded; - private readonly Action onPlayerRemoved; - internal InputPlayer DefaultPlayer { get; } + internal InputPlayer DefaultPlayer { get; private set; } private IEnumerable Players => players.Where(player => player != null); private int PlayerCount => Players.Count(); + private readonly InputActionAsset inputActionAsset; + private readonly Transform inputParent; + private Action onPlayerAdded; + private Action onPlayerRemoved; private InputPlayer[] players = Array.Empty(); internal InputPlayerCollection(InputActionAsset asset, Action playerAddedListener, Action playerRemovedListener) { inputActionAsset = asset; inputParent = CreateInputParentInScene(); + + // Add default player before setting player added listener, + // since this object is not created yet and external listeners may try to access it. + DefaultPlayer = GetOrAdd(DEFAULT_PLAYER_PLAYER_ID); + onPlayerAdded = playerAddedListener; onPlayerRemoved = playerRemovedListener; - - DefaultPlayer = GetOrAdd(DEFAULT_PLAYER_PLAYER_ID); } public IEnumerator GetEnumerator() => Players.GetEnumerator(); @@ -100,7 +102,9 @@ internal void Terminate() #endif } - players.DefaultAll(); + onPlayerAdded = null; + onPlayerRemoved = null; + players = null; } public void SetMultiplayer(bool isMultiplayer) diff --git a/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs new file mode 100644 index 0000000..4638909 --- /dev/null +++ b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Utilities; + +namespace NPTP.InputSystemWrapper.Utilities +{ + internal class AnyButtonPressListenerCollection + { + internal int Count => listeners.Count; + + private readonly HashSet listeners = new(); + private IDisposable anyButtonPressCaller; + + protected virtual void HandleAnyButtonPressed(InputControl inputControl) + { + // Temp arrays for invocation instead of enumerating the stored collections, since + // listeners could unsubscribe during invocation which would modify those collections. + + foreach (AnyButtonPressListener listener in listeners.ToArray()) + { + listener?.Invoke(inputControl); + } + } + + internal void Clear() + { + listeners.Clear(); + DisposeAnyButtonPressCallerIfNoListeners(); + } + + private void PopulateAnyButtonPressCaller() + { + anyButtonPressCaller ??= InputSystem.onAnyButtonPress.Call(HandleAnyButtonPressed); + } + + private void DisposeAnyButtonPressCallerIfNoListeners() + { + if (listeners.Count > 0 || anyButtonPressCaller == null) + { + return; + } + + anyButtonPressCaller.Dispose(); + anyButtonPressCaller = null; + } + + internal bool Add(AnyButtonPressListener listener) + { + if (listener == null) + { + return false; + } + + if (listeners.Add(listener)) + { + PopulateAnyButtonPressCaller(); + return true; + } + + return false; + } + + internal bool Remove(AnyButtonPressListener listener) + { + if (listeners.Remove(listener)) + { + DisposeAnyButtonPressCallerIfNoListeners(); + return true; + } + + return false; + } + } + + internal class SpecificPlayerAnyButtonPressListenerCollection : AnyButtonPressListenerCollection + { + private readonly InputPlayer inputPlayer; + + public SpecificPlayerAnyButtonPressListenerCollection(InputPlayer inputPlayer) + { + this.inputPlayer = inputPlayer; + } + + protected override void HandleAnyButtonPressed(InputControl inputControl) + { + if (inputPlayer == null || !inputPlayer.IsDevicePaired(inputControl.device)) + { + return; + } + + base.HandleAnyButtonPressed(inputControl); + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs.meta b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs.meta new file mode 100644 index 0000000..823d519 --- /dev/null +++ b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 32b3b99659814280834b081f74316ce9 +timeCreated: 1761333035 \ No newline at end of file From 3ccbdd378d2235aa270ac97a559fc169e5788540 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 25 Oct 2025 18:39:27 -0400 Subject: [PATCH 04/12] Step in moving generated code, ISW script partial elements moved to generated folder --- Editor/Scripts/Helper.cs | 14 ++++--- Editor/Scripts/InputScriptGenerator.cs | 6 +-- ...ContentBuilder.cs => ISWContentBuilder.cs} | 7 +--- ...lder.cs.meta => ISWContentBuilder.cs.meta} | 0 Runtime/Resources/OfflineInputData.asset | 5 +-- Runtime/Scripts/Data/OfflineInputData.cs | 9 +++-- Runtime/Scripts/Generated/Complete.meta | 8 ++++ Runtime/Scripts/Generated/Partial.meta | 8 ++++ .../Generated/Partial/ISW.Generated.cs | 35 +++++++++++++++++ .../Generated/Partial/ISW.Generated.cs.meta | 3 ++ Runtime/Scripts/ISW.cs | 38 +++++++------------ 11 files changed, 88 insertions(+), 45 deletions(-) rename Editor/Scripts/ScriptContentBuilders/{InputManagerContentBuilder.cs => ISWContentBuilder.cs} (87%) rename Editor/Scripts/ScriptContentBuilders/{InputManagerContentBuilder.cs.meta => ISWContentBuilder.cs.meta} (100%) create mode 100644 Runtime/Scripts/Generated/Complete.meta create mode 100644 Runtime/Scripts/Generated/Partial.meta create mode 100644 Runtime/Scripts/Generated/Partial/ISW.Generated.cs create mode 100644 Runtime/Scripts/Generated/Partial/ISW.Generated.cs.meta diff --git a/Editor/Scripts/Helper.cs b/Editor/Scripts/Helper.cs index f0e4947..acb886e 100644 --- a/Editor/Scripts/Helper.cs +++ b/Editor/Scripts/Helper.cs @@ -18,28 +18,32 @@ internal static class Helper private const string START = "Start"; private const string END = "End"; internal const string GENERATED = "Generated"; + internal const string PARTIAL = "Partial"; + internal const string COMPLETE = "Complete"; internal const string ACTIONS = "Actions"; // Assets internal static InputActionAsset InputActionAsset => EditorAssetGetter.GetFirst().InputActionAsset; internal static OfflineInputData OfflineInputData => EditorAssetGetter.GetFirst(); - internal static string InputNamespace => GetNamespace(InputManagerFileSystemPath); + internal static string InputNamespace => GetNamespace(ISWFileSystemPath); // Existing script paths - internal static string InputManagerFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.MainInputScriptFile); + private static string ISWFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ISWScriptFile); + private static string ISWFolderSystemPath => EditorAssetGetter.GetSystemFolderPath(OfflineInputData.ISWScriptFile); + internal static string ISWPartialFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ISWPartialScriptFile); internal static string InputPlayerFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string ControlSchemeFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string InputContextFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string RuntimeInputDataFileSystemPath => EditorScriptGetter.GetSystemFilePath(); internal static string BindingChangerFileSystemPath => EditorScriptGetter.GetSystemFilePath(typeof(BindingChanger)); - private static string InputManagerFolderSystemPath => EditorAssetGetter.GetSystemFolderPath(OfflineInputData.MainInputScriptFile); // Template paths internal static string ActionsTemplateFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ActionsTemplateFile); // Generated script paths - internal static string GeneratedFolderSystemPath => InputManagerFolderSystemPath + Sep + GENERATED + Sep; - internal static string GeneratedActionsSystemPath => GeneratedFolderSystemPath + ACTIONS + Sep; + private static string GeneratedFolderSystemPath => ISWFolderSystemPath + Sep + GENERATED + Sep; + internal static string GeneratedPartialFolderSystemPath => GeneratedFolderSystemPath + PARTIAL + Sep; + internal static string GeneratedCompleteFolderSystemPath => GeneratedFolderSystemPath + COMPLETE + Sep; private static char Sep => Path.DirectorySeparatorChar; // String extensions for code generation diff --git a/Editor/Scripts/InputScriptGenerator.cs b/Editor/Scripts/InputScriptGenerator.cs index 5f79a99..e89dfee 100644 --- a/Editor/Scripts/InputScriptGenerator.cs +++ b/Editor/Scripts/InputScriptGenerator.cs @@ -28,13 +28,13 @@ internal static void GenerateInputScriptCode() return; } - Helper.ClearFolderRecursive(Helper.GeneratedFolderSystemPath); + Helper.ClearFolderRecursive(Helper.GeneratedCompleteFolderSystemPath); GenerateActionClasses(offlineInputData.RuntimeInputData.InputActionAsset); ModifyExistingFile(Helper.ControlSchemeFileSystemPath, new ControlSchemeContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputContextFileSystemPath, new InputContextContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputPlayerFileSystemPath, new InputPlayerContentBuilder(offlineInputData)); - ModifyExistingFile(Helper.InputManagerFileSystemPath, new InputManagerContentBuilder(offlineInputData)); + ModifyExistingFile(Helper.ISWPartialFileSystemPath, new ISWContentBuilder(offlineInputData)); ModifyExistingFile(Helper.RuntimeInputDataFileSystemPath, new RuntimeInputDataContentBuilder(offlineInputData)); ModifyExistingFile(Helper.BindingChangerFileSystemPath, new BindingChangerContentBuilder(offlineInputData)); @@ -48,7 +48,7 @@ private static void GenerateActionClasses(InputActionAsset asset) GenerateFile(map, Helper.ActionsTemplateFileSystemPath, ActionsContentBuilder.AddContent, - Helper.GeneratedActionsSystemPath + map.name.AsType() + "Actions.cs"); + Helper.GeneratedCompleteFolderSystemPath + map.name.AsType() + "Actions.cs"); } } diff --git a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs similarity index 87% rename from Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs rename to Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs index 5e57882..ec61aa0 100644 --- a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs @@ -6,7 +6,7 @@ namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders { - internal class InputManagerContentBuilder : ContentBuilder + internal class ISWContentBuilder : ContentBuilder { private const string DEFAULT_PLAYER_FIELD = "DefaultPlayer"; @@ -14,9 +14,6 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) { switch (info.MarkerName) { - case "RuntimeInputDataPath": - info.NewLines.Add($" private const string RUNTIME_INPUT_DATA_PATH = \"{OfflineInputData.RUNTIME_INPUT_DATA_PATH}\";"); - break; case "SinglePlayerFieldsAndProperties": string[] mapNames = Helper.GetMapNames(Asset).ToArray(); info.NewLines.AddRange(mapNames.Select(mapName => $" public static {mapName.AsType()}Actions {mapName.AsType()} => {DEFAULT_PLAYER_FIELD}.{mapName.AsType()};")); @@ -47,7 +44,7 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) } } - internal InputManagerContentBuilder(OfflineInputData offlineInputData) : base(offlineInputData) + internal ISWContentBuilder(OfflineInputData offlineInputData) : base(offlineInputData) { } } diff --git a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs.meta b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs.meta similarity index 100% rename from Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs.meta rename to Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs.meta diff --git a/Runtime/Resources/OfflineInputData.asset b/Runtime/Resources/OfflineInputData.asset index 5494c12..66cee40 100644 --- a/Runtime/Resources/OfflineInputData.asset +++ b/Runtime/Resources/OfflineInputData.asset @@ -14,11 +14,10 @@ MonoBehaviour: m_EditorClassIdentifier: rootPathIdentifier: {fileID: 4900000, guid: 173c3a19d3220cc4894e20b2a481a462, type: 3} runtimeInputData: {fileID: 11400000, guid: ebfe31d05ccd848469684445391d2296, type: 2} - mainInputScriptFile: {fileID: 11500000, guid: 4a92096acb86454458f614b09a41859f, type: 3} + iswScriptFile: {fileID: 11500000, guid: 4a92096acb86454458f614b09a41859f, type: 3} + iswPartialScriptFile: {fileID: 11500000, guid: fea2854135df48ce886420b202708bed, type: 3} actionsTemplateFile: {fileID: 11500000, guid: 295049b3d9040d947b3adc148595ae0a, type: 3} initializationMode: 0 - enableMultiplayer: 0 - maxPlayers: 2 defaultContext: 0 inputContexts: - name: Default diff --git a/Runtime/Scripts/Data/OfflineInputData.cs b/Runtime/Scripts/Data/OfflineInputData.cs index 5a33d0d..d4b8d40 100644 --- a/Runtime/Scripts/Data/OfflineInputData.cs +++ b/Runtime/Scripts/Data/OfflineInputData.cs @@ -19,8 +19,6 @@ namespace NPTP.InputSystemWrapper.Data internal class OfflineInputData : ScriptableObject { #if UNITY_EDITOR - internal const string RUNTIME_INPUT_DATA_PATH = nameof(RuntimeInputData); - [SerializeField] private TextAsset rootPathIdentifier; internal string AssetsPathToPackage { @@ -34,8 +32,11 @@ internal string AssetsPathToPackage [SerializeField] private RuntimeInputData runtimeInputData; internal RuntimeInputData RuntimeInputData => runtimeInputData; - [SerializeField] private TextAsset mainInputScriptFile; - internal TextAsset MainInputScriptFile => mainInputScriptFile; + [SerializeField] private TextAsset iswScriptFile; + internal TextAsset ISWScriptFile => iswScriptFile; + + [SerializeField] private TextAsset iswPartialScriptFile; + internal TextAsset ISWPartialScriptFile => iswPartialScriptFile; [SerializeField] private TextAsset actionsTemplateFile; internal TextAsset ActionsTemplateFile => actionsTemplateFile; diff --git a/Runtime/Scripts/Generated/Complete.meta b/Runtime/Scripts/Generated/Complete.meta new file mode 100644 index 0000000..88900ea --- /dev/null +++ b/Runtime/Scripts/Generated/Complete.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0158d7cf6e58b324b8af0b474ea5d585 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Generated/Partial.meta b/Runtime/Scripts/Generated/Partial.meta new file mode 100644 index 0000000..05e375b --- /dev/null +++ b/Runtime/Scripts/Generated/Partial.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 925f39cfb63938746a3afe9ce157fe31 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs new file mode 100644 index 0000000..69ea82f --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs @@ -0,0 +1,35 @@ +using NPTP.InputSystemWrapper.Bindings; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Generated.Actions; +using UnityEngine; + +namespace NPTP.InputSystemWrapper +{ + public static partial class ISW + { + // MARKER.SinglePlayerFieldsAndProperties.Start + public static GameplayActions Gameplay => DefaultPlayer.Gameplay; + public static UIActions UI => DefaultPlayer.UI; + public static ControlScheme CurrentControlScheme => DefaultPlayer.CurrentControlScheme; + // MARKER.SinglePlayerFieldsAndProperties.End + + // MARKER.DefaultContextProperty.Start + private static InputContext DefaultContext => InputContext.Default; + // MARKER.DefaultContextProperty.End + + // MARKER.Initialize.Start + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Initialize() + // MARKER.Initialize.End + { + InitializationProcess(); + } + + private static void SetUpBindings() + { + // MARKER.LoadAllBindingsOnInitialization.Start + LoadBindingsForAllPlayers(); + // MARKER.LoadAllBindingsOnInitialization.End + } + } +} diff --git a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs.meta b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs.meta new file mode 100644 index 0000000..118efe3 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fea2854135df48ce886420b202708bed +timeCreated: 1761428830 \ No newline at end of file diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index cd35d93..b81fe07 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -9,7 +9,6 @@ using UnityEngine.InputSystem.Users; using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Data; -using NPTP.InputSystemWrapper.Generated.Actions; using NPTP.InputSystemWrapper.CustomSetups; using NPTP.InputSystemWrapper.Utilities; using RebindingOperation = UnityEngine.InputSystem.InputActionRebindingExtensions.RebindingOperation; @@ -28,9 +27,7 @@ public static partial class ISW { #region Fields & Properties - // MARKER.RuntimeInputDataPath.Start - private const string RUNTIME_INPUT_DATA_PATH = "RuntimeInputData"; - // MARKER.RuntimeInputDataPath.End + private const string RUNTIME_INPUT_DATA_RESOURCES_PATH = "RuntimeInputData"; /// /// For use with any localization system in your project: handle this event by taking the passed request, @@ -60,10 +57,7 @@ public static event AnyButtonPressListener OnAnyButtonPress public static event Action OnAnyPlayerInputUserChange; public static event Action OnAnyPlayerControlSchemeChanged; public static event Action OnAnyPlayerKeyboardTextInput; - - // MARKER.SinglePlayerFieldsAndProperties.Start - // MARKER.SinglePlayerFieldsAndProperties.End - + private static bool allowPlayerJoining; public static bool AllowPlayerJoining { @@ -80,10 +74,7 @@ public static bool AllowPlayerJoining } public static Vector2 MousePosition => Mouse.current.position.ReadValue(); - - // MARKER.DefaultContextProperty.Start - private static InputContext DefaultContext => InputContext.Default; - // MARKER.DefaultContextProperty.End + private static InputPlayer DefaultPlayer => playerCollection.DefaultPlayer; private static bool initialized; @@ -95,11 +86,8 @@ public static bool AllowPlayerJoining #endregion #region Setup - - // MARKER.Initialize.Start - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Initialize() - // MARKER.Initialize.End + + private static void InitializationProcess() { if (initialized) { @@ -113,8 +101,8 @@ private static void Initialize() } SetUpTerminationConditions(); - - runtimeInputData = Resources.Load(RUNTIME_INPUT_DATA_PATH); + + runtimeInputData = Resources.Load(RUNTIME_INPUT_DATA_RESOURCES_PATH); if (runtimeInputData == null || runtimeInputData.InputActionAsset == null) { throw new Exception($"{nameof(RuntimeInputData)} is null or its input action asset is null - input will not work!"); @@ -131,11 +119,7 @@ private static void Initialize() playerCollection.EDITOR_OnPlayerInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif UpdateAfterPlayerCollectionChange(); - - // MARKER.LoadAllBindingsOnInitialization.Start - LoadBindingsForAllPlayers(); - // MARKER.LoadAllBindingsOnInitialization.End - + SetUpBindings(); SetContextForAllPlayers(DefaultContext); anyButtonPressListenerCollection = new AnyButtonPressListenerCollection(); @@ -186,11 +170,15 @@ private static void Terminate() #region Public Interface - public static void AddPlayer(int playerID) => Player(playerID); public static InputPlayer Player(int playerID) { return playerCollection.GetOrAdd(playerID); } + + public static void AddPlayer(int playerID) + { + Player(playerID); + } public static void RemovePlayer(int playerID) { From a33773df2f9a22c50dcee2745c4283424b38f379 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sun, 26 Oct 2025 13:13:27 -0400 Subject: [PATCH 05/12] Move all generated portions into new Generated/Partial folder, clean up assets --- Editor/Scripts/Helper.cs | 22 +++--- .../ActionsContentBuilder.cs | 2 +- .../Scripts/Utilities/EditorScriptGetter.cs | 1 + Runtime/Resources/OfflineInputData.asset | 10 +-- Runtime/Scripts/Bindings/BindingChanger.cs | 16 +---- Runtime/Scripts/Data/OfflineInputData.cs | 39 ++++++++--- Runtime/Scripts/Data/RuntimeInputData.cs | 22 +----- .../Partial/BindingChanger.Generated.cs | 24 +++++++ .../Partial/BindingChanger.Generated.cs.meta | 3 + .../Partial}/ControlScheme.cs | 10 +-- .../Partial}/ControlScheme.cs.meta | 0 .../Generated/Partial/ISW.Generated.cs | 5 +- .../Partial}/InputContext.cs | 1 - .../Partial}/InputContext.cs.meta | 0 .../Partial/InputPlayer.Generated.cs | 69 +++++++++++++++++++ .../Partial/InputPlayer.Generated.cs.meta | 3 + .../Partial/RuntimeInputData.Generated.cs | 23 +++++++ .../RuntimeInputData.Generated.cs.meta | 3 + Runtime/Scripts/InputPlayer.cs | 66 +----------------- Runtime/Scripts/InputPlayerCollection.cs | 3 +- 20 files changed, 184 insertions(+), 138 deletions(-) create mode 100644 Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs create mode 100644 Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs.meta rename Runtime/Scripts/{Enums => Generated/Partial}/ControlScheme.cs (99%) rename Runtime/Scripts/{Enums => Generated/Partial}/ControlScheme.cs.meta (100%) rename Runtime/Scripts/{Enums => Generated/Partial}/InputContext.cs (89%) rename Runtime/Scripts/{Enums => Generated/Partial}/InputContext.cs.meta (100%) create mode 100644 Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs create mode 100644 Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs.meta create mode 100644 Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs create mode 100644 Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs.meta diff --git a/Editor/Scripts/Helper.cs b/Editor/Scripts/Helper.cs index acb886e..5c33192 100644 --- a/Editor/Scripts/Helper.cs +++ b/Editor/Scripts/Helper.cs @@ -3,9 +3,7 @@ using System.IO; using System.Linq; using System.Text; -using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.Utilities.Extensions; -using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Data; using NPTP.InputSystemWrapper.Editor.Utilities; using UnityEngine.InputSystem; @@ -14,16 +12,14 @@ namespace NPTP.InputSystemWrapper.Editor { internal static class Helper { + private const string GENERATED = "Generated"; private const string MARKER = "// MARKER"; private const string START = "Start"; private const string END = "End"; - internal const string GENERATED = "Generated"; - internal const string PARTIAL = "Partial"; - internal const string COMPLETE = "Complete"; - internal const string ACTIONS = "Actions"; - + private const string PARTIAL = "Partial"; + private const string COMPLETE = "Complete"; + // Assets - internal static InputActionAsset InputActionAsset => EditorAssetGetter.GetFirst().InputActionAsset; internal static OfflineInputData OfflineInputData => EditorAssetGetter.GetFirst(); internal static string InputNamespace => GetNamespace(ISWFileSystemPath); @@ -31,11 +27,11 @@ internal static class Helper private static string ISWFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ISWScriptFile); private static string ISWFolderSystemPath => EditorAssetGetter.GetSystemFolderPath(OfflineInputData.ISWScriptFile); internal static string ISWPartialFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ISWPartialScriptFile); - internal static string InputPlayerFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string ControlSchemeFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string InputContextFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string RuntimeInputDataFileSystemPath => EditorScriptGetter.GetSystemFilePath(); - internal static string BindingChangerFileSystemPath => EditorScriptGetter.GetSystemFilePath(typeof(BindingChanger)); + internal static string InputPlayerFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.InputPlayerPartialScriptFile); + internal static string ControlSchemeFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ControlSchemeScriptFile); + internal static string InputContextFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.InputContextScriptFile); + internal static string RuntimeInputDataFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.RuntimeInputData); + internal static string BindingChangerFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.BindingChangerPartialScriptFile); // Template paths internal static string ActionsTemplateFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.ActionsTemplateFile); diff --git a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs index 353f203..b6a6a61 100644 --- a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs @@ -21,7 +21,7 @@ internal static void AddContent(string markerName, InputActionMap map, List new string[] - { - // MARKER.BindingExcludedPaths.Start - // MARKER.BindingExcludedPaths.End - }; - - private static string[] CancelPaths => new string[] - { - // MARKER.BindingCancelPaths.Start - "/Keyboard/escape" - // MARKER.BindingCancelPaths.End - }; + private static string[] ExcludedPaths => GetExcludedPathsGenerated(); + private static string[] CancelPaths => GetCancelPathsGenerated(); internal static RebindingOperation StartInteractiveRebind(ActionBindingInfo actionBindingInfo, int bindingIndex, Action callback) { diff --git a/Runtime/Scripts/Data/OfflineInputData.cs b/Runtime/Scripts/Data/OfflineInputData.cs index d4b8d40..2b83d0c 100644 --- a/Runtime/Scripts/Data/OfflineInputData.cs +++ b/Runtime/Scripts/Data/OfflineInputData.cs @@ -19,6 +19,9 @@ namespace NPTP.InputSystemWrapper.Data internal class OfflineInputData : ScriptableObject { #if UNITY_EDITOR + + #region Fields Hidden From User + [SerializeField] private TextAsset rootPathIdentifier; internal string AssetsPathToPackage { @@ -28,19 +31,33 @@ internal string AssetsPathToPackage return assetFilePath[..assetFilePath.LastIndexOf('/')]; } } - + [SerializeField] private RuntimeInputData runtimeInputData; internal RuntimeInputData RuntimeInputData => runtimeInputData; - + [SerializeField] private TextAsset iswScriptFile; internal TextAsset ISWScriptFile => iswScriptFile; [SerializeField] private TextAsset iswPartialScriptFile; internal TextAsset ISWPartialScriptFile => iswPartialScriptFile; + + [SerializeField] private TextAsset inputPlayerPartialScriptFile; + internal TextAsset InputPlayerPartialScriptFile => inputPlayerPartialScriptFile; + + [SerializeField] private TextAsset controlSchemeScriptFile; + public TextAsset ControlSchemeScriptFile => controlSchemeScriptFile; + + [SerializeField] private TextAsset inputContextScriptFile; + public TextAsset InputContextScriptFile => inputContextScriptFile; + + [SerializeField] private TextAsset bindingChangerPartialScriptFile; + public TextAsset BindingChangerPartialScriptFile => bindingChangerPartialScriptFile; [SerializeField] private TextAsset actionsTemplateFile; internal TextAsset ActionsTemplateFile => actionsTemplateFile; + #endregion + [SerializeField] private InitializationMode initializationMode = InitializationMode.BeforeSceneLoad; internal InitializationMode InitializationMode => initializationMode; @@ -86,24 +103,24 @@ internal string AssetsPathToPackage // TODO (architecture): these can probably just be ActionReference, now (and change how they get initialized then) [Header("Default Event System Actions")] [SerializeField] private InputActionReference point; - [SerializeField] private InputActionReference leftClick; - [SerializeField] private InputActionReference middleClick; - [SerializeField] private InputActionReference rightClick; - [SerializeField] private InputActionReference scrollWheel; - [SerializeField] private InputActionReference move; - [SerializeField] private InputActionReference submit; - [SerializeField] private InputActionReference cancel; - [SerializeField] private InputActionReference trackedDevicePosition; - [SerializeField] private InputActionReference trackedDeviceOrientation; internal InputActionReference Point => point; + [SerializeField] private InputActionReference leftClick; internal InputActionReference LeftClick => leftClick; + [SerializeField] private InputActionReference middleClick; internal InputActionReference MiddleClick => middleClick; + [SerializeField] private InputActionReference rightClick; internal InputActionReference RightClick => rightClick; + [SerializeField] private InputActionReference scrollWheel; internal InputActionReference ScrollWheel => scrollWheel; + [SerializeField] private InputActionReference move; internal InputActionReference Move => move; + [SerializeField] private InputActionReference submit; internal InputActionReference Submit => submit; + [SerializeField] private InputActionReference cancel; internal InputActionReference Cancel => cancel; + [SerializeField] private InputActionReference trackedDevicePosition; internal InputActionReference TrackedDevicePosition => trackedDevicePosition; + [SerializeField] private InputActionReference trackedDeviceOrientation; internal InputActionReference TrackedDeviceOrientation => trackedDeviceOrientation; internal int GetEventSystemActionNonNullOverrideCount() diff --git a/Runtime/Scripts/Data/RuntimeInputData.cs b/Runtime/Scripts/Data/RuntimeInputData.cs index 474d4dd..9107f5a 100644 --- a/Runtime/Scripts/Data/RuntimeInputData.cs +++ b/Runtime/Scripts/Data/RuntimeInputData.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; -using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.CustomSetups; -using NPTP.InputSystemWrapper.Enums; using UnityEngine; using UnityEngine.InputSystem; @@ -12,7 +9,7 @@ namespace NPTP.InputSystemWrapper.Data /// Input Data used at runtime, containing the input action asset template on which new assets are cloned, /// and the data that lets us resolve input bindings to display names & sprites on the UI. /// - internal class RuntimeInputData : ScriptableObject + internal partial class RuntimeInputData : ScriptableObject { [SerializeField] private InputActionAsset inputActionAsset; internal InputActionAsset InputActionAsset => inputActionAsset; @@ -20,7 +17,7 @@ internal class RuntimeInputData : ScriptableObject [SerializeField] private CustomLayout[] customLayouts; [SerializeField] private CustomBinding[] customBindings; [SerializeField] private CustomInteraction[] customInteractions; - + public IEnumerable AllCustomSetups { get @@ -32,18 +29,5 @@ public IEnumerable AllCustomSetups return customSetups; } } - - // MARKER.ControlSchemeBindingData.Start - // MARKER.ControlSchemeBindingData.End - - internal BindingData GetControlSchemeBindingData(ControlScheme controlScheme) - { - return controlScheme switch - { - // MARKER.EnumToBindingDataSwitch.Start - // MARKER.EnumToBindingDataSwitch.End - _ => throw new ArgumentOutOfRangeException(nameof(controlScheme), controlScheme, null) - }; - } } -} +} \ No newline at end of file diff --git a/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs new file mode 100644 index 0000000..e742663 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs @@ -0,0 +1,24 @@ +namespace NPTP.InputSystemWrapper.Bindings +{ + internal static partial class BindingChanger + { + private static string[] GetExcludedPathsGenerated() + { + return new string[] + { + // MARKER.BindingExcludedPaths.Start + // MARKER.BindingExcludedPaths.End + }; + } + + private static string[] GetCancelPathsGenerated() + { + return new string[] + { + // MARKER.BindingCancelPaths.Start + "/Keyboard/escape" + // MARKER.BindingCancelPaths.End + }; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs.meta b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs.meta new file mode 100644 index 0000000..4b4f50d --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6c02e2df86514a9293e8b7f3a0c6ad8a +timeCreated: 1761432128 \ No newline at end of file diff --git a/Runtime/Scripts/Enums/ControlScheme.cs b/Runtime/Scripts/Generated/Partial/ControlScheme.cs similarity index 99% rename from Runtime/Scripts/Enums/ControlScheme.cs rename to Runtime/Scripts/Generated/Partial/ControlScheme.cs index 9731e87..c1d703d 100644 --- a/Runtime/Scripts/Enums/ControlScheme.cs +++ b/Runtime/Scripts/Generated/Partial/ControlScheme.cs @@ -34,6 +34,11 @@ public static bool IsGamepadBased(this ControlScheme controlScheme) internal static class InternalControlSchemeExtensions { + internal static InputBinding ToBindingMask(this ControlScheme controlScheme) + { + return new InputBinding(groups: controlScheme.ToInputAssetName(), path: default); + } + /// /// Convert the enum to the string name in the asset from which the control scheme originates, /// so the string name can be used in the Input System API. @@ -60,10 +65,5 @@ internal static ControlScheme ToControlSchemeEnum(this string controlSchemeName) _ => throw new ArgumentOutOfRangeException(nameof(controlSchemeName), controlSchemeName, null) }; } - - internal static InputBinding ToBindingMask(this ControlScheme controlScheme) - { - return new InputBinding(groups: controlScheme.ToInputAssetName(), path: default); - } } } diff --git a/Runtime/Scripts/Enums/ControlScheme.cs.meta b/Runtime/Scripts/Generated/Partial/ControlScheme.cs.meta similarity index 100% rename from Runtime/Scripts/Enums/ControlScheme.cs.meta rename to Runtime/Scripts/Generated/Partial/ControlScheme.cs.meta diff --git a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs index 69ea82f..98ece85 100644 --- a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs @@ -8,13 +8,10 @@ namespace NPTP.InputSystemWrapper public static partial class ISW { // MARKER.SinglePlayerFieldsAndProperties.Start - public static GameplayActions Gameplay => DefaultPlayer.Gameplay; - public static UIActions UI => DefaultPlayer.UI; - public static ControlScheme CurrentControlScheme => DefaultPlayer.CurrentControlScheme; // MARKER.SinglePlayerFieldsAndProperties.End // MARKER.DefaultContextProperty.Start - private static InputContext DefaultContext => InputContext.Default; + private static InputContext DefaultContext => 0; // MARKER.DefaultContextProperty.End // MARKER.Initialize.Start diff --git a/Runtime/Scripts/Enums/InputContext.cs b/Runtime/Scripts/Generated/Partial/InputContext.cs similarity index 89% rename from Runtime/Scripts/Enums/InputContext.cs rename to Runtime/Scripts/Generated/Partial/InputContext.cs index 958ad33..9775ab3 100644 --- a/Runtime/Scripts/Enums/InputContext.cs +++ b/Runtime/Scripts/Generated/Partial/InputContext.cs @@ -3,7 +3,6 @@ namespace NPTP.InputSystemWrapper.Enums public enum InputContext { // MARKER.Members.Start - Default, // MARKER.Members.End } } diff --git a/Runtime/Scripts/Enums/InputContext.cs.meta b/Runtime/Scripts/Generated/Partial/InputContext.cs.meta similarity index 100% rename from Runtime/Scripts/Enums/InputContext.cs.meta rename to Runtime/Scripts/Generated/Partial/InputContext.cs.meta diff --git a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs new file mode 100644 index 0000000..6fcb813 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs @@ -0,0 +1,69 @@ +using System; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Generated.Actions; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.UI; + +namespace NPTP.InputSystemWrapper +{ + public sealed partial class InputPlayer + { + // MARKER.ActionsProperties.Start + // MARKER.ActionsProperties.End + + internal InputPlayer(InputActionAsset asset, int id, bool isMultiplayer, Transform parent) + { + Asset = InstantiateNewActions(asset); + ID = id; + + // MARKER.ActionsInstantiation.Start + // MARKER.ActionsInstantiation.End + + SetUpInputPlayerGameObject(isMultiplayer, parent); + PopulateEventSystemActionsPool(); + + // Input context gets set by top ISW class after this instantiation, which sets up maps & event system actions/overrides, so we don't have to handle that here. + } + + private void SetEventSystemOptions() + { + // MARKER.EventSystemOptions.Start + // MARKER.EventSystemOptions.End + } + + /// + /// Adds all default and override event system InputActionReferences to a shared pool to + /// reduce duplication and lookup time. + /// + private void PopulateEventSystemActionsPool() + { + // MARKER.PopulateEventSystemActionsPool.Start + // MARKER.PopulateEventSystemActionsPool.End + } + + private void DisableAllMapsAndRemoveCallbacks() + { + // MARKER.DisableAllMapsAndRemoveCallbacksBody.Start + // MARKER.DisableAllMapsAndRemoveCallbacksBody.End + } + + private void EnableMapsForContext(InputContext context) + { + if (!Enabled) + { + return; + } + + SetDefaultEventSystemActions(); + + switch (context) + { + // MARKER.EnableContextSwitchMembers.Start + // MARKER.EnableContextSwitchMembers.End + default: + throw new ArgumentOutOfRangeException(nameof(context), context, null); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs.meta b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs.meta new file mode 100644 index 0000000..a4c6531 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e81925db712f4314a964e72efbd1a3b2 +timeCreated: 1761496132 \ No newline at end of file diff --git a/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs new file mode 100644 index 0000000..3beeb0b --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs @@ -0,0 +1,23 @@ +using System; +using NPTP.InputSystemWrapper.Bindings; +using NPTP.InputSystemWrapper.Enums; +using UnityEngine; + +namespace NPTP.InputSystemWrapper.Data +{ + internal partial class RuntimeInputData + { + // MARKER.ControlSchemeBindingData.Start + // MARKER.ControlSchemeBindingData.End + + internal BindingData GetControlSchemeBindingData(ControlScheme controlScheme) + { + return controlScheme switch + { + // MARKER.EnumToBindingDataSwitch.Start + // MARKER.EnumToBindingDataSwitch.End + _ => throw new ArgumentOutOfRangeException(nameof(controlScheme), controlScheme, null) + }; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs.meta b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs.meta new file mode 100644 index 0000000..5dab5d6 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b5e5ace4232440f8afccf1127b1840e5 +timeCreated: 1761496372 \ No newline at end of file diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/InputPlayer.cs index 4faca61..ac94a6d 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/InputPlayer.cs @@ -15,7 +15,7 @@ namespace NPTP.InputSystemWrapper { - public sealed class InputPlayer + public sealed partial class InputPlayer { #region Field & Properties @@ -99,9 +99,6 @@ private set } } - // MARKER.ActionsProperties.Start - // MARKER.ActionsProperties.End - private InputDevice lastUsedDevice; internal InputDevice LastUsedDevice { @@ -153,20 +150,6 @@ internal bool IsMultiplayer #region Setup & Teardown - internal InputPlayer(InputActionAsset asset, int id, bool isMultiplayer, Transform parent) - { - Asset = InstantiateNewActions(asset); - ID = id; - - // MARKER.ActionsInstantiation.Start - // MARKER.ActionsInstantiation.End - - SetUpInputPlayerGameObject(isMultiplayer, parent); - PopulateEventSystemActionsPool(); - - // Input context gets set by top ISW class after this instantiation, which sets up maps & event system actions/overrides, so we don't have to handle that here. - } - internal void Terminate() { Enabled = false; @@ -223,27 +206,6 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) // CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); } - private void SetEventSystemOptions() - { - // MARKER.EventSystemOptions.Start - uiInputModule.moveRepeatDelay = 0.5f; - uiInputModule.moveRepeatRate = 0.1f; - uiInputModule.deselectOnBackgroundClick = false; - uiInputModule.pointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack; - uiInputModule.cursorLockBehavior = InputSystemUIInputModule.CursorLockBehavior.OutsideScreen; - // MARKER.EventSystemOptions.End - } - - /// - /// Adds all default and override event system InputActionReferences to a shared pool to - /// reduce duplication and lookup time. - /// - private void PopulateEventSystemActionsPool() - { - // MARKER.PopulateEventSystemActionsPool.Start - // MARKER.PopulateEventSystemActionsPool.End - } - private void SetDefaultEventSystemActions() { uiInputModule.point = defaultPoint; @@ -454,35 +416,11 @@ private void UpdateLastUsedDevice(InputDevice fallbackDevice = null) lastUsedDevice = fallbackDevice; } } - - private void DisableAllMapsAndRemoveCallbacks() - { - // MARKER.DisableAllMapsAndRemoveCallbacksBody.Start - // MARKER.DisableAllMapsAndRemoveCallbacksBody.End - } - + private void HandleTextInput(char c) { OnKeyboardTextInput?.Invoke(c); } - - private void EnableMapsForContext(InputContext context) - { - if (!Enabled) - { - return; - } - - SetDefaultEventSystemActions(); - - switch (context) - { - // MARKER.EnableContextSwitchMembers.Start - // MARKER.EnableContextSwitchMembers.End - default: - throw new ArgumentOutOfRangeException(nameof(context), context, null); - } - } #endregion diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs index 02b919e..88d1ab8 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/InputPlayerCollection.cs @@ -54,8 +54,7 @@ internal InputPlayer GetOrAdd(int playerID) Array.Copy(players, extended, players.Length); players = extended; } - - if (players[playerID] != null) + else if (players[playerID] != null) { return players[playerID]; } From c2ae10f789b8d6d6c4d2a9cc19eef0fe272c5a7d Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sun, 26 Oct 2025 13:18:23 -0400 Subject: [PATCH 06/12] Fix missing using directives in partial generated scripts --- Runtime/Scripts/Generated/Partial/ISW.Generated.cs | 1 + Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs index 98ece85..23b7c05 100644 --- a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs @@ -1,3 +1,4 @@ +using NPTP.InputSystemWrapper.Actions; using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Generated.Actions; diff --git a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs index 6fcb813..66b851c 100644 --- a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs @@ -1,4 +1,5 @@ -using System; +using System; +using NPTP.InputSystemWrapper.Actions; using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Generated.Actions; using UnityEngine; From 78b95588773e4f4323bc8a6351db6c8c9c9cd5e7 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sun, 26 Oct 2025 13:19:08 -0400 Subject: [PATCH 07/12] Fix comment indent --- Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs index ec61aa0..99caf15 100644 --- a/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs @@ -23,7 +23,7 @@ internal override void AddContent(InputScriptGeneratorMarkerInfo info) string defaultContextValue = $"{nameof(InputContext)}.{Data.DefaultContext}"; if (Data.InputContexts.Length == 0) { - info.NewLines.Add(">>> WARNING: No InputContexts have been defined in your OfflineInputData asset. Comment out this line to allow recompilation, add at least 1 InputContext, then re-save the asset."); + info.NewLines.Add(" // >>> WARNING: No InputContexts have been defined in your OfflineInputData asset. Add at least 1 InputContext, then re-save the asset."); defaultContextValue = "0"; } else if (Enum.GetNames(typeof(InputContext)).Length == 0) From 3f966b7d3eebf8fe8d6b9b1d54fc108dbbf302b5 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Fri, 3 Apr 2026 14:47:37 -0400 Subject: [PATCH 08/12] 4.0.0 --- CHANGELOG.md | 8 ++++ Runtime/Scripts/Bindings/BindingChanger.cs | 2 +- .../Partial/BindingChanger.Generated.cs | 7 +-- .../Generated/Partial/ControlScheme.cs | 10 +++- .../Generated/Partial/ISW.Generated.cs | 1 + .../Scripts/Generated/Partial/InputContext.cs | 1 + .../Partial/InputPlayer.Generated.cs | 1 + .../Partial/RuntimeInputData.Generated.cs | 1 + Runtime/Scripts/ISW.WaitForAnyButtonPress.cs | 46 ++++++++++++++----- .../Scripts/ISW.WaitForAnyButtonPress.cs.meta | 2 +- Runtime/Scripts/ISW.cs | 14 +++++- Runtime/Scripts/InputPlayer.cs | 13 +++--- Runtime/Scripts/InputPlayerCollection.cs | 7 ++- package.json | 2 +- root-path-identifier.htm => root.htm | 0 ...-path-identifier.htm.meta => root.htm.meta | 0 16 files changed, 84 insertions(+), 31 deletions(-) rename root-path-identifier.htm => root.htm (100%) rename root-path-identifier.htm.meta => root.htm.meta (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a0bf3d..21cb35a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Input System Wrapper ## Changelog +4.0.0 +- Rename `Input` class to `ISW` (acronym) to avoid needing aliases against Unity's built-in "Input" class. +- Multiplayer support initial version working. +- `OnAnyButtonPress` event uses new custom delegate `AnyButtonPressListener` (same signature as before). This applies to all devices. + - Individual players now have their own non-global `OnAnyButtonPress` event which applies only to devices paired with that player at the time of invocation. +- ActionWrapper events pass a custom struct now instead of Unity's `InputAction.CallbackContext`, for better encapsulation and cordoning-off of properties that were accessible in Unity's struct that could break the ISW architecture. +- Separated auto-generated code into partial classes in separate folder to make package updates simpler. + 3.2.3 - Editor-only changes: - Use root path identifier serialized field instead of making user set script path diff --git a/Runtime/Scripts/Bindings/BindingChanger.cs b/Runtime/Scripts/Bindings/BindingChanger.cs index c2cccc3..1ec2610 100644 --- a/Runtime/Scripts/Bindings/BindingChanger.cs +++ b/Runtime/Scripts/Bindings/BindingChanger.cs @@ -81,7 +81,7 @@ private static RebindingOperation WithCancelingThroughMultiple(this RebindingOpe { // >>> NOTE: OnPotentialMatch will not read inputs outside of your current control scheme. So if you're // rebinding on gamepad and hit Escape to cancel, Escape had better be your primaryCancelPath (above) - // or else it won't get caught here. TODO: Find a better solution for this. + // or else it won't get caught here. TODO: Find a better solution for this, perhaps an AnyButtonPress listener that catches cancel paths. rebindingOperation.OnPotentialMatch(operation => { if (paths.Any(path => operation.selectedControl.path == path)) diff --git a/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs index e742663..eadd9e0 100644 --- a/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs @@ -1,4 +1,5 @@ -namespace NPTP.InputSystemWrapper.Bindings +// ReSharper disable once CheckNamespace +namespace NPTP.InputSystemWrapper.Bindings { internal static partial class BindingChanger { @@ -16,9 +17,9 @@ private static string[] GetCancelPathsGenerated() return new string[] { // MARKER.BindingCancelPaths.Start - "/Keyboard/escape" + "/Keyboard/escape" // MARKER.BindingCancelPaths.End }; } } -} \ No newline at end of file +} diff --git a/Runtime/Scripts/Generated/Partial/ControlScheme.cs b/Runtime/Scripts/Generated/Partial/ControlScheme.cs index c1d703d..e7f0bb3 100644 --- a/Runtime/Scripts/Generated/Partial/ControlScheme.cs +++ b/Runtime/Scripts/Generated/Partial/ControlScheme.cs @@ -5,6 +5,11 @@ namespace NPTP.InputSystemWrapper.Enums { public enum ControlScheme { + /// + /// Corresponds to "Null" string for newly created, unassigned players in Unity's PlayerInput. + /// + None, + // MARKER.Members.Start // MARKER.Members.End } @@ -54,7 +59,8 @@ internal static string ToInputAssetName(this ControlScheme controlSchemeEnum) } /// - /// Convert the control scheme asset name to the corresponding enum value. + /// Try to convert the control scheme name from the input actions asset, + /// used internally by Unity's input system, to its corresponding enum value. /// internal static ControlScheme ToControlSchemeEnum(this string controlSchemeName) { @@ -62,7 +68,7 @@ internal static ControlScheme ToControlSchemeEnum(this string controlSchemeName) { // MARKER.StringToEnumSwitch.Start // MARKER.StringToEnumSwitch.End - _ => throw new ArgumentOutOfRangeException(nameof(controlSchemeName), controlSchemeName, null) + _ => ControlScheme.None }; } } diff --git a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs index 23b7c05..3227a06 100644 --- a/Runtime/Scripts/Generated/Partial/ISW.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs @@ -4,6 +4,7 @@ using NPTP.InputSystemWrapper.Generated.Actions; using UnityEngine; +// ReSharper disable once CheckNamespace namespace NPTP.InputSystemWrapper { public static partial class ISW diff --git a/Runtime/Scripts/Generated/Partial/InputContext.cs b/Runtime/Scripts/Generated/Partial/InputContext.cs index 9775ab3..33bc201 100644 --- a/Runtime/Scripts/Generated/Partial/InputContext.cs +++ b/Runtime/Scripts/Generated/Partial/InputContext.cs @@ -1,3 +1,4 @@ +// ReSharper disable once CheckNamespace namespace NPTP.InputSystemWrapper.Enums { public enum InputContext diff --git a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs index 66b851c..69fb97a 100644 --- a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs @@ -6,6 +6,7 @@ using UnityEngine.InputSystem; using UnityEngine.InputSystem.UI; +// ReSharper disable once CheckNamespace namespace NPTP.InputSystemWrapper { public sealed partial class InputPlayer diff --git a/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs index 3beeb0b..29f5573 100644 --- a/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs @@ -3,6 +3,7 @@ using NPTP.InputSystemWrapper.Enums; using UnityEngine; +// ReSharper disable once CheckNamespace namespace NPTP.InputSystemWrapper.Data { internal partial class RuntimeInputData diff --git a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs index 7be6824..7950385 100644 --- a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs +++ b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs @@ -7,27 +7,24 @@ public static partial class ISW { /// /// Custom yield instruction for coroutines to make waiting for any button press a lot more syntactically convenient. - /// Use like: + /// To listen for ANY player: /// yield return new ISW.WaitForAnyButtonPress(); + /// To listen to a specific player: + /// yield return new ISW.WaitForAnyButtonPress(int playerID); /// - // TODO: Player-specific version specified by playerID int public class WaitForAnyButtonPress : CustomYieldInstruction { public override bool keepWaiting { get { - if (anyButtonPressed) + if (anyButtonPressed || !DoesPlayerExist(playerID)) { ResetYieldInstruction(); return false; } - if (!ListeningForAnyButtonPress) - { - ListeningForAnyButtonPress = true; - } - + ListeningForAnyButtonPress = true; return !anyButtonPressed; } } @@ -35,27 +32,52 @@ public override bool keepWaiting private bool listeningForAnyButtonPress; private bool ListeningForAnyButtonPress { - get => listeningForAnyButtonPress; set { if (listeningForAnyButtonPress == value) + { return; + } + + if (DoesPlayerExist(playerID)) + { + if (value) Player(playerID).OnAnyButtonPress += HandleAnyButtonPress; + else Player(playerID).OnAnyButtonPress -= HandleAnyButtonPress; + } + else + { + if (value) OnAnyButtonPress += HandleAnyButtonPress; + else OnAnyButtonPress -= HandleAnyButtonPress; + } - if (value) OnAnyButtonPress += HandleAnyButtonPress; - else OnAnyButtonPress -= HandleAnyButtonPress; listeningForAnyButtonPress = value; } } - + + private readonly int playerID = -1; private bool anyButtonPressed; ~WaitForAnyButtonPress() => ListeningForAnyButtonPress = false; + /// + /// Listen for any button press for any player/device. + /// public WaitForAnyButtonPress() { ListeningForAnyButtonPress = true; } + /// + /// Listen for any button press for a specific player. + /// If that player doesn't exist yet, the yield will end immediately, but can be reused again later + /// after the player has been created to properly wait for their button press. + /// + public WaitForAnyButtonPress(int playerID) + { + this.playerID = playerID; + ListeningForAnyButtonPress = true; + } + private void HandleAnyButtonPress(InputControl inputControl) { anyButtonPressed = true; diff --git a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta index 391c891..84c971a 100644 --- a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta +++ b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta @@ -1,3 +1,3 @@ fileFormatVersion: 2 -guid: 652b452fb278460890114114bc3f1de7 +guid: 8f637f3356ec13f4ead3697ade46642e timeCreated: 1754108857 \ No newline at end of file diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index b81fe07..c5428fb 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -68,6 +68,7 @@ public static bool AllowPlayerJoining return; allowPlayerJoining = value; + playerCollection.SetMultiplayer(value); if (value) OnAnyButtonPress += JoinPlayerByActivatedInputControl; else OnAnyButtonPress -= JoinPlayerByActivatedInputControl; } @@ -105,7 +106,7 @@ private static void InitializationProcess() runtimeInputData = Resources.Load(RUNTIME_INPUT_DATA_RESOURCES_PATH); if (runtimeInputData == null || runtimeInputData.InputActionAsset == null) { - throw new Exception($"{nameof(RuntimeInputData)} is null or its input action asset is null - input will not work!"); + throw new Exception($"{nameof(RuntimeInputData)} is null or its input action asset is null - input will not work! Did you move the asset from its original location in 'Resources'?"); } // Clear out anything in the scene that would interfere with the ISW's autonomous operation. @@ -327,6 +328,11 @@ internal static bool TryGetActionWrapper(int playerID, InputAction inputAction, #region Private Runtime Functionality + private static bool DoesPlayerExist(int playerID) + { + return playerCollection.TryGetPlayer(playerID, out _); + } + private static void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { OnAnyPlayerInputUserChange?.Invoke(inputUserChangeInfo); @@ -374,36 +380,42 @@ private static void JoinPlayerByActivatedInputControl(InputControl inputControl) if (device == null) { + Debug.Log("Device is null"); return; } // Mouse + Keyboard is always joined. if (device is Mouse or Keyboard) { + Debug.Log("Device is MKB"); return; } // Any devices already in use can't be stolen. if (playerCollection.IsDeviceLastUsedByAnyPlayer(device)) { + Debug.Log($"Already using {device.name}"); return; } // Allow stealing a device paired to, but currently unused by, another player. if (playerCollection.TryGetPlayerPairedWithDevice(device, out InputPlayer pairedPlayer)) { + Debug.Log($"Unpairing {device.name} from player {pairedPlayer.ID}"); pairedPlayer.UnpairDevice(device); } // Find a player to pair the device to. if (playerCollection.TryPairDeviceToFirstDisabledPlayer(device, out InputPlayer disabledPlayer)) { + Debug.Log("Paired to disabled player"); disabledPlayer.Enabled = true; return; } // If no disabled players exist, create and pair to a new player. playerCollection.PairDeviceToNewPlayer(device); + Debug.Log("Paired to new player"); } private static void HandleInputUserChange(InputUser inputUser, InputUserChange inputUserChange, InputDevice inputDevice) diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/InputPlayer.cs index ac94a6d..cd6a1ef 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/InputPlayer.cs @@ -183,7 +183,7 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInputGameObject = new GameObject { - name = $"{ID}Input", + name = $"Player[{ID}]Input", transform = { position = Vector3.zero, parent = parent} }; @@ -197,13 +197,14 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInput.actions = Asset; playerInput.uiInputModule = uiInputModule; + + // TODO: Unity means to add a "None" behavior to the InputSystem which we will use once it's available. + // This is because any events here are unnecessary overhead that we don't use. + // C# events are just the lowest overhead in the meantime. + playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; - // TODO: Unity means to add a "None" behavior which we will use once it's available since any events here are unnecessary overhead that we don't use - playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; - - // TODO: Fix for new players where the string reads "Null" // Set this manually because the initial control scheme gets set before we are able to respond to it with event handlers. - // CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); + CurrentControlScheme = playerInput.currentControlScheme.ToControlSchemeEnum(); } private void SetDefaultEventSystemActions() diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs index 88d1ab8..9f6edeb 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/InputPlayerCollection.cs @@ -17,11 +17,10 @@ internal sealed class InputPlayerCollection : IEnumerable { private const int DEFAULT_PLAYER_PLAYER_ID = 0; - internal InputPlayer DefaultPlayer { get; private set; } + internal InputPlayer DefaultPlayer { get; } private IEnumerable Players => players.Where(player => player != null); - private int PlayerCount => Players.Count(); - + private readonly InputActionAsset inputActionAsset; private readonly Transform inputParent; private Action onPlayerAdded; @@ -200,7 +199,7 @@ internal void SetContextForAll(InputContext inputContext) /// /// Add a new player at the first possible player ID. - /// This may be between, or greater than any existing player IDS. + /// This may be between, or greater than any existing player IDs. /// private InputPlayer AddFirstPossiblePlayerID() { diff --git a/package.json b/package.json index cd6fec2..dbbc2c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.nptp.unity-input-system-wrapper", "displayName": "Input System Wrapper", - "version": "3.2.3", + "version": "4.0.0", "unity": "2021.3", "description": "Make Input easier to use! Must import directly into assets for code generation to work properly.", "homepage": "https://github.com/NPTP/UnityInputSystemWrapper", diff --git a/root-path-identifier.htm b/root.htm similarity index 100% rename from root-path-identifier.htm rename to root.htm diff --git a/root-path-identifier.htm.meta b/root.htm.meta similarity index 100% rename from root-path-identifier.htm.meta rename to root.htm.meta From 059c814b8ba06af864414aed1204ef85b8bed66d Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 4 Apr 2026 22:23:44 -0400 Subject: [PATCH 09/12] Organize files, simplify usages --- .../InputWrapperDebuggerWindow.cs | 1 + Runtime/Scripts/Actions/ActionEventInfo.cs | 18 ++-- Runtime/Scripts/AnyButtonPress.meta | 8 ++ .../AnyButtonPressListener.cs | 2 +- .../AnyButtonPressListener.cs.meta | 0 .../AnyButtonPress/WaitForAnyButtonPress.cs | 90 ++++++++++++++++++ .../WaitForAnyButtonPress.cs.meta} | 0 Runtime/Scripts/Bindings/BindingSaveLoad.cs | 1 + .../Scripts/Components/InputActionUpdater.cs | 1 + .../Generated/Partial/ControlScheme.cs | 1 + .../Partial/InputPlayer.Generated.cs | 2 +- Runtime/Scripts/ISW.WaitForAnyButtonPress.cs | 93 ------------------- Runtime/Scripts/ISW.cs | 45 +++++---- Runtime/Scripts/Player.meta | 8 ++ Runtime/Scripts/{ => Player}/InputPlayer.cs | 5 +- .../Scripts/{ => Player}/InputPlayer.cs.meta | 0 .../{ => Player}/InputPlayerCollection.cs | 4 +- .../InputPlayerCollection.cs.meta | 0 .../{ => Player}/InputUserChangeInfo.cs | 2 +- .../{ => Player}/InputUserChangeInfo.cs.meta | 0 .../AnyButtonPressListenerCollection.cs | 2 + 21 files changed, 157 insertions(+), 126 deletions(-) create mode 100644 Runtime/Scripts/AnyButtonPress.meta rename Runtime/Scripts/{ => AnyButtonPress}/AnyButtonPressListener.cs (69%) rename Runtime/Scripts/{ => AnyButtonPress}/AnyButtonPressListener.cs.meta (100%) create mode 100644 Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs rename Runtime/Scripts/{ISW.WaitForAnyButtonPress.cs.meta => AnyButtonPress/WaitForAnyButtonPress.cs.meta} (100%) delete mode 100644 Runtime/Scripts/ISW.WaitForAnyButtonPress.cs create mode 100644 Runtime/Scripts/Player.meta rename Runtime/Scripts/{ => Player}/InputPlayer.cs (99%) rename Runtime/Scripts/{ => Player}/InputPlayer.cs.meta (100%) rename Runtime/Scripts/{ => Player}/InputPlayerCollection.cs (99%) rename Runtime/Scripts/{ => Player}/InputPlayerCollection.cs.meta (100%) rename Runtime/Scripts/{ => Player}/InputUserChangeInfo.cs (93%) rename Runtime/Scripts/{ => Player}/InputUserChangeInfo.cs.meta (100%) diff --git a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs index b3de751..8872b80 100644 --- a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs +++ b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs @@ -3,6 +3,7 @@ using NPTP.InputSystemWrapper.Data; using NPTP.InputSystemWrapper.Editor.Utilities; using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Player; using UnityEditor; using UnityEngine; using FontStyle = UnityEngine.FontStyle; diff --git a/Runtime/Scripts/Actions/ActionEventInfo.cs b/Runtime/Scripts/Actions/ActionEventInfo.cs index 0e482cf..bcefc9a 100644 --- a/Runtime/Scripts/Actions/ActionEventInfo.cs +++ b/Runtime/Scripts/Actions/ActionEventInfo.cs @@ -5,19 +5,19 @@ namespace NPTP.InputSystemWrapper.Actions public readonly struct ActionEventInfo { public InputActionPhase Phase { get; } - public int PlayerID => action.PlayerID; + public int PlayerID => actionWrapper.PlayerID; - private readonly ActionWrapper action; + private readonly ActionWrapper actionWrapper; public ActionEventInfo(ActionWrapper actionWrapper, InputAction.CallbackContext callbackContext) { Phase = callbackContext.phase; - action = actionWrapper; + this.actionWrapper = actionWrapper; } public T ReadValue() where T : struct { - return action.InputAction.ReadValue(); + return actionWrapper.InputAction.ReadValue(); } } @@ -25,15 +25,15 @@ public T ReadValue() where T : struct public readonly struct ActionEventInfo where T : struct { public InputActionPhase Phase { get; } - public int PlayerID => action.PlayerID; - public T Value => action.ReadValue(); + public int PlayerID => valueActionWrapper.PlayerID; + public T Value => valueActionWrapper.ReadValue(); - private readonly ValueActionWrapper action; + private readonly ValueActionWrapper valueActionWrapper; - public ActionEventInfo(ValueActionWrapper actionWrapper, InputAction.CallbackContext callbackContext) + public ActionEventInfo(ValueActionWrapper valueActionWrapper, InputAction.CallbackContext callbackContext) { Phase = callbackContext.phase; - action = actionWrapper; + this.valueActionWrapper = valueActionWrapper; } } } diff --git a/Runtime/Scripts/AnyButtonPress.meta b/Runtime/Scripts/AnyButtonPress.meta new file mode 100644 index 0000000..dfa3207 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPress.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: abd15620f757b8d40973e583c688c93c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/AnyButtonPressListener.cs b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs similarity index 69% rename from Runtime/Scripts/AnyButtonPressListener.cs rename to Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs index 97fda82..f7dcd35 100644 --- a/Runtime/Scripts/AnyButtonPressListener.cs +++ b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs @@ -1,6 +1,6 @@ using UnityEngine.InputSystem; -namespace NPTP.InputSystemWrapper +namespace NPTP.InputSystemWrapper.AnyButtonPress { public delegate void AnyButtonPressListener(InputControl inputControl); } \ No newline at end of file diff --git a/Runtime/Scripts/AnyButtonPressListener.cs.meta b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs.meta similarity index 100% rename from Runtime/Scripts/AnyButtonPressListener.cs.meta rename to Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs.meta diff --git a/Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs b/Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs new file mode 100644 index 0000000..5a5a847 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs @@ -0,0 +1,90 @@ +using UnityEngine; +using UnityEngine.InputSystem; + +namespace NPTP.InputSystemWrapper.AnyButtonPress +{ + /// + /// Custom yield instruction for coroutines to make waiting for any button press a lot more syntactically convenient. + /// To listen for ANY player: + /// yield return new WaitForAnyButtonPress(); + /// To listen to a specific player: + /// yield return new WaitForAnyButtonPress(int playerID); + /// + public class WaitForAnyButtonPress : CustomYieldInstruction + { + public override bool keepWaiting + { + get + { + if (anyButtonPressed || !ISW.DoesPlayerExist(playerID)) + { + ResetYieldInstruction(); + return false; + } + + ListeningForAnyButtonPress = true; + return !anyButtonPressed; + } + } + + private bool listeningForAnyButtonPress; + private bool ListeningForAnyButtonPress + { + set + { + if (listeningForAnyButtonPress == value) + { + return; + } + + if (ISW.DoesPlayerExist(playerID)) + { + if (value) ISW.Player(playerID).OnAnyButtonPress += HandleAnyButtonPress; + else ISW.Player(playerID).OnAnyButtonPress -= HandleAnyButtonPress; + } + else + { + if (value) ISW.OnAnyButtonPress += HandleAnyButtonPress; + else ISW.OnAnyButtonPress -= HandleAnyButtonPress; + } + + listeningForAnyButtonPress = value; + } + } + + private readonly int playerID = -1; + private bool anyButtonPressed; + + ~WaitForAnyButtonPress() => ListeningForAnyButtonPress = false; + + /// + /// Listen for any button press for any player/device. + /// + public WaitForAnyButtonPress() + { + ListeningForAnyButtonPress = true; + } + + /// + /// Listen for any button press for a specific player. + /// If that player doesn't exist yet, the yield will end immediately, but can be reused again later + /// after the player has been created to properly wait for their button press. + /// + public WaitForAnyButtonPress(int playerID) + { + this.playerID = playerID; + ListeningForAnyButtonPress = true; + } + + private void HandleAnyButtonPress(InputControl inputControl) + { + anyButtonPressed = true; + } + + private void ResetYieldInstruction() + { + anyButtonPressed = false; + ListeningForAnyButtonPress = false; + } + } +} \ No newline at end of file diff --git a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta b/Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs.meta similarity index 100% rename from Runtime/Scripts/ISW.WaitForAnyButtonPress.cs.meta rename to Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs.meta diff --git a/Runtime/Scripts/Bindings/BindingSaveLoad.cs b/Runtime/Scripts/Bindings/BindingSaveLoad.cs index 76880ef..7256487 100644 --- a/Runtime/Scripts/Bindings/BindingSaveLoad.cs +++ b/Runtime/Scripts/Bindings/BindingSaveLoad.cs @@ -1,4 +1,5 @@ using System.IO; +using NPTP.InputSystemWrapper.Player; using NPTP.InputSystemWrapper.Utilities; using UnityEngine; using UnityEngine.InputSystem; diff --git a/Runtime/Scripts/Components/InputActionUpdater.cs b/Runtime/Scripts/Components/InputActionUpdater.cs index b8155fd..25ce73a 100644 --- a/Runtime/Scripts/Components/InputActionUpdater.cs +++ b/Runtime/Scripts/Components/InputActionUpdater.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NPTP.InputSystemWrapper.Actions; using NPTP.InputSystemWrapper.Bindings; +using NPTP.InputSystemWrapper.Player; using UnityEngine; namespace NPTP.InputSystemWrapper.Components diff --git a/Runtime/Scripts/Generated/Partial/ControlScheme.cs b/Runtime/Scripts/Generated/Partial/ControlScheme.cs index e7f0bb3..179461a 100644 --- a/Runtime/Scripts/Generated/Partial/ControlScheme.cs +++ b/Runtime/Scripts/Generated/Partial/ControlScheme.cs @@ -1,6 +1,7 @@ using System; using UnityEngine.InputSystem; +// ReSharper disable once CheckNamespace namespace NPTP.InputSystemWrapper.Enums { public enum ControlScheme diff --git a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs index 69fb97a..f72fc5a 100644 --- a/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs @@ -7,7 +7,7 @@ using UnityEngine.InputSystem.UI; // ReSharper disable once CheckNamespace -namespace NPTP.InputSystemWrapper +namespace NPTP.InputSystemWrapper.Player { public sealed partial class InputPlayer { diff --git a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs b/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs deleted file mode 100644 index 7950385..0000000 --- a/Runtime/Scripts/ISW.WaitForAnyButtonPress.cs +++ /dev/null @@ -1,93 +0,0 @@ -using UnityEngine; -using UnityEngine.InputSystem; - -namespace NPTP.InputSystemWrapper -{ - public static partial class ISW - { - /// - /// Custom yield instruction for coroutines to make waiting for any button press a lot more syntactically convenient. - /// To listen for ANY player: - /// yield return new ISW.WaitForAnyButtonPress(); - /// To listen to a specific player: - /// yield return new ISW.WaitForAnyButtonPress(int playerID); - /// - public class WaitForAnyButtonPress : CustomYieldInstruction - { - public override bool keepWaiting - { - get - { - if (anyButtonPressed || !DoesPlayerExist(playerID)) - { - ResetYieldInstruction(); - return false; - } - - ListeningForAnyButtonPress = true; - return !anyButtonPressed; - } - } - - private bool listeningForAnyButtonPress; - private bool ListeningForAnyButtonPress - { - set - { - if (listeningForAnyButtonPress == value) - { - return; - } - - if (DoesPlayerExist(playerID)) - { - if (value) Player(playerID).OnAnyButtonPress += HandleAnyButtonPress; - else Player(playerID).OnAnyButtonPress -= HandleAnyButtonPress; - } - else - { - if (value) OnAnyButtonPress += HandleAnyButtonPress; - else OnAnyButtonPress -= HandleAnyButtonPress; - } - - listeningForAnyButtonPress = value; - } - } - - private readonly int playerID = -1; - private bool anyButtonPressed; - - ~WaitForAnyButtonPress() => ListeningForAnyButtonPress = false; - - /// - /// Listen for any button press for any player/device. - /// - public WaitForAnyButtonPress() - { - ListeningForAnyButtonPress = true; - } - - /// - /// Listen for any button press for a specific player. - /// If that player doesn't exist yet, the yield will end immediately, but can be reused again later - /// after the player has been created to properly wait for their button press. - /// - public WaitForAnyButtonPress(int playerID) - { - this.playerID = playerID; - ListeningForAnyButtonPress = true; - } - - private void HandleAnyButtonPress(InputControl inputControl) - { - anyButtonPressed = true; - } - - private void ResetYieldInstruction() - { - anyButtonPressed = false; - ListeningForAnyButtonPress = false; - } - } - } -} \ No newline at end of file diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index c5428fb..fa1b0b4 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using NPTP.InputSystemWrapper.Actions; +using NPTP.InputSystemWrapper.AnyButtonPress; using NPTP.InputSystemWrapper.Bindings; using UnityEngine; using UnityEngine.EventSystems; @@ -10,8 +11,8 @@ using NPTP.InputSystemWrapper.Enums; using NPTP.InputSystemWrapper.Data; using NPTP.InputSystemWrapper.CustomSetups; +using NPTP.InputSystemWrapper.Player; using NPTP.InputSystemWrapper.Utilities; -using RebindingOperation = UnityEngine.InputSystem.InputActionRebindingExtensions.RebindingOperation; #if UNITY_EDITOR using UnityEditor; @@ -81,7 +82,7 @@ public static bool AllowPlayerJoining private static bool initialized; private static InputPlayerCollection playerCollection; private static RuntimeInputData runtimeInputData; - private static RebindingOperation rebindingOperation; + private static InputActionRebindingExtensions.RebindingOperation rebindingOperation; private static AnyButtonPressListenerCollection anyButtonPressListenerCollection; #endregion @@ -233,24 +234,36 @@ public static void ResetBindingForAction(ActionReference actionReference, Contro BindingChanger.ResetBindingToDefaultForControlScheme(actionBindingInfo, controlScheme); } - public static void ResetAllBindingsForControlScheme(ControlScheme controlScheme, int playerID = 0) + public static void ResetAllBindingsForControlScheme(ControlScheme controlScheme, int? playerID = null) { - BindingChanger.ResetBindingsToDefaultForControlScheme(Player(playerID).Asset, controlScheme); + if (playerID.HasValue) + BindingChanger.ResetBindingsToDefaultForControlScheme(Player(playerID.Value).Asset, controlScheme); + else foreach (InputPlayer player in playerCollection) + BindingChanger.ResetBindingsToDefaultForControlScheme(player.Asset, controlScheme); } - public static void LoadAllBindings(int playerID = 0) + public static void LoadAllBindings(int? playerID = null) { - BindingSaveLoad.LoadBindingsFromDiskForPlayer(Player(playerID)); + if (playerID.HasValue) + BindingSaveLoad.LoadBindingsFromDiskForPlayer(Player(playerID.Value)); + else foreach (InputPlayer player in playerCollection) + BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); } - public static void SaveAllBindings(int playerID = 0) + public static void SaveAllBindings(int? playerID = null) { - BindingSaveLoad.SaveBindingsToDiskForPlayer(Player(playerID)); + if (playerID.HasValue) + BindingSaveLoad.SaveBindingsToDiskForPlayer(Player(playerID.Value)); + else foreach (InputPlayer player in playerCollection) + BindingSaveLoad.SaveBindingsToDiskForPlayer(player); } - public static void ResetAllBindings(int playerID = 0) + public static void ResetAllBindings(int? playerID = 0) { - BindingChanger.ResetBindingsToDefault(Player(playerID).Asset); + if (playerID.HasValue) + BindingChanger.ResetBindingsToDefault(Player(playerID.Value).Asset); + else foreach (InputPlayer player in playerCollection) + BindingChanger.ResetBindingsToDefault(player.Asset); } #endregion @@ -323,16 +336,16 @@ internal static bool TryGetActionWrapper(int playerID, InputAction inputAction, { return Player(playerID).TryGetMatchingActionWrapper(inputAction, out actionWrapper); } - - #endregion - - #region Private Runtime Functionality - - private static bool DoesPlayerExist(int playerID) + + internal static bool DoesPlayerExist(int playerID) { return playerCollection.TryGetPlayer(playerID, out _); } + #endregion + + #region Private Runtime Functionality + private static void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { OnAnyPlayerInputUserChange?.Invoke(inputUserChangeInfo); diff --git a/Runtime/Scripts/Player.meta b/Runtime/Scripts/Player.meta new file mode 100644 index 0000000..276f44e --- /dev/null +++ b/Runtime/Scripts/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 546309bffd31a884aa67863fb0d7afd1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Scripts/InputPlayer.cs b/Runtime/Scripts/Player/InputPlayer.cs similarity index 99% rename from Runtime/Scripts/InputPlayer.cs rename to Runtime/Scripts/Player/InputPlayer.cs index cd6a1ef..bfc4ce9 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/Player/InputPlayer.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; using NPTP.InputSystemWrapper.Actions; +using NPTP.InputSystemWrapper.AnyButtonPress; using NPTP.InputSystemWrapper.Bindings; using NPTP.InputSystemWrapper.Enums; -using NPTP.InputSystemWrapper.Generated.Actions; using NPTP.InputSystemWrapper.Utilities; using UnityEngine; -using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.InputSystem.UI; using UnityEngine.InputSystem.Users; using UnityEngine.InputSystem.Utilities; using Object = UnityEngine.Object; -namespace NPTP.InputSystemWrapper +namespace NPTP.InputSystemWrapper.Player { public sealed partial class InputPlayer { diff --git a/Runtime/Scripts/InputPlayer.cs.meta b/Runtime/Scripts/Player/InputPlayer.cs.meta similarity index 100% rename from Runtime/Scripts/InputPlayer.cs.meta rename to Runtime/Scripts/Player/InputPlayer.cs.meta diff --git a/Runtime/Scripts/InputPlayerCollection.cs b/Runtime/Scripts/Player/InputPlayerCollection.cs similarity index 99% rename from Runtime/Scripts/InputPlayerCollection.cs rename to Runtime/Scripts/Player/InputPlayerCollection.cs index 9f6edeb..1e98646 100644 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ b/Runtime/Scripts/Player/InputPlayerCollection.cs @@ -2,13 +2,13 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using NPTP.InputSystemWrapper.Utilities.Extensions; using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Utilities.Extensions; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Users; -namespace NPTP.InputSystemWrapper +namespace NPTP.InputSystemWrapper.Player { /// /// Useful interface layer for dealing with a collection of multiple players. diff --git a/Runtime/Scripts/InputPlayerCollection.cs.meta b/Runtime/Scripts/Player/InputPlayerCollection.cs.meta similarity index 100% rename from Runtime/Scripts/InputPlayerCollection.cs.meta rename to Runtime/Scripts/Player/InputPlayerCollection.cs.meta diff --git a/Runtime/Scripts/InputUserChangeInfo.cs b/Runtime/Scripts/Player/InputUserChangeInfo.cs similarity index 93% rename from Runtime/Scripts/InputUserChangeInfo.cs rename to Runtime/Scripts/Player/InputUserChangeInfo.cs index 72290ad..5a9dbd0 100644 --- a/Runtime/Scripts/InputUserChangeInfo.cs +++ b/Runtime/Scripts/Player/InputUserChangeInfo.cs @@ -1,7 +1,7 @@ using NPTP.InputSystemWrapper.Enums; using UnityEngine.InputSystem.Users; -namespace NPTP.InputSystemWrapper +namespace NPTP.InputSystemWrapper.Player { public struct InputUserChangeInfo { diff --git a/Runtime/Scripts/InputUserChangeInfo.cs.meta b/Runtime/Scripts/Player/InputUserChangeInfo.cs.meta similarity index 100% rename from Runtime/Scripts/InputUserChangeInfo.cs.meta rename to Runtime/Scripts/Player/InputUserChangeInfo.cs.meta diff --git a/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs index 4638909..93561e7 100644 --- a/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs +++ b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using NPTP.InputSystemWrapper.AnyButtonPress; +using NPTP.InputSystemWrapper.Player; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Utilities; From 67ed0b3bef21e16c80a3897db5941bc5e6014bbf Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 4 Apr 2026 22:25:25 -0400 Subject: [PATCH 10/12] Better warning comment --- Runtime/Scripts/Templates/ActionsTemplate.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Runtime/Scripts/Templates/ActionsTemplate.cs b/Runtime/Scripts/Templates/ActionsTemplate.cs index cd25b4e..8ce45bc 100644 --- a/Runtime/Scripts/Templates/ActionsTemplate.cs +++ b/Runtime/Scripts/Templates/ActionsTemplate.cs @@ -6,9 +6,10 @@ using UnityEngine.InputSystem.XR; using Button = UnityEngine.InputSystem.HID.HID.Button; // MARKER.Ignore.Start -// ---------------------------------- WARNING ! --------------------------------------- +// ---------------------------------- WARNING ! ------------------------------------------- // This template script is used to auto-generate new C# input classes and their respective // .cs files. Do not modify it unless you know what you're doing! +// ---------------------------------------------------------------------------------------- // MARKER.Ignore.End // MARKER.GeneratorNotice.Start From 96903c2a363c2b67ba8770abac22ddcf5a0b5cb6 Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 4 Apr 2026 23:51:40 -0400 Subject: [PATCH 11/12] Prevent null refs on application quit --- .../CustomEditors/OfflineInputDataEditor.cs | 4 +--- Runtime/Scripts/ISW.cs | 17 ++++++----------- Runtime/Scripts/Player/InputPlayer.cs | 1 - Runtime/Scripts/Player/InputPlayerCollection.cs | 1 - 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs index 221fb55..8dfbb37 100644 --- a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs +++ b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs @@ -142,9 +142,7 @@ public override void OnInspectorGUI() continue; SerializedProperty specProperty = basisProperty.FindPropertyRelative(nameof(basis.Basis).ToLower()); - specProperty.enumValueIndex = (int)(ControlSchemeBasis.BasisSpec)EditorGUILayout.EnumPopup( - basis.ControlScheme.ToInputAssetName(), - (ControlSchemeBasis.BasisSpec)specProperty.enumValueIndex); + specProperty.enumValueIndex = (int)(ControlSchemeBasis.BasisSpec)EditorGUILayout.EnumPopup(basis.ControlScheme.ToInputAssetName(), (ControlSchemeBasis.BasisSpec)specProperty.enumValueIndex); } } diff --git a/Runtime/Scripts/ISW.cs b/Runtime/Scripts/ISW.cs index fa1b0b4..7907835 100644 --- a/Runtime/Scripts/ISW.cs +++ b/Runtime/Scripts/ISW.cs @@ -102,7 +102,7 @@ private static void InitializationProcess() ReflectionUtility.ResetStaticClassMembersToDefault(typeof(ISW)); } - SetUpTerminationConditions(); + SetUpQuittingConditions(); runtimeInputData = Resources.Load(RUNTIME_INPUT_DATA_RESOURCES_PATH); if (runtimeInputData == null || runtimeInputData.InputActionAsset == null) @@ -134,7 +134,7 @@ private static void InitializationProcess() initialized = true; } - private static void SetUpTerminationConditions() + private static void SetUpQuittingConditions() { #if UNITY_EDITOR EditorApplication.playModeStateChanged -= handlePlayModeStateChanged; @@ -143,29 +143,24 @@ void handlePlayModeStateChanged(PlayModeStateChange playModeStateChange) { if (playModeStateChange is PlayModeStateChange.ExitingPlayMode) { - Terminate(); + OnQuitting(); } } -#else - Application.quitting -= Terminate; - Application.quitting += Terminate; #endif } - private static void Terminate() + private static void OnQuitting() { - anyButtonPressListenerCollection.Clear(); #if UNITY_EDITOR + anyButtonPressListenerCollection.Clear(); playerCollection.EDITOR_OnPlayerInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; -#endif OnAnyPlayerInputUserChange -= BroadcastControlsUpdated; OnBindingsChanged -= BroadcastControlsUpdated; OnAnyPlayerControlSchemeChanged -= BroadcastControlsUpdated; - playerCollection.Terminate(); - playerCollection = null; --InputUser.listenForUnpairedDeviceActivity; InputUser.onChange -= HandleInputUserChange; +#endif } #endregion diff --git a/Runtime/Scripts/Player/InputPlayer.cs b/Runtime/Scripts/Player/InputPlayer.cs index bfc4ce9..abe9d9d 100644 --- a/Runtime/Scripts/Player/InputPlayer.cs +++ b/Runtime/Scripts/Player/InputPlayer.cs @@ -155,7 +155,6 @@ internal void Terminate() anyButtonPressListenerCollection?.Clear(); DisableKeyboardTextInput(); DisableAllMapsAndRemoveCallbacks(); - Object.Destroy(playerInputGameObject); } private InputActionAsset InstantiateNewActions(InputActionAsset actions) diff --git a/Runtime/Scripts/Player/InputPlayerCollection.cs b/Runtime/Scripts/Player/InputPlayerCollection.cs index 1e98646..4fea900 100644 --- a/Runtime/Scripts/Player/InputPlayerCollection.cs +++ b/Runtime/Scripts/Player/InputPlayerCollection.cs @@ -102,7 +102,6 @@ internal void Terminate() onPlayerAdded = null; onPlayerRemoved = null; - players = null; } public void SetMultiplayer(bool isMultiplayer) From 14f8f6e5b12194b01204b3704ac373327ca3bb1a Mon Sep 17 00:00:00 2001 From: Nick Perrin Date: Sat, 4 Apr 2026 23:58:56 -0400 Subject: [PATCH 12/12] Fix inspector bug with ControlScheme.None --- Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs index 8dfbb37..d80bab8 100644 --- a/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs +++ b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs @@ -140,7 +140,10 @@ public override void OnInspectorGUI() SerializedProperty basisProperty = controlSchemeBases.GetArrayElementAtIndex(i); if (basisProperty.boxedValue is not ControlSchemeBasis basis) continue; - + + if (basis.ControlScheme == ControlScheme.None) + continue; + SerializedProperty specProperty = basisProperty.FindPropertyRelative(nameof(basis.Basis).ToLower()); specProperty.enumValueIndex = (int)(ControlSchemeBasis.BasisSpec)EditorGUILayout.EnumPopup(basis.ControlScheme.ToInputAssetName(), (ControlSchemeBasis.BasisSpec)specProperty.enumValueIndex); }