《UIS》 第 3 篇 物品系統核心

本篇文章、圖片出處來自 Ultimate Inventory System
軟體版本與本翻譯文件可能會有落差,本翻譯文件僅供參考。
本譯文為本站譯者原創翻譯內容,文字著作權歸本站所有。
未經授權,請勿任意轉載、改作或商業使用。

物品系統核心

庫存(Inventory)

Inventory 是多個 Item Collection 物件的集合。以一個實際例子來說明:假設角色可以將物品放在背包、口袋,或手持裝備中,這樣就需要三個 Item Collections:Bag、Pocket 和 Equipped。建立 Inventory 時,可以新增任意數量的 Item Collections,並透過命名和旗標(Flag)來指定它們的用途。Item Collection 名稱在同一個 Inventory 中必須唯一,且 Inventory 至少需要一個 Item Collection。

Inventory 是一個 MonoBehaviour,因此可以掛載在任意 GameObject 上,適用於角色庫存、敵人庫存、商店庫存、寶箱庫存、倉庫、撿取物等各種使用情境。

Inspector 搜尋 Token

在 Inventory Inspector 的物品選擇清單中,支援以下特殊搜尋語法:

Token說明範例
a:<屬性名稱>篩選擁有指定屬性的物品a:Attack
c:<類別名稱>篩選繼承指定類別的物品c:Weapon
組合查詢可以混合字串、c:、a: 三種條件str c:Weapon a:Attack
即時除錯 Inventory 在執行期間也會即時顯示各 Item Collection 中的物品狀態,讓你快速查看物品所在位置。你甚至可以直接在 Inspector 中修改物品屬性值(但不會觸發事件)。

Inventory API

取得所有物品

// 取得 Inventory 中所有物品(不含 Hide 和 Loadout 的 Collection)
var allItems = inventory.AllItemInfos;

以名稱新增 / 移除

// 以名稱新增物品
m_Inventory.AddItem("potion", 5);

// 以名稱移除物品
m_Inventory.RemoveItem("potion", 3);

建立物品並以 ItemInfo 操作

// 取得 Item Definition 並建立物品
var myItemDefinition = InventorySystemManager.GetItemDefinition("MyItemDefinition");
var myItem = InventorySystemManager.CreateItem(myItemDefinition);

// 建立 ItemInfo(注意雙括號,(1,myItem) 是 ItemAmount)
var myItemInfo = new ItemInfo( (1, myItem) );
inventory.AddItem(myItemInfo);

// 或直接用強制轉型(兩者都是 struct)
// 回傳值記錄了實際加入的數量與加入哪個 Stack
var itemInfoActuallyAdded = inventory.AddItem((ItemInfo)(1, myItem));

取得 Item Collection

// 以名稱取得 Item Collection
var itemCollection = m_Inventory.GetItemCollection("Item Collection Name");

// 若有特定類型,直接轉型
var itemSlotCollection = m_Inventory.GetItemCollection("Equipment") as ItemSlotCollection;

檢查物品是否可被加入

var itemInfoToAdd = (ItemInfo)(5, myItem);
var targetCollection = inventory.GetItemCollection(ItemCollectionPurpose.Equipped);

// CanAddItem 回傳 Nullable ItemInfo(? 代表可為 null)
var canAddResult = targetCollection.CanAddItem(itemInfoToAdd);
if (canAddResult.HasValue) {
    if (canAddResult.Value.Amount == itemInfoToAdd.Amount) {
        // 可以完整加入
    } else {
        // 只能部分加入
    }
} else {
    // 完全無法加入
}

取得與移除物品

// 取得 Inventory 中第一個符合的物品 Stack
var retrievedItemInfo = inventory.GetItemInfo(myItem);
if (retrievedItemInfo.HasValue == false) {
    // 庫存中找不到該物品
}

// 移除物品,回傳值記錄實際被移除的 ItemInfo
var itemInfoRemoved = inventory.RemoveItem((ItemInfo)(1, myItem));

// 指定 ItemStack 移除(會優先從該 Stack 移除)
inventory.RemoveItem(retrievedItemInfo.Value);

// 指定不同的數量移除
inventory.RemoveItem( (1, retrievedItemInfo.Value) );

以篩選器取得物品清單

// 建議使用 Pool 陣列避免重複建立(注意用完後務必歸還)
var pooledArray = GenericObjectPool.Get<ItemInfo[]>();
var filterParameter = InventorySystemManager.GetItemCategory("MyCategory");

// 取得所有繼承指定 Category 的物品
var itemInfoListSlice = inventory.GetItemInfos(ref pooledArray, filterParameter,
    (candidateItemInfo, category) => { return category.InherentlyContains(candidateItemInfo.Item); }
);

// 重要:用完後一定要歸還
GenericObjectPool.Return(pooledArray);

物品集合(Item Collections)

Item Collection 是物品的集合容器,物品以 Item Stack(物品堆疊)的形式儲存在清單中。Item Stack 是一個類別(不是 struct),有物品和數量,並與一個 Item Collection 連結。

Item Collection 的用途(Purpose)

用途說明
None無指定用途(亦即 Other)。
Main庫存的預設主要 Collection。
Secondary自訂用途的 Collection。
Equipped已裝備的物品。
Loadout配裝用,物品不會顯示在 UI 中。
Hide隱藏用,物品不會顯示在 UI 中。
Drop用於丟棄物品。
建議 若 Inventory 中有多個 Item Collections,建議將所有用途設為 None(Other),只使用名稱來區分,以避免混淆。

基礎 Item Collection 的特性

  • 物品數量只能為正數,不允許負值。
  • Unique 物品預設數量只能為 1;若相同 Unique 物品被加入兩次,會建立一個擁有新 ID 的複製品。
  • 預設每個物品只有一個 Item Stack(若需多個 Stack,請使用 Multi Stack Item Collection)。
  • 預設沒有物品數量上限(使用 Item Restrictions 可以添加限制)。

Overflow(溢出)選項

當 Item Restrictions 導致物品無法加入時,Item Collection 的 Overflow 機制會被觸發。預設行為是退回至物品的來源,也可以設定 Item Overflow Action 來客製化處理邏輯(例如設定「Drop Item Action」讓物品自動掉落)。

建立 Item Overflow Action:

// 在專案視窗右鍵:
Create → Ultimate Inventory System → Item Overflow Actions

也可以監聽溢出事件:

// 監聽溢出事件
"c_Inventory_OnAddItemOverflow_ItemInfoToAdd_ItemInfoAdded_ItemInfoRejected"

Item Collection API

新增 / 移除物品

// 新增物品
var myItemInfo = new ItemInfo( (1, myItem) );
itemCollection.AddItem(myItemInfo);

// 移除物品
var itemInfoRemoved = itemCollection.RemoveItem((ItemInfo)(1, myItem));

查詢物品數量

// 取得物品在 Collection 中的總數量(包含多個 Stack)
var amount = itemCollection.GetItemAmount(myItem);

// 以 Item Definition 查詢(布林參數決定是否包含繼承定義)
var amountDefinition = itemCollection.GetItemAmount(myItemDefinition, true);

遍歷所有物品

// 用 Item Stack 遍歷
var allItemStacks = itemCollection.GetAllItemStacks();
for (int i = 0; i < allItemStacks.Count; i++) {
    var item = allItemStacks[i].Item;
    if (item == null) { continue; }
    // 對物品進行操作
}

// 用 Item Info 遍歷(需要 Pool 陣列)
var pooledArray = GenericObjectPool.Get<ItemInfo[]>();
var allItemInfos = itemCollection.GetAllItemInfos(ref pooledArray);
for (int i = 0; i < allItemInfos.Count; i++) {
    var itemInfo = allItemInfos[i];
    // 對 ItemInfo 進行操作
}
// 重要:用完歸還
GenericObjectPool.Return(pooledArray);

使用篩選與排序取得物品

// 取得所有 Mutable 物品,並依 Item Definition 名稱排序
var filteredAndSorted = itemCollection.GetItemInfos(ref itemInfos,
    x => x.Item.IsMutable,
    Comparer<ItemStack>.Create((i1, i2) =>
        i1.Item.ItemDefinition.Name.CompareTo(i2.Item.ItemDefinition.Name))
);

Item Slot Collection

Item Slot Collection 是一種特殊的 Item Collection,只允許將物品加入由 Item Slot Set(ScriptableObject)定義的特定槽位中。這種 Collection 類型常用於儲存裝備中的物品。

建立 Item Slot Set:

// 在專案視窗右鍵:
Create → Ultimate Inventory System → Inventory → Item Slot Set

Item Slot Collection API

// 取得 Item Slot Collection 並轉型
var equipmentCollection = m_Inventory.GetItemCollection(m_EquipmentItemCollectionID)
    as ItemSlotCollection;

// 以槽位索引新增 / 取得 / 移除
var itemAdded  = equipmentCollection.AddItem(itemInfo, slotIndex);
var itemInSlot = equipmentCollection.GetItemInfoAtSlot(slotIndex);
var itemRemoved = equipmentCollection.RemoveItem(slotIndex, amountToRemove);

// 也可以用槽位名稱操作
itemAdded  = equipmentCollection.AddItem(itemInfo, slotName);
itemInSlot = equipmentCollection.GetItemInfoAtSlot(slotName);

// 取得物品實際所在槽位的索引
var realSlotIndex = equipmentCollection.GetItemSlotIndex(myItem);

// 取得物品若加入時可能放入哪個槽位
var potentialSlotIndex = equipmentCollection.GetTargetSlotIndex(myItem);

Item Transaction Collection

Item Transaction Collection 是一種特殊的 Item Collection,一次只持有一個物品。它通常作為主要 Item Collection(Main),負責將新加入的物品自動轉移至正確的 Item Collection。當你的 Inventory 有多個子 Collection 且需要依規則分配物品時,這個元件非常實用。

可設定屬性

屬性說明
Item Collection Names指定要依序嘗試加入的 Item Collection 名稱清單。
Overflow Back To Origin物品被拒絕時,是否退回至來源位置(需要 ItemInfo 中含有來源資訊)。
Rejected Item Actions物品被拒絕時,對該物品執行的 Item Actions。
Return Real Added Item Amount是否只回傳實際加入的數量,或也包含被拒絕的數量。

運作邏輯:依序檢查所有連結的 Item Collections,使用 Item Restrictions 判斷物品是否符合,符合則加入;若全部都不符合,則觸發拒絕事件。

// 監聽物品被拒絕的事件
EventHandler.RegisterEvent<ItemInfo>(m_Inventory,
    EventNames.c_Inventory_OnRejected_ItemInfo, HandleItemInfoRejected);

Multi Stack Item Collection

Multi Stack Item Collection 允許同一個物品擁有多個 Item Stack。每個 Stack 有一個上限(由預設值或物品屬性指定),超過上限後會溢出到新的 Stack。

屬性說明
Default Stack Size LimitCommon 物品每個 Stack 的預設上限。
Stack Size Limit Attribute Name指向整數屬性的屬性名稱,用來針對特定 Common 物品覆寫 Stack 上限。

物品限制(Item Restrictions)

大多數遊戲的庫存都有限制(如空間、堆疊上限等)。UIS 預設沒有限制,你可以依需求自行設定。設置限制的三種主要方式:

  1. 使用 Item Restriction Set Object(ScriptableObject)
  2. 建立繼承 IItemRestriction 介面的自訂元件
  3. 建立自訂 Item Collections
搭配 Item Transaction Collection 使用 若需要處理物品被拒絕的情況(例如庫存已滿時的回饋),建議以 Item Transaction Collection 作為主要 Item Collection,並設定拒絕事件處理器。

Item Restriction Set Object

建立路徑:

Create → Ultimate Inventory System → Inventory → Item Restriction Set

最常用的兩種內建限制類型:

Item Collection Category Restriction
限制某些 Item Collections 只允許特定類別的物品加入。
Item Collection Stack Amount Restriction
限制 Inventory 中的 Item Stack 總數,設定最大持有物品種類上限。

自訂 Item Restriction

實作 IItemRestriction 介面即可建立自訂限制,並掛在 Inventory 旁邊,或加入 Item Restriction Set 中。

/// <summary>物品集合限制介面。</summary>
public interface IItemRestriction
{
    /// <summary>初始化。</summary>
    void Initialize(IInventory inventory, bool force);

    /// <summary>判斷物品是否可被加入。回傳 null 表示不允許。</summary>
    ItemInfo? AddCondition(ItemInfo itemInfo, ItemCollection receivingCollection);

    /// <summary>判斷物品是否可被移除。回傳 null 表示不允許。</summary>
    ItemInfo? RemoveCondition(ItemInfo itemInfo);
}

監聽物品拒絕事件

private void Start() {
    EventHandler.RegisterEvent<ItemInfo, ItemInfo, ItemInfo>(m_Inventory,
        EventNames.c_Inventory_OnAddItemRejected_ItemInfoToAdd_ItemInfoAdded_ItemInfoRejected,
        HandleItemRejection);
}

private void HandleItemRejection(ItemInfo itemInfoToAdd, ItemInfo itemInfoAdded, ItemInfo itemInfoRejected) {
    // ItemInfoToAdd:原始嘗試加入的 ItemInfo
    // ItemInfoAdded:成功加入的部分(部分加入時)
    // ItemInfoRejected:被拒絕的數量
}

動態庫存大小(Dynamic Inventory Size)

Dynamic Inventory Size 元件用來動態調整 Inventory 可以攜帶的物品數量上限。它會自動尋找「Bag」類型的物品(可以增減庫存空間上限的物品),並依此調整庫存大小。

  • 可以設定多個 Dynamic Inventory Size 元件,分別限制不同的 Item Collection 子集(例如武器欄最多 X 個,資源欄最多 Y 個)。
  • 可綁定至 Inventory Grid,透過 Dynamic Inventory Size Inventory Grid Binding 元件在 UI 中視覺化顯示庫存限制。
  • Display Amount Format 中使用 {0} 顯示目前物品數量,{1} 顯示最大空間。

物品(Item)

Item 類別是庫存系統的核心。Items 是簡單的資料物件(非 MonoBehaviour 或 Unity Object),可以是唯一的(Unique)或共用的(Common)。詳細差異請參考第 1 篇的術語說明。

Item API

建立物品

// 以名稱建立
var potion = InventorySystemManager.CreateItem("Potion");

// 指定預設 ID 建立(適合 Unique/Mutable 物品的存檔或網路同步)
var armor = InventorySystemManager.CreateItem("Armor", 777);
var armorIsUnique  = armor.IsUnique;
var armorIsMutable = armor.IsMutable;

// 以 Item Definition 建立
var swordDef = InventorySystemManager.GetItemDefinition("Sword");
var sword = InventorySystemManager.CreateItem(swordDef);

// 複製物品(連同執行期間修改過的屬性值一起複製)
var swordCopy = InventorySystemManager.CreateItem(sword);

取得與設定屬性

// 取得屬性並讀取值
var attackAttr = sword.GetAttribute<Attribute<int>>("Attack");
var attack = attackAttr.GetValue();

// 設定 Override 值(僅 Mutable 物品允許)
attackAttr.SetOverrideValue(attack + 5);

檢查類別 / 定義歸屬

var weapon = InventorySystemManager.GetItemCategory("Weapon");

// 檢查物品是否屬於指定 Category
var swordIsWeapon = weapon.InherentlyContains(sword);

// 檢查物品是否屬於指定 Definition(及其子 Definition)
var swordIsDef = swordDef.InherentlyContains(sword);

// 取得系統中所有已載入物件
var allCategories  = InventorySystemManager.ItemCategoryRegister.GetAll();
var allDefinitions = InventorySystemManager.ItemDefinitionRegister.GetAll();
var allItems       = InventorySystemManager.ItemRegister.GetAll();

物品資訊(Item Info)

ItemInfo 是一個包含豐富資訊的 struct,理解它需要先區分以下幾個容易混淆的概念:

類型說明特性
Item執行期間從 Item Definition 建立的物件Unique / Immutable 物品可能共用同一個參照
Item AmountItem + 數量的簡單結構(struct)不含 Stack 或 Collection 資訊;常用於 Inspector 序列化
Item StackItem + 數量(class),連結至 Item Collection不應在 Collection 外使用
Item InfoItem Amount + Item Stack + Item Collection 的完整資訊(struct)可明確指定來源和目標 Stack / Collection
Item Info 使用情境範例

假設庫存中有兩個 Stack:

Stack 1: 5 顆蘋果
Stack 2: 10 顆蘋果

若要從 Stack 2 移除 3 顆蘋果,可以使用 Item Info 明確指定來源 Stack。

操作後的回傳值:

Item Amount: 3(實際移除數量)
Item Stack: Stack 2 剩餘 7 顆(原 Stack 參照)
Item Collection: 該 Stack 所在的 Collection

建立 Item Info 的多種方式

new ItemInfo(itemAmount, itemCollection, itemStack);
new ItemInfo("MyItem", 1);
new ItemInfo(item, amount);
new ItemInfo(itemAmount);
new ItemInfo(itemStack);
new ItemInfo(itemAmount, otherItemInfo);
new ItemInfo(amount, otherItemInfo);
new ItemInfo(itemDefinition, amount);
new ItemInfo(otherItemInfo, amount);
new ItemInfo(amount, item);

物品升級(Item Upgrades)

UIS 提供多種實作物品升級系統的方式,沒有絕對最佳解,選擇取決於你遊戲的設計需求。以下介紹四種主要方案:

方案 1:Parent / Child Item Definitions

為每個升級等級建立一個獨立的 Item Definition,以父子關係連結(例如 Sword → Sword+1 → Sword+2)。

透過 Upgrade 屬性(型別為 Item Definition)記錄下一個升級目標。

簡單直觀 線性升級
方案 2:Item Definition 與 Item 屬性

在 Item Definition 上設定陣列屬性(如 UpgradeName[]、UpgradeAttack[]),在 Item 上設定當前等級和攻擊力,透過腳本計算升級結果。

物品數量少時適用
方案 3:升級物品 + Slots(Demo 方案)

可升級物品擁有 Slots 屬性(ItemAmounts 類型),可插入「升級物品」。升級物品可套用至任何同類別的可升級物品。

高度靈活 需要自訂腳本
方案 4:合成系統(Crafting)

以合成配方定義升級邏輯(如 1 武器 + 5 材料),客製化 Crafting Processor 計算升級結果,同一配方可用於所有同類別武器。

複雜系統適用 中階程式能力需求

物品技能(Item Skills)

由於屬性系統的高度彈性,UIS 非常適合將物品實作為「技能」——即在同一類別下,每個物品都有獨特的功能邏輯。這與 UI 中使用 Category Item Action Set 讓同類別物品執行相同動作的方式不同。以下是三種主要實作選項:

選項 1:使用 ScriptableObject 作為屬性

在 Item 上設定一個 ScriptableObject 型別的屬性(例如 Item Action Set),透過程式碼取得該屬性並呼叫其中的函式,以 Item 和相關資訊作為參數。

/// <summary>嘗試使用物品技能。</summary>
protected bool TryUseItem(ItemInfo itemInfo, ItemUser itemUser) {
    if (itemInfo.Item == null) { return false; }

    // 取得 ScriptableObject 屬性
    var attribute = itemInfo.Item.GetAttribute<Attribute<ItemActionSet>>(m_AttributeName);
    if (attribute == null) { return false; }

    var actionSet = attribute.GetValue();
    if (actionSet == null) { return false; }

    // 執行指定索引的 Item Action
    if (m_UseOne) { itemInfo = (1, itemInfo); }
    actionSet.ItemActionCollection[m_ActionIndex].InvokeAction(itemInfo, itemUser);

    if (m_RemoveOnUse) { itemInfo.ItemCollection?.RemoveItem(itemInfo); }
    return true;
}
提示 使用 Use Item Action Set Attribute 這個內建 Item Action,可以在 UI 中無需撰寫任何程式碼,直接將物品作為技能使用。透過 itemUser.gameObject.GetCachedComponent<T>() 可以高效取得角色身上的元件。

選項 2:在 Item Object 旁邊加入元件

只適用於物品作為 Item Object 出現在場景中的情況。透過 Item Object Behaviours Handler,根據綁定的 Item 動態切換不同的 Item Object Behaviour,改變 Item Object 在場景中的行為邏輯。

選項 3:結合選項 1 和選項 2

最靈活的方案。用 ScriptableObject 屬性定義技能邏輯,在需要場景互動時(如角色位置、物理偵測)透過 Item User 取得場景中的元件。適合複雜的技能系統設計。

物品數值(Item Stats)

物品透過屬性系統,可以非常靈活地影響角色或武器的各種數值。以下是幾種常見的實作模式:

方式 1:統計 Collection 中屬性總和

// 直接用 Item Collection 取得所有物品指定屬性的 Float 總和
var statSum = itemCollection.GetFloatSum("MyStat");

// 或使用 AttributeUtility 對任意 Item Stack 清單求和
var statSum = AttributeUtility.GetFloatSum("MyStat", itemCollection.GetAllItemStacks());

方式 2:角色數值(搭配 Equipper)

最常見的模式是裝備物品時更新角色數值。監聽 Equipper 的 c_Equipper_OnChange 事件,在裝備變更時重新計算。

public class ExampleCharacterStats
{
    protected IEquipper m_Equipper;
    protected int m_BaseMaxHp, m_BaseAttack, m_BaseDefense;
    protected int m_MaxHp, m_Attack, m_Defense;

    public ExampleCharacterStats(int maxHp, int attack, int defense, IEquipper equipper)
    {
        m_BaseMaxHp = maxHp;  m_BaseAttack = attack;  m_BaseDefense = defense;
        m_Equipper = equipper;
        if (m_Equipper != null) {
            // 監聽裝備變更事件
            EventHandler.RegisterEvent(m_Equipper, EventNames.c_Equipper_OnChange, UpdateStats);
        }
        UpdateStats();
    }

    public void UpdateStats()
    {
        // 每次裝備變更都重新計算(避免浮點數累積誤差)
        m_MaxHp    = m_BaseMaxHp    + m_Equipper.GetEquipmentStatInt("MaxHp");
        m_Attack   = m_BaseAttack   + m_Equipper.GetEquipmentStatInt("Attack");
        m_Defense  = m_BaseDefense  + m_Equipper.GetEquipmentStatInt("Defense");
    }
}
重要設計原則 每次重新計算數值(而不是加減),可以避免浮點數精度累積誤差。建議永遠保持基礎數值與裝備加成分離。

物品動作(Item Actions)

Item Actions 用於從庫存中對物品執行操作,例如「使用」、「丟棄」、「裝備」等。不應與 Item Object Behaviours 混淆——後者是在遊戲世界中使用物品(如揮劍、射擊),需要 GameObject 的參照。

Item Actions 有兩個核心方法:

  • CanInvoke(ItemInfo, ItemUser):判斷動作是否可以執行。
  • Invoke(ItemInfo, ItemUser):執行動作邏輯。

Item User 允許傳入玩家角色的參照,方便取得角色身上的元件。

Item Action Set 與 Category Item Action Set

建立路徑:

Create → Ultimate Inventory System → Item Actions → Item Action Set

Item Action Set 可以設定一組與特定 Item Category 相關的 Item Actions(例如「Pickupable」類別對應「Drop」動作)。也可以設定「排除類別(Exclude Categories)」,由 Category Item Action Set 使用。

Category Item Action Set 是多個 Item Action Sets 的集合,由 Item View Slots Container 上的 Binding 元件使用,可根據選取的物品自動匹配可用的動作。

自訂 Item Action

[System.Serializable]
public class MyItemAction : ItemAction
{
    /// <summary>判斷動作是否可執行。</summary>
    protected override bool CanInvokeInternal(ItemInfo itemInfo, ItemUser itemUser)
    {
        return true; // 自訂判斷邏輯
    }

    /// <summary>執行動作。</summary>
    protected override void InvokeActionInternal(ItemInfo itemInfo, ItemUser itemUser)
    {
        // 自訂動作邏輯
    }
}

透過程式碼直接呼叫 Item Action

public void ManuallyCallDropItemAction()
{
    // 1. 取得物品的 ItemInfo
    var result = m_Inventory.GetItemInfo(InventorySystemManager.GetItemDefinition("Apple"));
    if (!result.HasValue) { return; }

    var appleItemInfo = result.Value;
    appleItemInfo = new ItemInfo(Mathf.Max(2, appleItemInfo.Amount), appleItemInfo);

    // 2. 取得 Item User
    var itemUser = m_Inventory.GetComponent<ItemUser>();

    // 3. 建立並初始化 Item Action(可快取重複使用)
    var dropItemAction = new DropItemAction();
    dropItemAction.Initialize(false);

    // 執行動作
    dropItemAction.InvokeAction(appleItemInfo, itemUser);

    // 部分動作提供額外資訊(例如 Drop Action 記錄最後掉落的物件)
    var droppedPickup = dropItemAction.PickUpGameObject;
}
注意 部分 Item Actions(如 MoveItemAction)繼承了 IActionWithPanel,需要 ItemViewSlotContainer 或面板的參照,無法直接在程式碼中呼叫,必須透過 UI 觸發或手動在程式碼中設定這些參照。

內建物品動作

簡單動作(立即執行)

動作名稱說明
Debug Item Action在 Console 印出物品所有屬性值,適合除錯。
Debug Item Object Action同上,同時也印出 Item Object 的資訊。
Drop Item Action將物品從庫存丟出,需設定含有 Item Pickup 元件的 Prefab 參照。物品會在 Item User 附近生成。
Duplicate Item Action複製物品並加入至同一 Inventory。
Move To Collection Item Action在同一 Inventory 的不同 Item Collection 之間移動物品,可用來實作裝備/取下。
Remove Item Action從庫存刪除物品(不掉落,直接移除)。
Use Item Action Set Attribute呼叫物品上 Item Action Set 屬性中的 Item Action,並支援自動設定 Cooldown。
Multi Item Action將多個簡單 Item Actions 合併成一個,不需撰寫程式碼即可組合複雜動作。

帶面板的動作(需玩家互動)

動作名稱說明
Quantity Drop Item Action顯示數量選擇面板讓玩家選擇丟棄數量。
Assign Hotbar Item Action讓玩家將物品指派至快捷列某個槽位。需要 Item Hotbar Owner 介面。
Item Action With Confirmation Pop Up在執行巢狀 Item Action 前,先顯示確認視窗讓玩家確認。
Move Item Action使用 Item View Slot Move Cursor 在 Item View Slots 之間移動物品。
Open Other Item View Slot Container開啟另一個面板,監聽物品選取事件(例如搜尋並選擇物品)。

物品物件(Item Objects)

Item Object 元件將特定物品(Item)與 GameObject 連結起來。任何需要在遊戲世界中顯示的物品(如撿取物、裝備)都應加上 Item Object 元件。Item Object 可以靈活地綁定/解綁物品,因此非常適合物件池(Object Pool)重複利用,只需更換綁定的物品即可。

當物品被附加或分離時,Item Object 會觸發事件,其他元件可監聽此事件更新遊戲世界中的外觀。

Item Object API

// 監聽物品變更事件
EventHandler.RegisterEvent(m_ItemObject,
    EventNames.c_ItemObject_OnItemChanged, HandleItemChanged);

private void HandleItemChanged() {
    var itemInfo = m_ItemObject.ItemInfo;
}

// 設定物品
var newItem = InventorySystemManager.CreateItem("MyItemName");
m_ItemObject.SetItem(newItem);
m_ItemObject.SetAmount(5);
m_ItemObject.SetItem((ItemInfo)(5, newItem));

// 從 Unique 物品取得其 Item Object
var itemObject = newItem.GetLastItemObject();

// 若物品非 Unique,可能綁定多個 Item Objects
var count = newItem.GetItemObjectCount();
for (int i = 0; i < count; i++) {
    var obj = newItem.GetItemObjectAt(i);
}

Item Object Behaviour Handler

Item Object Behaviour Handler 坐在 Item Object 旁邊,由 Equipper 使用來觸發已裝備物品的使用行為。一個 Handler 可以連結多個 Item Object Behaviours(例如「槍」可以有開火、換彈、瞄準等動作)。

自訂 Item Object Behaviour 繼承基礎類別即可:

public abstract class ItemObjectBehaviour : MonoBehaviour
{
    protected float m_NextUseTime;

    /// <summary>是否可以使用(預設:冷卻時間已過)。</summary>
    public virtual bool CanUse => Time.time >= m_NextUseTime;

    /// <summary>使用物品物件。</summary>
    public abstract void Use(ItemObject itemObject, ItemUser itemUser);
}
提示 Item Object Behaviours 與 Item Bindings 搭配使用效果極佳——Item Binding 負責將物品屬性(如攻擊力、子彈數)自動同步至武器元件的 Property,Item Object Behaviour 負責執行實際動作邏輯。

裝備系統(Equipping Items)

裝備系統由三個核心元件組成:Item Slot CollectionItem Slot Set、以及 Equipper

說明 裝備系統本身不負責數值變化。數值計算由 Character Stats 等自訂腳本處理。系統完全獨立,你可以自行替換。

Item Slot Set(槽位定義)

Item Slot Set 是一個 ScriptableObject,定義了一組裝備槽位。每個槽位由名稱、Item Category 限制和數量上限組成。例如「Right Hand」槽位可以設定只允許 Weapon 類別的物品,且上限為 1 個。

Item Slot Collection(裝備 Collection)

物品加入 Item Slot Collection 時,系統會依序:

  1. 確認物品符合某個槽位的限制。
  2. 若槽位為空則直接加入;若槽位已有物品,則替換之,被替換的物品會移至 Main Item Collection。

Equipper(裝備器)

Equipper 負責生成並視覺化裝備 Item Slot Collection 中的物品。針對每個槽位,可設定是否為 Skinned Mesh,以及生成位置。

Equipper 支援兩種 Prefab 屬性:

Equipment Prefab(裝備外觀 Prefab)
通常設為 Item Definition 屬性。裝備時生成,包含視覺模型(Skinned Mesh 或 Mesh Renderer)。一般不含邏輯腳本。
Usable Item Prefab(可使用物品 Prefab,選用)
通常設為 Item Category 屬性。包含使用邏輯(如近戰攻擊、射擊腳本),但不含模型。Equipment Prefab 生成時會作為此 Prefab 的子物件。

這種分離設計讓多個物品可共用同一套使用邏輯 Prefab,只需替換外觀模型。搭配 Item Binding 可動態切換「攻擊力」、「子彈數量」等屬性。

Skinned Mesh 裝備

Skinned Mesh 裝備 Prefab 的結構建議為:頂層 Transform → 兩個子物件(Skinned Mesh 和完整角色骨架)。

重要 Skinned Mesh 裝備的 Prefab 必須包含與角色相同層級結構的骨架。沒有骨架時,系統無法將物品的骨骼與角色骨骼對應。生成時,骨架會被丟棄,只留下 Skinned Mesh 附著至角色骨骼。

屬性綁定(Item Binding)

Item Binding 元件可以將物品的屬性值自動同步至遊戲物件上任何元件的同類型 Property。這讓同步物品資料變得極為簡單,完全可以在 Inspector 中設定,無需撰寫程式碼。

設定步驟

  1. 選取要加入 Binding 的 GameObject(通常是武器的視覺呈現物件)。
  2. 在 GameObject 上加入 Item Object(或 Usable Item Object)元件。
  3. 加入 Item Binding 元件。
  4. 選取資料庫和要綁定的屬性所屬 Item Category。
  5. 指定包含目標 Property 的元件。
  6. 選擇要綁定的 Property。
注意 Item Binding 作用於 Property,而非 Field。確保你要綁定的變數有公開的 getter/setter Property,而不只是 SerializeField。
Field 與 Property 範例
[SerializeField] protected int m_BulletsPerFire; // 這是 Field,無法綁定
public int BulletsPerFire { // 這是 Property,可以綁定
    get => m_BulletsPerFire;
    set => m_BulletsPerFire = value;
}

物品撿取(Item Pickups)

Item Pickup 系統使用 Interactable(可互動物件)和 Interactor(互動者)機制。主要的撿取類型有四種:

類型說明
Item Pickup以 Item Object 元件決定撿取哪個物品。支援 Item Object Visualizer 動態更換外觀。
Inventory Pickup讓互動者撿取整個 Inventory 中所有物品的複製品。
Currency Pickup使用 Currency Collection 設定金幣數量,撿取後給予互動者。
Random Inventory Pickup以 Inventory 中的物品數量作為機率表,隨機抽取部分物品(適合戰利品箱)。

撿取物件的必要元件

角色 GameObject 需要:

  • Rigidbody / Rigidbody2D + Collider / Collider2D
  • Inventory 元件
  • 繼承「Interactor With Inventory」介面的元件(負責讓撿取物知道角色有 Inventory)

撿取物 GameObject 需要:

  • Item Pickup 元件(或其他撿取類型)
  • Interactable 元件 + Rigidbody / Rigidbody2D + Collider / Collider2D
  • Collider 必須設為 Trigger
  • 在 Interactable 元件上選擇 2D 或 3D 觸發事件,並設定角色所在的 Layer

丟棄物品的多種方式

  • Item Action:Drop Item Action / Quantity Drop Item Action
  • Item View Drop Action(拖放動作):Spawn Item Object(需場景中有 Item Object Spawner)
  • Item Object Spawner 元件(通常在 Game 物件上)
  • 自訂腳本(生成 Pickup Prefab 後設定 Item Object 元件上的物品)

物品掉落(Droppers)

Dropper 元件是最方便設計師使用的掉落解決方案,適用於敵人死亡、解謎完成、砍樹等各種觸發情境。共有四種 Dropper:

元件說明
Item Dropper掉落特定物品,支援 Item 和 Inventory Pickups。
Currency Dropper掉落特定貨幣。
Random Item Dropper以 Inventory 中的物品數量作為機率表,隨機掉落。適合敵人死亡時的戰利品生成。
Random Currency Dropper以正態分佈在最小和最大範圍內隨機掉落貨幣(以 Currency Owner 的基礎貨幣金額加上百分比偏移為範圍)。

Item Object Spawner API

// 取得 Item Object Spawner(建議放在 InventorySystemManager 附近)
var itemObjectSpawner = InventorySystemManager.GetGlobal<ItemObjectSpawner>(spawnerID);

// 生成物品
itemObjectSpawner.Spawn(itemInfo, spawnPosition);
itemObjectSpawner.SpawnWithDelay(itemInfo, spawnPosition, delay);
itemObjectSpawner.SpawnWithAutoDestroy(itemInfo, spawnPosition, autoDestroyTimer);
itemObjectSpawner.Spawn(itemInfo, spawnPosition, delay, autoDestroyTimer);

呼叫 Dropper

// 取得 Dropper 元件並呼叫 Drop()
dropper.Drop();

// 指定掉落特定物品
dropper.DropItemPickup(item);
dropper.DropInventoryPickup(itemList);

自訂 Random Item Dropper(隨機屬性範例)

繼承 RandomItemDropper 並覆寫 Drop() 方法,可以在掉落前修改物品的隨機屬性(如稀有度、攻擊力)。以下是完整實作範例:

[Serializable]
public enum Rarity { Common, Rare, Legendary }

[Serializable]
public struct RarityAmount {
    public Rarity Rarity;
    public int Amount;
}

[Serializable]
public struct RarityDistribution {
    public Rarity Rarity;
    public AnimationCurve Distribution; // X 軸需在 0~1 之間
}

public class CustomRandomRarityStatDropper : RandomItemDropper
{
    [SerializeField] protected RarityAmount[]      m_RarityProbability;
    [SerializeField] protected RarityDistribution[] m_AttackProbability;

    public override void Drop()
    {
        var itemsToDrop = GetItemsToDrop();
        AssignRandomStats(itemsToDrop);  // 在掉落前設定隨機屬性
        DropItemsInternal(itemsToDrop);
    }

    protected virtual void AssignRandomStats(ListSlice<ItemInfo> itemAmounts)
    {
        for (int i = 0; i < itemAmounts.Count; i++) {
            var item = itemAmounts[i].Item;
            if (item == null || !item.HasAttribute("Rarity")) { continue; }

            // 設定隨機稀有度
            var itemRarity = GetRandomRarity();
            item.GetAttribute<Attribute<Rarity>>("Rarity").SetOverrideValue(itemRarity);

            // 依稀有度設定隨機攻擊力
            var newAttack = GetAttackForRarity(itemRarity);
            item.GetAttribute<Attribute<float>>("Attack").SetOverrideValue(newAttack);
        }
    }

    public float GetAttackForRarity(Rarity rarity) {
        foreach (var dist in m_AttackProbability) {
            if (dist.Rarity == rarity)
                return dist.Distribution.Evaluate(Random.value);
        }
        return 0;
    }
}
前提條件 若要在執行期間覆寫屬性值,物品必須設定為 Mutable & Unique

此網誌的熱門文章

哥利亞遙控炸彈 (Leichter Ladungsträger Goliath)

O-I(試製120t超重戰車「オイ」)