《UIS》 第 4 篇 屬性、貨幣、商店與合成系統
本篇文章、圖片出處來自 Ultimate Inventory System
軟體版本與本翻譯文件可能會有落差,本翻譯文件僅供參考。
本譯文為本站譯者原創翻譯內容,文字著作權歸本站所有。
未經授權,請勿任意轉載、改作或商業使用。
本篇文章、圖片出處來自 Ultimate Inventory System
軟體版本與本翻譯文件可能會有落差,本翻譯文件僅供參考。
本譯文為本站譯者原創翻譯內容,文字著作權歸本站所有。
未經授權,請勿任意轉載、改作或商業使用。
屬性深入解析(Attributes)
屬性(Attribute)是一個擁有名稱和數值的物件,數值可以是任意類型。預設所有常見類型(int、float、string、Vector3 等)都可在屬性下拉選單中選擇,也可透過 Unit Options 視窗新增自訂類型(路徑:Tools → Opsive → Unit Options)。
屬性類別也可以繼承並擴展,以修改表達式的解析邏輯。建立後在 Unit Options 編輯器中新增,系統就會自動偵測繼承了 Attribute<T> 的類別,讓你在下拉選單中選用。
三種變體類型與繼承關係
每個屬性以三種變體之一取得其值:
| 變體 | 取值邏輯 |
|---|---|
| Inherit | 從父層屬性取得數值,父層的定義依附加對象而定。 |
| Override | 直接使用自訂指定的數值。 |
| Modify | 使用表達式計算出依賴其他屬性或繼承值的結果。 |
Inherit 變體的繼承鏈:
| 子屬性附加於 | 父屬性附加於 |
|---|---|
| Item | Default Item |
| Default Item | 父 Item Definition 的 Default Item,或 Item Category(Item 屬性) |
| Item Definition | 父 Item Definition,或 Item Category(Item Definition 屬性) |
| Item Category | 第一個包含此屬性的父 Item Category,或 null(回傳類型預設值) |
Modify 變體與表達式語法
Modify 變體支援的屬性類型:Int、Float、String。
其他類型若需要自訂 Modify 表達式,必須建立自訂 Attribute 類型並實作解析器。為了一致性,建議遵循官方的語法規範。
表達式語法參考
| 語法 | 說明 |
|---|---|
| [myOtherAttribute] | 取得名稱為 myOtherAttribute 的屬性值。 |
| <Override> | 取得目前屬性自身的 Override 值。 |
| <Inherited> | 取得繼承自父層的值。 |
| $[myOtherAttribute] | 從持有表達式的屬性本身的視角取值(而非請求值的那一側)。 |
| $<Inherited> | 同上,從持有表達式的屬性視角取繼承值。 |
$ 符號。
<Inherited> + 5 → 繼承值加 5[BaseAttack] * 1.5 → BaseAttack 屬性的 1.5 倍$[BaseAttack] * 2 → 從持有表達式那側取 BaseAttack 的 2 倍<Inherited> * 20 + [BonusDamage] → 繼承值乘以 20 再加上 BonusDamage
屬性 API(Get / Set)
在 Item Categories、Item Definitions 和 Items 上取得和設定屬性值的方式大致相同,以下以 Item 為例:
讀取屬性
// 取得攻擊力屬性
var attackAttribute = item.GetAttribute<Attribute<int>>("Attack");
if (attackAttribute != null) {
var myAttack = attackAttribute.GetValue(); // 實際值
var myAttackOverride = attackAttribute.OverrideValue; // Override 原始值
var myAttackInherited = attackAttribute.GetInheritedValue(); // 繼承值
}
// 用 TryGetAttributeValue 快速取得值(推薦用於 Sprite 等類型)
if (item.TryGetAttributeValue("Icon", out Sprite myIcon)) {
// myIcon 已取得
}
// 遍歷物品上所有屬性
var includeItemDef = true;
var includeItemCategory = true;
var count = item.GetAttributeCount(includeItemDef, includeItemCategory);
for (int i = 0; i < count; i++) {
var attribute = item.GetAttributeAt(i, includeItemDef, includeItemCategory);
Debug.Log(attribute.GetValueAsObject()); // 以 object 取得值(類型未知時使用)
}
設定屬性值(僅 Mutable 物品允許)
if (!item.IsMutable) { /* Immutable 物品無法設定屬性值 */ }
var attackAttribute = item.GetAttribute<Attribute<int>>("Attack");
if (attackAttribute != null) {
// 切換變體類型
attackAttribute.SetVariantType(VariantType.Inherit);
// 設定 Override 值(自動切換至 Override 變體)
attackAttribute.SetOverrideValue(10);
// 設定 Modify 表達式(自動切換至 Modify 變體,並標記為 Pre-evaluated)
attackAttribute.SetModifyExpression("<Inherited> + 5", true);
}
處理類別(Class)屬性的注意事項
var itemSlotsAttr = item.GetAttribute<Attribute<ItemAmounts>>("Slots");
if (itemSlotsAttr != null) {
var itemSlots = itemSlotsAttr.GetValue();
// 切勿直接修改!必須先確認是否為 Override 值
if (itemSlots == itemSlotsAttr.OverrideValue) {
itemSlots.Add(newItem); // 是 Override 值,可直接修改
} else {
// 是繼承值,需先複製再修改,最後設回屬性
itemSlots = new ItemAmounts(itemSlots);
itemSlots.Add(newItem);
itemSlotsAttr.SetOverrideValue(itemSlots);
}
}
SetOverrideValue 設回。
常用屬性(Common Attributes)
以下列出系統內建元件預設會使用的屬性名稱。只要在你的 Item Category 中使用相同名稱,對應的元件就能自動識別並使用它們。
| 屬性名稱(類型) | 通常定義在 | 使用說明 |
|---|---|---|
| Description <string> | Item Definition 屬性 | 用於 Item Description 元件顯示說明文字。 |
| Icon <Sprite> | Item Definition 屬性 | 用於 Icon Item View 元件顯示物品圖示,以及 Editor 中的預覽方塊(若 Editor Icon 為 null)。 |
| CategoryIcon <Sprite> | Item Category 屬性 | 用於 Editor 中 Item Category 的預覽圖示(若 Editor Icon 為 null)。 |
| Price / SellPrice / BuyPrice <CurrencyAmounts> | Item Definition 屬性 | 定義物品價格。商店中可分別設定購買價和販售價的屬性名稱,部分 UI Prefab(如 Big Item Description)預設使用 BuyPrice 和 SellPrice。 |
| PickupPrefab <GameObject> | Item Definition 屬性 | Item Object View 元件預設使用此屬性,在物品綁定至 Item Object 時(如丟棄撿取物)生成對應的視覺 Prefab。 |
| UsableItemPrefab <GameObject> | Item Category 屬性 | Equipper 使用,裝備非 SkinMesh 物品時生成的可使用物品 Prefab(含邏輯腳本,如近戰攻擊、射擊)。 |
| EquipmentPrefab <GameObject> | Item Definition 屬性 | Equipper 使用,裝備物品時生成的視覺模型 Prefab。會生成在 UsableItemPrefab 的子物件下。 |
| ItemActionSet <ItemActionSet> | Item Definition 屬性 | 與「Use Item Action Set Attribute」Item Action 搭配,讓每個物品可以有各自不同的 Item Actions(例如各種魔法卷軸)。 |
| Shape <ItemShape> | Item Definition 屬性 | 用於 Item Shape Inventory Grid 系統,定義物品在格子中的形狀。 |
| Slots <ItemAmounts> | Item Definition / Item 屬性 | 用於「Item Slots Item View」,顯示物品上附加的其他物品圖示(例如武器的升級插槽)。 |
| IsEquipped <bool> | Item 屬性(需 Mutable) | 用於「Equipped Item View」,標記物品是否已裝備。通常可由系統自動判斷,此屬性為選用。 |
| StackSizeLimit <int> | Item Definition 屬性 | 用於 Multi Stack Item Collection,限制特定物品每個 Stack 的最大數量。 |
| CategorySizeLimit <int> | Item Category 屬性 | 用於 Group Item Restriction,限制該 Item Category 物品在庫存中的持有上限(選用)。 |
| DefinitionSizeLimit <int> | Item Definition 屬性 | 用於 Group Item Restriction,限制該 Item Definition 物品的持有上限(選用)。 |
| ItemSizeLimit <int> | Item 屬性 | 用於 Group Item Restriction,限制特定物品個體的持有上限(選用)。 |
常用屬性類型(Common Types)
| 類型 | 說明 | 支援 Modify 表達式 |
|---|---|---|
| Int | 整數,適合生命值、攻擊力、等級等整數數值。 | ✓ |
| Float | 浮點數,適合速度、百分比、精確數值。 | ✓ |
| Bool | 布林值,適合旗標性質的開關狀態。 | ✗ |
| String | 字串,適合名稱、說明、技能描述。 | ✓ |
| Unity Objects (GameObject, Sprite, Material…) | 任何 Unity 物件皆可作為屬性,適合參照 Prefab、圖示、音效等資源。 | ✗ |
| Item Amount | 物品與數量,用於物品插槽和附件。支援巢狀物品的序列化 / 反序列化(存讀檔)。 | ✗ |
| Currency Amounts | 多種貨幣的金額集合,通常作為 Item Definition 屬性定義物品售價。 | ✗ |
| Item Shape | 以布林格子和錨點定義物品在 Item Shape Inventory Grid 中的外形。 | ✗ |
| Item Action Set | Scriptable Object,可用來為每個物品定義各自專屬的 Item Actions(非 Category 統一的動作)。 | ✗ |
貨幣系統(Currency)
UIS 內建的貨幣功能允許商店買賣、在 Inventory 之間交換物品。貨幣實作已被抽象化,讓你可以實作自訂貨幣類型;預設的實作非常通用,足以應對大多數使用情境。
Currency 物件本身只包含「如何轉換至另一種貨幣」的資料,必須搭配 Currency Collection 才能真正運作。
Max Value: 99 │ Overflow Currency: SilverSilver(銀幣)
Base Currency: Bronze │ Base Exchange Rate: 1 Silver = 100 BronzeMax Value: 99 │ Overflow Currency: Gold │ Fraction Currency: BronzeGold(金幣)
Base Currency: Silver │ Base Exchange Rate: 1 Gold = 100 SilverMax Value: 99 │ Fraction Currency: SilverRoot Currency(根貨幣)為 Bronze。不共享同一根貨幣的兩種貨幣無法互相換算。
貨幣 API
// 取得貨幣
var dollars = InventorySystemManager.GetCurrency("Dollars");
// 建立 CurrencyAmount(兩種寫法皆可)
var fiftyDollars1 = new CurrencyAmount(50, dollars);
var fiftyDollars2 = (50, dollars); // 隱式轉型語法
// 換算匯率
var euro = InventorySystemManager.GetCurrency("Euro");
if (dollars.TryGetExchangeRateTo(euro, out var dollarsToEuro)) {
var fiftyDollarsInEuros = (50 * dollarsToEuro, euro);
}
Currency Collection
Currency Collection 包含貨幣的集合,並提供加法、減法、除法和乘法運算,且運算結果會自動符合各幣種的 Max Value 和 Fraction/Overflow 規則。
1 Silver 99 Bronze + 1 Gold 1 Silver 1 Bronze = 1 Gold 2 Silver 0 Bronze1 Silver – 99 Bronze = 1 Bronze3 Silver / 40 Bronze → 商:7,餘:20 Bronze1.5 × (2 Silver 1 Bronze) = 3 Silver 1 Bronze50 × (3 Silver 10 Bronze) = 1 Gold 55 Silver 0 Bronze
Currency Owner
Currency Owner 本質上是角色的「貨幣庫存」。若角色需要使用貨幣,就應在角色身上加入 Currency Owner 元件。貨幣的概念非常抽象,可以是物品、數值或任何概念,系統不強制限制貨幣的類型。若需要自訂貨幣邏輯,可繼承 CurrencyOwner<CurrencyT> 類別。
Currency Owner API
// 從 Inventory 取得 Currency Owner
var inventory = InventorySystemManager.GetInventoryIdentifier(0).Inventory;
var currencyOwner = inventory.GetCurrencyComponent<CurrencyCollection>() as CurrencyOwner;
// 或直接從 InventoryIdentifier 取得
currencyOwner = InventorySystemManager.GetInventoryIdentifier(0).CurrencyOwner;
// 取得貨幣集合
var ownerCurrencyCollection = currencyOwner.CurrencyAmount;
var gold = InventorySystemManager.GetCurrency("Gold");
var silver = InventorySystemManager.GetCurrency("Silver");
// 建立獨立的 Currency Collection
var otherCollection = new CurrencyCollection();
otherCollection.AddCurrency(new CurrencyAmount[] { (10, silver), (10, gold) });
// 新增貨幣(三種方式)
ownerCurrencyCollection.AddCurrency(gold, 10);
ownerCurrencyCollection.AddCurrency(new CurrencyAmount[] { (10, silver), (10, gold) });
ownerCurrencyCollection.AddCurrency(otherCollection);
// 扣除貨幣
ownerCurrencyCollection.RemoveCurrency(gold, 10);
ownerCurrencyCollection.RemoveCurrency(new CurrencyAmount[] { (10, silver), (10, gold) });
ownerCurrencyCollection.RemoveCurrency(otherCollection);
// 設定貨幣(直接覆寫)
ownerCurrencyCollection.SetCurrency(new CurrencyAmount[] { (10, silver), (10, gold) });
// 移除全部
ownerCurrencyCollection.RemoveAll();
// 查詢擁有量
Debug.Log(currencyOwner.CurrencyAmount);
ownerCurrencyCollection.AddCurrency(dollars, 50);
Debug.Log(ownerCurrencyCollection.HasCurrency(dollars, 30)); // true
Debug.Log(ownerCurrencyCollection.HasCurrency(dollars, 70)); // false
商店(Shop)
商店用來以貨幣買賣物品。預設使用 Currency Collection 作為貨幣類型,也可以繼承 Shop<CurrencyType> 實作自訂貨幣的商店。
買賣 API
/// 向商店購買物品
BuyItem(buyerInventory, currencyOwnerBase, item, amount);
/// 向商店販售物品
SellItem(sellerInventory, currencyOwnerBase, item, amount);
物品定價
物品價格以屬性方式定義在 Item Definition 上。建議分別建立名稱為 BuyPrice 和 SellPrice 的 Item Definition 屬性,類型為 Currency Amounts。
BuyPrice,讓買賣都從同一個屬性取得價格。
Shop 元件設定
| 設定 | 說明 |
|---|---|
| Buy Price Attribute Name | 購買時讀取的屬性名稱(預設:BuyPrice)。 |
| Sell Price Attribute Name | 販售時讀取的屬性名稱(預設:SellPrice)。 |
| Buy Price Modifier | 購買價格乘數。例如設 1.2 表示購買價提高 20%。 |
| Sell Price Modifier | 販售價格乘數。例如設 -0.2 表示販售價降低 20%。 |
商店運作模式
預設情況下,商店老闆擁有一個 Inventory,其中包含可販售的物品。物品售出時會被複製,讓商店保持無限供貨。也可以修改此行為:
- 有限庫存:不複製物品,售完即止。
- 買回機制:販售後物品加入商店 Inventory,讓玩家可以買回。
這些功能需繼承 Shop Currency Collection 類別自行實作。
Shop Add Remove Binding
Shop Add Remove Binding 元件讓你輕鬆控制:購買時從商店移除物品(有限庫存),以及販售時將物品加入商店(買回機制)。
合成系統(Crafting)
合成系統非常通用且可擴展,設計上讓簡單的合成也能輕鬆使用。以下是核心元件架構:
Category
Recipe
Processor
Result
各元件說明:
Recipe 彈性範例
2 治療草 + 1 泉水 → 1 治療藥水1 治療藥水(品質 == good)+ 1 魔法寶珠 → 1 生命魔法寶珠1 劍 + 5 材料(任意材料類別)→ 1 神劍例如:
1 劍 + 2 鐵錠 + 1 棉花 + 2 石塊 → 1 神劍
Crafting Processor API
/// <summary>合成物品。</summary>
public CraftingResult Craft(
CraftingRecipe recipe,
IInventory inventory,
ListSlice<ItemInfo> selectedIngredients, // 若未指定,處理器會自動從 Inventory 挑選
int quantity = 1
)
Crafter 元件
Crafter 是場景中觸發合成的核心元件,持有 Crafting Processor 和可合成的配方清單。
| 屬性 | 說明 |
|---|---|
| Crafting Processor | 預設使用「Simple Crafting Processor With Currency」,可在 Inspector 的下拉選單中更換。自訂 Processor 建立後會自動出現在清單中。 |
| Remove Ingredients Externally | 若為 true,材料不會由系統自動移除,而是透過事件 c_InventoryGameObject_OnCraftRemoveItem_… 通知外部處理。 |
| Crafting Categories | 指定的 Crafting Category 下的所有配方都會加入可合成清單。 |
| Miscellaneous Recipes | 手動加入的獨立配方清單(不隸屬於任何 Category)。 |
// 在執行期間編輯可合成配方清單
List<CraftingRecipes> recipes = m_Crafter.CraftinRecipes;
// 隨時更換 Crafting Processor
m_Crafter.Processor = newProcessor;
客製化 Crafting Processors
系統內建兩種 Processor:Simple Crafting Processor 和 Simple Crafting Processor With Currency,可以作為繼承的基礎進行擴展。
範例一:加入等級限制
有兩種實作方式:(1)以 Crafting Category 區分等級,(2)建立含等級欄位的自訂 Recipe 類型。兩者都透過覆寫 CanCraftInternal 實作:
public class CustomCraftingProcessor : SimpleCraftingProcessor
{
protected override bool CanCraftInternal(
CraftingRecipe recipe,
IInventory inventory,
int quantity,
ListSlice<ItemInfo> selectedIngredients)
{
// 取得角色等級
var charLevel = inventory.gameObject.GetComponent<MyCharacter>().level;
var levelRequired = 0;
// 方式 1:以配方 Category 對應所需等級
levelRequired = GetLevelFor(recipe.Category);
// 方式 2:自訂 Recipe 類型含等級欄位
if (recipe is MyCustomRecipe customRecipe) {
levelRequired = customRecipe.level;
}
if (charLevel < levelRequired) { return false; }
// 通過等級檢查後,執行基礎類別的判斷
return base.CanCraftInternal(recipe, inventory, quantity, selectedIngredients);
}
}
範例二:依外部來源改變合成結果
覆寫 CraftInternal 方法,可以在合成時根據外部數值(小遊戲分數、角色屬性、材料品質等)改變輸出結果。提供三種方案:
public class CustomCraftingProcessor : SimpleCraftingProcessor
{
[SerializeField] protected MyCraftingQualityProvider m_QualityProvider;
[SerializeField] protected int m_Option; // 1 / 2 / 3 對應三種方案
protected override CraftingResult CraftInternal(
CraftingRecipe recipe,
IInventory inventory,
int quantity,
ListSlice<ItemInfo> selectedIngredients)
{
if (!CanCraftInternal(recipe, inventory, quantity, selectedIngredients))
return new CraftingResult(null, false);
if (!RemoveIngredients(inventory, selectedIngredients))
return new CraftingResult(null, false);
var quality = m_QualityProvider.CraftingQuality;
ItemAmount[] resultItemAmounts;
if (m_Option == 1) {
// 方案 1:從 Output 清單依品質選取一個結果
var index = Mathf.Min(quality, recipe.DefaultOutput.ItemAmounts.Count - 1);
var itemAmount = recipe.DefaultOutput.ItemAmounts[index];
resultItemAmounts = new[] {
new ItemAmount(InventorySystemManager.CreateItem(itemAmount.Item),
itemAmount.Amount * quantity)
};
}
else if (m_Option == 2) {
// 方案 2:直接修改 Mutable 物品的品質屬性
resultItemAmounts = new ItemAmount[recipe.DefaultOutput.ItemAmounts.Count];
for (int i = 0; i < resultItemAmounts.Length; i++) {
var ia = recipe.DefaultOutput.ItemAmounts[i];
var craftedItem = InventorySystemManager.CreateItem(ia.Item);
craftedItem.GetAttribute<Attribute<int>>("Quality")?
.SetOverrideValue(quality);
resultItemAmounts[i] = new ItemAmount(craftedItem, ia.Amount * quantity);
}
}
else {
// 方案 3:自訂 Recipe 類型提供多種輸出可能
if (!(recipe is MyCustomQualityRecipe qualityRecipe))
return new CraftingResult(null, false);
var itemOutputAmounts = qualityRecipe.GetOutputForQuality(quality);
resultItemAmounts = new ItemAmount[itemOutputAmounts.Count];
for (int i = 0; i < resultItemAmounts.Length; i++) {
var ia = itemOutputAmounts[i];
var craftedItem = InventorySystemManager.CreateItem(ia.Item);
resultItemAmounts[i] = new ItemAmount(craftedItem, ia.Amount * quantity);
}
}
// 將合成結果加入 Inventory
foreach (var ra in resultItemAmounts) {
inventory.AddItem((ItemInfo)ra);
}
return new CraftingResult(new CraftingOutput(resultItemAmounts), true);
}
}
輸入系統(Input)
UIS 內建的輸入使用 Unity Input Manager,支援滑鼠、鍵盤,以及有限的控制器支援。若需要完整的控制器支援,強烈建議使用專用資產(如 Rewired、InControl 或 Unity Input System),UIS 已整合這些系統,可從官方頁面下載對應整合套件。
UIS 的 UI 輸入使用 Unity 預設的 Navigation 系統,因此任何輸入系統都能開箱即用。
內建輸入名稱
| 輸入名稱 | 指定位置 | 用途 |
|---|---|---|
| Action | 玩家角色的 Inventory Interactor 元件 | 與場景中可互動物件互動(如撿取、開箱)。 |
| Close Panel / Open Panel | Canvas 上的 Display Panel Manager Handler | 開啟主面板 / 關閉目前面板。 |
| Next / Previous | Inventory Grid 上的 Item Info Grid 元件 | 在格子的頁面或 Tab 之間切換。 |
輸入通常序列化為「Simple Inputs」,可選擇輸入類型(Manual 或 Custom 允許透過程式碼控制)。
啟用 / 停用輸入
UIS 無法完全停用所有輸入(因為開啟庫存選單時仍需部分輸入)。系統的做法是讓所有需要輸入的元件監聽 OnEnableGamePlayInput 事件,各自決定是否要停用。
使用 EnableDisableInventoryInput 元件可以輕鬆管理 Gameplay 輸入的啟停,無需撰寫程式碼。
透過程式碼停用輸入
using UnityEngine;
using Opsive.Shared.Events;
public class MyObject : MonoBehaviour
{
[SerializeField] private GameObject m_Character;
private void Start()
{
// false = 停用輸入;true = 啟用輸入
EventHandler.ExecuteEvent(m_Character, "OnEnableGameplayInput", false);
}
}
PlayerInput API
PlayerInput API 與 Unity 的 Input 類別介面相同,支援以下方法:
GetButton(name)、GetButtonDown(name)、GetButtonUp(name)GetAxis(name)GetDoublePress(name):偵測雙擊。GetLongPress(name):偵測長按。
using UnityEngine;
using Opsive.UltimateCharacterController.Input;
public class MyObject : MonoBehaviour
{
[SerializeField] private GameObject m_Character;
private PlayerInput m_PlayerInput;
private void Awake()
{
m_PlayerInput = m_Character.GetComponent<PlayerInput>();
}
private void Update()
{
if (m_PlayerInput.GetButtonDown("Jump")) {
// 執行跳躍邏輯
}
}
}
PlayerInput Inspector 欄位
| 欄位 | 說明 |
|---|---|
| Horizontal / Vertical Look Input Name | 水平 / 垂直視角輸入的映射名稱。 |
| Look Vector Mode | 視角向量的賦值方式:Smoothed(平滑)、Unity Smoothed(Unity 平滑)、Raw(直接值)、Manual(手動,適合 VR)。 |
| Look Sensitivity | 滑鼠靈敏度倍數(平滑模式下使用)。 |
| Look Sensitivity Multiplier | 靈敏度的額外乘數。 |
| Smooth Look Steps | 平滑模式下保留的歷史幀數。 |
| Smooth Look Weight | 每幀歷史值的加權(0~1)。趨近 0 相當於 Raw 模式;趨近 1 為簡單平均(有延遲感)。 |
| Smooth Exponent | 給小幅輸入更平滑感的指數值。 |
| Controller Connected Check Rate | 偵測控制器連線的間隔秒數(不支援控制器時設為 0)。 |
| Force Input | 強制指定輸入類型(例如在行動裝置強制使用 Standalone 輸入)。 |
| Disable Cursor | 是否隱藏游標。 |
- 第 1 篇:入門指南與基本概念
- 第 2 篇:編輯器與資料庫設定
- 第 3 篇:物品系統核心
- 第 4 篇:屬性、貨幣、商店與合成系統
- 第 5 篇:輸入處理、存檔、互動與 UI 框架
- 第 6 篇:進階功能與第三方整合