主要技术:UGUI,背包显示与功能,背包数据库,世界物品拾取与掉落,武器装备

背包基本UI

  • 步骤

    1. 建立 UI-canvens,子物体panel,设置 panel 背景图片比例

      • 对素材UI需要进行缩放,选中素材本身,Inspector 窗口Sprite Editor 绿色框线外是保持比例不缩放的,只对绿色框内进行缩放

      • 然后将Panel的ImageType设置为Sliced

    2. 新建标题图片文本框,新建退出按钮

      • Set Native Size 恢复原始尺寸
    3. 新建背包内格子

      • 格子网格化控制使用Grid Layout Group

      • 每个格子添加 图像(背包物品) 和 文本(数量)

  • 最终效果

世界地图上的物品

人物收集到物品 添加到背包

  • 地图上的物体 挂载rigibodycollider,数据等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # ItemData_SO
    public enum ItemType { Useable, Weapon, Armor }
    [CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item Data")]
    public class ItemData_SO : ScriptableObject
    {
    public ItemType itemType;
    public string itemname;
    public Sprite itemTcon;
    public int itemAmount;
    // 可以输入一段文字
    [TextArea]
    public string description = "";
    // 是否可以堆叠
    public bool stackable;
    [Header("Weapon")]
    // 拾取的武器
    public GameObject weaponPrefab;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # ItemPickUp
    public class ItemPickUp : MonoBehaviour
    {
    public ItemData_SO itemdata;

    // 物体被触发
    private void OnTriggerEnter(Collider other)
    {
    if (other.CompareTag("Player"))
    {
    //TODO: 物品添加到背包
    //装备武器
    // 捡起物体后,原本地面上的物体销毁
    Destroy(gameObject);
    }
    }
    }
  • 人物拾取后,由于原本地面的上的物体有重力会掉落,拾取后重新生成一个

  • 修改指针和移动

装备武器

  • 角色死亡 掉落武器 武器与地面有碰撞

  • 生成武器与原始武器的角度位置需要一致

  • 步骤

    1. 触碰武器,在player原位置生成一个prefab Sword_eq

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #CharacterStates
      #region Equip Weapon from ItemData_SO
      public void EquipWeapon(ItemData_SO weapon)
      {
      if (weapon.weaponPrefab != null)
      // Instantiate重载方法可以直接生成在父物体下面
      Instantiate(weapon.weaponPrefab, weaponSlot.parent);
      //TODO:更新属性
      attackData.ApplyWeaponData(weapon.weaponData);
      }
      #endregion
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class ItemPickUp : MonoBehaviour
      {
      public ItemData_SO itemdata;
      // 物体被触发
      private void OnTriggerEnter(Collider other)
      {
      if (other.CompareTag("Player"))
      {
      //TODO: 物品添加到背包
      //装备武器
      // 想获取其他数据可以通过GameManager,装备武器
      GameManager.Instance.playStats.EquipWeapon(itemdata);
      // 捡起物体后,原本地面上的物体销毁
      Destroy(gameObject);
      }
      }
      }
    2. 为武器加载攻击数值ItemData_SO添加AttackData_SO weaponData

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      # AttackData_SO 
      # "添加武器后的数值更新"
      public void ApplyWeaponData(AttackData_SO weapon)
      {
      attackRange = weapon.attackRange;
      skillRange = weapon.skillRange;
      coolDown = weapon.coolDown;

      minDange = weapon.minDange;
      maxDamage = weapon.maxDamage;

      criticalMultiplier = weapon.criticalMultiplier;
      criticalChance = weapon.criticalChance;
      }

背包数据库

  • 背包中的物体放在一个列表中

  • 创建一个包含物体名字和数量类的列表:创建单独的 class InventoryItem 用来记录背包里的物品和现有数量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 能够看见
    [System.Serializable]
    public class InventoryItem
    {
    // 该类数据的名字
    public ItemData_SO itemData;
    // 数量
    public int amount;
    }

    [System.Serializable]效果

  • 在背包中添加物体,考虑到堆叠:创建 InventoryData_SO 作为不同背包的数据库,新建Inventory Data 数据 Bag

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    # InventoryDats_SO
    [CreateAssetMenu(fileName = "New Inventory", menuName = "Inventory/Inventory Data")]
    public class InventoryDats_SO : ScriptableObject
    {
    public List<InventoryItem> items = new List<InventoryItem>();
    // 在背包中添加
    public void AddItem(ItemData_SO newItemData, int amount)
    {
    // 是否可以堆叠
    // 添加一个物体,首先判断背包中是否有,有就堆叠,没有就新占一个格子
    bool found = false;

    // 可以堆叠
    if (newItemData.stackable)
    {
    foreach (var item in items)
    {
    // 是同一类物品
    if (item.itemData == newItemData)
    {
    item.amount += amount;
    found = true;
    break;
    }
    }
    }
    // 不能堆叠 放到最近的一个空格
    for (int i = 0; i < items.Count; i++)
    {
    // 背包为空 或 没有同类物体
    if (items[i].itemData == null && !found)
    {
    items[i].itemData = newItemData;
    items[i].amount = amount;
    break;
    }
    }
    }
    }
    [System.Serializable]
    // 添加物品的类
    public class InventoryItem
    {
    // 该类数据的名字
    public ItemData_SO itemData;
    // 数量
    public int amount;
    }
  • 使用管理器管理背包中的格子:挂载给背包UIInventoryCanvas

    1
    2
    3
    4
    5
    6
    7
    # InventoryManager
    public class InventoryManager : Singleton<InventoryManager>
    {
    // TODO: 最后添加模板保存数据
    [Header("Inventory Data")]
    public InventoryDats_SO inventoryData;
    }
  • 修改 ItemPickUp 实现碰撞物体 拾取到背包里

    1
    2
    # ItemPickUp
    InventoryManager.Instance.inventoryData.AddItem(itemdata, itemdata.itemAmount);

背包显示

  • 显示整个背包:ContainerUI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ContainerUI : MonoBehaviour
    {
    public SlotHolder[] slotHolders;

    public void RefreshUI()
    {
    for (int i = 0; i < slotHolders.Length; i++)
    {
    // Index 也是从 0 开始
    slotHolders[i].itemUI.Index = i;
    // 对里面的每一个格子调用更新物品
    slotHolders[i].UpdateItem();
    }
    }
    }
  • 显示每个格子:SlotHolder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 不同的格子类型
    public enum SlotType { BAG, WEAPON, ARMOR, ACTION}
    public class SlotHolder : MonoBehaviour
    {
    public SlotType slotType;
    public ItemUI itemUI;

    public void UpdateItem()
    {
    // 枚举在切换类型时最好对应Switch
    switch (slotType)
    {
    case SlotType.BAG:
    // inventoryData 传给 itemUI 的 Bag
    itemUI.Bag = InventoryManager.Instance.inventoryData;
    break;
    case SlotType.WEAPON:
    break;
    case SlotType.ARMOR:
    break;
    case SlotType.ACTION:
    break;
    }
    // 匹配数据库中物体
    // UI拿到 背包,不同的itemUI拿到不同的数据库
    // 找到inventoryData list items里面格子index数量同样的物品
    var item = itemUI.Bag.items[itemUI.Index];
    // 给到UI
    itemUI.SetItemUI(item.itemData, item.amount);
    }
    }
  • 控制背包内物体的文本和图像:ItemUI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class ItemUI : MonoBehaviour
    {
    // 图标 数量
    public Image icon = null;
    public Text amount = null;

    public InventoryDats_SO Bag { get; set; }
    public int Index { get; set; } = -1;

    public void SetItemUI(ItemData_SO item, int itemAmount)
    {
    if (item != null)
    {
    // item 的图标和数量 传给 当前背包格子
    icon.sprite = item.itemTcon;
    amount.text = itemAmount.ToString();
    // 开启这个格子的图标和数量
    icon.gameObject.SetActive(true);
    }
    else
    icon.gameObject.SetActive(false);
    }
    }

  • !!!! 记得设置image按比例缩放

设置快捷栏和信息面板的UI

  • 设置下面的物品栏同背包:新建 ImageUI 设置里面的格子 每个格子挂载 ItemSlot
  • 设置左侧属性栏同背包

拖拽物品

  • 建三个类型的UI和数据库
    • Bag 背包
    • Action 下方工具栏
    • Equipment 身上装备
  • 拖拽的脚本DragItem挂载到能拖拽的UI上,也就是ItemSlot
  • 拖拽接口
    • using UnityEngine.EventSystems
    • IBeginDragHandler
    • IDragHandler
    • IEndDragHandler
  • 直接拖拽时,会受到UI之间层级的影响,新建一个Cavens覆盖在他们上面
    • 创建 DragData 用来记录每一个 UI 物品原始数据
    • 放下时还在 新建的Cavens里面,要改,

交换物品

  • 通过射线

  • 判断位置

    • 拖拽物品到位置position 判断position是在 inventoryUI.slotHolders 的哪一个格子里或者不在

    • Rectransform

      EventSystem.current.IsPointerOverGameObjet() :返回true范围下是UI,反之不是

    • RectransformUtility.RectangleContainsScreenPoint:是否包含给定摄像机观察到的屏幕点

  1. 是否在格子里
  2. 切换数据
  • 使用 EventSystem 判断指针位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # DragItem
    public void OnEndDrag(PointerEventData eventData)
    {
    // 放下物品 交换数据
    // 是否指向UI物品
    if (EventSystem.current.IsPointerOverGameObject())
    {
    // 判断是否在格子内
    // 执行 SwapItem()
    }
    // 刷新
    }
  • 利用 RectTransformUtility.RectangleContainsScreenPoint 判断鼠标位置是否包含在每一个格子范围内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    # InventoryManager
    // 拖拽物品到位置position
    // 判断position是在 inventoryUI.slotHolders 的哪一个格子里或者不在
    // 检测30个背包格子
    public bool CheckInventoryUI(Vector3 position)
    {
    for (int i = 0; i < inventoryUI.slotHolders.Length; i++)
    {
    RectTransform t = (RectTransform)inventoryUI.slotHolders[i].transform;
    if (RectTransformUtility.RectangleContainsScreenPoint(t, position))
    return true;
    }
    return false;
    }
    // 检测6个工具栏
    public bool CheckInActionUI(Vector3 position)
    {
    for (int i = 0; i < actionUI.slotHolders.Length; i++)
    {
    RectTransform t = (RectTransform)actionUI.slotHolders[i].transform;
    if (RectTransformUtility.RectangleContainsScreenPoint(t, position))
    return true;
    }
    return false;
    }
    // 检测2个装备栏
    public bool CheckInEquipmentUI(Vector3 position)
    {
    for (int i = 0; i < equipmentUI.slotHolders.Length; i++)
    {
    RectTransform t = (RectTransform)equipmentUI.slotHolders[i].transform;
    if (RectTransformUtility.RectangleContainsScreenPoint(t, position))
    return true;
    }
    return false;
    }
  • 交换图片的同时要交换真是的数据列表排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # DragItem
    public void SwapItem()
    {
    var targetItem = targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index];
    // 背包里真实数据
    var tempItem = currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index];
    // 是否相同物体
    bool isSameItem = tempItem.itemData == targetItem.itemData;

    if (isSameItem && targetItem.itemData.stackable)
    {
    // 相同可堆叠就加上
    targetItem.amount += tempItem.amount;
    // 移动的那个物体归0
    tempItem.itemData = null;
    tempItem.amount = 0;
    }
    else
    {
    currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index] = targetItem;
    targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index] = tempItem;
    }
    }
  • 调整 RectTransform 的 offset 确保图片在正确位置显示

    1
    2
    3
    4
    5
    # DragItem
    RectTransform t = (RectTransform)transform;
    // 放到格子中间
    t.offsetMax = -Vector2.one * 5;
    t.offsetMin = Vector2.one * 5;
  • DragItem

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    // 没有的时候自动挂载
    [RequireComponent(typeof(ItemUI))]
    public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    {
    ItemUI currentItemUI;
    // 拖动的格子
    SlotHolder currentHolder;
    // 目标格子
    SlotHolder targetHolder;

    void Awake()
    {
    currentItemUI = GetComponent<ItemUI>();
    // DragItem挂载在ItemSlot上,格子是他的父级
    currentHolder = GetComponentInParent<SlotHolder>();
    }
    // 开始拖拽
    public void OnBeginDrag(PointerEventData eventData)
    {
    // 记录原始信息:最终没有拖走要回到原始位置
    InventoryManager.Instance.currentDrag = new InventoryManager.DragData();
    InventoryManager.Instance.currentDrag.originalHolder = GetComponentInParent<SlotHolder>();
    InventoryManager.Instance.currentDrag.originalParent = (RectTransform)transform.parent;
    // 解决遮挡问题:当前格子的父级设置为新的Canvas在顶层
    transform.SetParent(InventoryManager.Instance.dragCanvas.transform, true);
    }
    // 拖拽过程
    public void OnDrag(PointerEventData eventData)
    {
    // 跟随鼠标位置
    transform.position = eventData.position;
    }
    // 结束拖拽
    public void OnEndDrag(PointerEventData eventData)
    {
    // 放下物品 交换数据
    // 是否指向UI物品,然后判断是否在格子范围内
    if (EventSystem.current.IsPointerOverGameObject())
    {
    // 三种类型的格子都可
    if (InventoryManager.Instance.CheckInventoryUI(eventData.position) ||
    InventoryManager.Instance.CheckInActionUI(eventData.position) ||
    InventoryManager.Instance.CheckInEquipmentUI(eventData.position))
    {
    // 判断点击的是不是 SlotHolder 类型,空格子能直接拿到SlotHolder
    if (eventData.pointerEnter.gameObject.GetComponent<SlotHolder>())
    targetHolder = eventData.pointerEnter.gameObject.GetComponent<SlotHolder>();
    // 非空格子 拿到的是 image,需要拿到其父级格子
    else
    targetHolder = eventData.pointerEnter.gameObject.GetComponentInParent<SlotHolder>();
    // 目标格子类型
    switch(targetHolder.slotType)
    {
    case SlotType.BAG:
    SwapItem();
    break;
    case SlotType.ACTION:
    break;
    case SlotType.WEAPON:
    break;
    case SlotType.ARMOR:
    break;
    }
    currentHolder.UpdateItem();
    targetHolder.UpdateItem();
    }
    }
    transform.SetParent(InventoryManager.Instance.currentDrag.originalParent);
    RectTransform t = (RectTransform)transform;
    // 放到格子中间
    t.offsetMax = -Vector2.one * 5;
    t.offsetMin = Vector2.one * 5;
    }

    public void SwapItem()
    {
    var targetItem = targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index];
    // 背包里真实数据
    var tempItem = currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index];
    // 是否相同物体
    bool isSameItem = tempItem.itemData == targetItem.itemData;

    if (isSameItem && targetItem.itemData.stackable)
    {
    // 相同可堆叠就加上
    targetItem.amount += tempItem.amount;
    // 移动的那个物体归0
    tempItem.itemData = null;
    tempItem.amount = 0;
    }
    else
    {
    currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index] = targetItem;
    targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index] = tempItem;
    }
    }
    }

切换武器

  • 在 MouseManager 添加函数方法 避免 UI 互动时影响人物控制

    • 判定UI,对UI不使用鼠标控制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # MouseManager
    bool InteractWithUI()
    {
    // 碰到UI
    if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
    return true;
    else
    return false;
    }

    private void Update()
    {
    SetCursorTexture();
    if (InteractWithUI()) return;
    MouseControl();
    }
  • 在 CharacterStats 中加入 ChangeWeapon 的方法

    • 有武器先卸除
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # CharacterStats  
    //切换武器
    public void ChangeWeapon(ItemData_SO weapon)
    {
    UnEquipment();
    EquipWeapon(weapon);
    }

    public void EquipWeapon(ItemData_SO weapon)
    {
    if (weapon.weaponPrefab != null)
    // Instantiate重载方法可以直接生成在父物体下面
    Instantiate(weapon.weaponPrefab, weaponSlot.parent);

    //TODO:更新属性
    attackData.ApplyWeaponData(weapon.weaponData);
    }
    //卸下武器
    public void UnEquipment()
    {
    // 有其他武器
    if (weaponSlot.transform.childCount != 0)
    for (int i = 0; i < weaponSlot.transform.childCount; i++)
    Destroy(weaponSlot.transform.GetChild(i).gameObject);
    attackData.ApplyWeaponData(baseAttackData);
    // TODO:切换动画
    }
  • 在 SlotHolder 中切换所属背包的数据库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # SlotHolder
    switch (slotType)
    {
    case SlotType.BAG:
    // inventoryData 传给 itemUI 的 Bag
    itemUI.Bag = InventoryManager.Instance.inventoryData;
    break;
    case SlotType.WEAPON:
    itemUI.Bag = InventoryManager.Instance.equipmentData;
    // 装备武器 切换武器
    if (itemUI.Bag.items[itemUI.Index].itemData != null)
    GameManager.Instance.playStats.ChangeWeapon(itemUI.Bag.items[itemUI.Index].itemData);
    else
    GameManager.Instance.playStats.UnEquipment();
    break;
    case SlotType.ARMOR:
    itemUI.Bag = InventoryManager.Instance.equipmentData;
    break;
    case SlotType.ACTION:
    itemUI.Bag = InventoryManager.Instance.actionData;
    break;
    }
  • DragItem 中添加物品属性和 SlotType 的匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # DragItem
    // 目标格子类型,相同类型才能放入对应格子
    switch(targetHolder.slotType)
    {
    case SlotType.BAG:
    SwapItem();
    break;
    // 武器格子的类型只能放武器
    case SlotType.WEAPON:
    if (currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Weapon)
    SwapItem();
    break;
    case SlotType.ARMOR:
    if (currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Armor)
    SwapItem();
    break;
    // ACTION类型格子可以放 Useable 物品
    case SlotType.ACTION:
    if (currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Useable)
    SwapItem();
    break;
    }
    currentHolder.UpdateItem();
    targetHolder.UpdateItem();
    }
  • 不同的格子类型:public enum SlotType { BAG, WEAPON, ARMOR, ACTION}

  • 不同的Item类型:public enum ItemType { Useable, Weapon, Armor }

可使用物品

  • 建立可使用物品的ItemData 数据,添加useable item data

  • 蘑菇的设置和剑类似,UseableItemData_SO 制作可使用物品的属性模版

  • 吃蘑菇效果涨10点血

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # CharacterStates    
    // 吃蘑菇血量修改
    public void ApplyHealh(int amount)
    {
    if (CurrentHealth + amount <= MaxHealth)
    CurrentHealth += amount;
    else
    CurrentHealth = MaxHealth;
    }
  • 鼠标点击实现效果:利用 IPointerClickHandler 接口实现双击使用物品

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # SlotHolder
    public void OnPointerClick(PointerEventData eventData)
    {
    // 鼠标双击:取余防止三击判断
    if (eventData.clickCount % 2 == 0&& itemUI.Bag.items[itemUI.Index].amount > 0)
    {
    // 使用物品
    UseItem();
    }
    }
    // 使用物品功能
    public void UseItem()
    {
    if (itemUI.GetItem() != null)
    {
    if (itemUI.GetItem().itemType == ItemType.Useable && itemUI.Bag.items[itemUI.Index].amount > 0)
    {
    GameManager.Instance.playStats.ApplyHealh(itemUI.GetItem().useableItemData.healthPoint);
    itemUI.Bag.items[itemUI.Index].amount -= 1;
    }
    }
    // 使用之后物品数量-1 刷新
    UpdateItem();
    }
  • 背包物品为0时设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     # ItemUI
    public void SetItemUI(ItemData_SO item, int itemAmount)
    {
    // 物品数量为0时
    if (itemAmount == 0)
    {
    Bag.items[Index].itemData = null;
    icon.gameObject.SetActive(false);
    return;
    }
    }

按键控制UI

  • 添加实现 CloseBtn 关闭当前 UI 面板

    QQ截图20220910201324

  • 添加按键控制打开关闭 UI 面板

    QQ截图20220910201224

  • 创建 ActionButton 脚本设置独立按键使用快捷栏物品

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # ActionBtn
    // 按下按键实现对应方法
    public class ActionBtn : MonoBehaviour
    {
    // 显示所有按键
    public KeyCode actionKey;

    private SlotHolder currentSlotHolder;

    void Awake()
    {
    currentSlotHolder = GetComponent<SlotHolder>();
    }
    void Update()
    {
    // 按下对应按键 使用对应格子的物体
    if (Input.GetKeyDown(actionKey) && currentSlotHolder.itemUI.GetItem())
    currentSlotHolder.UseItem();
    }
    }

显示Player相关信息

  • UI上实时看到玩家模型:创建 RawImage 和 独立 Camera 实现映射 Player 的模型

    • 在属性界面新建一个 RayImage,设置好尺寸

      Image组件的图片必须是Sprite,而RawImage可以是任何类型的Texture。

      Image组件功能更多,代码更复杂。

    • 创建保存模型材质的 Render Text:project creat -> Render Text,尺寸为以上尺寸

    • 为当前Player新建一个子摄像机

      QQ截图20220912110125

    • 给RawImage添加以上纹理,最终效果(预览显示武器需要武器layer设置为player,因为渲染的是player层)

      QQ截图20220912110242

  • 获得 Text 的面板信息

  • 创建函数方法实时更新面板信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # InventoryManager
    // 属性面板属性值
    [Header("Stats Text")]
    public Text healthText;
    public Text attackText;

    // 更新属性面板函数
    public void UpdataStatsData(int health, int min, int max)
    {
    // 当前生命值
    healthText.text = health.ToString();
    // 最小攻击力到最大攻击力
    attackText.text = min + "-" + max;
    }

动画切换控制

  • 没拿武器一套动画 拿武器一套动画
  • 使用更多动画来创建 Animator override controller

    以拿武器为模板创建 Animator override controller,修改无武器攻击动画

    以原模板为攻击动画

  • ItemData_SO 中添加武器配套动画控制器

    1
    2
    #ItemData_SO
    public AnimatorOverrideController weaponAnimator;
  • 在 EquipWeapon 和 UnEquipment 中添加代码切换武器伴随动画

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # CharacterStates
    public void EquipWeapon(ItemData_SO weapon)
    {
    if (weapon.weaponPrefab != null)
    // Instantiate重载方法可以直接生成在父物体下面
    Instantiate(weapon.weaponPrefab, weaponSlot);

    //TODO:更新属性
    attackData.ApplyWeaponData(weapon.weaponData);
    // 切换武器同时切换另一套动画:runtimeAnimatorController当前控制器
    GetComponent<Animator>().runtimeAnimatorController = weapon.weaponAnimator;
    }
    //卸下武器
    public void UnEquipment()
    {
    // 有其他武器
    if (weaponSlot.transform.childCount != 0)
    {
    for (int i = 0; i < weaponSlot.transform.childCount; i++)
    Destroy(weaponSlot.transform.GetChild(i).gameObject);
    }
    attackData.ApplyWeaponData(baseAttackData);
    // TODO:切换动画,还原原始动画
    GetComponent<Animator>().runtimeAnimatorController = baseAnimator;
    }

Item Tooltip 物品信息显示栏

  • 创建 UI

  • 创建Image,下面两个text,一个名字一个内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # ItemTooltip
    public class ItemTooltip : MonoBehaviour
    {
    public Text itemNameText;
    public Text itemInfoText;

    // 设置物品信息
    public void SetupTooltip(ItemData_SO item)
    {
    itemNameText.text = item.itemname;
    itemInfoText.text = item.description;
    }
    }
  • 学习 Content size fitter 的用法实现自动拉伸缩放,Vertical Layout Group 文件排布

    QQ截图20220913161536

QQ截图20220913161550

  • 利用 IPointEnterHandler 接口实现鼠标悬停显示 Tooltip

    • 悬停进入:IPointerEnterHandler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # SlotHolder
    // 鼠标悬停显示:悬停进入
    public void OnPointerEnter(PointerEventData eventData)
    {
    if (itemUI.GetItem())
    {
    // 传入该格子的物体(ItemData_SO)
    InventoryManager.Instance.tooltip.SetupTooltip(itemUI.GetItem());
    // 显示UI
    InventoryManager.Instance.tooltip.gameObject.SetActive(true);
    }
    }
    • 悬停离开退出:IPointerExitHandler

      1
      2
      3
      4
      5
      // 鼠标悬停离开:悬停退出
      public void OnPointerExit(PointerEventData eventData)
      {
      InventoryManager.Instance.tooltip.gameObject.SetActive(false);
      }
  • RectTransform.GetWorldCorners 概念说明及使用方法

    QQ截图20220913172326

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    # ItemTooltip
    // 先更新位置防治鼠标闪烁
    void OnEnable()
    {
    UpdataPosition();
    }

    void Update()
    {
    UpdataPosition();
    }

    // 信息UI显示在鼠标位置
    public void UpdataPosition()
    {
    // 当前鼠标坐标
    Vector3 mousePos = Input.mousePosition;

    // 直接取UI位置为鼠标位置会发生遮挡
    // UI位置为锚点中心,应该加锚点到边缘的高度
    Vector3[] corners = new Vector3[4];
    rectTransform.GetWorldCorners(corners);

    // UI的宽和高
    float width = corners[3].x - corners[0].x;
    float heigh = corners[1].y - corners[0].y;

    // UI显示位置与鼠标位置关系
    // 物体在鼠标位置上方,显示在上方
    if (mousePos.y < heigh)
    rectTransform.position = mousePos + Vector3.up * heigh * 0.6f;
    // 屏幕与鼠标的宽度大于ui宽度就显示在接近屏幕侧
    else if (Screen.width - mousePos.x > width)
    rectTransform.position = mousePos + Vector3.right * width * 0.6f;
    // 否则相反
    else
    rectTransform.position = mousePos + Vector3.left * width * 0.6f;
    }

    # SlotHolder
    void OnDisable()
    {
    InventoryManager.Instance.tooltip.gameObject.SetActive(false);
    }

掉落物品

  • 创建 LootItem 类 记录要掉落的物品和权重

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # LootSpande
    // 随机掉落
    public class LootSpander : MonoBehaviour
    {
    // 掉落物品类
    [System.Serializable]
    public class LootItem
    {
    public GameObject item;
    // 权重 出现概率
    [Range(0, 1)]
    public float weight;
    }

    // 掉落物品数组
    public LootItem[] lootItems;
    }
  • 使用 [Range] 控制掉落权重的百分比

    1
    2
    [Range(0, 1)]
    public float weight;
  • QQ截图20220913224625

  • 实现多物品比较权重掉落

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # LootSpande
    // 按掉落物品权重生成
    public void Spawnloot()
    {
    float currentValue = Random.value;

    // 遍历列表中的掉落物品 根据随机生成的值对比权重
    for (int i = 0; i < lootItems.Length; i++)
    {
    if (currentValue <= lootItems[i].weight)
    {
    GameObject obj = Instantiate(lootItems[i].item);
    obj.transform.position = transform.position + Vector3.up * 2f;
    break; // 保证只掉落一个
    }
    }
    }
  • 敌人死亡调用

    1
    2
    3
    4
    5
    6
    7
    8
    # EnemyController
    // 人物消失和游戏停止
    void OnDisable()
    {
    // 死亡掉落物品
    if (GetComponent<LootSpander>() && isDead)
    GetComponent<LootSpander>().Spawnloot();
    }

完成背包

  • 添加背包的保存和读取方法

    • 进入游戏拿到数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # InventoryManager
    // 单例模式 Awake 是 Override
    protected override void Awake()
    {
    // 进入游戏保证拿到数据
    base.Awake();
    if (inventoryTemplate != null)
    inventoryTemplate = Instantiate(inventoryTemplate);
    if (actionTemplate != null)
    actionTemplate = Instantiate(actionTemplate);
    if (equipmentTemplate != null)
    equipmentTemplate = Instantiate(equipmentTemplate);
    }

    // 用之前的存档方法:PlayerPrefs
    // 保存背包数据
    public void SaveDate()
    {
    SaveManager.Instance.Save(inventoryData, inventoryData.name);
    SaveManager.Instance.Save(actionData, actionData.name);
    SaveManager.Instance.Save(equipmentData, equipmentData.name);
    }
    // 读取背包数据
    public void LoadData()
    {
    SaveManager.Instance.Load(inventoryData, inventoryData.name);
    SaveManager.Instance.Load(actionData, actionData.name);
    SaveManager.Instance.Load(equipmentData, equipmentData.name);
    }
    • 转换场景

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # SceneControllor
      // 传送场景名字和终点tag(坐标)
      IEnumerator Transition(string sceneName, TransitionDestination.DestinationTag destinationTag)
      {
      // 保存数据
      SaveManager.Instance.SavePlayerData();
      InventoryManager.Instance.SaveDate();
      }
      // 切换菜单场景
      IEnumerator LoadLevel(string scene)
      {
      // 保存数据
      SaveManager.Instance.SavePlayerData();
      InventoryManager.Instance.SaveDate();
      }

      添加Canves预制一定要加EventSystem

  • 添加 DragPanel 实现拖拽背包面板

    • 挂载在属性和背包界面
    • 鼠标拖拽界面任何位置都可以拖动:中心位置+偏移

    • 匹配分辨率和位移差: canvas.scaleFactor 画布缩放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # DragPanel
    public class DragPanel : MonoBehaviour, IDragHandler
    {
    RectTransform rectTransform;
    Canvas canvas;

    void Awake()
    {
    rectTransform = GetComponent<RectTransform>();
    canvas = InventoryManager.Instance.GetComponent<Canvas>();
    }

    public void OnDrag(PointerEventData eventData)
    {
    // 鼠标拖拽界面任何位置都可以拖动:中心位置+偏移
    // anchoredPosition 界面中心
    // eventData.delta 每秒位移
    rectTransform.anchoredPosition += eventData.delta / canvas.scaleFactor;
    // 匹配分辨率和位移差: canvas.scaleFactor 画布缩放
    }
    }
  • 鼠标选中界面在前面: 利用调整 Hierarchy 排序的方法改变渲染前后层级,越往下的越在前

    QQ截图20220914010754

    • rectTransform.GetSiblingIndex() 索引排序
    1
    2
    3
    4
    5
    6
    7
    # DragPanel
    // 鼠标点下
    public void OnPointerDown(PointerEventData eventData)
    {
    // 拖拽的设为第3个子物体,不影响DragItem
    rectTransform.SetSiblingIndex(2);
    }
    • 判断移动后的格子是否是原来格子