Files
zeling_v2/Assets/_Game/Scripts/World/WorldStateRegistry.cs
Joywayer 4189a6210b perf(editor): 关闭 Domain/Scene Reload 加速进入 Play,并补齐域重载安全
关闭 Domain Reload 后 SO 的 OnEnable/OnDisable 不再于进入/退出 Play 触发、
静态字段不再重置,运行时态会跨 Play 会话残留。补齐重置路径:

- 新增 PlayModeResetHook:编辑器内在"进入 Play 前"统一回调各 SO 的重置,
  复刻 Domain Reload 曾提供的干净起点;运行时构建为空实现、零开销。
- 静态态用 RuntimeInitializeOnLoadMethod 重置:
  ServiceLocator._services / SettingsManager.SettingsChanged /
  EventChainManager.OnChainExecutedInEditor。
- SO 运行时态经 PlayModeResetHook 重置:
  BaseEventChannelSO(粘性值+订阅委托) / VoidBaseEventChannelSO /
  WorldStateRegistry(状态字典+变更事件) / InputReaderSO。
- InputReaderSO 增加解绑追踪:跨会话重建前先解绑旧输入回调,
  避免 InputActionAsset 存活导致同一输入重复绑定、多次触发。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:40:37 +08:00

189 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>世界对象的分类枚举,作为 WorldStateRegistry 的泛化 key。</summary>
public enum WorldObjectCategory
{
Collectible,
SavePoint,
Door,
Destroyed,
Flag,
}
/// <summary>
/// 运行时世界状态缓存。ScriptableObject通过 [SerializeField] 注入各组件。
/// WorldStateRegistrySaverISaveable负责将本对象与存档管道连接
/// SaveAsync → WorldStateRegistrySaver.OnSave → 写出全部分类到 SaveData
/// LoadAsync → WorldStateRegistrySaver.OnLoad → 调用 LoadFromSave(data) 恢复缓存。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/World/WorldStateRegistry")]
public class WorldStateRegistry : ScriptableObject, IWorldStateReader
{
// ── 统一状态字典 ─────────────────────────────────────────────────────
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
/// <summary>
/// 状态变更时广播:(类别, id)。UI / 测试代码可订阅此事件做响应式刷新。
/// 若需批量写入多个 ID推荐使用 <see cref="BatchMark"/> 避免同帧多次重绘。
/// </summary>
public event Action<WorldObjectCategory, string> OnStateChanged;
/// <summary>
/// 批次状态变更时广播:(类别, 新增标记的 ID 数组)。
/// 由 <see cref="BatchMark"/> 触发,一次性广播所有新增 ID避免 UI 同帧重绘 N 次。
/// </summary>
public event Action<WorldObjectCategory, string[]> OnBatchStateChanged;
/// <summary>
/// ScriptableObject 会在多次 Play 会话间保留运行时状态,进入 Play 前必须清干净。
/// 关闭 Domain Reload 后 OnEnable 不再于每次进入 Play 触发,故同时登记到 PlayModeResetHook
/// 由编辑器钩子在进入 Play 前兜住重置(构建中仍由 OnEnable 负责)。
/// </summary>
private void OnEnable()
{
ResetRuntimeState();
PlayModeResetHook.Register(ResetRuntimeState);
}
/// <summary>清空运行时状态字典与变更事件订阅者,回到干净起点。</summary>
private void ResetRuntimeState()
{
_states.Clear();
OnStateChanged = null;
OnBatchStateChanged = null;
}
// ── 泛化 API ─────────────────────────────────────────────────────────
/// <summary>检查指定类别中 id 是否已标记。</summary>
public bool IsMarked(WorldObjectCategory category, string id)
=> _states.TryGetValue(category, out var set) && set.Contains(id);
/// <summary>标记指定类别中的 id幂等。</summary>
public void Mark(WorldObjectCategory category, string id)
{
if (!_states.TryGetValue(category, out var set))
{
set = new HashSet<string>();
_states[category] = set;
}
if (set.Add(id))
OnStateChanged?.Invoke(category, id);
}
// ── 语义化具名 API泛化方法快捷方式───────────────────────────────
public bool IsCollected(string id) => IsMarked(WorldObjectCategory.Collectible, id);
public void MarkCollected(string id) => Mark(WorldObjectCategory.Collectible, id);
public bool IsSavePointActivated(string id) => IsMarked(WorldObjectCategory.SavePoint, id);
public void MarkSavePointActivated(string id) => Mark(WorldObjectCategory.SavePoint, id);
public bool IsDestroyed(string id) => IsMarked(WorldObjectCategory.Destroyed, id);
public void MarkDestroyed(string id) => Mark(WorldObjectCategory.Destroyed, id);
public bool IsDoorOpened(string id) => IsMarked(WorldObjectCategory.Door, id);
public void MarkDoorOpened(string id) => Mark(WorldObjectCategory.Door, id);
public bool HasFlag(string key) => IsMarked(WorldObjectCategory.Flag, key);
public void SetFlag(string key) => Mark(WorldObjectCategory.Flag, key);
public void ClearFlag(string key) => Clear(WorldObjectCategory.Flag, key);
/// <summary>
/// 通用清除接口:移除指定类别中 id 的标记状态(幂等)。
/// 用于调试重置、测试、或撤销错误标记。
/// </summary>
public void Clear(WorldObjectCategory category, string id)
{
if (_states.TryGetValue(category, out var set) && set.Remove(id))
OnStateChanged?.Invoke(category, id);
}
/// <summary>
/// 一次性标记多个 ID批次写入。已标记的 ID 被幂等跳过;
/// 全部写入后触发单次 <see cref="OnBatchStateChanged"/>,而非逐个触发 <see cref="OnStateChanged"/>
/// 适合 EventChain 同帧连续设置多个标志时使用以避免 UI 同帧重绘 N 次。
/// </summary>
/// <returns>实际新增标记的 ID 数量。</returns>
public int BatchMark(WorldObjectCategory category, System.Collections.Generic.IEnumerable<string> ids)
{
if (ids == null) return 0;
if (!_states.TryGetValue(category, out var set))
{
set = new HashSet<string>();
_states[category] = set;
}
var added = new System.Collections.Generic.List<string>();
foreach (var id in ids)
if (!string.IsNullOrEmpty(id) && set.Add(id))
added.Add(id);
if (added.Count > 0)
OnBatchStateChanged?.Invoke(category, added.ToArray());
return added.Count;
}
// ── Persistence ───────────────────────────────────────────────────────
/// <summary>
/// 从存档数据恢复全部状态。由 WorldStateRegistrySaver.OnLoad 调用。
/// </summary>
public void LoadFromSave(SaveData data)
{
_states.Clear();
if (data == null) return;
var world = data.World;
if (world?.CollectedIds != null) foreach (var id in world.CollectedIds) Mark(WorldObjectCategory.Collectible, id);
if (world?.ActivatedSavePoints != null) foreach (var id in world.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id);
if (world?.OpenedDoors != null) foreach (var id in world.OpenedDoors) Mark(WorldObjectCategory.Door, id);
if (world?.DestroyedObjectIds != null) foreach (var id in world.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id);
if (data.EventChains?.WorldFlags != null)
foreach (var kv in data.EventChains.WorldFlags)
if (kv.Value) Mark(WorldObjectCategory.Flag, kv.Key);
}
/// <summary>
/// 返回指定分类中所有已标记 ID 的快照副本(数组)。
/// 由 WorldStateRegistrySaver.OnSave 调用,将运行时状态写入 SaveData。
/// 返回数组副本而非内部集合引用,防止调用方意外修改内部状态。
/// </summary>
public IReadOnlyCollection<string> GetAllIds(WorldObjectCategory category)
{
if (_states.TryGetValue(category, out var set) && set.Count > 0)
{
var copy = new string[set.Count];
set.CopyTo(copy);
return copy;
}
return System.Array.Empty<string>();
}
/// <summary>重置所有状态(开始新游戏时调用)。</summary>
public void Reset() => _states.Clear();
/// <summary>
/// 反向查询:在所有分类中查找 <paramref name="id"/> 首次出现的类别。
/// 适用于调试工具(如 DataHub / WorldState 检视面板)快速定位一个 id 属于哪类。
/// O(k) 其中 k = 分类数(最多 5
/// </summary>
/// <param name="id">要查找的 ID。</param>
/// <param name="category">找到时输出该 ID 所属的类别;未找到时输出 <see cref="WorldObjectCategory.Flag"/>(默认值)。</param>
/// <returns>true = 找到false = 任何类别中均无此 id。</returns>
public bool TryGetCategory(string id, out WorldObjectCategory category)
{
if (!string.IsNullOrEmpty(id))
foreach (var (cat, set) in _states)
if (set.Contains(id)) { category = cat; return true; }
category = default;
return false;
}
}
}