地图系统
This commit is contained in:
19
Assets/_Game/Scripts/Inventory/BaseGames.Inventory.asmdef
Normal file
19
Assets/_Game/Scripts/Inventory/BaseGames.Inventory.asmdef
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "BaseGames.Inventory",
|
||||
"rootNamespace": "BaseGames.Inventory",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Localization"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e81405e158a5a0438e877577a9a2d84
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
54
Assets/_Game/Scripts/Inventory/IInventoryService.cs
Normal file
54
Assets/_Game/Scripts/Inventory/IInventoryService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 背包管理服务接口。UI 层(ItemInventoryPanel)通过此接口读取道具状态,
|
||||
/// 与 <see cref="InventoryManager"/> 具体实现解耦,便于独立测试和跨场景复用。
|
||||
/// 设计对照 <see cref="BaseGames.Equipment.IEquipmentService"/>。
|
||||
/// </summary>
|
||||
public interface IInventoryService
|
||||
{
|
||||
/// <summary>当前持有的全部道具条目(只读视图,含数量)。</summary>
|
||||
IReadOnlyList<InventoryEntry> Items { get; }
|
||||
|
||||
/// <summary>持有道具种类数(不含数量)。</summary>
|
||||
int DistinctCount { get; }
|
||||
|
||||
/// <summary>查询指定道具的持有数量;未持有返回 0。</summary>
|
||||
int GetCount(string itemId);
|
||||
|
||||
/// <summary>是否持有指定道具(数量 ≥ 1)。</summary>
|
||||
bool Has(string itemId);
|
||||
|
||||
/// <summary>
|
||||
/// 增加道具。stackable 道具按 amount 叠加(受 maxStack 限制);
|
||||
/// 非 stackable 恒为 1。返回实际新增数量(0 表示已满或道具无效)。
|
||||
/// </summary>
|
||||
int AddItem(string itemId, int amount = 1);
|
||||
|
||||
/// <summary>移除道具(如消耗品使用 / 剧情消耗)。返回实际移除数量。</summary>
|
||||
int RemoveItem(string itemId, int amount = 1);
|
||||
|
||||
/// <summary>清除某道具的"新获得"未读标记(玩家查看背包后调用)。</summary>
|
||||
void MarkSeen(string itemId);
|
||||
|
||||
/// <summary>道具集合变化(增删 / 数量变更)时触发;UI 据此刷新。</summary>
|
||||
event System.Action OnInventoryChanged;
|
||||
}
|
||||
|
||||
/// <summary>背包条目:道具数据 + 当前数量 + 未读标记。</summary>
|
||||
public readonly struct InventoryEntry
|
||||
{
|
||||
public readonly ItemSO Item;
|
||||
public readonly int Count;
|
||||
public readonly bool IsNew;
|
||||
|
||||
public InventoryEntry(ItemSO item, int count, bool isNew)
|
||||
{
|
||||
Item = item;
|
||||
Count = count;
|
||||
IsNew = isNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Inventory/IInventoryService.cs.meta
Normal file
11
Assets/_Game/Scripts/Inventory/IInventoryService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3e9ff4a34b86f94381595d2872aa5cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
209
Assets/_Game/Scripts/Inventory/InventoryManager.cs
Normal file
209
Assets/_Game/Scripts/Inventory/InventoryManager.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 运行时背包管理器。
|
||||
/// 挂在 Persistent 场景 [Services] 下,事件驱动记录玩家持有的道具,
|
||||
/// 实现 <see cref="ISaveable"/> 持久化。设计对照 <see cref="BaseGames.World.Map.MapManager"/>
|
||||
/// 与 <see cref="BaseGames.Equipment.EquipmentManager"/> 的服务注册 / 存档模式。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-700)]
|
||||
public class InventoryManager : MonoBehaviour, ISaveable, IInventoryService
|
||||
{
|
||||
[SerializeField] private ItemDatabaseSO _database;
|
||||
|
||||
[Header("Event Channels - Listen")]
|
||||
[Tooltip("道具拾取(itemId)。Collectible 拾取 Item 类型时广播 EVT_ItemPickup。")]
|
||||
[SerializeField] private StringEventChannelSO _onItemPickup;
|
||||
|
||||
[Header("Event Channels - Raise")]
|
||||
[Tooltip("道具首次获得时广播(itemId),供 HUD Toast / 地图碎片揭示等订阅。")]
|
||||
[SerializeField] private StringEventChannelSO _onItemAcquired;
|
||||
[Tooltip("背包内容变化时广播(无负载),供 UI 轻量刷新。")]
|
||||
[SerializeField] private VoidEventChannelSO _onInventoryChanged;
|
||||
[Tooltip("地图碎片道具获得时广播揭示区域(regionId)。对应 EVT_RevealRegion。")]
|
||||
[SerializeField] private StringEventChannelSO _onRevealRegion;
|
||||
|
||||
private readonly Dictionary<string, int> _counts = new();
|
||||
private readonly HashSet<string> _newItems = new();
|
||||
private readonly List<InventoryEntry> _view = new();
|
||||
private bool _viewDirty = true;
|
||||
private bool _isDuplicate;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
public event Action OnInventoryChanged;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
if (ServiceLocator.GetOrDefault<IInventoryService>() != null) { _isDuplicate = true; Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<IInventoryService>(this);
|
||||
Debug.Assert(_database != null, "[InventoryManager] _database 未赋值,请在 Inspector 中指定 ItemDatabaseSO。", this);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
_onItemPickup?.Subscribe(OnItemPickup).AddTo(_subs);
|
||||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
_subs.Clear();
|
||||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_isDuplicate) return;
|
||||
ServiceLocator.Unregister<IInventoryService>(this);
|
||||
}
|
||||
|
||||
// ── 事件驱动拾取 ──────────────────────────────────────────────────────
|
||||
private void OnItemPickup(string itemId) => AddItem(itemId, 1);
|
||||
|
||||
// ── IInventoryService ─────────────────────────────────────────────────
|
||||
public IReadOnlyList<InventoryEntry> Items
|
||||
{
|
||||
get { RebuildViewIfDirty(); return _view; }
|
||||
}
|
||||
|
||||
public int DistinctCount => _counts.Count;
|
||||
|
||||
public int GetCount(string itemId)
|
||||
=> !string.IsNullOrEmpty(itemId) && _counts.TryGetValue(itemId, out var c) ? c : 0;
|
||||
|
||||
public bool Has(string itemId) => GetCount(itemId) > 0;
|
||||
|
||||
public int AddItem(string itemId, int amount = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId) || amount <= 0) return 0;
|
||||
var item = _database != null ? _database.Find(itemId) : null;
|
||||
if (item == null) { Debug.LogWarning($"[InventoryManager] 找不到道具: {itemId}", this); return 0; }
|
||||
|
||||
bool firstTime = !_counts.ContainsKey(itemId);
|
||||
int current = firstTime ? 0 : _counts[itemId];
|
||||
int target;
|
||||
|
||||
if (!item.stackable)
|
||||
{
|
||||
if (current >= 1) return 0; // 非叠加道具已持有,忽略
|
||||
target = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = current + amount;
|
||||
if (item.maxStack > 0) target = Mathf.Min(target, item.maxStack);
|
||||
}
|
||||
|
||||
int added = target - current;
|
||||
if (added <= 0) return 0;
|
||||
|
||||
_counts[itemId] = target;
|
||||
_newItems.Add(itemId);
|
||||
_viewDirty = true;
|
||||
|
||||
if (firstTime)
|
||||
{
|
||||
_onItemAcquired?.Raise(itemId);
|
||||
// 地图碎片:揭示对应区域
|
||||
if (item.category == ItemCategory.MapShard && !string.IsNullOrEmpty(item.revealRegionId))
|
||||
_onRevealRegion?.Raise(item.revealRegionId);
|
||||
}
|
||||
|
||||
RaiseChanged();
|
||||
return added;
|
||||
}
|
||||
|
||||
public int RemoveItem(string itemId, int amount = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId) || amount <= 0) return 0;
|
||||
if (!_counts.TryGetValue(itemId, out var current) || current <= 0) return 0;
|
||||
|
||||
int removed = Mathf.Min(current, amount);
|
||||
int target = current - removed;
|
||||
if (target <= 0)
|
||||
{
|
||||
_counts.Remove(itemId);
|
||||
_newItems.Remove(itemId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_counts[itemId] = target;
|
||||
}
|
||||
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void MarkSeen(string itemId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId)) return;
|
||||
if (_newItems.Remove(itemId))
|
||||
{
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
private void RaiseChanged()
|
||||
{
|
||||
_onInventoryChanged?.Raise();
|
||||
OnInventoryChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void RebuildViewIfDirty()
|
||||
{
|
||||
if (!_viewDirty) return;
|
||||
_view.Clear();
|
||||
foreach (var kv in _counts)
|
||||
{
|
||||
var item = _database != null ? _database.Find(kv.Key) : null;
|
||||
if (item == null) continue;
|
||||
_view.Add(new InventoryEntry(item, kv.Value, _newItems.Contains(kv.Key)));
|
||||
}
|
||||
// 按分类枚举顺序、再按本地化名/ID 稳定排序,供 UI 分组展示
|
||||
_view.Sort((a, b) =>
|
||||
{
|
||||
int c = a.Item.category.CompareTo(b.Item.category);
|
||||
return c != 0 ? c : string.CompareOrdinal(a.Item.itemId, b.Item.itemId);
|
||||
});
|
||||
_viewDirty = false;
|
||||
}
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData data)
|
||||
{
|
||||
data.Inventory.ItemCounts.Clear();
|
||||
foreach (var kv in _counts) data.Inventory.ItemCounts[kv.Key] = kv.Value;
|
||||
data.Inventory.NewItemIds.Clear();
|
||||
data.Inventory.NewItemIds.AddRange(_newItems);
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData data)
|
||||
{
|
||||
_counts.Clear();
|
||||
_newItems.Clear();
|
||||
if (data.Inventory?.ItemCounts != null)
|
||||
foreach (var kv in data.Inventory.ItemCounts)
|
||||
if (!string.IsNullOrEmpty(kv.Key) && kv.Value > 0)
|
||||
_counts[kv.Key] = kv.Value;
|
||||
if (data.Inventory?.NewItemIds != null)
|
||||
foreach (var id in data.Inventory.NewItemIds)
|
||||
if (_counts.ContainsKey(id)) _newItems.Add(id);
|
||||
|
||||
_viewDirty = true;
|
||||
RaiseChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Inventory/InventoryManager.cs.meta
Normal file
11
Assets/_Game/Scripts/Inventory/InventoryManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dec9b91d226ae7e49b97ee7b4d3c88d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/_Game/Scripts/Inventory/ItemCategory.cs
Normal file
20
Assets/_Game/Scripts/Inventory/ItemCategory.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 道具分类(背包 Tab 内分组与排序依据)。
|
||||
/// 新增分类时在此追加;ItemInventoryPanel 的分组顺序与此枚举声明顺序一致。
|
||||
/// </summary>
|
||||
public enum ItemCategory
|
||||
{
|
||||
/// <summary>关键剧情道具(推进主线 / 解锁区域,不可丢弃)。</summary>
|
||||
KeyItem = 0,
|
||||
/// <summary>钥匙类(开启特定门 / 机关)。</summary>
|
||||
Key = 1,
|
||||
/// <summary>可消耗品(药水、临时增益等,拥有数量可叠加)。</summary>
|
||||
Consumable = 2,
|
||||
/// <summary>地图碎片(拾取后揭示对应区域地图)。</summary>
|
||||
MapShard = 3,
|
||||
/// <summary>收藏 / 杂项(图鉴、纪念物,无直接用途)。</summary>
|
||||
Collectible = 4,
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Inventory/ItemCategory.cs.meta
Normal file
11
Assets/_Game/Scripts/Inventory/ItemCategory.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b44605b768d73034bae0e0bb77ab63bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/Inventory/ItemDatabaseSO.cs
Normal file
46
Assets/_Game/Scripts/Inventory/ItemDatabaseSO.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 道具目录 SO(背包系统)。
|
||||
/// 全局唯一资产(建议 Assets/_Game/Data/Inventory/ItemDatabase.asset),
|
||||
/// 通过 itemId 查找 <see cref="ItemSO"/> 引用。
|
||||
/// 由 <see cref="InventoryManager"/> 在拾取 / OnLoad 时查询。
|
||||
/// 设计对照 <see cref="BaseGames.Equipment.CharmCatalogSO"/>,但内部用字典缓存以支持高频查询。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Inventory/ItemDatabase")]
|
||||
public class ItemDatabaseSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] private ItemSO[] _items;
|
||||
|
||||
public IReadOnlyList<ItemSO> AllItems => _items;
|
||||
|
||||
private Dictionary<string, ItemSO> _index;
|
||||
|
||||
/// <summary>按 itemId 查找道具,找不到返回 null。</summary>
|
||||
public ItemSO Find(string itemId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(itemId) || _items == null) return null;
|
||||
EnsureIndex();
|
||||
return _index.TryGetValue(itemId, out var item) ? item : null;
|
||||
}
|
||||
|
||||
private void EnsureIndex()
|
||||
{
|
||||
if (_index != null) return;
|
||||
_index = new Dictionary<string, ItemSO>(_items.Length);
|
||||
foreach (var item in _items)
|
||||
if (item != null && !string.IsNullOrEmpty(item.itemId))
|
||||
_index[item.itemId] = item;
|
||||
}
|
||||
|
||||
/// <summary>编辑器修改 _items 后清空索引,下次 Find 重建。</summary>
|
||||
public void InvalidateIndex() => _index = null;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() => _index = null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Inventory/ItemDatabaseSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Inventory/ItemDatabaseSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c67c60ad34e291548acdd508379d848d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
51
Assets/_Game/Scripts/Inventory/ItemSO.cs
Normal file
51
Assets/_Game/Scripts/Inventory/ItemSO.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Localization;
|
||||
|
||||
namespace BaseGames.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 道具数据 SO(背包系统)。
|
||||
/// 资产路径建议: Assets/_Game/Data/Inventory/Items/Item_{Name}.asset
|
||||
/// 命名/本地化约定与 <see cref="BaseGames.Equipment.CharmSO"/> 保持一致(Items 表)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Inventory/Item")]
|
||||
public class ItemSO : ScriptableObject, ILocalizableAsset
|
||||
{
|
||||
[Header("Identity")]
|
||||
[Tooltip("全局唯一 ID,如 \"Item_AncientKey\"。与拾取广播的 itemId 对应。")]
|
||||
public string itemId;
|
||||
[Tooltip("本地化 Key(Items 表)。")]
|
||||
public string displayNameKey;
|
||||
[TextArea(2, 4)]
|
||||
public string descriptionKey;
|
||||
|
||||
[Header("Classification")]
|
||||
public ItemCategory category;
|
||||
|
||||
[Header("Visual")]
|
||||
public Sprite icon;
|
||||
|
||||
[Header("Stacking")]
|
||||
[Tooltip("可叠加(消耗品等)。false 时同 ID 只记 1 份。")]
|
||||
public bool stackable;
|
||||
[Tooltip("叠加上限(stackable=true 时生效,<=0 表示不限)。")]
|
||||
public int maxStack = 99;
|
||||
|
||||
[Header("Map Reveal")]
|
||||
[Tooltip("category=MapShard 时,拾取后揭示的区域 ID(对应 MapManager 区域)。")]
|
||||
public string revealRegionId;
|
||||
|
||||
[Header("Lore")]
|
||||
[Tooltip("是否唯一(剧情关键 / 一次性)。")]
|
||||
public bool isUnique;
|
||||
|
||||
public IEnumerable<LocalizationKeyRef> GetLocalizationKeys()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(displayNameKey))
|
||||
yield return new LocalizationKeyRef(displayNameKey, LocalizationTable.Items, nameof(displayNameKey));
|
||||
if (!string.IsNullOrEmpty(descriptionKey))
|
||||
yield return new LocalizationKeyRef(descriptionKey, LocalizationTable.Items, nameof(descriptionKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Inventory/ItemSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Inventory/ItemSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0c62dd99935af1458f9144755a69b43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user