158 lines
6.2 KiB
C#
158 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.InputSystem.Utilities;
|
|
|
|
namespace InputGlyphsFramework
|
|
{
|
|
/// <summary>
|
|
/// Handles interactive rebinding and conflict resolution for keyboard mappings.
|
|
/// Conflicts are detected within configured scopes (e.g. Gameplay vs UI) and the conflicting binding(s)
|
|
/// will be cleared (erased) while keeping the new mapping.
|
|
/// </summary>
|
|
[DefaultExecutionOrder(-150)]
|
|
public class RebindManager : MonoBehaviour
|
|
{
|
|
public static RebindManager Instance { get; private set; }
|
|
|
|
[Tooltip("Action map names treated as Gameplay scope (conflicts resolved inside this group)")]
|
|
public string[] gameplayMapNames = new string[] { "Player", "Other" };
|
|
|
|
[Tooltip("Action map names treated as UI scope")]
|
|
public string[] uiMapNames = new string[] { "UI" };
|
|
|
|
[Header("Rebind Options")]
|
|
public bool excludeMousePosition = true;
|
|
public string cancelControl = "<Keyboard>/escape";
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this) { Destroy(this); return; }
|
|
Instance = this;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start interactive rebind on given action & bindingIndex. scope chooses which maps to treat as same-scope
|
|
/// for conflict detection. Use scope = "Gameplay" or "UI".
|
|
/// </summary>
|
|
public void StartInteractiveRebind(InputActionReference actionRef, int bindingIndex, string scope = "Gameplay", Action onComplete = null)
|
|
{
|
|
if (actionRef == null || actionRef.action == null) return;
|
|
var action = actionRef.action;
|
|
|
|
if (bindingIndex < 0 || bindingIndex >= action.bindings.Count)
|
|
{
|
|
Debug.LogWarning("RebindManager: bindingIndex out of range");
|
|
return;
|
|
}
|
|
|
|
var rebind = action.PerformInteractiveRebinding(bindingIndex)
|
|
.WithCancelingThrough(cancelControl)
|
|
.OnComplete(op =>
|
|
{
|
|
try
|
|
{
|
|
var newPath = action.bindings[bindingIndex].effectivePath; // may be set via override
|
|
|
|
// Persist override for this binding
|
|
action.ApplyBindingOverride(bindingIndex, newPath);
|
|
|
|
// Resolve conflicts inside scope (keyboard only)
|
|
ResolveConflicts(action, bindingIndex, newPath, scope);
|
|
|
|
// Save using InputService
|
|
if (InputService.Instance != null) InputService.Instance.SaveOverridesToPrefs();
|
|
|
|
// Notify
|
|
if (InputService.Instance != null) InputService.Instance.NotifyBindingChanged(action, bindingIndex);
|
|
|
|
onComplete?.Invoke();
|
|
}
|
|
finally
|
|
{
|
|
op.Dispose();
|
|
}
|
|
});
|
|
|
|
if (excludeMousePosition)
|
|
rebind.WithControlsExcluding("<Mouse>/position");
|
|
|
|
rebind.Start();
|
|
}
|
|
|
|
private IEnumerable<string> GetMapsForScope(string scope)
|
|
{
|
|
if (string.Equals(scope, "UI", StringComparison.OrdinalIgnoreCase)) return uiMapNames;
|
|
return gameplayMapNames;
|
|
}
|
|
|
|
private void ResolveConflicts(InputAction changedAction, int changedBindingIndex, string newPath, string scope)
|
|
{
|
|
if (string.IsNullOrEmpty(newPath)) return;
|
|
|
|
// only care about keyboard rebinding conflicts
|
|
if (!newPath.ToLowerInvariant().Contains("keyboard")) return;
|
|
|
|
var maps = GetMapsForScope(scope);
|
|
if (maps == null || maps.Count() == 0) return;
|
|
|
|
if (InputService.Instance == null || InputService.Instance.playerInput == null) return;
|
|
|
|
var asset = InputService.Instance.playerInput.actions;
|
|
var actionsToCheck = new List<(InputAction action, int bindingIndex)>();
|
|
|
|
// Gather candidates: all bindings in maps matching scope
|
|
foreach (var map in asset.actionMaps)
|
|
{
|
|
if (!maps.Contains(map.name)) continue;
|
|
foreach (var action in map.actions)
|
|
{
|
|
for (int i = 0; i < action.bindings.Count; ++i)
|
|
{
|
|
var b = action.bindings[i];
|
|
// ignore the binding we just changed on the changedAction
|
|
if (action == changedAction && i == changedBindingIndex) continue;
|
|
|
|
// consider only keyboard bindings (or empty path) for conflict
|
|
var path = !string.IsNullOrEmpty(b.effectivePath) ? b.effectivePath : b.path;
|
|
if (string.IsNullOrEmpty(path)) continue;
|
|
|
|
if (path.Equals(newPath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
actionsToCheck.Add((action, i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (actionsToCheck.Count == 0) return;
|
|
|
|
// Log conflicts, then clear conflicting bindings (erase)
|
|
foreach (var item in actionsToCheck)
|
|
{
|
|
Debug.LogWarning($"Rebind conflict detected: New binding '{newPath}' for '{changedAction.name}' conflicts with '{item.action.name}' binding index {item.bindingIndex} (will be cleared)");
|
|
|
|
try
|
|
{
|
|
// Erase the conflicting binding entry
|
|
item.action.ChangeBinding(item.bindingIndex).Erase();
|
|
|
|
// Notify for the cleared action
|
|
if (InputService.Instance != null) InputService.Instance.NotifyBindingChanged(item.action, item.bindingIndex);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning("RebindManager: Failed to erase conflicting binding: " + e.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|