Siki_Unity_3-3_揹包系統

Unity 3-3 揹包系統(基於UGUI)

任務1&2&3:演示、介紹、類圖分析

揹包面板、箱子面板、鍛造合成面板、裝備佩戴面板、商店面板等html

面板的顯示和隱藏、保存和加載、拾起物品、物品移動、物品出售和購買等算法

導入素材UI.unitypackagejson

UML圖設計:canvas

物品Item分爲幾類:消耗品Consumable、裝備Equipment、武器Weapon、材料Material
  消耗品影響HP/MP
  裝備影響strength/ intelligence/ agility/ stamina等
    裝備類型有:head/ neck/ chest/ ring/ leg/ bracer/ boots/ shoulder/ belt/ offHand
  武器影響damage
    武器類型有:offHand/ mainHand
  材料用於合成裝備和武器c#

  物品共有變量:
    id/ name/ type/ quality/ description/ capacity/ buyprice/ sellprice
    消耗品變量:
      hp/ mp
    裝備變量:
      strength/ intelligence/ agility/ stamina等/ 還有equipmentType
    武器變量:
      damage/ 還有weaponType
    材料變量:無數組

任務5&6:開發Item類(根據類圖建立類)

使用get;set;的方式,能夠很靈活地控制變量的訪問權限安全

public class Item {
    public int ID { get; set; }
    public string Name { get; set; }
    public ItemType Type { get; set; }
    public ItemQuality Quality { get; set; }
    public string Description { get; set; }
    public int Capacity { get; set; }
    public int buyprice { get; set; }
    public int sellprice { get; set; }

    public Item(int id, string name, ItemType type, ItemQuality quality, string desc, int capacity, int buyprice, int sellprice){
        this.ID = id;
        this.Name = name;
        ...
        this.buyprice = buyprice;
        this.sellprice = sellprice;
    }
    public enum ItemType {
        Consumable, Equipment, Weapon, Material
    }
    public enum ItemQuality {
        Common, Uncommon, Rare, Epic, Legendary, Artifact
}}

-- 注意:兩個枚舉類型ItemType和ItemQuality是在類內部聲明的,在外部使用時須要經過類名,好比Item.ItemType來使用
   並且聲明的時候須要爲public的
-- 改進:每一個Item都有本身的UI圖標
  public string SpritePath { get; set; }
  並在Project中建立Resources文件夾,將全部Item圖標的Sprite移入該文件夾
  其餘類的構造函數裏也得加上spritePathapp

public class Consumable : Item {
    public int HP { get; set; }
    public int MP { get; set; }

    public Consumable(int id, string name, ItemType type, ItemQuality quality, 
        string desc, int capacity, int buyprice, int sellprice, int hp, int mp) 
        : base(id, name, type, quality, desc, capacity, buyprice, sellprice) { 
        this.HP = hp;
        this.MP = mp;
}}
public class Equipment : Item {
    public int Strength { get; set; }
    public int Intelligence { get; set; }
    public int Agility { get; set; }
    public int Stamina { get; set; }
    public EquipmentType EquipType { get; set; }

    public Equipment(int id, string name, ItemType type, ItemQuality quality, 
        string desc, int capacity, int buyprice, int sellprice, int strength, 
        int intelligence, int agility, int stamina, EquipmentType equipType) :     
        base(id, name, type, quality, desc, capacity, buyprice, sellprice) {
        this.Strength = strength;
        this.Intelligence = intelligence;
        this.Agility = agility;
        this.Stamina = stamina;
        this.EquipType = equipType;
    }
    public enum EquipmentType {
        Head, Neck, Chest, Ring, Leg, Bracer, Boots, Shoulder, Belt, OffHand
}}
public class Weapon : Item {
    public int Damage { get; set; }
    public WeaponType WeapType { get; set; }

    public Weapon(int id, string name, ItemType type, ItemQuality quality, 
        string desc, int capacity, int buyprice, int sellprice, int damage, WeaponType weapType)
        : base(id, name, type, quality, desc, capacity, buyprice, sellprice) {
        this.Damage = damage;
        this.WeapType = weapType;
    }
    public enum WeaponType {
        OffHand, MainHand
}}

  -- 注意,這裏由於Weapon不是繼承與Equipment,所以這裏使用的EquipmentType須要寫成Equipment.EquipmentTypedom

public class Material : Item {
    public Material(int id, string name, ItemType type, ItemQuality quality, 
        string desc, int capacity, int buyprice, int sellprice)
        : base(id, name, type, quality, desc, capacity, buyprice, sellprice) {
}}

-- 由於子類必須提供一個構造方法去構造父類,而父類沒有空的構造方法,因此Material必須寫對應的構造方法去構造父類
  不然須要在Item中寫一個空的構造方法異步

任務7:Item類的Json文件 -- 策劃

https://www.bejson.com/jsoneditoronline -- 在線Json編輯器

有不少種物品,在Json文件中保存成一個數組
  屬性根據類中成員變量來肯定

[
    {
        "id": 1,
        "name": "血瓶",
        "type": "Consumable",
        "quality": "Common",
        "description": "這個是用來加血的",
        "capacity": 10,
        "buyprice": 10,
        "sellprice": 5,
        "hp": 10,
        "mp": 0,
        "spritePath": "Sprites/Items/hp"
    }
]

暫時先寫一個物品,用於測試

在Project->Items下保存一個記事本Items.Json文件,編碼格式改成UTF-8

任務8:InventoryManager物品管理器
&& 任務14:改進Knapsack和Chest的設計

建立空物體InventoryManager,添加腳本InventoryManager.cs -- 用於管理全部物品

以後還有兩個分管理器:揹包Knapsack,箱子Chest
  Knapsack和Chest不是繼承於InventoryManager的,只是功能結構關係而已
  揹包和箱子之間有一些交互,好比移動物品等,這些交互方法就在InventoryManager中實現
  注意:InventoryManager和這些通常都爲單例模式

InventoryManager.cs中

單例模式的實現
1. _instance爲private,由於不能在外界訪問
2. Instance爲public,做爲在外界訪問的接口
3. 構造函數爲private,不能在外界直接調用,而必須經過Instance進行調用

private static InventoryManager _instance;
public static InventoryManager Instance {
  get {
    if(_instance == null) {
      // 第一次想要獲得的時候,未賦值,給它賦值
      _instance = GameObject.Find("InventoryManager").GetComponent<InventoryManager>();
    }
    return _instance;
}}

任務14:改進Knapsack和Chest的設計

由於Knapsack和Chest是有共有功能的,所以能夠建立一個類Inventory做爲他倆的父類

任務9&10&11:Json解析 -- LitJSON 和 JsonObject

InventoryManager須要進行Items.Json數據的解析

在Json官網 www.json.org中找到c#的 LitJSON
  或前往 https://litjson.net/
  額。。。下載失敗,我直接在csdn下載了
    https://download.csdn.net/download/blackbord/10016032

下載dll文件,導入unity中就可使用dll中的相關類了

在Project文件夾下建立Plugins文件夾,這個文件夾下的文件會被預編譯,通常用於放置插件

在InventoryManager中建立解析Json文件的方法:
  ParseItemJson()

解析出來的結果爲不少Item,新建一個List列表來存儲
  private List<Item> itemList;

  itemList = new List<Item>();

取得Json文件的內容
  TextAsset jsonTextAsset = Resources.Load<TextAsset>("Items");
  string jsonString = jsonTextAsset.text; // 獲得了文本文件中的字符串

解析
  using LitJson;
  LitJson的教程 -- https://www.cnblogs.com/Firepad-magic/p/5532650.html
  // Siki老師下載失敗後,從AssetStore上import了JsonObject
  -- 會和LitJson有所區別

思路:
1. 經過API獲得存儲數據的對象(該對象爲一個集合)
2. 經過遍歷該對象,獲得每個數據對象
3. 經過"type"字段的值,判斷Item的類型
4. 聲明對應類型的對象,並經過構造函數新建對象
5. 將新建的對象添加到list中

LitJson版本:

// 獲得的jsonData爲一個集合,每個元素也是JsonData類型
JsonData jsonData = JsonMapper.ToObject(jsonString);
foreach (JsonData data in jsonData) {
    // 將JsonData對象中存儲的值,經過Item或子類的構造函數,新建一個對應的Item對象
    // 先獲得共有的屬性
    int id = int.Parse(data["id"].ToString());
    string name = data["name"].ToString();
    string type = data["type"].ToString();
    Item.ItemType itemType = (Item.ItemType)System.Enum.Parse(typeof(Item.ItemType), type);
    Item.ItemQuality itemQuality = (Item.ItemQuality)System.Enum.Parse(typeof(Item.ItemQuality), data["quality"].ToString());
    string description = data["description"].ToString();
    int capacity = int.Parse(data["capacity"].ToString());
    int buyprice = int.Parse(data["buyprice"].ToString());
    int sellprice = int.Parse(data["sellprice"].ToString());
    string spritePath = data["spritePath"].ToString();
    Item item = null;

    // 首先須要經過"type"的值,確認該Item是什麼類型的
    switch (itemType) {
        case Item.ItemType.Consumable:
            int hp = int.Parse(data["hp"].ToString());
            int mp = int.Parse(data["mp"].ToString());
            // 經過JsonData的數據,新建一個Consumable對象
            item = new Consumable(id, name, itemType, itemQuality, description, 
                capacity, buyprice, sellprice, spritePath, hp, mp);
            break;
        case Item.ItemType.Equipment:
            break;
        case Item.ItemType.Weapon:
            break;
        case Item.ItemType.Material:
            break;
        default:
            break;
    }
    // 將新建的Item對象添加到list中
    itemList.Add(item);
}

JsonObject版本:

JsonObject講解:readme.txt
  直接經過JSONObject的構造函數進行Json數據的解析
  獲得的多個JsonObject對象會存儲在list中
    事實上Json數據中的任何一個總體都是一個JsonObject類的對象
    好比一個鍵值對,或一個對象,或一個數組
  對於每一個對象,經過jsonObject["key"]訪問對應的value,根據value類型
    經過.n表示float,.b表示bool,.str表示string等等,還有Object、數組等類型

// 獲得的jsonObject爲一個list集合,每個元素也是JsonObject類型
JSONObject jsonObject = new JSONObject(jsonString);
// 遍歷JSONObject.list,獲得每個對象
foreach(JSONObject elem in jsonObject.list) {
    // 將對象轉換爲Item類
    // 經過索引器獲得的爲JsonObject類型
    // ToString()後發現,數據帶有引號""
    // 不能使用 elementObject["name"].ToString());
    int id = (int)elem["id"].n;
    string name = elem["name"].str;
    Item.ItemType type=(Item.ItemType)System.Enum.Parse(typeof(Item.ItemType),elem["type"].str);
    ...
    Item item = null;

    switch (type) {
        case Item.ItemType.Consumable:
            int hp = (int)elem["hp"].n;
            int mp = (int)elem["mp"].n;
            item = new Consumable(id, name, type, quality, description, capacity, 
                buyprice, sellprice, spritePath, hp, mp);
            break;
        ...
        default:
            break;
    }
    itemList.Add(item);
}

任務12&13:揹包的UI

全部物品的信息都保存在了InventoryManager.itemList中,
如今開發數據和UI之間的連通,將item顯示在UI上

開發揹包的UI

新建UI->Panel,命名KnapsackPanel,SourceImage: panel,調節顏色

屏幕自適應
  Canvas--CanvasScaler--UI Scale Mode = Scale With Screen Size
    表示按控件佔屏幕的比例來顯示,而不是按像素來顯示
    Match = Width,表示按寬度的比例來,而高度的肯定按照控件的寬高比而定

顯示效果很差 -- 去掉天空盒子,Window->Lighting->Skybox選擇None

新建子物體UI->Panel,命名ItemsContainer,做爲全部物品的容器
  調整大小
  由於不須要顯示,因此alpha=0;

新建子物體UI->Image,命名Slot,做爲一個物品的容器
  SourceImage: button_square

由於須要不少個Slot,所以在ItemsContainer中添加組件Grid Layout Group,用於排序
  調整Cell大小,調整Spacing

新建Knapsack的子物體,UI->Image,命名TitleBg,SourceImage: button_long
  新建子物體, UI->Text,揹包,字體等微調

由於不須要交互,取消勾選Knapsack、TitleBg、Text、ItemsContainer的Raycast Target
  只有Slot須要交互

Slot的完善:

實現鼠標移入的顏色變化效果
  在Slot上添加組件Button

製做成prefab

在Slot下建立子物體UI->Image,命名Item,做爲Slot中存儲的物品
  調整大小,SourceImage: 先隨便選一個,由於最後是動態賦值的

  在Item下建立子物體UI->Text,命名Amount,用於顯示物品數量,微調顏色等

  由於在Slot中作了交互,因此Item和Amount中的Raycast Target取消勾選

  將Item製做成prefab

給Slot添加腳本Slot.cs,用於管理自身

給Item添加腳本ItemUI.cs,用於管理Item自身的顯示等功能
  由於ItemUI表示的爲存在該Slot中的物體,所以須要保存該Item和數量
  public Item Item {get; set; }
  public int Amount {get; set; }

任務14~18:Inventory的實現 -- 物品存儲功能
&任務19:物品存儲後的UI更新顯示
&任務25:Bugfixing
&任務39:添加物品時的動畫顯示

Inventory.cs腳本 -- 管理自身中全部的Slot
  在Knapsack中添加腳本Knapsack 繼承自 Inventory

// 存儲全部的Slot
private Slot[] slotList;

// 在Start()中獲取全部的Slot
public virtual void Start() {
  slotList = GetComponentsInChildren<Slot>();
}
-- 由於在Knapsack等子類中也須要用到這個Start(),所以設置爲virtural,方便子類訪問

拾起物品並存儲進揹包的功能:
  public bool StoreItem(int id)
  public bool StoreItem(Item itemToStore)
  // 返回bool表示是否存儲成功,由於一些緣由好比揹包滿了

  -- InventoryManager 中根據item id返回Item對象的方法
  public Item GetItemById(int id) {
    foreach(Item item in itemList) {
      if(item.ID == id) {
        return item;
    }}
    return null;
  }

public bool StoreItem(int id) {
  // 先進行轉換
  Item item = InventoryManager.Instance.GetItemById(id);
  return StoreItem(item);
}

public bool StoreItem(Item item) {
  // 安全判斷
  if(item == null) { Debug.LogWarning("要存儲的物品id不存在"); }

  // 存儲
  // 兩種狀況
  // 1. 以前揹包沒有該類物品
    實例化一個該類物體,將其放入一個Slot
     2. 揹包已有該類物品
    找到該Slot
      若物品個數小於Capacity,Amount+1 (裝備等capacity爲1)
      若放滿了,則實例化另外一個Item,並放入另外一個Slot

  if(item.capacity == 1) {
    Slot slotToStore = FindEmptySlot();
    if(slotToStore == null) { Debug.LogWarning("沒有空位"); return false; }
    else {  // 將物品放入該slot
      slotToStore.StoreItem(item);
  }} else {
    // 判斷當前是否已經存在該類物體
    Slot slotToStore = FindSlotWithSameItemType(item);
    if(slotToStore != null) { // 找到已存在同類未滿Slot
      slotToStore.StoreItem(item);
    } else { // 未找到
      // 新建一個slot存儲
      slotToStore = FindEmptySlot();
      if(slotToStore == null) { ... 警告已滿; return false; }
      else {
        slotToStore.StoreItem(item);
  }}}

  如何找到空格子呢?
    private Slot FindEmptySlot() 
      foreach(Slot slot in slotList) {
        if slot.transform.childCount == 0) {
          return Slot;
      }}
      return null;
    }

  如何找到類型相同的物品槽呢?
    private Slot FindSlotWithSameItemType(Item item) {
      foreach(Slot slot in slotList) {
        if(slot.transform.childCount == 1) { // 有一個子物體
          if(slot.GetItemId() == item.ID && !slot.IsSlotFilled()) {  // 符合類型且數量未滿

          // ------- 在Slot中實現GetItemType()方法
          //  public Item.ItemType GetItemType() {
          //    return transfrom.GetChild(0).GetComponent<ItemUI>().Item.Type;
          //  }

          // ------- 任務25中發現:不該該判斷GetItemType()
          // 這樣若是血瓶和藍瓶都是Consumable的Type,就會互相疊加了

          //  public int GetItemId() {
          //    return transfrom.GetChild(0).GetComponent<ItemUI>().Item.ID;
          //  }

          // ------- 在Slot中實現IsSlotFilled()方法
          //  public bool IsSlotFilled() {
          //    ItemUI itemUI = transform.GetChild(0).GetComponent<ItemUI>();
          //    return itemUI.Amount >= itemUI.Item.Capacity;
          //  }

            return slot;
      }}}
      return null;
    }

  如何將物品存入Slot呢?
    在Slot.cs中
    public void StoreItem(Item item) {
      if(transform.ChildCount == 0) { // 空slot
        // 實例化Item,並存入Slot
          -- public GameObject itemPrefab;
        GameObject itemObject = Instantiate(itemPrefab);
        itemObject.transform.SetParent(transform);
        itemObject.transform.localPosition = Vector3.zero;

        // 這裏的Scale顯示會出現Bug,在任務39中(即本節最後)會詳細說明

        // 給實例化出來的空Item進行賦值
        // ---------在ItemUI中實現Item的賦值 public void SetItem(Item item, int amount = 1) {
        //  this.Item = item;
        //  this.Amout = amount;
        //  // 更新UI
        //  }

        itemObject.GetComponent<ItemUI>().SetItem(item);

      } else { // 自己已經存儲了物體
        Item itemInSlot = transform.GetChild(0);
        ItemUI itemUI = itemInSlot.GetComponent<ItemUI>();
        // 這裏沒必要判斷Slot滿的狀況,由於在外界判斷完了
        // -------- 在ItemUI中實現數量+1的方法 public void AddAmount(int num = 1) {
        //  this.Amount += num;
        //  // 更新UI
        //  }

        itemUI.AddAmount();

      }

  如何更新UI呢?

// 顯示有兩個部分,一個部分是Sprite,一個部分是Amount.text
private Image itemImage;
private Text amountText;

// 若是將初始化寫在Start中,會報空指針,由於在一開始的時候就執行了賦值初始化
// 因此寫成get的形式
public Image ItemImage {
  get{
    if(itemImage == null) {
      itemImage = GetComponent<Image>();
    }
  return itemImage;
}
public Text AmountText {
  // 類似
  amountText = GetComponentInChildren<Text>();
}

public void UpdateUISprite() {
  ItemImage.sprite = Resources.Load<Sprite>(Item.SpritePath);

public void UpdateUIText() {
  AmountText.text = Amount.ToString();

測試:

新建腳本Player.cs
  -- 由於操做物品的來源通常爲Player(隨意啦,一個解釋而已)
  -- 經過鍵盤按鍵G,隨機獲得一個物品放到揹包中

在 Update()中

if(Input.GetKeyDown(KeyCode.G) {
  // 隨機生成一個id
  int id = Random.Range(1, 2);

  // 調用Knapsack (即Inventory中)的StoreItem(id)進行存儲
  // ---------- 將Inventory作成單例模式
  // 可是不能在Inventory中實現,應該在Knapsack和Chest中實現
  // 由於若是在Inventory中實現,那麼Knapsack和Chest就會共用了
  // 將Knapsack作成單例模式

//  private static Knapsack _instance;
//  public static Knapsack Instance {
//    get{
//      if(_instance == null) {
//        _instance = GameObject.Find("KnapsackPanel").GetComponent<Knapsack>();
//      }
//      return _instance;
//  }}

Knapsack.Instance.StoreItem(id);

代碼:

Player.cs

public class Player : MonoBehaviour {
    void Update () {
        if(Input.GetKeyDown(KeyCode.G)) {
            // 隨機生成一個id
            int id = Random.Range(1, 2);
            Knapsack.Instance.StoreItem(id);
}}}

Knapsack.cs中只有單例模式的實現代碼

Inventory.cs

public class Inventory : MonoBehaviour {
    private Slot[] slotList;

    public virtual void Start () {
        slotList = GetComponentsInChildren<Slot>();
    }

    public bool StoreItem(Item itemToStore) {
        // 存儲一個Item,有兩種狀況
        // 1. 在Inventory中沒有此類Item,則尋找空Slot存儲
        // 2. 在Inventory中已有此類Item
        //      若數量已滿,則尋找空Slot存儲;若數量未滿,則增長數量便可
        // 另外一種判斷:
        // 1. 若Item.Capacity爲1,則須要尋找空Slot存儲
        // 2. 若不爲1,尋找是否已經存在該類物品
        //      已存在,則數量增長;沒有存在,尋找空Slot
        Slot slotToStore = FindSlotWithSameItemType(itemToStore);
        if(slotToStore == null) {
            // 沒有找到相同類型且未滿的Slot -- 故尋找空slot存儲
            slotToStore = FindEmptySlot();
            if(slotToStore == null) {
                Debug.LogWarning("空間已滿,不可進行存儲");
                return false;
            } else {
                //找到空slot,進行存儲
                slotToStore.StoreItem(itemToStore);
            }
        } else {
            // 找到相同類型且未滿的Slot,存儲
            slotToStore.StoreItem(itemToStore);
        }
        return true;
    }
    public bool StoreItem(int itemId) {
        Item itemToStore = InventoryManager.Instance.GetItemById(itemId);
        if(itemToStore == null) { // 未找到該Item
            return false;
        }
        return StoreItem(itemToStore);
    }

    private Slot FindSlotWithSameItemType(Item item) {
        foreach(Slot slot in slotList) {
            if(slot.transform.childCount == 1) {
                // 不是空slot
                if(slot.GetItemType() == item.Type && !slot.IsSlotFilled()) {
                    // 相同類型的slot,且未滿
                    return slot;
        }}}
        return null;
    }
    private Slot FindEmptySlot() {
        foreach(Slot slot in slotList) {
            if(slot.transform.childCount == 0) {
                // 找到空slot
                return slot;
        }}
        return null;
}}

Slot.cs

public class Slot : MonoBehaviour {
    public GameObject itemPrefab;

    public Item.ItemType GetItemType() {
        return transform.GetChild(0).GetComponent<ItemUI>().Item.Type;
    }
    public bool IsSlotFilled() {
        ItemUI itemUI = transform.GetChild(0).GetComponent<ItemUI>();
        return itemUI.Amount >= itemUI.Item.Capacity;
    }

    public void StoreItem(Item itemToStore) {
        // 兩種狀況下調用該方法:
        // 1. 本Slot爲空,須要實例化Item進行存儲
        // 2. 本Slot不爲空,只須要增長數量便可
        if(transform.childCount == 0) {
            // 實例化Item
            GameObject itemObject = GameObject.Instantiate(itemPrefab) as GameObject;
            itemObject.transform.SetParent(transform);
            itemObject.transform.localPosition = Vector3.zero;
            // 給該Item賦值
            itemObject.GetComponent<ItemUI>().SetItem(itemToStore);
        } else {
            // 數量增長
            transform.GetChild(0).GetComponent<ItemUI>().AddAmount();
}}}

ItemUI.cs

public class ItemUI : MonoBehaviour {
    public Item Item { get; set; }
    public int Amount { get; set; }
    private Text amountText;
    public Text AmountText {
        get { ... }
    }
    private Image itemImage;
    public Image ItemImage {
        get { ...  }
    }

    public void SetItem(Item item, int amount  = 1) {
        // amount默認爲1,由於該方法意爲被空Slot存儲item時調用
        this.Item = item;
        this.Amount = amount;
        // UI更新
        UpdateUISprite();
        UpdateUIText();
    }
    public void AddAmount(int num = 1) {
        // 默認+1,由於該方法意爲存儲item時調用,一般存儲爲1個
        this.Amount += num;
        // UI更新
        UpdateUIText();
    }
    private void UpdateUIText() {
        AmountText.text = Amount.ToString();
    }
    private void UpdateUISprite() {
        ItemImage.sprite = Resources.Load<Sprite>(Item.SpritePath);
}}

任務39:添加物品時的動畫顯示
  -- 物品添加到Slot中時,會先放大一下物品表示強調,再縮小到應有大小

在ItemUI中控制動畫的播放

流程解釋:Player.Update() -> Knapsack.StoreItem(id/item) -> Slot.StoreItem(Item) -> ItemUI.SetItem(item)

ItemUI.SetItem(item)中,傳遞設置了item和amount,並更新了sprite和text的UI顯示
  所以在ItemUI.UpdateUISprite()中
  添加
    直接在UpdateUISprite()中完成動畫效果嗎?
    不行,須要在Update()中不斷調用Lerp來實現

定義屬性
  private float targetScale = 1;

Update() {
  if(Mathf.Abs(transform.localScale.x - targetScale) > 0.05f) {
    // 進行動畫播放
    transform.localScale = Vector3.one * Mathf.Lerp(transform.localScale.x, targetScale, Time.deltaTime*smooth);
  } else {
    transform.localScale = Vector3.one * targetScale;  // 節約性能
    if(targetScale != 1) {
      // 每當添加物品時,會將targetScale設大,播放動畫
      // 結束動畫後localScale=targetScale>1,此時自動將targetScale設爲1,開始變小動畫
      targetScale = 1;
}}}

Bug修復:在Slot.cs的StoreItem()裏有一個scale自動變化的問題

public void StoreItem(Item itemToStore) {
    if(transform.childCount == 0) {
        // 實例化Item
        GameObject itemObject = GameObject.Instantiate(itemPrefab) as GameObject;
        // 大小顯示一直有問題,在這裏手動設置
        // 爲何呢,由於實例化的時候是在slot裏面實例化的
        // 實例化出來的時候,首先會放在場景的根目錄下
        // 而後設置位置的時候,好比設置Parent的時候纔會移動到Parent下面
        // 由於Canvas自身是有scale的大小設置的,所以會影響到實例化物體的scale變化
        itemObject.transform.SetParent(transform);
        itemObject.transform.localPosition = Vector3.zero;
        itemObject.transform.localScale = Vector3.one;
        // 給該Item賦值
        itemObject.GetComponent<ItemUI>().SetItem(itemToStore);
    } else {
        // 數量增長
        transform.GetChild(0).GetComponent<ItemUI>().AddAmount();
    }
}

任務20&21:實現ToolTip

什麼是ToolTip?
  當光標懸浮在某個控件上時,會有一個彈窗顯示對控件的解釋說明

實現物品Item的ToolTip,顯示對應的description

新建UI->Image,命名ItemDescToolTipPanel,SourceImage: panel
  新建子物體UI->Text,命名ItemDescText,微調顏色大小等

這個時候,ToolTip的大小是固定的,不會隨着Text而改變
  子類會隨着父類的變化而變化,當父類的大小不會受子類的大小影響
  可是由於每一個Item的desc長度不一樣,須要的Panel長度也不一樣
解決方法:取巧
  將Image設置爲Text的子物體

如今只須要實現Text框大小隨着文字數量而改變便可
  在Text添加組件Content Size Fitter 
  Horizontal/ Vertical Fit:
    Preferred Size -- 讓組件隨着內容的變化而變化
    Min Size
    Unconstrained

如今,實現了大小的自適應變化
可是,Text的內容由於Image的覆蓋而看不見了
解決方法:取巧
  複製一份Text,命名Content,做爲Image子物體,實現顯示的功能

  注意,要將Image的pivot設置爲四周拉伸,纔會隨着Text而改變大小

由於鼠標懸浮時,ToolTip須要顯示在鼠標的右下方而不是以鼠標爲中心
-- 設置ToolTip的中心點爲左上角
  可是縮放的時候會發現,會隨着縮放而跑偏了
  緣由:pivot會根據縮放的比例而定
解決方法:pivot設置在Text的左上角,而不是背景框的左上角

ToolTip不須要進行交互,所以將全部的Raycast Target取消勾選

代碼實現:
  1. ToolTip框的顯示與隱藏
  2. Desc文字的變化

ToolTip.cs 信息提示類
  在InventoryManager.cs中進行調用

給ToolTip物品添加ToolTip.cs腳本

// 由於須要實現Desc內容的顯示和隱藏,所以須要獲得
private Text descSizeController = GetComponent<Text>();
private Text contentText = transform.GetChild(0).Find("ContentText").GetComponent<Text>();
// contentText爲ToolTip.transform的子物體的子物體,所以須要先獲得子物體,在使用transform.Find()

// 經過Canvas Group.Alpha組件控制顯示和隱藏
  -- 給ToolTip物體添加CanvasGroup組件,並取消Interactable和Blocks Raycasts的勾選
private CanvasGroup canvasGroup = GetComponent<CanvasGroup>();

// 顯示和隱藏功能
private float targetAlpha = 0; // 默認不顯示
public void DisplayToolTip(String content) {
  // 改變框大小和顯示內容
  descSizeController.text = content;
  ContentText.text = content;
  targetAlpha = 1; // 顯示
}

public void HideToolTip() {
  targetAlpha = 0; // 不顯示
}

Update() {
  // 控制將Alpha變化成targetAlpha值
  if(canvasGroup.alpha != targetAlpha) {
    canvasGroup.alpha = Mathf.Lerp(canvasGroup.alpha, targetAlpha, Time.deltaTime * smooth);
    if(Mathf.Abs(canvasGroup.alpha - targetAlpha) > 0.05f) {
      // 由於Lerp是逐漸趨近而不會到達
      canvasGroup.alpha = targetAlpha;
}}

public class ToolTip : MonoBehaviour {
    private Text itemDescSizeController;
    private Text contentText;private CanvasGroup canvasGroup;
    private float targetAlpha = 1;
    private float smoothing = 4;

    void Start () {
        itemDescSizeController = GetComponent<Text>();
        contentText = transform.GetChild(0).Find("ContentText").GetComponent<Text>();
        canvasGroup = GetComponent<CanvasGroup>();
    }
    
    void Update () {
        if(canvasGroup.alpha != targetAlpha) {
            // 改變透明度
            canvasGroup.alpha = Mathf.Lerp(
                canvasGroup.alpha, targetAlpha, Time.deltaTime * smoothing);
            if(Mathf.Abs(targetAlpha - canvasGroup.alpha) < 0.05f) {
                canvasGroup.alpha = targetAlpha;
    }}}
    public void DisplayToolTip(string content) {
        itemDescSizeController.text = content;
        contentText.text = content;
        targetAlpha = 1;
    }
    public void HideToolTip() {
        targetAlpha = 0;
}}

任務22&23&24:使用InventoryManager管理ToolTip && 實現ToolTip的顯示

爲何不將ToolTip寫成單例模式呢?
  private static ToolTip _instance;
  public static ToolTip Instance {
    get{
      if(_instance == null) {
        _instance = GameObject.Find("...").GetComponent<ToolTip>();
      }
      return _instance;
  }}

Siki老師經過private ToolTip toolTip = GameObject.FindObjectOfType<ToolTip>(); 進行訪問

建立兩個方法進行Display和Hide
  public void DisplayToolTip(string description) {
    ToolTip(.Instance).DisplayToolTip();
  }
  public void HideToolTip() {
    ToolTip(.Instance).HideToolTip();
  }

檢測鼠標的進入和移出:
  UnityEngine.EventSystems -- Interfaces -- 
    IPointerEnterHandler和IPointerExitHandler分別對應鼠標的進入和移出

在Slot.cs中監聽這兩個事件
  由於Slot爲button,且Item的Raycast Target取消勾選了

using UnityEngine.EventSystems;
public class Slot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler

實現這兩個接口的OnPointerEnter() 和OnPointerExit()方法

public void OnPointerEnter(PointerEventData eventData) {
  // 若是Slot爲空,就不進行任何操做
  if(transform.childCount != 0) {
    InventoryManager.Instance.DisplayToolTip(GetComponentInChildren<ItemUI>().Item.Description);
    // ---------- 傳遞的參數能夠爲Item中的用來獲得Item須要被顯示的Content的方法
    // public virtual string GetToolTipContent() {
    //  return Name + ": " + Description;
    // }
}}

public void OnPointerExit(PointerEventData eventData) {
  // 類似,先判斷是否爲空slot,若不是,則執行InventoryManager中的HideToolTip()
  // 若是不判斷也是能夠的,由於若是Slot爲空
  // 那麼InventoryManager.HideToolTip() -> ToopTip.Instance.HideToolTip() -> targetAlpha=0;
  // 沒有什麼實質影響
}

控制提示ToolTip面板的跟隨:
  修改ToolTip的位置

  經過RectTransformUtility.ScreenPointToLocalPointInRectangle(RectTransform rect, Vector2 screenPoint, Camera cam, out Vector2 localPos)

在InventoryManager中實現ToolTip位置的控制

上述方法參數爲:
  1. rect: The RectTransform to find a point inside
  2. For a RectTransform in a Canvas set to ScreenSpace-Overlay mode, it should be null.
  3. localPos: Point in local space of the rect

在UpdateToolTipPosition()中控制ToolTip的位置

private void UpdateToolTipPosition() {
    Vector2 position;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(
        canvas.transform as RectTransform, Input.mousePosition, null, out position);
    ToolTip.Instance.transform.localPosition = position;
}

在Update()中判斷是否須要調用UpdateToolTipPosition()
  // 當須要顯示ToolTip時,調用
  // ---- private bool isToolTipDisplayed = false;
  // ---- 在DisplayToolTip()中改變值isToolTipDisplayed = true;
  // ---- 在HideToolTip()中改變值isToolTipDisplayed = false;

if(isToolTipDisplayed) {
  UpdateToolTipPosition();
}

運行,發現如今實現了ToolTip的跟隨鼠標顯示效果
可是,鼠標會在ToolTip的右上角 (Text的右上角)顯示,而不是在邊框外顯示

-- 添加一個偏移 private Vector2 toolTipPosOffset = new Vector2(18, -28);

任務25&26&27:添加藍瓶、胸甲的Json && 完善Equipment類型
任務28&29&30:完善全部Json數據

{
    "id": 2,
    "name": "藍瓶",
    "type": "Consumable",
    "quality": "Common",
    "description": "這個是用來加藍的",
    "capacity": 10,
    "buyprice": 10,
    "sellprice": 5,
    "hp": 0,
    "mp": 10,
    "spritePath": "Sprites/Items/mp"
}
{
    "id": 3,
    "name": "胸甲",
    "type": "Equipment",
    "quality": "Uncommon",
    "description": "這個胸甲很牛逼",
    "capacity": 1,
    "buyprice": 500,
    "sellprice": 250,
    "strength": 10,
    "intelligence": 2,
    "agility": 0,
    "stamina": 10,
    "equipType": "Chest",
    "spritePath": "Sprites/Items/armor"
}
case Item.ItemType.Equipment:
    int strength = int.Parse(data["strength"].ToString());
    int intelligence = int.Parse(data["intelligence"].ToString());
    int agility = int.Parse(data["agility"].ToString());
    int stamina = int.Parse(data["stamina"].ToString());
    Equipment.EquipmentType equipType = (Equipment.EquipmentType)
        System.Enum.Parse(typeof(Equipment.EquipmentType), data["equipType"].ToString());
    // 經過JsonData的數據,新建一個Consumable對象
    item = new Equipment(id, name, itemType, itemQuality, description, capacity, 
        buyprice, sellprice, spritePath, strength, intelligence, agility, stamina, equipType);
    break;

改進 -- 這時能夠顯示胸甲和藍瓶了
可是:胸甲的數量就不須要顯示,由於恆爲1

在ItemUI.UpdateUIText()中加入判斷Item.Capacity的狀況

private void UpdateUIText() {
    if (this.Item.Capacity == 1) {
        // 不須要顯示Amount
        HideAmountText();
    } else {
        AmountText.text = Amount.ToString();
    }
}

不管是已存在物品的AddAmount()或是未存在物品的SetItem(),都適用了

 

經過Equipment.EquipType,一共12種,一種對應一個便可
完善Json數據

Equipment的Json信息完善:

{
    "id": 3, "name": "胸甲", "type": "Equipment", "quality": "Uncommon",
    "description": "這個胸甲很牛逼", "capacity": 1, "buyprice": 500, "sellprice": 250,
    "strength": 10, "intelligence": 2, "agility": 0, "stamina": 10,
    "equipType": "Chest", "spritePath": "Sprites/Items/armor"
},
{
    "id": 4, "name": "皮腰帶", "type": "Equipment", "quality": "Epic",
    "description": "這個腰帶很靈活哦", "capacity": 1, "buyprice": 200, "sellprice": 100,
    "strength": 0, "intelligence": 0, "agility": 10, "stamina": 5,
    "equipType": "Belt", "spritePath": "Sprites/Items/belts"
},
{
    "id": 5, "name": "靴子", "type": "Equipment", "quality": "Legendary",
    "description": "這個靴子很快很快", "capacity": 1, "buyprice": 300, "sellprice": 150,
    "strength": 0, "intelligence": 0, "agility": 30, "stamina": 5,
    "equipType": "Boots", "spritePath": "Sprites/Items/boots"
},
{
    "id": 6, "name": "護腕", "type": "Equipment", "quality": "Rare",
    "description": "這個護腕很聰明", "capacity": 1, "buyprice": 300, "sellprice": 150,
    "strength": 0, "intelligence": 20, "agility": 0, "stamina": 0,
    "equipType": "Bracer", "spritePath": "Sprites/Items/bracers"
},
{
    "id": 7, "name": "神奇手套", "type": "Equipment", "quality": "Artifact",
    "description": "這個手套很神奇", "capacity": 1, "buyprice": 5000, "sellprice": 2500,
    "strength": 50, "intelligence": 0, "agility": 0, "stamina": 35,
    "equipType": "OffHand", "spritePath": "Sprites/Items/gloves"
},
{
    "id": 8, "name": "頭盔", "type": "Equipment", "quality": "Rare",
    "description": "這個頭盔很重哦", "capacity": 1, "buyprice": 1000, "sellprice": 500,
    "strength": 10, "intelligence": 5, "agility": 0, "stamina": 25,
    "equipType": "Head", "spritePath": "Sprites/Items/helmets"
},
{
    "id": 9, "name": "白銀項鍊", "type": "Equipment", "quality": "Common",
    "description": "這個項鍊只是鍍了一層白銀", "capacity": 1, "buyprice": 400, "sellprice": 200,
    "strength": 0, "intelligence": 15, "agility": 15, "stamina": 0,
    "equipType": "Neck", "spritePath": "Sprites/Items/necklace"
},
{
    "id": 10, "name": "戒指", "type": "Equipment", "quality": "Rare",
    "description": "這個戒指颳了一下,金色沒了誒", "capacity": 1, "buyprice": 500,
   "sellprice": 250, "strength": 0, "intelligence": 30, "agility": 0, "stamina": 0,
    "equipType": "Ring", "spritePath": "Sprites/Items/rings"
},
{
    "id": 11, "name": "皮褲", "type": "Equipment", "quality": "Common",
    "description": "豬皮製成的褲子,汪峯最喜歡", "capacity": 1, "buyprice": 300,
    "sellprice": 150,"strength": 10, "intelligence": 0, "agility": 0, "stamina": 20,
    "equipType": "Leg", "spritePath": "Sprites/Items/pants"
},
{
    "id": 12, "name": "皮護肩", "type": "Equipment", "quality": "Common",
    "description": "豬皮製成的褲子,此次汪峯不喜歡了", "capacity": 1, "buyprice": 300,
    "sellprice": 150, "strength": 10, "intelligence": 0, "agility": 0, "stamina": 15,
    "equipType": "Shoulder", "spritePath": "Sprites/Items/shoulders"
}

武器的Json信息完善:

{
    "id": 13,
    "name": "木斧",
    "type": "Weapon",
    "quality": "Common",
    "description": "砍木頭用的斧子",
    "capacity": 1,
    "buyprice": 500,
    "sellprice": 250,
    "damage": 50;
    "weapType": "MainHand",
    "spritePath": "Sprites/Items/axe"
},
{
    "id": 14,
    "name": "玄鐵劍",
    "type": "Weapon",
    "quality": "Artifact",
    "description": "上古時期,蚩尤用玄鐵鍛造的劍",
    "capacity": 1,
    "buyprice": 15000,
    "sellprice": 7500,
    "damage": 450;
    "weapType": "OffHand",
    "spritePath": "Sprites/Items/sword"
}

武器的Json解析:

case Item.ItemType.Weapon:
    int damage = int.Parse(data["damage"].ToString());
    Weapon.WeaponType weaponType = (Weapon.WeaponType)System.Enum.Parse
        (typeof(Weapon.WeaponType), data["weaponType"].ToString());
    item = new Weapon(id, name, itemType, itemQuality, description, capacity, 
        buyprice, sellprice, spritePath, damage, weaponType);
    break;

材料的Json信息完善:

{
    "id": 15, "name": "鐵塊", "type": "Material", "quality": "Common",
    "description": "用於合成裝備和武器", "capacity": 20, "buyprice": 10,
    "sellprice": 5, "spritePath": "Sprites/Items/ingots"
},
{
    "id": 16, "name": "玄鐵劍的鍛造祕籍", "type": "Material", "quality": "Artifact",
    "description": "用來鍛造玄鐵劍的祕籍", "capacity": 5, "buyprice": 1000,
    "sellprice": 500, "spritePath": "Sprites/Items/book"
},
{
    "id": 17, "name": "頭盔的鍛造祕籍", "type": "Material", "quality": "Rare",
    "description": "用於合成裝備和武器", "capacity": 5, "buyprice": 100,
    "sellprice": 50, "spritePath": "Sprites/Items/scroll"
}

材料的Json信息解析:

由於Material中只有基類Item的成員屬性,所以不須要進行Json解析
item = new Material(id, name, itemType, itemQuality, description, capacity, buyprice, sellprice, spritePath);

任務31&32:完善物品的提示信息顯示

以前在Item.GetToolTipContent()中簡單顯示了物品的信息,如今來完善這個功能

實現效果:Item.Name + Item.Quality + Item.Desc + Item.prices

public virtual string GetToolTipContent() {
    return Name + '\n' + Quality + '\n' + Description + 
        "\n購買價格: " + Buyprice + "  賣出價格: " + Sellprice;
}

實現效果:對於不一樣品質的Item,顯示的顏色、大小不一樣
  對於部分文字的顏色修改:經過標記<color=...>...</color>
    <color=red>text</color>
    <size=16>text</size>
  勾選Text中的Rich Text,表示須要解析標記

顏色列表
  Quality   顏色
  Common  white
  Uncommon lime
  Rare    navy
  Epic     megenta
  Legendary   orange
  Artifact    red

public virtual string GetToolTipContent() {
    string color;
    switch (Quality) {
        case ItemQuality.Common:
            color = "white";
            break;
        case ItemQuality.Uncommon:
            color = "lime";
            break;
        case ItemQuality.Rare:
            color = "navy";
            break;
        case ItemQuality.Epic:
            color = "megenta";
            break;
        case ItemQuality.Legendary:
            color = "orange";
            break;
        case ItemQuality.Artifact:
            color = "red";
            break;
        default:
            color = "white";
            break;
    }
    return string.Format("<color={0}><size=16>{1}</size></color>\n{2}\n
        購買價格: {3}  賣出價格: {4}", color, Name, Description, Buyprice, Sellprice);
}

消耗品的自有屬性顯示:
  override Item中的GetToolTipContent()

public override string GetToolTipContent() {
    string text = base.GetToolTipContent();
    return string.Format("{0}\n\n<color=red>HP + {1}</color>\n
        <color=blue>MP + {2}</color>", text, HP, MP);
}

裝備的自有屬性顯示:

public override string GetToolTipContent() {
    // Equipment.EquipmentType的中文
    string equipTypeText;
    switch (EquipType) {
        case EquipmentType.Head:
            equipTypeText = "頭部";
            break;
        case EquipmentType.Neck:
            equipTypeText = "脖子";
            break;
        // ... "胸部" "戒指" "腿部" "護腕" "鞋子" "肩部" "腰帶"
        case EquipmentType.OffHand:
            equipTypeText = "副手";
            break;
        default:
            equipTypeText = "";
            break;
    }
    string oldText = base.GetToolTipContent();
    return string.Format("{0}\n\n<color=blue>裝備類型: {5}\n力量 + {1}\n
        智力 + {2}\n敏捷 + {3}\n體力 + {4}\n</color>", oldText, Strength, 
        Intelligence, Agility, Stamina, equipTypeText);
}

武器的自有屬性顯示:

public override string GetToolTipContent() {
    string oldText = base.GetToolTipContent();
    string weaponTypeText;
    switch (WeapType) {
        case WeaponType.OffHand:
            weaponTypeText = "副手";
            break;
        case WeaponType.MainHand:
            weaponTypeText = "主武器";
            break;
        default:
            weaponTypeText = "";
            break;
    }
    return string.Format("{0}\n<color=blue>武器類型: {2}\n攻擊 + {1}</color>", 
        oldText, weaponTypeText, Damage);
}

任務33~38&40~43&48:PickedItem的移動功能實現

任務33:在InventoryManager中管理PickedItem

當物品被鼠標點擊後,會被鼠標選中,隨着鼠標位置的移動而移動

由於點擊的是Slot,因此將Slot繼承自IPointerDownHandler接口,並實現OnPointerDown()

在Canvas下建立一個Item物體PickedItem
  在ItemUI中實現功能
  在InventoryManager中控制該物體

在InventoryManager中:
  private ItemUI pickedItem;
  在Start中初始化
  pickedItem = GameObject.Find("PickedItem").GetComponent<ItemUI>();
  pickedItem.Hide();
  // -------- ItemUI的一些基本功能,如顯示本身、隱藏本身、控制自身位置等
  //  public void Hide/ Display() {
  //    gameObject.SetActive(false/ true);
  //  }

  // -------- 設置本身的localPosition
  //  public void SetLocalPosition(Vector3 pos) {
  //    // 由於是設置在Canvas下的位置,所以爲LocalPos
  //    transform.localPosition = pos;
  //  }
  }

任務34:實現Slot中的OnPointerDown() -- 按下算選中一個Slot

分析物體移動的多種狀況:
  移動功能擴展:
    當按住ctrl鍵,進行PickedItem選中時,會選擇一半物品
    當按住ctrl鍵,進行PickedItem放置時,會放置一個物品

  1. 按下的Slot爲空
    1. 鼠標按下以前,已經選中了物品 -- 將PickedItem放入slot
      按下Ctrl,會放置一個物品
      沒有按Ctrl,會放置鼠標上的全部物品
    2. 鼠標按下以前,沒有選中物品 -- 不作操做
  2. 按下的Slot不爲空
    1. 鼠標按下以前,已經選中了物品
      若是物品Id不一樣 -- 交換物品
      若是物品Id相同
        可疊加
          能夠徹底疊加
            按下Ctrl -- 疊加一個
            沒有按Ctrl -- 疊加全部
          不能夠徹底疊加 -- 將當前物品數量設爲capacity,原物品爲剩下數量
        不可疊加 -- 交換物品
    2. 鼠標按下以前,沒有選中物品
      按下Ctrl,會選擇一半物品
      沒有按Ctrl,會選擇所有物品

代碼實現:

任務35&36&37:物品的選中功能:
  Slot不爲空,且鼠標按下以前沒有選中物品

  if(transform.ChildCount != 0) {  // 當前slot不爲空
    if(InventoryManager.Instance.IsPickedItemEmpty()) { // 還未選中物體
      // -------- InventoryManager.IsPickedItemEmpty() {
      //    return pickedItem.Item == null;
      //   }

      ItemUI currItemUI = transform.GetCompInChildren<ItemUI>()

      if(Input.GetKey(KeyCode.LeftControl)) {  // 注意是按住而不是按下
        // 按下Ctrl,取走一半物品
        // 撿起一半物品,放置在鼠標的PickedItem上
        int amountPicked = (currItem.Amount+1) / 2;  // 進一法,若是原個數爲1,則取走1
        int amountLeft = currItem.Amount - amountPicked;
        InventoryManager.Instance.SetPickedItem(currItemUI.Item, amountPicked);
        // ------- InventoryManager.SetPickedItem(Item item, int amount) {
        //    pickedItem.SetPickedItem(item, amount);
        //   }

        // ------- ItemUI.SetPickedItem(Item item, int amount) {
        //    SetItem(item, amount);
        //  }

        // 更新Slot中剩下的物品
        if(amountLeft == 0) {
          Destroy(currItemUI.gameObject);
        } else {
          currItem.SetAmount(amountLeft);
          // ------- ItemUI.SetAmount(int amount) {
          //    Amount = amount;
          //    UpdateUIText();
          //   }
        }
        
      } else {
        // 沒有按Ctrl,取走全部物品
        // 把當前Slot中的Item設置給PickedItem中的Item,還有Amount
        InventoryManager.Instance.SetPickedItem(currItemUI.Item, currItemUI.Amount);

        // 銷燬原來空格中的物品顯示
        Destroy(transform.GetChild(0).gameObject);

      }

  }}

任務38:將選中的PickedItem顯示出來,並更新位置

在InventoryManager.SetPickedItem(Item item, int amount) {
  以前是作了pickedItem.SetPickedItem(item, amount);
  設置了相關的Item給了PickedItem
  那麼如今,pickedItem中已經包含了當前選中的item,須要顯示
  pickedItem.Display();
}

在InventoryManager.Update()中控制pickedItem的位置跟隨(和以前作的toolTip的跟隨同樣)
  if(!IsPickedItemEmpty()) {
    UpdatePickedItemPosition();
    // ------- InventoryManager.UpdatePickedItemPosition() {
    //    Vector2 targetPos;
    //    RectTransformUtility.ScreenPointToLocalPointInRectangle(
    //      canvas.transform as RectTransform, Input.mousePosition, null, out targetPos);
    //    pickedItem.SetLocalPosition(targetPos);
    //  }
  }

如今可以使pickedItem隨鼠標移動了
可是,選中物品後,ToolTip仍然顯示,須要將其自動隱藏
  在InventoryManager.SetPickedItem(Item item, int amount) {
    最後一句添加上
    ToolTip.Instance.HideToolTip();
  }

  // Siki認爲當pickedItem不爲空時,即手上已經有選定物品時,移到其餘物品時的ToolTip就不應顯示
  //  若是想實現的話,能夠在InventoryManager.DisplayToolTip(string desc)中判斷IsPickeItemEmpty()便可
  // 但我認爲仍是須要顯示的

任務40&41&42&43&48:放置物品

以前完成的是物品的選取
if(transform.childCount != 0) {
  if(InventoryManager.IsPickedItemEmpty()) {
    // 取走必定數量的物品
    // ...上一節實現了
  } else {
    // 當前slot不爲空,且手上已經有選中物品了
    ItemUI pickedItemUI = InventoryManager.Instance.PickedItem;
    if(currItemUI.Item.ID == pickedItemUI.Item.ID) {

// 當兩個ID相同時
if(IsSlotFilled()) {
  // 當前Slot滿了,不可疊加,交換物品位置便可
  // 任務48

  ExchangeWithPickeItem();

  // ------- Slot.ExchangeWithPickedItem() {
  //    ItemUI currItemUI = GetComponentInChildren<ItemUI>();
  //    Item tempItem = InventoryManager.Instance.PickedItem.Item;
  //    int tempAmount = InventoryManager.Instance.PickedItem.Amount;
  //    InventoryManger.Instance.SetPickedItem(currItemUI.Item, currItemUI.Amount);
  //    currItemUI.SetItem(tempItem, tempAmount);
  //  }

} else {

// 可進行疊加
int amount = currItem.Item.Amount; // 記錄當前slot中item要變成的數量
int amountToAdd;  // 須要添加到currItem中的數量
int leftAmount = pickedItemUI.Amount; // 記錄pickedItem中item要變成的數量
if(Input.GetKey(KeyCode.LeftControl)) {

// 按下Ctrl,一次放一個
// if((1+currItemUI.Amount) > currItemUI.Item.Capacity) {
// 若放入,則超出數量 -- 無操做
// 這個無需判斷,由於當slot未滿,則必然slot數量+1不會超過capacity
amountToAdd = 1;

} else {

// 沒有按Ctrl,所有放入
if((amount + leftamount) > currItemUI.Item.Capacity) {
  // 須要放入的數量太多,不能徹底疊加
  amountToAdd = currItemUI.Item.Capacity - amount;
} else {
  // 能夠徹底疊加
  amountToAdd = leftAmount;
}

}
amount += amountToAdd;
leftAmount -= amountToAdd;
currItemUI.SetAmount(amount);
// 剩餘個數判斷
if(leftAmount == 0) {
  // 銷燬pickedItem
  InventoryManager.Instance.ResetPickedItem();
  // ------- InventoryManager.ResetPickedItem() {
  //    pickedItem.ResetItem();
  //    pickedItem.Hide();
  //  }

  // ------- ItemUI.ResetItem() {
  //    this.Item = null;
  //    this.Amount = 0;
  //  }

} else {
  InventoryManager.Instance.SetPickedItemAmount(leftAmount);
  // 不知道爲何不直接經過pickedItem.SetAmount()解決
  // 多是由於pickedItem最好統一經過InventoryManager進行訪問?
  // ------- InventoryManager.SetPickedItemAmount(int amount) {
  //    pickedItem.SetAmount(amount);
  //  }
}

}

} else { // 當兩個ID不一樣時 -- 交換物品

 ExchangeWithPickedItem();

}

}

} else {
  // 當前slot爲空
  if(!InventoryManager.Instance.IsPickedItemEmpty()) {
    // pickedItem不爲空,即已經選定了物品 -- 將物品放入該空slot

ItemUI pickedItemUI = InventoryManager.Instance.PickedItem;
if(Input.GetKey(KeyCode.LeftControl) {
  // 按下Ctrl -- 一次放一個

// 經過StoreItem將pickedItem存入當前slot中
StoreItem(pickedItemUI.Item);
InventoryManager.Instance.SetPickedItemAmount(pickedItemUI.Amount - 1);

} else {
  // 沒有按Ctrl -- 一次性全放(由於不存在溢出的狀況)

StoreItem(pickedItemUI.Item);
transform.GetComponentInChildren<ItemUI>().SetAmount(pickedItemUI.Amount);
InventoryManager.Instance.SetPickedItemAmount(0);

}}}

任務44:添加Chest箱子

複製Knapsack物體,更名Chest,設置位置大小
子物體TitleBg的Text修改成箱子
container中slot數量設置爲8個
將附帶的Knapsack.cs腳本替換爲Chest.cs腳本

Chest.cs中實現單例模式

private static Chest _instance;
public static Chest Instance {
    get {
        if(_instance == null) {
            _instance = GetComponent<Chest>();
        }
        return _instance;
}}

任務45:物品的丟棄

思路:若是pickedItem不爲空,且鼠標點擊的位置沒有UI,則進行丟棄操做

在哪裏寫代碼呢?Slot

不,這個功能並非跟Slot掛鉤的,而是跟InventoryManager很相關

InventoryManager.Update()

// 物品丟棄操做
if (!IsPickedItemEmpty()) {
    if (Input.GetMouseButtonDown(0) && 
        !UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject(-1)) {
        // 按下鼠標左鍵,且點擊處沒有任何物體
        int amount = pickedItem.Amount;
        if(Input.GetKey(KeyCode.LeftControl)) {
            // 若是按住ctrl,一個一個扔
            amount--;
        } else {
            // 沒有按住ctrl,所有扔掉
            amount = 0;
        }
        SetPickedItemAmount(amount);
}}

BugFix -- 
運行,發現若是鼠標點擊不精確,將pickedItem放置到slot之間的空位
  會執行丟棄物品的操做
  由於此時EventSystem沒有獲得點擊的反饋

解決方法 -- 將Knapsack等Inventory的Image.RaycastTarget勾選上便可

任務46&47:控制揹包、箱子的顯示和隱藏

在父類Inventory中實現

在Knapsack和Chest物體上添加Canvas Group,經過alpla進行透明度控制

private float targetAlpha = 1;
private float smooth = 5;

在Inventory.Update中,實現顯示和隱藏

if(Mathf.Abs(canvasGroup.alpha - targetAlpha) > 0.05f) {
    canvasGroup.alpha = Mathf.Lerp(canvasGroup.alpha, targetAlpha, Time.deltaTime * smooth);
} else {
    canvasGroup.alpha = targetAlpha;
}

兩個方法實現顯示和隱藏
  public void Hide/ Display () {
    targetAlpha = 0 / 1;
  }

在Player中用I控制揹包的顯示和隱藏:
  if(Input.GetKeyDown(KeyCode.I) {
    // 這裏要根據當前顯示狀態進行更換顯示或隱藏狀態
    // 可是在這裏實現不大好
    // ------- Inventory.DisplaySwitch() {
    //    if(targetAlpha == 0) {
    //      Display();
    //    } else {
    //      Hide();
    //  }}

相同的,能夠用C鍵控制Chest的顯示和隱藏

發現bug -- 當箱子或揹包隱藏之後,東西仍然能夠移動給它
  隱藏之後,將CanvasGroup.BlocksRaycasts = false;便可

在Hide和Display()的最後,添加一句
  canvasGroup.blocksRaycasts = false/ true;

任務49&50&51:Character角色面板

角色面板會顯示當前角色佩戴的裝備和武器 -- 一共十一個部位

複製Chest
  命名Character,修改Text
  增長到11個slot
  刪除GridLayoutGroup,刪除Container,由於slot不須要自動排列
  刪除腳本Chest,添加腳本Character.cs

運行,發現Slot裏面沒有存儲限制,即其餘物品也能夠放入裝備面板

解決方法 -- 建立Slot的子類EquipmentSlot
  給每一個裝備添加對應的EquipmentType和WeaponType
  public Equipment.EquipmentType equipmentType;
  public Weapon.WeaponType weaponType;
  修改EquipmentType和WeaponType,各添加一個None的選擇,將不屬於的slot賦值爲None
    注:OffHandSlot便可以放裝備也能夠放武器

角色面板的功能(策劃)
  1. 在其餘地方直接右鍵,便可穿戴;在角色面板中直接右鍵,便可脫下
  2. 拖拽方式

任務52&53&54:裝備的穿戴與卸下 -- 拖拽方式

添加腳本Character.cs,繼承自Inventory

須要使用Inventory中的slotList,就不能聲明爲private,改成protected
  // 或者提供一個get方法

寫成單例模式:
  private static Character _instance;
  public static Character Instance {
    get {
      if(_instance == null) {
        _instance = GameObject.Find("Character").GetComponent<Character>();
      }
      return _instance;
  }}

由於在裝備槽中的斷定方式不大同樣
  須要斷定是否符合裝備類型,並且不須要判斷ctrl的狀況
  並且沒有Amount的加減問題
  override OnPointerDown()

分析:
  1. pickedItem爲空
    當前slot爲空 -- 無操做
    當前slot不爲空 -- 選取裝備
  2. pickedItem不爲空
    當前slot爲空
      判斷是否符合類型,符合就放入,不符合則無操做
    當前slot不爲空
      判斷是否符合類型,符合就交換,不符合則無操做

using UnityEngine.EventSystems;
public class EquipmentSlot : Slot {
    public Equipment.EquipmentType equipmentType;
    public Weapon.WeaponType weaponType;

    // 傳入item的類型是否與當前slot的類型匹配
    public bool IsItemMatchedSlotType(Item item) {
        return (item is Equipment && ((Equipment)item).EquipType == equipmentType ||
            item is Weapon && ((Weapon)item).WeapType == weaponType);
    }
    public override void OnPointerDown(PointerEventData eventData) {
        ItemUI pickedItemUI = InventoryManager.Instance.PickedItem;
        ItemUI currItemUI = GetComponentInChildren<ItemUI>();
        if (InventoryManager.Instance.IsPickedItemEmpty()    ) {
            if(transform.childCount == 1) {
                // pickedItem爲空,且當前slot不爲空
                // 選取裝備
          InventoryManager.Instance.SetPickedItem(currItemUI.Item, currItemUI.Amount);
                Destroy(currItemUI.gameObject);
        }} else {
            // pickedItem不爲空
            if(transform.childCount == 0) {
                // 當前slot爲空
                if (IsItemMatchedSlotType(pickedItemUI.Item)) {
                    // pickedItem知足slot的類型
                    // 放入slot
                    StoreItem(pickedItemUI.Item);
            InventoryManager.Instance.SetPickedItemAmount(pickedItemUI.Amount - 1);
            }} else {
                // 當前slot不爲空
                if (IsItemMatchedSlotType(pickedItemUI.Item)) {
                    // 交換物品
                    ExchangeWithPickedItem();
}}}}}

另外一種override的思路(未驗證可信性)

EquipmentSlot: Slot
  將Slot.StoreItem()聲明爲virtual的

// 在StoreItem中判斷是否符合slot的裝備類型
public override bool StoreItem(Item itemToStore) { // 判斷是否爲Equipment,不然不能存入 if(IsItemTypeEquipment()) {base.StoreItem(itemToStore); return true; } else { return false; }}

由於以前StoreItem是確定會將Item存入的,不存在不存入的狀況
  所以在Slot.OnPointerDown()中會報錯。
  解決方法:在Slot.StoreItem()最後返回return true;

任務55&56&57&58:裝備的穿戴與卸下 -- 右鍵方式

以前對與鼠標按鍵的檢測是經過IPointerDownHandler -- OnPointerDown()
  該事件當鼠標的任意按鍵按下時觸發
  所以以上操做能夠發生在鼠標左鍵/右鍵/滾輪按下時

要求:左鍵控制物品的移動,右鍵控制裝備的穿戴

PointerEventData eventData.button表示當前按下的鼠標按鍵類型

if(eventData.button == PointerEventData.InputButton.Left) {
  // 物品移動代碼
} else if (eventData.button == PointerEventData.InputButton.Right) {
  // 物品穿戴代碼
}

在Slot中的物品穿戴代碼 -- 由於物品的穿戴是在Slot上右鍵的而不是在Character中操做的
  分析:
    由於不須要判斷pickedItem的狀態,若是右鍵,就進行穿戴
      -- 仍是須要判斷pickedItem的狀態
        當pickedItem不爲空時,且當前slot不爲空時,進行
    當前slot不爲空 -- 進行穿戴

} else if (eventData.button == PointerEventData.InputButton.Right) {
    // 右鍵按下,進行物品的穿戴
    if (transform.childCount == 1) {
        ItemUI currItemUI = transform.GetChild(0).GetComponent<ItemUI>();
        if (currItemUI.Item is Equipment || currItemUI.Item is Weapon) {
            // 當前slot不爲空,且物品爲可穿戴類型的 -- 進行穿戴
            Item currItem = currItemUI.Item;
            Debug.Log("currItem" + currItem.Name);
            // DestroyImmediate是當即銷燬,當即釋放資源,作這個操做的時候,會消耗不少時間的,影響主線程運行
            // Destroy是異步銷燬,通常在下一幀就銷燬了,不會影響主線程的運行。
            // 可是這裏不能使用Destroy,不然在存回Knapsack時取得的EmptySlot就不許確了
            DestroyImmediate(currItemUI.gameObject);
            Character.Instance.PutOnEquipOrWeapon(currItem);
}}}

穿戴的方法Character.PutOnEquipOrWeapon(Item item):

public void PutOnEquipOrWeapon(Item item  ) {
    EquipmentSlot slot = FindSlotWithSameItemEquipOrWeaponType(item);
    if (slot != null) {
        // 若是找到匹配類型的slot
        if(slot.transform.childCount == 1) {
            // 若是slot不爲空
            // 存入物品,再將將原來裝備面板中的物品放回到揹包中
            Item itemToPutBack = slot.GetComponentInChildren<ItemUI>().Item;
            slot.GetComponentInChildren<ItemUI>().SetItem(item);
            Knapsack.Instance.StoreItem(itemToPutBack);
        } else {
            // 若是slot爲空 -- 直接將pickedItem放入
            slot.StoreItem(item);
}}}
public EquipmentSlot FindSlotWithSameItemEquipOrWeaponType(Item item) {
    foreach(EquipmentSlot slot in slotList) {
        if(slot.IsItemMatchedSlotType(item)) {
            Debug.Log(slot.name + " Matched!");
            return slot;
    }}
    return null;
}

注意點:
  1. 原來打算經過PickedItem來進行物品穿戴或交換,後來改成直接執行
  2. 注意knapsack中currItem的銷燬和storeBack的執行順序,會致使FindEmptySlot()的結果
  3. Destroy() 和 DestroyImmediate()的區別

BugFixing -- 

1. 
  發現當手上有pickedItem時,對其餘裝備進行右鍵,仍然能夠進行穿戴,並且pickedItem不變
  由於右鍵的操做沒有經過pickedItem來執行

  解決方法:
  在Slot.OnPointerDown()中穿戴裝備處進行判斷
    if(!InventoryManager.Instance.IsPickedItemEmpty() && ...)

2.
  在穿戴裝備後,仍然顯示該裝備的TooTip
  解決方法:
  在成功穿戴裝備後,隱藏ToolTip
    ToolTip.Instance.HideToolTip();

裝備欄中裝備的右鍵卸下:

由於卸下的操做只在Character面板中,所以在EquipmentSlot.cs中

} else if (eventData.button == PointerEventData.InputButton.Right) {
    // 按下右鍵時,進行物品的卸下
    if(InventoryManager.Instance.IsPickedItemEmpty()    ) {
        // 當手上沒有物品時,才能卸下裝備
        if(transform.childCount == 1) {
            // 噹噹前slot不爲空時
            if (Knapsack.Instance.FindEmptySlot()) {
                // 揹包中有空位能夠接收物品
                Destroy(currItemUI.gameObject);
                Character.Instance.TookOffEquipOrWeapon(currItemUI.Item);
                ToolTip.Instance.HideToolTip();
}}}}

對應的Character中的TookOffEquipOrWeapon() -- 很簡單,只須要存入揹包便可
  public void TookOffEquipOrWeapon(Item item) {
    Knapsack.Instance.StoreItem(item);
  }

任務59:角色面板的顯示和隱藏

在Player.cs中經過E鍵控制裝備面板的顯示和隱藏

if(Input.GetKeyDown(KeyCode.E)) {
  Character.Instance.DisplaySwitch();
}

在Canvas中顯示全部按鍵的提示信息

在Canvas中新建Text,命名KeyTip
內容:"G 獲得物品(換行)I揹包顯示 C箱子顯示 E裝備面板顯示"

任務60:控制角色面板的屬性顯示

武器和裝備對角色的屬性影響,須要在角色面板上顯示出來

在CharacterPanel下新建子物體UI->Panel,命名PropertyPanel
  新建子物體UI->Text
    居中,留些邊距,大小顏色微調

屬性的顯示在Character.cs中進行控制
  全部屬性彙總:
    裝備影響:
      strength力量
      intelligence智力
      agility敏捷度
      stamina體力
    武器影響:
      damage攻擊力

在Player中存放基礎屬性:
  private int basicStrength = 10;
  public int BasicStrength {
    get {
      return basicStrength;
  }}

在Character.cs中

private void UpdatePropertyTextUI() {
  int strength = 0, intelligence = 0, agility = 0, stamina = 0, damage = 0;
  // 取得每個裝備的屬性,並加到總屬性值中
  foreach (Slot slot in slotList) {
  if (slot.transform.childCount == 1) {
    // 若是該slot中有裝備
    Item currItem = slot.GetComponentInChildren<ItemUI>().Item;
    if (currItem is Equipment) {
      strength += ((Equipment)currItem).Strength;
      ......
    } else if (currItem is Weapon) {
      damage += ((Weapon)currItem).Damage;
  }}}
  // 加上基礎屬性
  Player player = GameObject.FindWithTag("Player").GetComponent<Player>();
  strength += player.BasicStrength;
  ......
  // 更新UI
  propertyText.text = string.Format("攻擊力:{0}\n力量:{1}\n智力:
    {2}\n敏捷:{3}\n體力:{4}\n", damage, strength, intelligence, agility, stamina);
}

何時須要調用UpdatePropertyTextUI呢?
  1. Start中
  2. PutOn()和TookOff()中 -- 右鍵穿戴和卸下
  3. EquipmentSlot中的OnPointerDown()中 -- 拖拽穿戴和卸下

在Start和PutOn()和TookOff()中,直接調用UpdatePropertyTextUI();便可
在EquipmentSlot中,在三個左鍵穿戴脫下裝備的地方
  transform.parent.SentMessage("UpdatePropertyTextUI");
  // EquipmentSlot的父類是Character,向其發送消息,調用UpdatePropertyTextUI()

任務61&62:商店面板

複製ChestPanel,命名VendorPanel
  Title Text內容爲小販
  Slot個數改成12格
  刪除Chest.cs替換爲Vendor.cs

小販面板的功能:
  不須要和其餘面板進行交換,只負責買賣
  小販的Slot中只須要作
    1. 右擊購買的功能
    2. 左鍵銷售的功能

小販面板的初始化 -- 開始時有本身售賣的物品

將12個格子改成VendorSlot,添加VendorSlot.cs,繼承自Slot.cs

在Vendor.cs中聲明數組,表示售賣的物品
public int[] itemIdArray; // 在Inspector面板中賦值
在Start()進行根據itemId進行實例化Item
  base.Start();
  InitVendor();
}
其中private void InitVendor() {
    for(int i = 0; i < itemIdArray.Length; i++) {
      StoreItem(itemIdArray[i]);
  }}

運行,報錯 -- 

NullReferenceException: Object reference not set to an instance of an object
InventoryManager.GetItemById (Int32 id)

緣由:由於GetItemById()中的itemList是在ParseItemJson()中初始化的,
  ParseItemJson()是在InventoryManager.Start()中被調用的
  而InitVendor()中調用了GetItemById(),InitVendor()也是在Vendor.Start()中被調用的
  -- 同時調用,所以會報空指針

解決方法:在InventoryManager中,將ParseItemJson()在Awake()中調用

任務63&64&65:角色的金幣屬性與購買販賣的金幣加減

在Canvas下,新建UI->Image
  SourceImage: coin,顏色金色,調整大小,位於右上角,Anchor右上角
    新建UI->Text,命名CoinAmount,金色

在Player中
  private int coinAmount = 100;
  private Text coinAmountText;
  Start中coinAmountText = GameObject.Find("Coin").GetComponentInChildren<Text>();
     coinAmountText.text = coinAmount.ToString();

  // 金錢的加減
  public bool Consume(int num) {
    if(coinAmount >= num) {
      coinAmount -= num;
      coinAmountText.text = coinAmount.ToString();
      return true;
    }
    return false;
  }

  public void EarnCoin(int num) {
    coinAmount += num;
    coinAmountText.text = coinAmount.ToString();
  }

物品的購買:

若是直接在VendorSlot中調用Player的方法進行買賣,會比較麻煩
買賣的操做放在Vendor中,再由VendorSlot調用

Vendor.cs中實現private void Purchase(Item item) {}

在VendorSlot中,override OnPointerDown()

若是按下右鍵,且手上沒有物品,且當前slot不爲空時,便可購買物品

public override void OnPointerDown(PointerEventData eventData) {
    if (eventData.button == PointerEventData.InputButton.Right
        && InventoryManager.Instance.IsPickedItemEmpty()) {
        // 當右鍵,且手上沒有東西時
        if (transform.childCount == 1) {
            // 若是slot不爲空
            // 買入物品
            Item currItem = transform.GetComponentInChildren<ItemUI>().Item;
            transform.parent.parent.SendMessage("Purchase", currItem);
}}}

Vendor.Purchase(Item item):
  若是Knapsack中有空位,則進行購買
    若購買成功,將item存入knapsack
    若購買不成功,不進行任何操做

private void Purchase(Item item) {if (Knapsack.Instance.FindEmptySlot() != null) {
        // 若是Knapsack中有空slot
        if (player.Consume(item.Buyprice)) {
            // 進行購買,成功購買
            Knapsack.Instance.StoreItem(item);
}}}

物品的售賣:

若當前pickedItem不爲空,則進行售賣
  若按下ctrl,則賣一個;若沒有按ctrl,則所有賣掉

VendorSlot中
  點擊左鍵時,且手上有東西時,銷售物品

} else if(eventData.button == PointerEventData.InputButton.Left
    && !InventoryManager.Instance.IsPickedItemEmpty()) {
    // 當左鍵,且手上有東西時 -- 銷售物品
    transform.parent.parent.SendMessage("Sell");
}

Vendor.SellItem()
  // 注意pickedItem是有數量的
  // 判斷Ctrl的按下

private void Sell() {
    int sellAmount = 0;
    ItemUI itemToSellUI = InventoryManager.Instance.PickedItem;
    if (Input.GetKey(KeyCode.LeftControl)) {
        // 若Ctrl鍵按下,一次賣一個
        sellAmount = 1;
    } else {
        // Ctrl沒有按下,全賣掉
        sellAmount = itemToSellUI.Amount;
    }
    player.EarnCoin(itemToSellUI.Item.Sellprice * sellAmount);
    InventoryManager.Instance.SetPickedItemAmount(itemToSellUI.Amount - sellAmount);
}

任務66:鍛造系統的UI設計

複製Chest,命名ForgePanel
  修改Text內容爲"鍛造"
  替換成腳本Forge: Inventory
  包含兩個Slot

  刪除Grid Layout Group

  添加子物體UI->Button
    SourceImage: button_square
    Text: "合成"

任務67&68:鍛造祕方的類和Json數據

祕方類 Fomula.cs
  須要兩個item的id和對應的數量,並完成完整的構造函數

public int item1ID { get; private set; }
public int item2ID { get; private set; }
public int item1Amount { get; private set; }
public int Item2Amount { get; private set; }

public int ResItemID { get; private set; }

  (擴展:若是須要多種物品,能夠聲明兩個數組,分別存儲材料類型和材料數量 -- 這裏不作展開)

Material類的種類一共有三種,id分別爲15鐵塊、16玄鐵劍的鍛造祕籍、17頭盔的鍛造祕籍

Formulas.Json文件中的格式,跟Fomula類保持一致

[
    {
        "Item1ID": 15,
        "Item1Amount": 5,
        "Item2ID": 16,
        "Item2Amount": 1,
        "ResItemId": 14
    },
    {
        "Item1ID": 15,
        "Item1Amount": 3,
        "Item2ID": 17,
        "Item2Amount": 1,
        "ResItemId": 8
    }
]

解析Json數據 -- 放在Forge.cs中實現,由於只有在鍛造面板纔會使用到

須要將全部獲得的Formula對象存放
private List<Formula> formulaList;

private void ParseFormulaJson() {
    TextAsset textAsset = Resources.Load<TextAsset>("ItemsData/Formulas");
    JsonData jsonData = JsonMapper.ToObject<JsonData>(textAsset.text);
    foreach(JsonData data in jsonData) {
        int item1ID = int.Parse(data["Item1ID"].ToString());
        int item1Amount = int.Parse(data["Item1Amount"].ToString());
        int item2ID = int.Parse(data["Item2ID"].ToString());
        int item2Amount = int.Parse(data["Item2Amount"].ToString());
        int resItemID = int.Parse(data["ResItemID"].ToString());
        Formula newFormula = new Formula(item1ID,item1Amount,item2ID,item2Amount,resItemID);
        formulaList.Add(newFormula);
}}

在Forge.Start()中進行解析
  public override void Start() {
    base.Start();
    ParseFormulaJson();
  }

任務69&70:物品合成的匹配算法

點擊合成按鈕時會在Forge中進行ForgeItem()處理

Siki老師的算法:
  獲得當前擁有的Material及數量
  一個Material存一個id,好比有五個鐵塊,就存5個15在list中

public void ForgeItemSikiVersion() {
    // 獲得當前已有的材料
    List<int> currMaterialIdList = new List<int>();
    foreach (Slot slot in slotList) {
        if (slot.transform.childCount == 1) {
            // slot不爲空
            ItemUI currItemUI = slot.GetComponentInChildren<ItemUI>();
            for (int i = 0; i < currItemUI.Amount; i++) {
                // 有多少個物品,就存入多少個id
                currMaterialIdList.Add(currItemUI.Item.ID);
    }}}
    foreach(Formula formula in formulaList) {
        if (formula.IsMaterialCompositionMatched(currMaterialIdList)) {
            // 獲得了合成之後的物品
}}}

在Formula的構造函數中調用GetRequiredMaterialIdList(),初始化requiredMaterialIdList

private void GetRequiredMaterialIdList() {
    requiredMaterialIdList = new List<int>();
    for(int i = 0; i<Item1Amount; i++) {
        requiredMaterialIdList.Add(Item1ID);
    }
    for (int i = 0; i < Item2Amount; i++) {
        requiredMaterialIdList.Add(Item2ID);
 }}

在Formula中實現判斷已有物品與自己Formula的配料是否匹配

// SikiVersion
public bool IsMaterialCompositionMatched(List<int> idList) {
    GetRequiredMaterialIdList();
    List<int> tempIdList = new List<int>(idList);
    for (int i = 0; i < requiredMaterialIdList.Count; i++) {
        if (tempIdList.Contains(requiredMaterialIdList[i])) {
            tempIdList.Remove(requiredMaterialIdList[i]);
        } else {
            return false;
        }
    }
    return true;
}    

附:遍歷List並刪除指定元素的正確方式
  https://blog.csdn.net/s_GQY/article/details/52273840

上述算法效率不高,特別是當一個材料需求數量很大的時候
可是優勢是靈活 -- 若是後期修改了合成的Material種類,好比四五種,這段代碼也能夠完美執行

另外一種方法(MyVersion)能夠經過Dictionary的key:value鍵值對實現

 void ForgeItem() {
    // 獲得當前的材料
    Dictionary<int, int> currMaterialDict = new Dictionary<int, int>();
    foreach (Slot slot in slotList) {
        if (slot.transform.childCount == 1) {
            // slot不爲空
            ItemUI currItemUI = slot.GetComponentInChildren<ItemUI>();
            currMaterialDict.Add(currItemUI.Item.ID, currItemUI.Amount);
        }
    }
    // 判斷符合哪個祕籍的要求
    foreach (Formula formula in formulaList) {
        if (formula.IsMaterialCompositionMatched(currMaterialDict)) {
            Debug.Log(formula.ResItemID);
        }
    }
    // 進行合成
}
private void GetRequiredMaterialDictionary() {
    requiredMaterialDict = new Dictionary<int, int>();
    requiredMaterialDict.Add(Item1ID, Item1Amount);
    requiredMaterialDict.Add(Item2ID, Item2Amount);
}
public bool IsMaterialCompositionMatched(Dictionary<int, int> materialDict) {
    foreach (var requiredMaterial in requiredMaterialDict) {
        if (materialDict.ContainsKey(requiredMaterial.Key)) {
            int amount;
            materialDict.TryGetValue(requiredMaterial.Key, out amount);
            if(amount < requiredMaterial.Value) {
                // 沒有足夠數量
                return false;
            }
        } else {
            // 沒有該類型材料
            return false;
        }
    }
    return true;
}

找到了對應的Formula配方,
須要進行合成,並消耗對應的材料

在上述ForgeItem()獲得matchedFormula後

    // 判斷符合哪個祕籍的要求
    Formula matchedFormula = null;
    foreach (Formula formula in formulaList) {
        if (formula.IsMaterialCompositionMatched(currMaterialDict)) {
            matchedFormula = formula;
            break;
        }
    }
    // 進行合成
    if (matchedFormula != null) {
        // 有對應物品生成時
        if (Knapsack.Instance.FindEmptySlot()) {
            // 確保揹包中有空slot能夠放置物品
            // 將新生成的物品存入揹包
            Knapsack.Instance.StoreItem(matchedFormula.ResItemID);
            // 對應材料減小
            ConsumeMaterials(matchedFormula);
        }
    }
}

對應材料的減小:Forge.ConsumeMaterials(Formula formula)

private void ConsumeMaterials(Formula matchedFormula) {
    foreach (Slot slot in slotList) {
        if (slot.transform.childCount == 1) {
            // 若該slot不爲空
            ItemUI currItemUI = slot.GetComponentInChildren<ItemUI>();
            if (currItemUI.Item.ID == matchedFormula.Item1ID) {
                // 減去對應的數量
                currItemUI.SetAmount(currItemUI.Amount-matchedFormula.Item1Amount);
            } else if (currItemUI.Item.ID == matchedFormula.Item2ID) {
                // 減去對應的數量
                currItemUI.SetAmount(currItemUI.Amount-matchedFormula.Item2Amount);
}}}}

任務71:控制鍛造界面和商店界面的顯示和隱藏

經過T控制商店頁面

經過F控制鍛造頁面

任務72&73&74:控制物品的存儲和加載

將揹包等Inventory所存的物品,和金幣數量保存到本地文件中

在Inventory中建立兩個方法:
  public void SaveInventory() -- PlayerPrefs.SetString(string name, string value);
  public void LoadInventory() -- PlayerPrefs.GetString(string name);

void SaveInventory() {
    StringBuilder sb = new StringBuilder();
    foreach(Slot slot in slotList) {
        if(slot.transform.childCount == 1) {
            // slot不爲空
            ItemUI currItemUI = slot.transform.GetComponentInChildren<ItemUI>();
            // 用,隔開物品的id和amount
            // 用 / 隔開不一樣物品
            sb.AppendFormat("{0},{1}/", currItemUI.Item.ID, currItemUI.Amount);
        } else {
            // ID是從1開始的,若是爲0,表示slot爲空
            sb.Append("0/");
        }
    }
    // 將上述字符串保存到本地
    PlayerPrefs.SetString(this.gameObject.name, sb.ToString());
}

public void LoadInventory() {
    if(PlayerPrefs.HasKey(this.gameObject.name)) {
        // 若是有匹配名稱的本地文件
        // 讀取本地文件到string
        string str = PlayerPrefs.GetString(this.gameObject.name);
        string[] stringArray = str.Split('/');
        for(int i=0;i<stringArray.Length;i++    ) {
            // 每個片斷的數據對應一個slot中的存儲狀況
            if(stringArray[i]!="0") {
                string[] tempStr = stringArray[i].Split(',');
                int itemID = int.Parse(tempStr[0]);
                int amount = int.Parse(tempStr[1]);
                slotList[i].StoreItem(InventoryManager.Instance.GetItemById(int.Parse(tempStr[0])));
                slotList[i].GetComponentInChildren<ItemUI>().SetAmount(int.Parse(tempStr[1]));
}}}

任務74:BugFix

在運行時發現,對stringArray[i] == "0"的狀況須要進行操做
若是在遊戲中按下了加載按鈕,本地文件中爲空的slot不會被加載爲空,而是不進行操做

解決方法:

} else {
    if (slotList[i].transform.childCount == 1) {
        // slot進行清空
        Destroy(slotList[i].transform.GetChild(0).gameObject);
    }
}

在Start中調用LoadInventory()
  由於itemList在InventoryManager.Awake()中初始化
  slotList在Inventory.Start()中初始化
  所以須要將LoadInventory等到這兩個初始化以後再調用

// 或是手動進行加載,見下

在Canvas下建立保存按鈕(和加載按鈕)

在InventoryManager中提供兩個方法
  public void SaveInventory() {
    Knapsack.Instance.SaveInventory();
    Chest.Instance.SaveInventory();
    Character.Instance.SaveInventory();
    // Vendor.Instance.SaveInventory(); -- 商店是不須要保存的
    Forge.Instance.SaveInventory();
  }

  public void LoadInventory() {
    Knapsack.Instance.LoadInventory();
    Chest.Instance.LoadInventory();
    Character.Instance.LoadInventory();
    // Vendor.Instance.LoadInventory(); -- 商店是不須要加載的
    Forge.Instance.LoadInventory();
  }

將這兩個方法分別註冊到上述兩個按鈕的點擊事件下

運行,保存,加載 -- 報錯:FormatException: Input string was not in the correct format
  通過Debug.Log查證,出錯位置爲每一個Inventory的最後stringArray

緣由:
  Save的時候,執行的是sb.AppendFormat("....../", ...);
    所以,在最後一個slot保存後,string也是以'/'結尾
  而在Load的時候,經過'/'進行了Split操做,分割出的最後一個string爲 ""空字符
    這個空字符在Load的時候就會報錯

解決方法:
  在Load中遍歷str的時候忽略最後一個string便可
  for(int i = 0; i < stringArray.Length - 1; i++)

任務74:金幣數量的保存加載 和 遊戲發佈

金幣數量的保存和加載:

在Player中(由於coin是Player的屬性)
  public int CoinAmount {
    get {
      return coinAmount;
    }
    set {
      coinAmount = value;
      coinAmountText.text = coinAmount.ToString();
  }}

在InventoryManager中的SaveInventory()中的最後添加一句
  PlayerPrefs.SetInt("CoinAmount", GameObject.FindWithTag("Player").GetComponent<Player>().CoinAmount);

在InventoryManager中的LoadInventory()中最後添加
  if(PlayerPrefs.HasKey("CoinAmount")) {
    GameObject.FindWithTag("Player").GetComponent<Player>().CoinAmount = PlayerPrefs.GetInt("CoinAmount");
  }

遊戲發佈:

File -> BuildSettings -> 選擇PC版本 -> 添加場景 -> 選擇路徑 -> Build

發現Bug -- 可是我好像視覺上沒有發現。。。
  Bug描述:
    pick up item時,item會有一幀顯示在另外一個地方,而後纔跟隨鼠標
  緣由:
    咱們在設置pickedItem時,InventoryManager.SetPickedItem(...)中
      設置了pickedItem的item和amount,並顯示出pickedItem
    在Update()中進行了PickedItem的位置跟隨
    所以先後相差了一幀
  解決方法:
    在SetPickedItem中,進行初始位置的設置

public void SetPickedItem(Item item, int amount) {
    pickedItem.SetPickedItem(item, amount);
    UpdatePickedItemPosition();
    pickedItem.Display();
    ToolTip.Instance.HideToolTip();
}
相關文章
相關標籤/搜索