Initial Unity project commit

This commit is contained in:
2026-07-08 19:53:15 +08:00
commit 75f254212e
15657 changed files with 11633879 additions and 0 deletions
@@ -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] 尝试添加空的 ItemDatareferenceData 为 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: