多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
namespace BaseGames.Input
{
/// <summary>
/// 输入冲突检测器(架构 04_InputModule §6
/// 扫描给定 Action 集合,返回所有绑定了相同按键路径的 Action 名称。
/// 挂载在与 RebindPanel 同一 GameObject 上(或作为子组件)。
/// </summary>
public class ConflictDetector : MonoBehaviour
{
/// <summary>
/// 返回存在冲突的 Action 名称集合。
/// 两个 Action 绑定了同一 effectivePath → 互为冲突。
/// </summary>
/// <param name="actions">待扫描的 Action 序列(通常来自 Gameplay Map。</param>
public HashSet<string> FindConflicts(IEnumerable<InputAction> actions)
{
var pathToActions = new Dictionary<string, List<string>>();
foreach (var action in actions)
{
foreach (var binding in action.bindings)
{
// 跳过复合绑定父项(如 WASD 组合中的 "2DVector"
if (binding.isComposite || string.IsNullOrEmpty(binding.effectivePath))
continue;
if (!pathToActions.TryGetValue(binding.effectivePath, out var list))
pathToActions[binding.effectivePath] = list = new List<string>();
list.Add(action.name);
}
}
var conflicted = new HashSet<string>();
foreach (var kv in pathToActions)
if (kv.Value.Count > 1)
foreach (var name in kv.Value)
conflicted.Add(name);
return conflicted;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ead9c6110166b424ab45c55ce99801c3
guid: 9125920fa11ef0b4abdeeb7a623cfdb6
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,56 @@
using UnityEngine;
namespace BaseGames.Input
{
/// <summary>
/// 在运行时启用 InputReaderSO 的 ActionMap。
/// 挂在 Persistent 场景的 InputReaderHolder 上。
/// </summary>
public sealed class InputReaderBootstrap : MonoBehaviour
{
[SerializeField] private InputReaderSO _inputReader;
private void OnEnable()
{
if (_inputReader == null)
{
_inputReader = FindDefaultInputReader();
if (_inputReader == null)
Debug.LogError("[InputReaderBootstrap] Could not find InputReaderSO by name or assignment!");
}
}
private void Start()
{
if (_inputReader != null)
{
_inputReader.LoadBindingOverrides(); // 从 PlayerPrefs 恢复用户自定义绑定
_inputReader.EnableGameplayInput();
}
else
{
Debug.LogError("[InputReaderBootstrap.Start] _inputReader is NULL!");
}
}
private void OnDisable()
{
_inputReader?.DisableAllInput();
}
private static InputReaderSO FindDefaultInputReader()
{
InputReaderSO[] readers = Resources.FindObjectsOfTypeAll<InputReaderSO>();
foreach (InputReaderSO reader in readers)
{
if (reader != null && reader.name == "InputReader")
return reader;
}
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec65fc21755ac2943ba5e0b526601b31
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using BaseGames.Core.Events;
@@ -29,6 +30,7 @@ namespace BaseGames.Input
public event Action SpiritSkill1CancelledEvent;
public event Action SpiritSkill2StartedEvent;
public event Action SpiritSkill2CancelledEvent;
public event Action SpellCastEvent;
public event Action InteractEvent;
// ── UI Events ─────────────────────────────────────────────────────────
@@ -46,12 +48,45 @@ namespace BaseGames.Input
private InputActionMap _ui;
private bool _isBound;
private void EnsureInitialized()
{
if (_inputActions == null)
{
Debug.LogError("[InputReaderSO] _inputActions is NULL! Asset not assigned in inspector?");
return;
}
// Always disable first so the Input System tears down stale state,
// then re-enable to create a fresh state for this Play session.
_inputActions.Disable();
_inputActions.Enable();
_gameplay = _inputActions.FindActionMap("Gameplay", throwIfNotFound: false);
if (_gameplay == null)
Debug.LogError("[InputReaderSO] Could not find 'Gameplay' action map in asset!");
_ui = _inputActions.FindActionMap("UI", throwIfNotFound: false);
if (_ui == null)
Debug.LogWarning("[InputReaderSO] Could not find 'UI' action map in asset!");
if (_gameplay != null && !_isBound)
{
BindActions();
}
}
private void OnEnable()
{
if (_inputActions == null) return;
_gameplay = _inputActions.FindActionMap("Gameplay", throwIfNotFound: false);
_ui = _inputActions.FindActionMap("UI", throwIfNotFound: false);
BindActions();
// Reset private state on every OnEnable so stale ScriptableObject
// references from a previous Play session don't cause
// 'Map must be contained in state' errors.
_gameplay = null;
_ui = null;
_isBound = false;
EnsureInitialized();
}
private void OnDisable()
@@ -62,14 +97,60 @@ namespace BaseGames.Input
}
// ── Action Map Switching ──────────────────────────────────────────────
public void EnableGameplayInput() { _ui?.Disable(); _gameplay?.Enable(); }
public void EnableUIInput() { _gameplay?.Disable(); _ui?.Enable(); }
public void EnableGameplayInput()
{
// Disable UI map if it's active
if (_ui != null && _ui.enabled)
{
_ui.Disable();
}
// Ensure gameplay map is enabled
if (_gameplay != null && !_gameplay.enabled)
{
_gameplay.Enable();
}
else if (_gameplay == null)
{
Debug.LogError("[InputReaderSO.EnableGameplayInput] _gameplay is NULL!");
}
}
public void EnableUIInput()
{
// Disable gameplay map if it's active
if (_gameplay != null && _gameplay.enabled)
{
_gameplay.Disable();
}
// Ensure UI map is enabled
if (_ui != null && !_ui.enabled)
{
_ui.Enable();
}
else if (_ui == null)
{
Debug.LogWarning("[InputReaderSO.EnableUIInput] _ui is NULL!");
}
}
public void DisableAllInput() { _gameplay?.Disable(); _ui?.Disable(); }
// ── Binding ───────────────────────────────────────────────────────────
private void BindActions()
{
if (_gameplay == null || _isBound) return;
if (_gameplay == null)
{
Debug.LogWarning("[InputReaderSO.BindActions] Skipped: _gameplay is NULL");
return;
}
BindPerformed(_gameplay, "Move", ctx =>
{
@@ -98,7 +179,10 @@ namespace BaseGames.Input
BindCanceled(_gameplay, "SpiritSkill1", () => SpiritSkill1CancelledEvent?.Invoke());
BindStarted(_gameplay, "SpiritSkill2", () => SpiritSkill2StartedEvent?.Invoke());
BindCanceled(_gameplay, "SpiritSkill2", () => SpiritSkill2CancelledEvent?.Invoke());
BindStarted(_gameplay, "Spell", () => SpellCastEvent?.Invoke());
BindStarted(_gameplay, "Interact", () => InteractEvent?.Invoke());
BindStarted(_gameplay, "Pause", HandlePause);
if (_ui != null)
@@ -116,14 +200,48 @@ namespace BaseGames.Input
private void HandlePause()
{
if (_onPauseRequested == null)
{
_onPauseRequested = FindPauseChannelByName();
if (_onPauseRequested == null)
Debug.LogError("[InputReaderSO.HandlePause] Could not find EVT_PauseRequested asset!");
}
PauseEvent?.Invoke();
_onPauseRequested?.Raise();
}
private static VoidEventChannelSO FindPauseChannelByName()
{
VoidEventChannelSO[] channels = Resources.FindObjectsOfTypeAll<VoidEventChannelSO>();
foreach (VoidEventChannelSO channel in channels)
{
if (channel != null && channel.name == "EVT_PauseRequested")
return channel;
}
return null;
}
private static void BindStarted(InputActionMap map, string name, Action callback)
{
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null) action.started += _ => callback();
if (action != null)
{
action.started += _ => callback();
}
else
{
Debug.LogWarning($"[BindStarted] Action '{name}' not found in map");
}
}
private static void BindPerformed(InputActionMap map, string name,
@@ -145,5 +263,59 @@ namespace BaseGames.Input
var action = map.FindAction(name, throwIfNotFound: false);
if (action != null) action.canceled += _ => callback();
}
// ── Rebinding API (P4-3) ──────────────────────────────────────────────
private const string PrefKey = "InputBindings";
/// <summary>查找单个 Action供 RebindActionRow 显示当前绑定路径)。</summary>
public InputAction FindAction(string name)
=> _inputActions?.FindAction(name);
/// <summary>返回 Gameplay Map 所有 Action供 ConflictDetector 扫描冲突)。</summary>
public IEnumerable<InputAction> GetAllActionMap()
{
var map = _inputActions?.FindActionMap("Gameplay");
return map != null ? (IEnumerable<InputAction>)map.actions : Array.Empty<InputAction>();
}
/// <summary>
/// 启动交互式重绑定Unity InputSystem RebindingOperation
/// 调用方无需持有返回值;完成或取消后自动 Dispose。
/// </summary>
public void StartRebinding(
string actionName, int bindingIndex, Action onComplete, Action onCancel)
{
var action = _inputActions?.FindAction(actionName);
if (action == null) { onCancel?.Invoke(); return; }
action.Disable();
action.PerformInteractiveRebinding(bindingIndex)
.OnComplete(op => { op.Dispose(); action.Enable(); onComplete?.Invoke(); })
.OnCancel(op => { op.Dispose(); action.Enable(); onCancel?.Invoke(); })
.Start();
}
/// <summary>将当前绑定覆盖序列化为 JSON存入 PlayerPrefskey = "InputBindings")。</summary>
public void SaveBindingOverrides()
{
if (_inputActions == null) return;
PlayerPrefs.SetString(PrefKey, _inputActions.SaveBindingOverridesAsJson());
PlayerPrefs.Save();
}
/// <summary>从 PlayerPrefs 加载并应用绑定覆盖(首次启动时无操作)。</summary>
public void LoadBindingOverrides()
{
if (_inputActions != null && PlayerPrefs.HasKey(PrefKey))
_inputActions.LoadBindingOverridesFromJson(PlayerPrefs.GetString(PrefKey));
}
/// <summary>重置所有绑定为默认值并清除 PlayerPrefs 记录。</summary>
public void ResetBindings()
{
_inputActions?.RemoveAllBindingOverrides();
PlayerPrefs.DeleteKey(PrefKey);
}
}
}

View File

@@ -1,3 +0,0 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Input { }