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/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs b/Editor/Scripts/CustomEditors/OfflineInputDataEditor.cs index c4694b7..d80bab8 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(); @@ -154,11 +140,12 @@ 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); + specProperty.enumValueIndex = (int)(ControlSchemeBasis.BasisSpec)EditorGUILayout.EnumPopup(basis.ControlScheme.ToInputAssetName(), (ControlSchemeBasis.BasisSpec)specProperty.enumValueIndex); } } diff --git a/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs b/Editor/Scripts/EditorWindows/InputWrapperDebuggerWindow.cs index 0dcabc1..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; @@ -26,7 +27,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 +53,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,19 +96,22 @@ 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 }); return; } - - GUILayout.BeginVertical(); - ShowDebugInfoField("Current Control Scheme", Input.CurrentControlScheme.ToString()); - ShowDebugInfoField("Current Context", Input.Context.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) @@ -124,7 +128,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..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,34 +12,34 @@ 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 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(InputManagerFileSystemPath); + internal static string InputNamespace => GetNamespace(ISWFileSystemPath); // Existing script paths - internal static string InputManagerFileSystemPath => EditorAssetGetter.GetSystemFilePath(OfflineInputData.MainInputScriptFile); - 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); + 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 => 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); // 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 dc598c4..e89dfee 100644 --- a/Editor/Scripts/InputScriptGenerator.cs +++ b/Editor/Scripts/InputScriptGenerator.cs @@ -28,15 +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.PlayerIDFileSystemPath, new PlayerIDContentBuilder(offlineInputData)); ModifyExistingFile(Helper.InputPlayerFileSystemPath, new InputPlayerContentBuilder(offlineInputData)); - ModifyExistingFile(Helper.InputManagerFileSystemPath, new InputManagerContentBuilder(offlineInputData)); - ModifyExistingFile(Helper.InputUserChangeInfoFileSystemPath, new InputUserChangeInfoContentBuilder(offlineInputData)); + ModifyExistingFile(Helper.ISWPartialFileSystemPath, new ISWContentBuilder(offlineInputData)); ModifyExistingFile(Helper.RuntimeInputDataFileSystemPath, new RuntimeInputDataContentBuilder(offlineInputData)); ModifyExistingFile(Helper.BindingChangerFileSystemPath, new BindingChangerContentBuilder(offlineInputData)); @@ -50,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/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..b6a6a61 100644 --- a/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/ActionsContentBuilder.cs @@ -21,10 +21,10 @@ 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/ISWContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs new file mode 100644 index 0000000..99caf15 --- /dev/null +++ b/Editor/Scripts/ScriptContentBuilders/ISWContentBuilder.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Data; +using NPTP.InputSystemWrapper.Enums.NPTP.InputSystemWrapper; + +namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders +{ + internal class ISWContentBuilder : ContentBuilder + { + private const string DEFAULT_PLAYER_FIELD = "DefaultPlayer"; + + internal override void AddContent(InputScriptGeneratorMarkerInfo info) + { + switch (info.MarkerName) + { + 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()};")); + info.NewLines.Add($" public static {nameof(ControlScheme)} CurrentControlScheme => {DEFAULT_PLAYER_FIELD}.CurrentControlScheme;"); + break; + case "DefaultContextProperty": + string defaultContextValue = $"{nameof(InputContext)}.{Data.DefaultContext}"; + if (Data.InputContexts.Length == 0) + { + 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) + { + defaultContextValue = $"{nameof(InputContext)}.{Data.InputContexts[0].Name}"; + } + info.NewLines.Add($" private static {nameof(InputContext)} DefaultContext => {defaultContextValue};"); + break; + case "Initialize": + if (Data.InitializationMode == InitializationMode.BeforeSceneLoad) + info.NewLines.Add(" [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]"); + info.NewLines.Add($" {(Data.InitializationMode == InitializationMode.Manual ? "public" : "private")} static void Initialize()"); + break; + case "LoadAllBindingsOnInitialization": + if (Data.LoadAllBindingOverridesOnInitialize) + info.NewLines.Add(" LoadBindingsForAllPlayers();"); + break; + } + } + + 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/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs deleted file mode 100644 index 8fab957..0000000 --- a/Editor/Scripts/ScriptContentBuilders/InputManagerContentBuilder.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Linq; -using NPTP.InputSystemWrapper.Enums; -using NPTP.InputSystemWrapper.Data; -using NPTP.InputSystemWrapper.Enums.NPTP.InputSystemWrapper; - -namespace NPTP.InputSystemWrapper.Editor.ScriptContentBuilders -{ - internal class InputManagerContentBuilder : ContentBuilder - { - 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(); - 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;"); - break; - case "DefaultContextProperty": - 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."); - defaultContextValue = "0"; - } - else if (Enum.GetNames(typeof(InputContext)).Length == 0) - { - defaultContextValue = $"{nameof(InputContext)}.{Data.InputContexts[0].Name}"; - } - info.NewLines.Add($" private static {nameof(InputContext)} DefaultContext => {defaultContextValue};"); - break; - case "Initialize": - if (Data.InitializationMode == InitializationMode.BeforeSceneLoad) - info.NewLines.Add(" [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]"); - info.NewLines.Add($" {(Data.InitializationMode == InitializationMode.Manual ? "public" : "private")} static void Initialize()"); - break; - case "LoadAllBindingsOnInitialization": - 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" + - " }"; - } - } - - internal InputManagerContentBuilder(OfflineInputData offlineInputData) : base(offlineInputData) - { - } - } -} diff --git a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs index 6c71e4b..17fb60a 100644 --- a/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs +++ b/Editor/Scripts/ScriptContentBuilders/InputPlayerContentBuilder.cs @@ -14,23 +14,13 @@ 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; }"); 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/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/Utilities/EditorScriptGetter.cs b/Editor/Scripts/Utilities/EditorScriptGetter.cs index 2a8812b..60170c9 100644 --- a/Editor/Scripts/Utilities/EditorScriptGetter.cs +++ b/Editor/Scripts/Utilities/EditorScriptGetter.cs @@ -4,6 +4,7 @@ namespace NPTP.InputSystemWrapper.Editor.Utilities { + // TODO: Save elsewhere, remove from this package internal static class EditorScriptGetter { private enum PathType diff --git a/Runtime/Resources/OfflineInputData.asset b/Runtime/Resources/OfflineInputData.asset index 5494c12..32e932e 100644 --- a/Runtime/Resources/OfflineInputData.asset +++ b/Runtime/Resources/OfflineInputData.asset @@ -14,17 +14,16 @@ 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} + inputPlayerPartialScriptFile: {fileID: 11500000, guid: e81925db712f4314a964e72efbd1a3b2, type: 3} + controlSchemeScriptFile: {fileID: 11500000, guid: af0b6eed9da99544b93d6a25a0255a20, type: 3} + inputContextScriptFile: {fileID: 11500000, guid: 90e1eb4fb9f799e4fa39382da0c021d3, type: 3} + bindingChangerPartialScriptFile: {fileID: 11500000, guid: 6c02e2df86514a9293e8b7f3a0c6ad8a, type: 3} actionsTemplateFile: {fileID: 11500000, guid: 295049b3d9040d947b3adc148595ae0a, type: 3} initializationMode: 0 - enableMultiplayer: 0 - maxPlayers: 2 defaultContext: 0 - inputContexts: - - name: Default - enableKeyboardTextInput: 0 - activeMaps: [] - eventSystemActionOverrides: [] + inputContexts: [] controlSchemeBases: [] loadAllBindingOverridesOnInitialize: 1 bindingExcludedPaths: [] diff --git a/Runtime/Scripts/Actions/ActionEventInfo.cs b/Runtime/Scripts/Actions/ActionEventInfo.cs new file mode 100644 index 0000000..bcefc9a --- /dev/null +++ b/Runtime/Scripts/Actions/ActionEventInfo.cs @@ -0,0 +1,39 @@ +using UnityEngine.InputSystem; + +namespace NPTP.InputSystemWrapper.Actions +{ + public readonly struct ActionEventInfo + { + public InputActionPhase Phase { get; } + public int PlayerID => actionWrapper.PlayerID; + + private readonly ActionWrapper actionWrapper; + + public ActionEventInfo(ActionWrapper actionWrapper, InputAction.CallbackContext callbackContext) + { + Phase = callbackContext.phase; + this.actionWrapper = actionWrapper; + } + + public T ReadValue() where T : struct + { + return actionWrapper.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 => valueActionWrapper.PlayerID; + public T Value => valueActionWrapper.ReadValue(); + + private readonly ValueActionWrapper valueActionWrapper; + + public ActionEventInfo(ValueActionWrapper valueActionWrapper, InputAction.CallbackContext callbackContext) + { + Phase = callbackContext.phase; + this.valueActionWrapper = valueActionWrapper; + } + } +} diff --git a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta b/Runtime/Scripts/Actions/ActionEventInfo.cs.meta similarity index 83% rename from Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta rename to Runtime/Scripts/Actions/ActionEventInfo.cs.meta index c6ab9c5..00b0777 100644 --- a/Editor/Scripts/ScriptContentBuilders/PlayerIDContentBuilder.cs.meta +++ b/Runtime/Scripts/Actions/ActionEventInfo.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 421af271854fba843b40295cbb372a6f +guid: 8b5708adf5949684fb5112686995a58b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Scripts/Actions/ActionReference.cs b/Runtime/Scripts/Actions/ActionReference.cs index e61d575..ae360a4 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; @@ -22,30 +31,39 @@ 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 + internal 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; } } + + public string ActionName => ActionWrapper != null ? ActionWrapper.InputAction.name : "Not found"; 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 +73,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 +93,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 +106,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..dc11bbf 100644 --- a/Runtime/Scripts/Actions/ActionWrapper.cs +++ b/Runtime/Scripts/Actions/ActionWrapper.cs @@ -14,10 +14,11 @@ namespace NPTP.InputSystemWrapper.Actions /// 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; @@ -25,25 +26,14 @@ public event Action OnEvent public bool DownThisFrame => InputAction.WasPerformedThisFrame() && (InputAction.type != InputActionType.PassThrough || !InputAction.WasReleasedThisFrame()); public bool IsDown => InputAction.phase == InputActionPhase.Performed; - - public void StartInteractiveRebind(ControlScheme controlScheme, Action callback = null) => - Input.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); - - public bool TryGetCurrentBindingInfo(out IEnumerable bindingInfos) => - Input.TryGetCurrentBindingInfo(this, CompositePart.DontIsolatePart, out bindingInfos); - - public bool TryGetCurrentBindingInfo(CompositePart compositePart, out IEnumerable bindingInfos) => - Input.TryGetCurrentBindingInfo(this, compositePart, out bindingInfos); - - public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) => - Input.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); - + + internal ActionWrapper(int playerID, InputAction inputAction, Dictionary table) + { + PlayerID = playerID; + InputAction = inputAction; + table.Add(inputAction.id, this); + } + internal void RegisterCallbacks() { InputAction.started += HandleActionEvent; @@ -57,13 +47,25 @@ internal void UnregisterCallbacks() 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); + public void StartInteractiveRebind(ControlScheme controlScheme, Action callback = null) => + ISW.StartInteractiveRebind(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), callback); + + public void StartInteractiveRebind(ControlScheme controlScheme, CompositePart compositePart, Action callback = null) => + ISW.StartInteractiveRebind(new ActionBindingInfo(this, compositePart, controlScheme), callback); + + public bool TryGetCurrentBindingInfo(out IEnumerable bindingInfos) => + ISW.TryGetCurrentBindingInfo(this, CompositePart.DontIsolatePart, out bindingInfos); + + public bool TryGetCurrentBindingInfo(CompositePart compositePart, out IEnumerable bindingInfos) => + ISW.TryGetCurrentBindingInfo(this, compositePart, out bindingInfos); + + public bool TryGetBindingInfo(ControlScheme controlScheme, out IEnumerable bindingInfos) => + ISW.TryGetBindingInfo(new ActionBindingInfo(this, CompositePart.DontIsolatePart, controlScheme), out bindingInfos); + + public bool TryGetBindingInfo(ControlScheme controlScheme, CompositePart compositePart, out IEnumerable bindingInfos) => + ISW.TryGetBindingInfo(new ActionBindingInfo(this, compositePart, controlScheme), out bindingInfos); + + private void HandleActionEvent(InputAction.CallbackContext context) => onEvent?.Invoke(new ActionEventInfo(this, 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/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/AnyButtonPress/AnyButtonPressListener.cs b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs new file mode 100644 index 0000000..f7dcd35 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs @@ -0,0 +1,6 @@ +using UnityEngine.InputSystem; + +namespace NPTP.InputSystemWrapper.AnyButtonPress +{ + public delegate void AnyButtonPressListener(InputControl inputControl); +} \ No newline at end of file diff --git a/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs.meta b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs.meta new file mode 100644 index 0000000..97b92f6 --- /dev/null +++ b/Runtime/Scripts/AnyButtonPress/AnyButtonPressListener.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc89431c7d9e4f0db3b7f95aeae576ed +timeCreated: 1761333051 \ No newline at end of file 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/Input.WaitForAnyButtonPress.cs.meta b/Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs.meta similarity index 54% rename from Runtime/Scripts/Input.WaitForAnyButtonPress.cs.meta rename to Runtime/Scripts/AnyButtonPress/WaitForAnyButtonPress.cs.meta index 391c891..84c971a 100644 --- a/Runtime/Scripts/Input.WaitForAnyButtonPress.cs.meta +++ b/Runtime/Scripts/AnyButtonPress/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/Bindings/BindingChanger.cs b/Runtime/Scripts/Bindings/BindingChanger.cs index eaf9a08..1ec2610 100644 --- a/Runtime/Scripts/Bindings/BindingChanger.cs +++ b/Runtime/Scripts/Bindings/BindingChanger.cs @@ -8,20 +8,10 @@ namespace NPTP.InputSystemWrapper.Bindings { - internal static class BindingChanger + internal static partial class BindingChanger { - private static string[] ExcludedPaths => 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) { @@ -60,7 +50,7 @@ void onComplete(RebindingOperation op) callback?.Invoke(new RebindInfo(actionWrapper, RebindInfo.Status.Completed, bindingInfos)); CleanUpRebindingOperation(ref rebindingOperation); - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } @@ -91,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)) @@ -115,7 +105,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 +119,7 @@ internal static void ResetBindingsToDefaultForControlScheme(InputActionAsset ass if (changed) { - Input.BroadcastBindingsChanged(); + ISW.BroadcastBindingsChanged(); } } @@ -140,7 +130,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..7256487 100644 --- a/Runtime/Scripts/Bindings/BindingSaveLoad.cs +++ b/Runtime/Scripts/Bindings/BindingSaveLoad.cs @@ -1,5 +1,5 @@ using System.IO; -using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Player; using NPTP.InputSystemWrapper.Utilities; using UnityEngine; using UnityEngine.InputSystem; @@ -11,9 +11,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..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 @@ -26,18 +27,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..2b83d0c 100644 --- a/Runtime/Scripts/Data/OfflineInputData.cs +++ b/Runtime/Scripts/Data/OfflineInputData.cs @@ -19,9 +19,9 @@ namespace NPTP.InputSystemWrapper.Data internal class OfflineInputData : ScriptableObject { #if UNITY_EDITOR - internal const string RUNTIME_INPUT_DATA_PATH = nameof(RuntimeInputData); - internal const int MAX_PLAYERS_LIMIT = 4; + #region Fields Hidden From User + [SerializeField] private TextAsset rootPathIdentifier; internal string AssetsPathToPackage { @@ -31,26 +31,36 @@ 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 mainInputScriptFile; - internal TextAsset MainInputScriptFile => mainInputScriptFile; + [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; - [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; @@ -93,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 037aa1d..9107f5a 100644 --- a/Runtime/Scripts/Data/RuntimeInputData.cs +++ b/Runtime/Scripts/Data/RuntimeInputData.cs @@ -1,7 +1,5 @@ -using System; -using NPTP.InputSystemWrapper.Bindings; +using System.Collections.Generic; using NPTP.InputSystemWrapper.CustomSetups; -using NPTP.InputSystemWrapper.Enums; using UnityEngine; using UnityEngine.InputSystem; @@ -11,31 +9,25 @@ 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; [SerializeField] private CustomLayout[] customLayouts; - internal CustomLayout[] CustomLayouts => customLayouts; - [SerializeField] private CustomBinding[] customBindings; - internal CustomBinding[] CustomBindings => customBindings; - [SerializeField] private CustomInteraction[] customInteractions; - internal CustomInteraction[] CustomInteractions => customInteractions; - - // MARKER.ControlSchemeBindingData.Start - // MARKER.ControlSchemeBindingData.End - - internal BindingData GetControlSchemeBindingData(ControlScheme controlScheme) + + public IEnumerable AllCustomSetups { - return controlScheme switch + get { - // MARKER.EnumToBindingDataSwitch.Start - // MARKER.EnumToBindingDataSwitch.End - _ => throw new ArgumentOutOfRangeException(nameof(controlScheme), controlScheme, null) - }; + List customSetups = new(); + customSetups.AddRange(customLayouts); + customSetups.AddRange(customBindings); + customSetups.AddRange(customInteractions); + return customSetups; + } } } -} +} \ No newline at end of file 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/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/BindingChanger.Generated.cs b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs new file mode 100644 index 0000000..eadd9e0 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/BindingChanger.Generated.cs @@ -0,0 +1,25 @@ +// ReSharper disable once CheckNamespace +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 + }; + } + } +} 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 83% rename from Runtime/Scripts/Enums/ControlScheme.cs rename to Runtime/Scripts/Generated/Partial/ControlScheme.cs index 9731e87..179461a 100644 --- a/Runtime/Scripts/Enums/ControlScheme.cs +++ b/Runtime/Scripts/Generated/Partial/ControlScheme.cs @@ -1,10 +1,16 @@ using System; using UnityEngine.InputSystem; +// ReSharper disable once CheckNamespace 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 } @@ -34,6 +40,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. @@ -49,7 +60,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) { @@ -57,13 +69,8 @@ internal static ControlScheme ToControlSchemeEnum(this string controlSchemeName) { // MARKER.StringToEnumSwitch.Start // MARKER.StringToEnumSwitch.End - _ => throw new ArgumentOutOfRangeException(nameof(controlSchemeName), controlSchemeName, null) + _ => ControlScheme.None }; } - - 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 new file mode 100644 index 0000000..3227a06 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/ISW.Generated.cs @@ -0,0 +1,34 @@ +using NPTP.InputSystemWrapper.Actions; +using NPTP.InputSystemWrapper.Bindings; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Generated.Actions; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace NPTP.InputSystemWrapper +{ + public static partial class ISW + { + // MARKER.SinglePlayerFieldsAndProperties.Start + // MARKER.SinglePlayerFieldsAndProperties.End + + // MARKER.DefaultContextProperty.Start + private static InputContext DefaultContext => 0; + // 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/Enums/InputContext.cs b/Runtime/Scripts/Generated/Partial/InputContext.cs similarity index 78% rename from Runtime/Scripts/Enums/InputContext.cs rename to Runtime/Scripts/Generated/Partial/InputContext.cs index 958ad33..33bc201 100644 --- a/Runtime/Scripts/Enums/InputContext.cs +++ b/Runtime/Scripts/Generated/Partial/InputContext.cs @@ -1,9 +1,9 @@ +// ReSharper disable once CheckNamespace 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..f72fc5a --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/InputPlayer.Generated.cs @@ -0,0 +1,71 @@ +using System; +using NPTP.InputSystemWrapper.Actions; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Generated.Actions; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.UI; + +// ReSharper disable once CheckNamespace +namespace NPTP.InputSystemWrapper.Player +{ + 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..29f5573 --- /dev/null +++ b/Runtime/Scripts/Generated/Partial/RuntimeInputData.Generated.cs @@ -0,0 +1,24 @@ +using System; +using NPTP.InputSystemWrapper.Bindings; +using NPTP.InputSystemWrapper.Enums; +using UnityEngine; + +// ReSharper disable once CheckNamespace +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/Input.cs b/Runtime/Scripts/ISW.cs similarity index 52% rename from Runtime/Scripts/Input.cs rename to Runtime/Scripts/ISW.cs index d9d168c..7907835 100644 --- a/Runtime/Scripts/Input.cs +++ b/Runtime/Scripts/ISW.cs @@ -1,20 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using NPTP.InputSystemWrapper.Actions; +using NPTP.InputSystemWrapper.AnyButtonPress; using NPTP.InputSystemWrapper.Bindings; using UnityEngine; using UnityEngine.EventSystems; 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; using NPTP.InputSystemWrapper.CustomSetups; +using NPTP.InputSystemWrapper.Player; using NPTP.InputSystemWrapper.Utilities; -using RebindingOperation = UnityEngine.InputSystem.InputActionRebindingExtensions.RebindingOperation; #if UNITY_EDITOR using UnityEditor; @@ -24,14 +22,13 @@ 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 + + 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, @@ -42,179 +39,169 @@ 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 OnAnyButtonPress + /// + /// Invoked on any button pressed on any connected device regardless of actions mapped, assets enabled, etc. + /// + public static event AnyButtonPressListener OnAnyButtonPress { - add => AddAnyButtonPressListener(value); - remove => RemoveAnyButtonPressListener(value); + add => anyButtonPressListenerCollection.Add(value); + remove => anyButtonPressListenerCollection.Remove(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; - } + // 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 OnKeyboardTextInput + private static bool allowPlayerJoining; + public static bool AllowPlayerJoining { - add => Player1.OnKeyboardTextInput += value; - remove => Player1.OnKeyboardTextInput -= value; - } + get => allowPlayerJoining; + set + { + if (value == allowPlayerJoining) + return; - public static InputContext Context - { - get => Player1.InputContext; - set => Player1.InputContext = value; + allowPlayerJoining = value; + playerCollection.SetMultiplayer(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 => 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 InputActionRebindingExtensions.RebindingOperation rebindingOperation; + private static AnyButtonPressListenerCollection anyButtonPressListenerCollection; + #endregion #region Setup - - // MARKER.Initialize.Start - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - private static void Initialize() - // MARKER.Initialize.End + + private static void InitializationProcess() { if (initialized) { 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(); - - runtimeInputData = Resources.Load(RUNTIME_INPUT_DATA_PATH); + SetUpQuittingConditions(); + + 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'?"); } - 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, HandlePlayerAdded, HandlePlayerRemoved); #if UNITY_EDITOR playerCollection.EDITOR_OnPlayerInputContextChanged += EDITOR_HandlePlayerInputContextChanged; #endif - // MARKER.LoadAllBindingsOnInitialization.Start - LoadBindingsForAllPlayers(); - // MARKER.LoadAllBindingsOnInitialization.End - - EnableContextForAllPlayers(DefaultContext); + UpdateAfterPlayerCollectionChange(); + SetUpBindings(); + SetContextForAllPlayers(DefaultContext); - anyButtonPressListeners = new HashSet>(); + anyButtonPressListenerCollection = new AnyButtonPressListenerCollection(); ++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; } - private static void SetUpTerminationConditions() + private static void SetUpQuittingConditions() { #if UNITY_EDITOR EditorApplication.playModeStateChanged -= handlePlayModeStateChanged; EditorApplication.playModeStateChanged += handlePlayModeStateChanged; void handlePlayModeStateChanged(PlayModeStateChange playModeStateChange) { - if (playModeStateChange is PlayModeStateChange.ExitingPlayMode) Terminate(); + if (playModeStateChange is PlayModeStateChange.ExitingPlayMode) + { + OnQuitting(); + } } -#else - Application.quitting -= Terminate; - Application.quitting += Terminate; #endif } - private static void Terminate() + private static void OnQuitting() { - UnregisterAllAnyButtonPressListeners(); #if UNITY_EDITOR + anyButtonPressListenerCollection.Clear(); 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; - - playerCollection.TerminateAll(); - playerCollection = null; + OnAnyPlayerInputUserChange -= BroadcastControlsUpdated; + OnBindingsChanged -= BroadcastControlsUpdated; + OnAnyPlayerControlSchemeChanged -= BroadcastControlsUpdated; + playerCollection.Terminate(); --InputUser.listenForUnpairedDeviceActivity; InputUser.onChange -= HandleInputUserChange; +#endif } #endregion #region Public Interface - // TODO (multiplayer): MP method signature which takes a PlayerID - public static bool ControlSchemeHas(ControlScheme controlScheme) where TDevice : InputDevice + public static InputPlayer Player(int playerID) { - return Player1.ControlSchemeHas(controlScheme); + return playerCollection.GetOrAdd(playerID); + } + + public static void AddPlayer(int playerID) + { + Player(playerID); + } + + public static void RemovePlayer(int playerID) + { + playerCollection.Remove(playerID); + } + + public static bool ControlSchemeHas(ControlScheme controlScheme, int playerID = 0) where TDevice : InputDevice + { + return Player(playerID).ControlSchemeHas(controlScheme); + } + + public static void SetContextForAllPlayers(InputContext inputContext) + { + 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.GetOrAdd(playerID); return player.TryGetMatchingActionWrapper(inputActionReference.action, out actionWrapper); } @@ -222,7 +209,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 +224,41 @@ 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 = null) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingChanger.ResetBindingsToDefaultForControlScheme(player.Asset, controlScheme); + if (playerID.HasValue) + BindingChanger.ResetBindingsToDefaultForControlScheme(Player(playerID.Value).Asset, controlScheme); + else foreach (InputPlayer player in playerCollection) + BindingChanger.ResetBindingsToDefaultForControlScheme(player.Asset, controlScheme); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void LoadAllBindings() + public static void LoadAllBindings(int? playerID = null) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); + if (playerID.HasValue) + BindingSaveLoad.LoadBindingsFromDiskForPlayer(Player(playerID.Value)); + else foreach (InputPlayer player in playerCollection) + BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); } - // TODO (multiplayer): MP method signature which takes a PlayerID - public static void SaveAllBindings() + public static void SaveAllBindings(int? playerID = null) { - // MARKER.PlayerGetter.Start - InputPlayer player = Player1; - // MARKER.PlayerGetter.End - BindingSaveLoad.SaveBindingsToDiskForPlayer(player); + if (playerID.HasValue) + BindingSaveLoad.SaveBindingsToDiskForPlayer(Player(playerID.Value)); + else foreach (InputPlayer player in playerCollection) + BindingSaveLoad.SaveBindingsToDiskForPlayer(player); } - // 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); + if (playerID.HasValue) + BindingChanger.ResetBindingsToDefault(Player(playerID.Value).Asset); + else foreach (InputPlayer player in playerCollection) + BindingChanger.ResetBindingsToDefault(player.Asset); } #endregion @@ -288,9 +275,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(); @@ -326,7 +312,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; @@ -341,86 +327,58 @@ 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 - // MARKER.EnableContextForAllPlayersSignature.Start - private static void EnableContextForAllPlayers(InputContext inputContext) - // MARKER.EnableContextForAllPlayersSignature.End + internal static bool DoesPlayerExist(int playerID) { - playerCollection.EnableContextForAll(inputContext); + return playerCollection.TryGetPlayer(playerID, out _); } - private static InputPlayer GetPlayer(PlayerID id) + #endregion + + #region Private Runtime Functionality + + private static void HandleAnyPlayerInputUserChange(InputUserChangeInfo inputUserChangeInfo) { - return playerCollection[id]; + OnAnyPlayerInputUserChange?.Invoke(inputUserChangeInfo); } - private static void AddAnyButtonPressListener(Action action) + private static void HandleAnyPlayerControlSchemeChanged(InputPlayer inputPlayer) { - if (action == null || anyButtonPressListeners.Contains(action)) - return; - anyButtonPressListeners.Add(action); - if (anyButtonPressCaller == null) - anyButtonPressCaller = InputSystem.onAnyButtonPress.Call(HandleAnyButtonPressed); + OnAnyPlayerControlSchemeChanged?.Invoke(inputPlayer); } - private static void RemoveAnyButtonPressListener(Action value) + private static void HandleAnyPlayerKeyboardTextInput(char c) { - if (value == null || !anyButtonPressListeners.Contains(value)) - return; - anyButtonPressListeners.Remove(value); - DisposeAnyButtonPressCallerIfNoListeners(); + OnAnyPlayerKeyboardTextInput?.Invoke(c); } - private static void DisposeAnyButtonPressCallerIfNoListeners() - { - if (anyButtonPressListeners.Count == 0 && anyButtonPressCaller != null) - { - anyButtonPressCaller.Dispose(); - anyButtonPressCaller = null; - } - } - - private static void UnregisterAllAnyButtonPressListeners() - { - anyButtonPressListeners.Clear(); - DisposeAnyButtonPressCallerIfNoListeners(); - } - - private static void HandleAnyButtonPressed(InputControl inputControl) + private static void HandlePlayerAdded(InputPlayer inputPlayer) => UpdateAfterPlayerCollectionChange(); + private static void HandlePlayerRemoved(int playerID) => UpdateAfterPlayerCollectionChange(); + + private static void UpdateAfterPlayerCollectionChange() { - InvokeAnyButtonPressListeners(inputControl); - - // Player joining is always disallowed in SinglePlayer mode. - if (!AllowPlayerJoining) + foreach (InputPlayer player in playerCollection) { - return; + player.OnInputUserChange -= HandleAnyPlayerInputUserChange; + player.OnInputUserChange += HandleAnyPlayerInputUserChange; + + player.OnControlSchemeChanged -= HandleAnyPlayerControlSchemeChanged; + player.OnControlSchemeChanged += HandleAnyPlayerControlSchemeChanged; + + player.OnKeyboardTextInput -= HandleAnyPlayerKeyboardTextInput; + player.OnKeyboardTextInput += HandleAnyPlayerKeyboardTextInput; } - - JoinPlayerByActivatedInputControl(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 (PlayerID playerID in Enum.GetValues(typeof(PlayerID))) + foreach (InputPlayer player in playerCollection) { - BindingSaveLoad.LoadBindingsFromDiskForPlayer(GetPlayer(playerID)); + BindingSaveLoad.LoadBindingsFromDiskForPlayer(player); } } @@ -428,22 +386,44 @@ 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) + { + 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. + // 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) @@ -455,10 +435,23 @@ 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; + + 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/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/Input.WaitForAnyButtonPress.cs b/Runtime/Scripts/Input.WaitForAnyButtonPress.cs deleted file mode 100644 index afb39b8..0000000 --- a/Runtime/Scripts/Input.WaitForAnyButtonPress.cs +++ /dev/null @@ -1,70 +0,0 @@ -using UnityEngine; -using UnityEngine.InputSystem; - -namespace NPTP.InputSystemWrapper -{ - public static partial class Input - { - /// - /// Custom yield instruction for coroutines to make waiting for any button press a lot more syntactically convenient. - /// Use like: - /// yield return new Input.WaitForAnyButtonPress(); - /// - public class WaitForAnyButtonPress : CustomYieldInstruction - { - public override bool keepWaiting - { - get - { - if (anyButtonPressed) - { - ResetYieldInstruction(); - return false; - } - - if (!ListeningForAnyButtonPress) - { - ListeningForAnyButtonPress = true; - } - - return !anyButtonPressed; - } - } - - private bool listeningForAnyButtonPress; - private bool ListeningForAnyButtonPress - { - get => listeningForAnyButtonPress; - set - { - if (listeningForAnyButtonPress == value) - return; - - if (value) OnAnyButtonPress += HandleAnyButtonPress; - else OnAnyButtonPress -= HandleAnyButtonPress; - listeningForAnyButtonPress = value; - } - } - - private bool anyButtonPressed; - - ~WaitForAnyButtonPress() => OnAnyButtonPress -= HandleAnyButtonPress; - - public WaitForAnyButtonPress() - { - 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/InputPlayerCollection.cs b/Runtime/Scripts/InputPlayerCollection.cs deleted file mode 100644 index ff3d9e0..0000000 --- a/Runtime/Scripts/InputPlayerCollection.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPTP.InputSystemWrapper.Utilities.Extensions; -using NPTP.InputSystemWrapper.Enums; -using UnityEngine; -using UnityEngine.InputSystem; -using UnityEngine.InputSystem.Users; - -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 IEnumerable Players => players; - internal InputPlayer this[PlayerID id] => players[(int)id]; - internal int Count => players.Length; - - private readonly InputPlayer[] players; - - #region Internal - - internal InputPlayerCollection(InputActionAsset asset, int size) - { - Transform parent = CreateInputParentInScene(); - bool isMultiplayer = size > 1; - - players = new InputPlayer[size]; - for (int i = 0; i < size; i++) - { - PlayerID id = (PlayerID)i; - InputPlayer newPlayer = new(asset, id, isMultiplayer, parent); - players[i] = newPlayer; - } - - // 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++) - { - InputPlayer player = players[i]; - player.OnEnabledOrDisabled += HandlePlayerEnabledOrDisabled; - player.Enabled = player.ID == PlayerID.Player1; -#if UNITY_EDITOR - player.EDITOR_OnInputContextChanged += EDITOR_HandlePlayerInputContextChanged; -#endif - } - } - - internal void TerminateAll() - { - players.ForEach(p => - { -#if UNITY_EDITOR - p.EDITOR_OnInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; -#endif - p.OnEnabledOrDisabled -= HandlePlayerEnabledOrDisabled; - p.Terminate(); - }); - - players.DefaultAll(); - } - - internal bool IsDeviceLastUsedByAnyPlayer(InputDevice device) - { - for (int i = 0; i < players.Length; i++) - { - if (players[i].LastUsedDevice == device) - { - return true; - } - } - - return false; - } - - internal bool AnyPlayerDisabled() - { - for (int i = 0; i < players.Length; i++) - { - InputPlayer player = players[i]; - if (!player.Enabled) return true; - } - - return false; - } - - internal bool TryGetPlayerPairedWithDevice(InputDevice device, out InputPlayer player) - { - for (int i = 0; i < players.Length; i++) - { - if (players[i].IsDevicePaired(device)) - { - player = players[i]; - return true; - } - } - - player = null; - return false; - } - - // 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) - { - for (int i = 0; i < players.Length; i++) - { - InputPlayer player = players[i]; - if (player.Asset == asset) - { - playerAssociatedWithAsset = player; - return true; - } - } - - playerAssociatedWithAsset = null; - return false; - } - - internal bool TryPairDeviceToFirstDisabledPlayer(InputDevice device, out InputPlayer pairedPlayer) - { - for (int i = 0; i < players.Length; i++) - { - InputPlayer player = players[i]; - if (player.Enabled) - { - continue; - } - - player.PairDevice(device); - pairedPlayer = player; - return true; - } - - pairedPlayer = null; - return false; - } - - internal void HandleInputUserChange(InputUser inputUser, InputUserChange inputUserChange, InputDevice inputDevice) - { - for (int i = 0; i < players.Length; i++) - { - InputPlayer player = players[i]; - if (player.IsUser(inputUser)) - { - player.HandleInputUserChange(inputUserChange, inputDevice); - break; - } - } - } - - internal void EnableContextForAll(InputContext inputContext) - { - players.ForEach(p => p.InputContext = inputContext); - } - - #endregion - - #region Private - - private Transform CreateInputParentInScene() - { - GameObject inputParentGameObject = new() { name = "InputPlayers", transform = { position = Vector3.zero } }; - UnityEngine.Object.DontDestroyOnLoad(inputParentGameObject); - Transform parent = inputParentGameObject.transform; - return parent; - } - - private void HandlePlayerEnabledOrDisabled(InputPlayer enabledOrDisabledPlayer) - { - // If the player is disabled, unpair all their devices to make them available to other players. - if (!enabledOrDisabledPlayer.Enabled) - { - enabledOrDisabledPlayer.UnpairDevices(); - } - - int enabledPlayersCount = players.Count(player => player.Enabled); - if (enabledPlayersCount > 1) - { - players.ForEach(p => p.EnableAutoSwitching(false)); - } - 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); - } - } - - #endregion - - #region Editor-Only Debug -#if UNITY_EDITOR - internal event Action EDITOR_OnPlayerInputContextChanged; - - private void EDITOR_HandlePlayerInputContextChanged(InputPlayer inputPlayer) - { - EDITOR_OnPlayerInputContextChanged?.Invoke(inputPlayer); - } -#endif - #endregion - } -} \ No newline at end of file diff --git a/Runtime/Scripts/InputUserChangeInfo.cs b/Runtime/Scripts/InputUserChangeInfo.cs deleted file mode 100644 index 01c4450..0000000 --- a/Runtime/Scripts/InputUserChangeInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using NPTP.InputSystemWrapper.Enums; -using UnityEngine.InputSystem.Users; - -namespace NPTP.InputSystemWrapper -{ - public struct InputUserChangeInfo - { - 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) - { - ControlScheme = inputPlayer.CurrentControlScheme; - InputUserChange = inputUserChange; - - // MARKER.PlayerIDConstructor.Start - // MARKER.PlayerIDConstructor.End - } - } -} 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 79% rename from Runtime/Scripts/InputPlayer.cs rename to Runtime/Scripts/Player/InputPlayer.cs index c29d2d7..abe9d9d 100644 --- a/Runtime/Scripts/InputPlayer.cs +++ b/Runtime/Scripts/Player/InputPlayer.cs @@ -1,20 +1,20 @@ 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 class InputPlayer + public sealed partial class InputPlayer { #region Field & Properties @@ -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. @@ -36,13 +36,23 @@ 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 { get => enabled; - internal set + set { - if (playerInput == null) + if (playerInput == null || enabled == value) { return; } @@ -72,11 +82,21 @@ public InputContext InputContext } } - public PlayerID ID { get; } - public ControlScheme CurrentControlScheme { get; private set; } - - // MARKER.ActionsProperties.Start - // MARKER.ActionsProperties.End + public int ID { get; } + + private ControlScheme currentControlScheme; + public ControlScheme CurrentControlScheme + { + get => currentControlScheme; + private set + { + if (CurrentControlScheme == value) + return; + + currentControlScheme = value; + OnControlSchemeChanged?.Invoke(this); + } + } private InputDevice lastUsedDevice; internal InputDevice LastUsedDevice @@ -87,6 +107,17 @@ internal InputDevice LastUsedDevice return lastUsedDevice; } } + + internal bool IsMultiplayer + { + set + { + if (playerInput != null) + { + playerInput.neverAutoSwitchControlSchemes = value; + } + } + } internal InputActionAsset Asset { get; } @@ -99,6 +130,7 @@ internal InputDevice LastUsedDevice private PlayerInput playerInput; private InputSystemUIInputModule uiInputModule; private bool keyboardTextInputEnabled; + private SpecificPlayerAnyButtonPressListenerCollection anyButtonPressListenerCollection; // Event System actions private readonly Dictionary eventSystemActionsPool = new(); @@ -117,26 +149,12 @@ internal InputDevice LastUsedDevice #region Setup & Teardown - internal InputPlayer(InputActionAsset asset, PlayerID 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 Input class after this instantiation, which sets up maps & event system actions/overrides, so we don't have to handle that here. - } - internal void Terminate() { - Asset.Disable(); + Enabled = false; + anyButtonPressListenerCollection?.Clear(); DisableKeyboardTextInput(); DisableAllMapsAndRemoveCallbacks(); - Object.Destroy(playerInputGameObject); } private InputActionAsset InstantiateNewActions(InputActionAsset actions) @@ -163,51 +181,30 @@ private void SetUpInputPlayerGameObject(bool isMultiplayer, Transform parent) playerInputGameObject = new GameObject { - name = $"{ID}Input", + name = $"Player[{ID}]Input", transform = { position = Vector3.zero, parent = parent} }; playerInput = playerInputGameObject.AddComponent(); playerInput.neverAutoSwitchControlSchemes = isMultiplayer; - - if (isMultiplayer) - playerInputGameObject.AddComponent(); - else - playerInputGameObject.AddComponent(); - + + playerInputGameObject.AddComponent(); uiInputModule = playerInputGameObject.AddComponent(); uiInputModule.actionsAsset = Asset; SetEventSystemOptions(); playerInput.actions = Asset; playerInput.uiInputModule = uiInputModule; - playerInput.notificationBehavior = PlayerNotifications.InvokeCSharpEvents; + + // 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; // 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(); } - 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; @@ -301,16 +298,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. @@ -331,12 +318,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(CurrentControlScheme); - } break; } @@ -356,7 +338,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) @@ -418,40 +415,17 @@ 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 #region Editor-Only Debug #if UNITY_EDITOR + // ReSharper disable once InconsistentNaming internal event Action EDITOR_OnInputContextChanged; #endif #endregion 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/Player/InputPlayerCollection.cs b/Runtime/Scripts/Player/InputPlayerCollection.cs new file mode 100644 index 0000000..4fea900 --- /dev/null +++ b/Runtime/Scripts/Player/InputPlayerCollection.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NPTP.InputSystemWrapper.Enums; +using NPTP.InputSystemWrapper.Utilities.Extensions; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Users; + +namespace NPTP.InputSystemWrapper.Player +{ + /// + /// Useful interface layer for dealing with a collection of multiple players. + /// + internal sealed class InputPlayerCollection : IEnumerable + { + private const int DEFAULT_PLAYER_PLAYER_ID = 0; + + internal InputPlayer DefaultPlayer { get; } + + private IEnumerable Players => players.Where(player => player != null); + + 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; + } + + public IEnumerator GetEnumerator() => Players.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #region Internal Methods + + internal InputPlayer GetOrAdd(int playerID) + { + if (playerID >= players.Length) + { + InputPlayer[] extended = new InputPlayer[playerID + 1]; + Array.Copy(players, extended, players.Length); + players = extended; + } + else if (players[playerID] != null) + { + return players[playerID]; + } + + InputPlayer newPlayer = new InputPlayer(inputActionAsset, playerID, true, inputParent); + players[playerID] = newPlayer; + newPlayer.OnEnabledOrDisabled += HandlePlayerEnabledOrDisabled; + newPlayer.Enabled = true; +#if UNITY_EDITOR + newPlayer.EDITOR_OnInputContextChanged += EDITOR_HandlePlayerInputContextChanged; +#endif + + onPlayerAdded?.Invoke(newPlayer); + return newPlayer; + } + + internal void Remove(int playerID) + { + if (playerID <= DEFAULT_PLAYER_PLAYER_ID) + { + Debug.LogError($"Cannot remove the default player or get a player with ID < {DEFAULT_PLAYER_PLAYER_ID}."); + return; + } + + if (playerID >= players.Length || players[playerID] == null) + { + return; + } + + players[playerID].Terminate(); + players[playerID] = null; + + onPlayerRemoved?.Invoke(playerID); + } + + internal void Terminate() + { + foreach (InputPlayer player in Players) + { + player.OnEnabledOrDisabled -= HandlePlayerEnabledOrDisabled; + player.Terminate(); +#if UNITY_EDITOR + player.EDITOR_OnInputContextChanged -= EDITOR_HandlePlayerInputContextChanged; +#endif + } + + onPlayerAdded = null; + onPlayerRemoved = null; + } + + public void SetMultiplayer(bool isMultiplayer) + { + foreach (InputPlayer player in Players) + { + player.IsMultiplayer = isMultiplayer; + } + } + + internal bool IsDeviceLastUsedByAnyPlayer(InputDevice device) + { + return Players.Any(player => player.LastUsedDevice == device); + } + + internal bool AnyPlayerDisabled() + { + return Players.Any(player => !player.Enabled); + } + + internal bool TryGetPlayer(int playerID, out InputPlayer player) + { + if (!players.IndexIsValid(playerID) || players[playerID] == null) + { + player = default; + return false; + } + + player = players[playerID]; + return true; + } + + internal bool TryGetPlayerPairedWithDevice(InputDevice device, out InputPlayer pairedPlayer) + { + foreach (var player in Players) + { + if (player.IsDevicePaired(device)) + { + pairedPlayer = player; + return true; + } + } + + pairedPlayer = null; + return false; + } + + internal bool TryPairDeviceToFirstDisabledPlayer(InputDevice device, out InputPlayer pairedPlayer) + { + foreach (var player in Players) + { + if (player.Enabled) + { + continue; + } + + player.PairDevice(device); + pairedPlayer = player; + return true; + } + + pairedPlayer = null; + return false; + } + + internal void PairDeviceToNewPlayer(InputDevice device) + { + AddFirstPossiblePlayerID().PairDevice(device); + } + + internal void HandleInputUserChange(InputUser inputUser, InputUserChange inputUserChange, InputDevice inputDevice) + { + foreach (InputPlayer player in Players) + { + if (player.IsUser(inputUser)) + { + player.HandleInputUserChange(inputUserChange, inputDevice); + break; + } + } + } + + internal void SetContextForAll(InputContext inputContext) + { + foreach (InputPlayer player in Players) + { + player.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() + { + GameObject inputParentGameObject = new() { name = "InputPlayers", transform = { position = Vector3.zero } }; + UnityEngine.Object.DontDestroyOnLoad(inputParentGameObject); + Transform parent = inputParentGameObject.transform; + return parent; + } + + private void HandlePlayerEnabledOrDisabled(InputPlayer enabledOrDisabledPlayer) + { + // If the player is disabled, unpair all their devices to make them available to other players. + if (!enabledOrDisabledPlayer.Enabled) + { + enabledOrDisabledPlayer.UnpairDevices(); + } + + int enabledPlayersCount = Players.Count(player => player.Enabled); + + if (enabledPlayersCount > 1) + { + foreach (InputPlayer player in Players) + { + player.IsMultiplayer = true; + } + } + else if (enabledPlayersCount == 1) + { + InputPlayer soleEnabledPlayer = Players.First(player => player.Enabled); + soleEnabledPlayer.IsMultiplayer = false; + } + } + + #endregion + + #region Editor-Only Debug Fields/Properties/Methods +#if UNITY_EDITOR + internal event Action EDITOR_OnPlayerInputContextChanged; + + private void EDITOR_HandlePlayerInputContextChanged(InputPlayer inputPlayer) + { + EDITOR_OnPlayerInputContextChanged?.Invoke(inputPlayer); + } +#endif + #endregion + } +} \ No newline at end of file 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/Player/InputUserChangeInfo.cs b/Runtime/Scripts/Player/InputUserChangeInfo.cs new file mode 100644 index 0000000..5a9dbd0 --- /dev/null +++ b/Runtime/Scripts/Player/InputUserChangeInfo.cs @@ -0,0 +1,19 @@ +using NPTP.InputSystemWrapper.Enums; +using UnityEngine.InputSystem.Users; + +namespace NPTP.InputSystemWrapper.Player +{ + public struct InputUserChangeInfo + { + public InputPlayer Player { get; } + public ControlScheme ControlScheme { get; } + public InputUserChange InputUserChange { get; } + + internal InputUserChangeInfo(InputPlayer inputPlayer, InputUserChange inputUserChange) + { + Player = inputPlayer; + ControlScheme = inputPlayer.CurrentControlScheme; + InputUserChange = inputUserChange; + } + } +} 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/Templates/ActionsTemplate.cs b/Runtime/Scripts/Templates/ActionsTemplate.cs index bd6ffb9..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 @@ -31,7 +32,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 +40,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/AnyButtonPressListenerCollection.cs b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs new file mode 100644 index 0000000..93561e7 --- /dev/null +++ b/Runtime/Scripts/Utilities/AnyButtonPressListenerCollection.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPTP.InputSystemWrapper.AnyButtonPress; +using NPTP.InputSystemWrapper.Player; +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 diff --git a/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs b/Runtime/Scripts/Utilities/Extensions/ArrayExtensions.cs index d2e5a24..5411c75 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; @@ -29,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 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 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