Initial Unity project commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Inventory
|
||||
{
|
||||
[System.Serializable]
|
||||
public class InventoryItem
|
||||
{
|
||||
public ItemData data;
|
||||
public int stackSize;
|
||||
|
||||
public InventoryItem(ItemData source, int amount)
|
||||
{
|
||||
data = source;
|
||||
stackSize = amount;
|
||||
}
|
||||
|
||||
public void AddToStack(int amount)
|
||||
{
|
||||
stackSize += amount;
|
||||
}
|
||||
|
||||
public void RemoveFromStack(int amount)
|
||||
{
|
||||
stackSize -= amount;
|
||||
}
|
||||
}
|
||||
|
||||
public class InventorySystem : MonoBehaviour
|
||||
{
|
||||
[Header("背包配置")]
|
||||
[Tooltip("背包最大格子数")]
|
||||
public int maxSlots = 20;
|
||||
|
||||
// 背包内容列表
|
||||
public List<InventoryItem> inventory = new List<InventoryItem>();
|
||||
|
||||
// 当前选中的格子索引(由 InventoryUI 同步)
|
||||
public int selectedSlotIndex = 0;
|
||||
|
||||
// 这是一个简单的单例模式,方便全局访问(但在大项目中建议用依赖注入)
|
||||
public static InventorySystem Instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = this;
|
||||
// 初始化固定大小的背包,填充 null
|
||||
InitializeInventory();
|
||||
SanitizeInventory();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeInventory()
|
||||
{
|
||||
inventory.Clear();
|
||||
for (int i = 0; i < maxSlots; i++)
|
||||
{
|
||||
inventory.Add(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void SanitizeInventory()
|
||||
{
|
||||
if (inventory == null) inventory = new List<InventoryItem>();
|
||||
|
||||
if (inventory.Count > maxSlots)
|
||||
{
|
||||
inventory.RemoveRange(maxSlots, inventory.Count - maxSlots);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (inventory.Count < maxSlots)
|
||||
{
|
||||
inventory.Add(null);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < inventory.Count; i++)
|
||||
{
|
||||
InventoryItem item = inventory[i];
|
||||
if (item == null) continue;
|
||||
if (item.data == null || item.stackSize <= 0)
|
||||
{
|
||||
inventory[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加物品到背包
|
||||
/// </summary>
|
||||
/// <param name="referenceData">物品数据</param>
|
||||
/// <param name="amount">添加数量</param>
|
||||
/// <returns>是否成功添加(哪怕只添加了一部分也算成功)</returns>
|
||||
public bool Add(ItemData referenceData, int amount)
|
||||
{
|
||||
if (referenceData == null)
|
||||
{
|
||||
Debug.LogError("[Inventory] 尝试添加空的 ItemData(referenceData 为 null)。");
|
||||
return false;
|
||||
}
|
||||
if (amount <= 0) return false;
|
||||
|
||||
SanitizeInventory();
|
||||
|
||||
// 剩余需要添加的数量
|
||||
int remainingAmount = amount;
|
||||
|
||||
// 1. 如果物品可堆叠,先尝试填满已有的堆叠
|
||||
if (referenceData.isStackable)
|
||||
{
|
||||
// 遍历所有格子寻找同类物品
|
||||
for (int i = 0; i < inventory.Count; i++)
|
||||
{
|
||||
InventoryItem item = inventory[i];
|
||||
if (item != null && item.data == referenceData)
|
||||
{
|
||||
// 如果这个堆叠还没满
|
||||
if (item.stackSize < referenceData.maxStackSize)
|
||||
{
|
||||
// 计算这个格子还能塞多少个
|
||||
int spaceInStack = referenceData.maxStackSize - item.stackSize;
|
||||
|
||||
// 实际能塞进去的数量
|
||||
int amountToAdd = Mathf.Min(remainingAmount, spaceInStack);
|
||||
|
||||
item.AddToStack(amountToAdd);
|
||||
remainingAmount -= amountToAdd;
|
||||
|
||||
Debug.Log($"[Inventory] 堆叠更新: {referenceData.displayName}, 当前堆叠数: {item.stackSize}");
|
||||
|
||||
if (remainingAmount <= 0) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果还有剩余,寻找新格子
|
||||
while (remainingAmount > 0)
|
||||
{
|
||||
int targetSlotIndex = -1;
|
||||
|
||||
// 优先级 A: 检查当前选中的格子是否为空
|
||||
if (selectedSlotIndex >= 0 && selectedSlotIndex < maxSlots && inventory[selectedSlotIndex] == null)
|
||||
{
|
||||
targetSlotIndex = selectedSlotIndex;
|
||||
}
|
||||
// 优先级 B: 如果选中格子不空,从头寻找第一个空位
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < maxSlots; i++)
|
||||
{
|
||||
if (inventory[i] == null)
|
||||
{
|
||||
targetSlotIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到空位,说明背包满了
|
||||
if (targetSlotIndex == -1)
|
||||
{
|
||||
Debug.LogWarning("[Inventory] 背包已满!无法添加更多物品。");
|
||||
return remainingAmount < amount;
|
||||
}
|
||||
|
||||
// 创建新物品项并填入目标格子
|
||||
int addCount = referenceData.isStackable ? Mathf.Min(remainingAmount, referenceData.maxStackSize) : 1;
|
||||
|
||||
InventoryItem newItem = new InventoryItem(referenceData, addCount);
|
||||
inventory[targetSlotIndex] = newItem; // 填入空位
|
||||
|
||||
remainingAmount -= addCount;
|
||||
Debug.Log($"[Inventory] 添加新物品到格子 {targetSlotIndex}: {referenceData.displayName}, 数量: {addCount}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Remove(ItemData referenceData, int amount)
|
||||
{
|
||||
if (referenceData == null) return;
|
||||
if (amount <= 0) return;
|
||||
|
||||
SanitizeInventory();
|
||||
|
||||
int remaining = amount;
|
||||
|
||||
for (int i = 0; i < inventory.Count; i++)
|
||||
{
|
||||
InventoryItem item = inventory[i];
|
||||
if (item == null) continue;
|
||||
if (item.data != referenceData) continue;
|
||||
|
||||
int take = Mathf.Min(remaining, item.stackSize);
|
||||
item.RemoveFromStack(take);
|
||||
remaining -= take;
|
||||
|
||||
if (item.stackSize <= 0)
|
||||
{
|
||||
inventory[i] = null;
|
||||
}
|
||||
|
||||
if (remaining <= 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定格子移除物品
|
||||
/// </summary>
|
||||
public void RemoveFromSlot(int slotIndex, int amount)
|
||||
{
|
||||
SanitizeInventory();
|
||||
if (slotIndex < 0 || slotIndex >= inventory.Count) return;
|
||||
|
||||
InventoryItem item = inventory[slotIndex];
|
||||
if (item != null)
|
||||
{
|
||||
item.RemoveFromStack(amount);
|
||||
if (item.stackSize <= 0)
|
||||
{
|
||||
inventory[slotIndex] = null; // 置空该格子
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b02bd46d5d1f9d5449972804f38719c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Inventory
|
||||
{
|
||||
public enum ItemType
|
||||
{
|
||||
Resource, // 资源(不可直接使用)
|
||||
Consumable, // 消耗品(吃/喝/用)
|
||||
Equipment, // 装备(平板、手电筒、武器)
|
||||
Tool // 工具(斧头,主要用于交互)
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Inventory/Item Data")]
|
||||
public class ItemData : ScriptableObject
|
||||
{
|
||||
[System.Serializable]
|
||||
public class SpecialUseUIPage
|
||||
{
|
||||
[TextArea(2, 8)] public string text;
|
||||
}
|
||||
|
||||
public string id;
|
||||
public string displayName;
|
||||
[TextArea(3, 10)] public string description;
|
||||
public Sprite icon;
|
||||
public GameObject pickupPrefab; // 掉落/生成在世界中的物体预设体
|
||||
public GameObject handModelPrefab; // 手持时的模型预设体
|
||||
|
||||
[Header("Usage Settings")]
|
||||
public ItemType itemType; // 物品类型
|
||||
public ItemData consumedResult; // 消耗后获得的物品(为空则直接消失)
|
||||
|
||||
[Header("Vitals Effects (Consumable)")]
|
||||
[FormerlySerializedAs("healthDelta")] public float thirstDelta;
|
||||
public float hungerDelta;
|
||||
public float sanityDelta;
|
||||
|
||||
public bool isStackable;
|
||||
public int maxStackSize = 1;
|
||||
|
||||
[Header("Hold Settings")]
|
||||
[Tooltip("是否使用自定义手持位置/旋转")]
|
||||
public bool useCustomHoldSettings;
|
||||
public Vector3 holdPositionOffset;
|
||||
public Vector3 holdRotationOffset;
|
||||
public Vector3 holdScale = Vector3.one;
|
||||
|
||||
[Header("Special Use UI")]
|
||||
public bool showSpecialUseUI;
|
||||
[TextArea(2, 8)] public string specialUseUIText;
|
||||
public Sprite specialUseSprite;
|
||||
public bool useSpecialUseUIPages;
|
||||
public List<SpecialUseUIPage> specialUseUIPages = new List<SpecialUseUIPage>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a6a9ccc104e0964687925ed730d8d9f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,215 @@
|
||||
using Interaction.Conditions;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Inventory
|
||||
{
|
||||
public class ItemGiveTakeStation : InteractConditionBehaviour, IInteractable
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
Auto = 0,
|
||||
GiveOnly = 1,
|
||||
TakeOnly = 2
|
||||
}
|
||||
|
||||
[Header("物品")]
|
||||
[SerializeField] private ItemData itemData;
|
||||
[SerializeField, Min(1)] private int amountPerInteract = 1;
|
||||
|
||||
[Header("容量")]
|
||||
[SerializeField, Min(0)] private int capacity = 10;
|
||||
[SerializeField, Min(0)] private int stock = 10;
|
||||
|
||||
[Header("模式")]
|
||||
[SerializeField] private Mode mode = Mode.Auto;
|
||||
[SerializeField] private bool preferTakeWhenPossible = true;
|
||||
|
||||
[Header("提示")]
|
||||
[SerializeField] private string outOfStockReason = "库存不足";
|
||||
[SerializeField] private string stationFullReason = "回收箱已满";
|
||||
[SerializeField] private string inventoryFullReason = "背包已满";
|
||||
[SerializeField] private string nothingToTakeReason = "没有可回收的物品";
|
||||
[SerializeField] private string missingInventoryReason = "缺少背包系统";
|
||||
[SerializeField] private string missingItemReason = "未配置物品";
|
||||
|
||||
[Header("事件")]
|
||||
[SerializeField] private UnityEvent onGaveItem;
|
||||
[SerializeField] private UnityEvent onTookBackItem;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (capacity < 0) capacity = 0;
|
||||
if (stock < 0) stock = 0;
|
||||
if (stock > capacity) stock = capacity;
|
||||
if (amountPerInteract < 1) amountPerInteract = 1;
|
||||
}
|
||||
|
||||
public override bool CanInteract(GameObject interactor, out string failReason)
|
||||
{
|
||||
failReason = null;
|
||||
|
||||
if (itemData == null)
|
||||
{
|
||||
failReason = missingItemReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
var inventory = InventorySystem.Instance;
|
||||
if (inventory == null)
|
||||
{
|
||||
failReason = missingInventoryReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
int playerCount = CountItemsById(inventory, itemData.id);
|
||||
int freeCapacity = Mathf.Max(0, capacity - stock);
|
||||
|
||||
bool canGive = stock > 0 && CanAdd(inventory, itemData, Mathf.Min(amountPerInteract, stock));
|
||||
bool canTake = playerCount > 0 && freeCapacity > 0;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Mode.GiveOnly:
|
||||
if (stock <= 0)
|
||||
{
|
||||
failReason = outOfStockReason;
|
||||
return false;
|
||||
}
|
||||
if (!canGive)
|
||||
{
|
||||
failReason = inventoryFullReason;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
case Mode.TakeOnly:
|
||||
if (playerCount <= 0)
|
||||
{
|
||||
failReason = nothingToTakeReason;
|
||||
return false;
|
||||
}
|
||||
if (freeCapacity <= 0)
|
||||
{
|
||||
failReason = stationFullReason;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
if (preferTakeWhenPossible && canTake) return true;
|
||||
if (canGive) return true;
|
||||
if (canTake) return true;
|
||||
|
||||
if (stock <= 0) failReason = outOfStockReason;
|
||||
else if (!canGive) failReason = inventoryFullReason;
|
||||
else if (freeCapacity <= 0) failReason = stationFullReason;
|
||||
else failReason = nothingToTakeReason;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInteractSucceeded(GameObject interactor)
|
||||
{
|
||||
}
|
||||
|
||||
public void Interact()
|
||||
{
|
||||
if (itemData == null) return;
|
||||
|
||||
var inventory = InventorySystem.Instance;
|
||||
if (inventory == null) return;
|
||||
|
||||
int playerCount = CountItemsById(inventory, itemData.id);
|
||||
int freeCapacity = Mathf.Max(0, capacity - stock);
|
||||
|
||||
bool didTake = false;
|
||||
|
||||
if (mode != Mode.GiveOnly)
|
||||
{
|
||||
int takeCount = Mathf.Min(amountPerInteract, Mathf.Min(playerCount, freeCapacity));
|
||||
if (takeCount > 0 && (mode == Mode.TakeOnly || (mode == Mode.Auto && preferTakeWhenPossible)))
|
||||
{
|
||||
inventory.Remove(itemData, takeCount);
|
||||
stock = Mathf.Min(capacity, stock + takeCount);
|
||||
didTake = true;
|
||||
onTookBackItem?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
if (didTake) return;
|
||||
if (mode == Mode.TakeOnly) return;
|
||||
if (stock <= 0) return;
|
||||
|
||||
int giveRequest = Mathf.Min(amountPerInteract, stock);
|
||||
if (giveRequest <= 0) return;
|
||||
|
||||
int before = CountItemsById(inventory, itemData.id);
|
||||
inventory.Add(itemData, giveRequest);
|
||||
int after = CountItemsById(inventory, itemData.id);
|
||||
|
||||
int given = Mathf.Clamp(after - before, 0, giveRequest);
|
||||
if (given <= 0) return;
|
||||
|
||||
stock = Mathf.Max(0, stock - given);
|
||||
onGaveItem?.Invoke();
|
||||
}
|
||||
|
||||
private static int CountItemsById(InventorySystem inventory, string itemId)
|
||||
{
|
||||
int total = 0;
|
||||
var list = inventory.inventory;
|
||||
if (list == null) return 0;
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var entry = list[i];
|
||||
if (entry?.data == null) continue;
|
||||
if (entry.data.id != itemId) continue;
|
||||
total += entry.stackSize;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static bool CanAdd(InventorySystem inventory, ItemData data, int amount)
|
||||
{
|
||||
if (inventory == null) return false;
|
||||
if (data == null) return false;
|
||||
if (amount <= 0) return true;
|
||||
|
||||
var list = inventory.inventory;
|
||||
if (list == null) return false;
|
||||
|
||||
int remaining = amount;
|
||||
|
||||
if (data.isStackable)
|
||||
{
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var entry = list[i];
|
||||
if (entry?.data != data) continue;
|
||||
if (entry.stackSize >= data.maxStackSize) continue;
|
||||
|
||||
int space = data.maxStackSize - entry.stackSize;
|
||||
int take = Mathf.Min(space, remaining);
|
||||
remaining -= take;
|
||||
if (remaining <= 0) return true;
|
||||
}
|
||||
}
|
||||
|
||||
int emptySlots = 0;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (list[i] == null) emptySlots++;
|
||||
}
|
||||
|
||||
if (!data.isStackable) return emptySlots >= remaining;
|
||||
|
||||
int perEmpty = Mathf.Max(1, data.maxStackSize);
|
||||
int capacityFromEmpty = emptySlots * perEmpty;
|
||||
return capacityFromEmpty >= remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b255b1f7372e1c42a4dd9797345e9f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Inventory
|
||||
{
|
||||
public class ItemPickup : MonoBehaviour, IInteractable
|
||||
{
|
||||
[Header("物品配置")]
|
||||
[Tooltip("这个物体对应的物品数据")]
|
||||
[SerializeField] private ItemData itemData;
|
||||
|
||||
[Tooltip("拾取数量")]
|
||||
[SerializeField] private int amount = 1;
|
||||
|
||||
public void Interact()
|
||||
{
|
||||
// 1. 安全检查
|
||||
if (itemData == null)
|
||||
{
|
||||
Debug.LogError($"[ItemPickup] 物体 {gameObject.name} 缺少 ItemData 配置!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 添加到背包
|
||||
// 注意:我们直接使用单例 Instance,确保场景里有 InventorySystem 挂在玩家身上
|
||||
if (InventorySystem.Instance != null)
|
||||
{
|
||||
bool isPickedUp = InventorySystem.Instance.Add(itemData, amount);
|
||||
|
||||
if (isPickedUp)
|
||||
{
|
||||
// 3. 视觉反馈 (可选:播放音效或特效)
|
||||
Debug.Log($"[Pickup] 捡起了 {amount} 个 {itemData.displayName}");
|
||||
|
||||
// 4. 销毁场景物体
|
||||
Destroy(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[Pickup] 背包已满,无法拾取!");
|
||||
// 这里可以播放一个“背包已满”的提示音效或 UI 提示
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("[ItemPickup] 场景中找不到 InventorySystem!请确保玩家身上挂载了该组件。");
|
||||
}
|
||||
}
|
||||
|
||||
// 这里的 GetInteractPrompt 是可选的,如果你的 UI 脚本需要显示 "按 F 拾取 [苹果]"
|
||||
public string GetInteractPrompt()
|
||||
{
|
||||
string itemName = itemData != null ? itemData.displayName : "物品";
|
||||
return $"拾取 {itemName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a5f635a7da940140a55b5004d8bd879
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user