Unity 简易背包系统:简单的拖动,储存和回退

普通背包的实现

系列文章
1.Unity 简易背包系统:简单的拖动,储存和回退
2.Unity 简易背包系统:物品展示框
3.Unity 简易背包系统:工作台合成和配方
4.Unity 简易背包系统:可数物品,数值化储存

〇. 实现原理

  • 运行视频
    运行视频
    请添加图片描述
      拖动单元:负责储存单元的信息,和基础的友元操作。
      储存单元:负责储存和管理拖动单元,并接受鼠标行为信息,再把信息转化为单元管理者的操作。
      单元管理者:负责将储存单元发送的信息整理和加工,再反馈给储存单元,以达到对拖动单元实现间接控制的效果;并储存一些基本的对储存单元的操作(比如:选择框显示,开始和结束对单元的操作,丢弃或保留单元内物品的指令的发送等等)。
      回退板:负责给单元管理者发送回退信息,如果鼠标进入回退板,则在鼠标松开后能够实现拖动单元的回退,否则不回退。
      单元容器板:负责储存储存单元和管理自己负责的储存单元,并储存基本的对储存单元的操作(比如:把所有储存单元储存的拖动单元转移到另一个单元容器板内)。

一. 具体实现步骤

1. 拖动单元

  • 仅仅储存数据和简单的自毁方法
using UnityEngine;

/// <summary>
/// 仅用于移动的 UIDragSlot
/// </summary>
public class UIDragSlot : MonoBehaviour
{
	// 储存位置信息
    [HideInInspector] public Vector2 startOffset;

	// 储存 RectTranform
    public RectTransform rectTrans;


	// 初始化
    public virtual void Start()
    {
        rectTrans = this.GetComponent<RectTransform>();
    }

    /// <summary>
    /// 销毁操作
    /// </summary>
    public void Dissolve()
    {
    	// 这个是我自己写的拓展销毁方法
    	// 可以改成对象池的回池操作或者直接 Destory(this.gameObject);
        gameObject.Destroy(); 
    }
}

2. 储存单元

  • 为了安全性,里面用的都是私有访问修饰符来修饰变量。
  • 为了可拓展性,里面有很多方法是可以继承重写的。
using UnityEngine;
using UnityEngine.EventSystems;

public delegate void StoreDo(UIStoreSlot slot);
public delegate void RemoveDo(UIStoreSlot slot);

/// <summary>
/// 必须需要一个 '回退板' 在后面,不然会当做丢掉拖动单元
/// </summary>
public class UIStoreSlot : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler,IPointerEnterHandler,IPointerExitHandler
{
    [SerializeField] private UIDragSlot hold;
    protected UIDragSlot Hold
    {
        get => hold;
        set => SetHold(value);
    }

    [SerializeField] private Vector2 m_StoreOffset;

    // 事件区 // 用在需要 '单元容器板' 操作的时候
    public StoreDo storeDo;
    public RemoveDo removeDo;

    public bool IsEmpty => hold == null;

    public virtual void Start()
    {
        if(UISlotManager.manager == null)
            UISlotManager.Create<UISlotManager>();
    }

    /// <summary>
    /// 将 hold 物品移动到指定位置
    /// <para> ==不安全== 需要外部检验 IsEmpty </para>
    /// </summary>
    public void Reback()
    {
        hold.transform.SetParent(this.transform);
        hold.rectTrans.anchoredPosition = m_StoreOffset;

        storeDo?.Invoke(this);
        SetHoldDo();
    }
    /// <summary>
    /// 将 hold 物品父集关系从 '储存单元' 脱离出来
    /// </summary>
    /// <param name="parent"> 脱离到的 parent </param>
    public void TakeOutTo(Transform parent)
    {
        hold.transform.SetParent(parent);
    }
    /// <summary>
    /// <para> 与另一个 '储存单元' 交换 hold </para>
    /// <para> ==不安全== 如果出现有一个 hold 为 null 则交换失败</para>
    /// </summary>
    /// <param name="other"></param>
    private void SwapWith(UIStoreSlot other)
    {
        UISlotManager.Swap(ref hold, ref other.hold);

        this.Reback();
        other.Reback();
    }
    /// <summary>
    /// <para> 把 hold 交给另一个 '储存单元'(强加给 other),然后自己丢失  </para>
    /// <para> ==不安全== 如果 otherSlot 存在 hold 则会丢弃原有的 hold,数据丢失! </para>
    /// </summary>
    /// <param name="other"></param>
    public void ImposeTo(UIStoreSlot other)
    {
        other.hold = this.hold;
        SetNull();

        other.Reback();
    }
    /// <summary>
    /// <para> 拿取一个 hold </para>
    /// <para> ==安全== 会在拿取前检验自身有无 hold,并 Reback() </para>
    /// <para> ==不安全== 如果凭空生成物体,则相当于数据滋生 </para>
    /// </summary>
    /// <param name="hold"></param>
    /// <returns> 自己有 hold 则拿取失败,返回 false;否则返回 true </returns>
    public bool SetHold(UIDragSlot hold)
    {
        if (this.hold == null && hold != null)
        {
            this.hold = hold;
            Reback();

            SetHoldDo();
            return true;
        }
        else
            return false;
    }
    /// <summary>
    /// <para> 强制失去 hold </para>
    /// <para> ==不安全== 数据上的置空,显示上 '拖拽单元' 不会消失 </para>
    /// </summary>
    private void SetNull()
    {
        hold = null;
        removeDo?.Invoke(this);

        SetNullDo();
    }

    /// <summary>
    /// <para> 当此 hold 不为 null 时,可以进行交换 </para>
    /// <para> ==稍微安全== 当此 hold 不为 null 时,可以进行交换 </para>
    /// </summary>
    /// <param name="other"></param>
    public void ExchangeWith(UIStoreSlot other)
    {
        if (other.IsEmpty)
            ImposeTo(other);
        else
            SwapWith(other);
    }


    #region 继承方法区
    /// <summary>
    /// 设置 hold 后要做的事情
    /// </summary>
    protected virtual void SetHoldDo() { }
    /// <summary>
    /// 设置 hold 为 null 后要做的事情
    /// </summary>
    protected virtual void SetNullDo() { }
    /// <summary>
    /// 比较两个 '储存单元' 的 hold 是否是一样的(不同的继承可以有不同的返回值)
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public virtual bool IsSameHold(UIStoreSlot other) => false;
    public virtual void Check() { }
    #endregion


    /// <summary>
    /// 丢弃 hold 物品
    /// </summary>
    public void Drop()
    {
        // 和外界的互动
        // ...

        Destory();
    }
    /// <summary>
    /// 销毁 hold 物品
    /// </summary>
    public void Destory()
    {
        hold.Dissolve();

        SetNull();
    }


    #region 事件区
    public void OnPointerDown(PointerEventData eventData)
    {
        UISlotManager.manager.Start(this);

        if (hold != null)
        {
            hold.startOffset = hold.rectTrans.anchoredPosition - (Vector2)Input.mousePosition;

            BeginDragDo();
        }
    }
    public void OnDrag(PointerEventData eventData)
    {
        if (hold != null && UISlotManager.manager.IsStartWith(this))
        {
            hold.rectTrans.anchoredPosition = (Vector2)Input.mousePosition + hold.startOffset;

            OnDragDo();
        }
    }
    public void OnPointerUp(PointerEventData eventData)
    {
        UISlotManager.manager.End();

        if (hold != null && UISlotManager.manager.IsStartWith(this))
        {
            EndDragDo();
        }
    }
    public void OnPointerEnter(PointerEventData eventData)
    {
        UISlotManager.manager.SlotChoose(this);
    }
    public void OnPointerExit(PointerEventData eventData)
    {
        UISlotManager.manager.SlotDischoose();
    }

    /// <summary>
    /// 开始拖动时调用的方法(在移动后调用)
    /// </summary>
    public virtual void BeginDragDo() { }
    /// <summary>
    /// 拖动时调用的方法(在移动后调用)
    /// </summary>
    public virtual void OnDragDo() { }
    /// <summary>
    /// 结束拖动时调用的方法(在移动后调用)
    /// </summary>
    public virtual void EndDragDo() { }
    #endregion
}

注意:里面大部分方法都是缺少安全性的,但是换来的是更精确的分类,需要对准需求来使用方法。而为了保障方法安全使用,会在单元管理者中将方法进行分类使用。

3. 单元管理者

  • 关于单元管理者的脚本编写
  • 细节解释:

  内部包括了选择选择框的使用,而其相关代码在这段代码结束的下面,其他的详细相关解释都在注解中。

using UnityEngine;

public class UISlotManager : MonoBehaviour
{
    public static UISlotManager manager;

	// 回退布尔
    public bool reback = false;

    [Header("关于选择 '储存单元' 的部分")]
    [SerializeField] private UISlotSelectShow m_SelectShow;
    protected UISlotSelectShow SelectShow => m_SelectShow;
    public UIStoreSlot SelectSlot => m_SelectShow.SelectSlot;


    [SerializeField] private UIStoreSlot m_StartSlot, m_EndSlot;
    protected UIStoreSlot StartSlot => m_StartSlot;
    protected UIStoreSlot EndSlot => m_EndSlot;


    [Header("要被拿出来时候放在的 parent")]
    [SerializeField] private Transform outestPanel;


    public void Awake()
    {
        if (manager != null && manager != this)
        {
            this.gameObject.Destroy();
            return;
        }

        manager = this;
    }
    public void Start()
    {
        outestPanel ??= GameObject.Find("Canvas").transform;

        m_SelectShow.Init();
    }

    #region 外部使用(选取和抓取)方法
    /// <summary>
    /// 记录下第一次点击拖拽的 '储存单元',开始移动操作
    /// </summary>
    /// <param name="slot"></param>
    public void Start(UIStoreSlot slot)
    {
        if (!slot.IsEmpty)
        {
            slot.TakeOutTo(outestPanel); // 将拖动的物品置顶,以防被挡到

            m_StartSlot = slot;
        }
        else if (m_SelectShow.IsOnSelect)
        {
            m_SelectShow.StartSelect(slot);

            MultiSelectDo();
        }
    }
    /// <summary>
    /// 结束移动操作
    /// </summary>
    public void End()
    {
        if (m_SelectShow.IsSelectingSlots)
        {
            if (m_SelectShow.SelectSlot.IsEmpty)
                m_SelectShow.UnSelect(outestPanel);

            m_SelectShow.EndSelect();

            return;
        }
        else if (m_StartSlot == null)
        {
            Debug.Log("没有选中 StartSlot");

            return;
        }
        else if (m_StartSlot.IsEmpty)
        {
            Debug.Log("选中的 StartSlot 是空的");

            m_SelectShow.UnSelect(outestPanel);

            return;
        }


        if (!reback)                    // 丢弃物品
        {
            m_StartSlot.Drop();
            m_SelectShow.UnSelect(outestPanel);
        }
        else if (m_EndSlot != null)     // 移动到合法的格子,两个格子的物品互相交换
        {
            if (m_StartSlot == m_EndSlot)
                SelectSwitch();
            else
            {
                EndDo();
                m_SelectShow.Select(m_EndSlot);
            }
        }
        else if (reback)                // 没有移动到合法的格子,物品回到原来的格子
        {
            m_StartSlot.Reback();
        }


        // 清除初始记录状态
        m_StartSlot = null;
    }
    /// <summary>
    /// 选择最后要放在的 '储存单元'
    /// </summary>
    /// <param name="slot"></param>
    public void SlotChoose(UIStoreSlot slot)
    {
        m_EndSlot = slot;

        if (m_SelectShow.IsSelectingSlots && slot.IsEmpty)
        {
            if (m_SelectShow.SelectSlot.IsEmpty)
            {
                End();
                return;
            }

            m_SelectShow.AppendSelect(slot);

            MultiSelectDo();
        }
    }
    /// <summary>
    /// 取消选择最后要放在的 slot
    /// </summary>
    public void SlotDischoose()
    {
        m_EndSlot = null;
    }


    /// <summary>
    /// 在外部强制取消显示框的显示
    /// </summary>
    public void UnSelect()
        => m_SelectShow.UnSelect(outestPanel);     
    /// <summary>
    /// 检验鼠标点下的是不是 other '储存单元'
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public bool IsStartWith(UIStoreSlot other)
        => m_StartSlot == other;
    #endregion


	// 子类继承和使用的方法
    protected virtual void EndDo()
    {
        m_StartSlot.ExchangeWith(m_EndSlot);
    }
    protected virtual void MultiSelectDo() { }

    protected void SelectSwitch()
    {
        if (m_SelectShow.SelectSame(m_StartSlot))
            m_SelectShow.UnSelect(outestPanel);
        else
            m_SelectShow.Select(m_StartSlot);

        m_StartSlot.Reback();
    }


    #region 其他静态方法
    public enum SlotsState
    {
        Error, // 移动主体是空
        Empty, // 移动到空位
        Equal, // 移动到的位置具有一样的物体
        Ocupy, // 移动到的位置被占领,且不相同
    }
    /// <summary>
    /// 移动物体
    /// </summary>
    /// <param name="startSlot"> 开始移动的位置 </param>
    /// <param name="endSlot"> 移动到的位置 </param>
    /// <returns> 
    /// <para> 如果有效物体移动到空位,并返回 SlotsState.Empty; </para>
    /// <para> 如果有效物体要移动的位置为值一样的非空位,并返回 SlotsState.Equal; </para>
    /// <para> 如果有效物体要移动的位置为非空位,则返回 SlotsState.Ocupy; </para>
    /// <para> 如果移动无效物体,则返回 SlotsState.Error; </para>
    /// </returns>
    public static SlotsState MoveTo(UIStoreSlot startSlot, UIStoreSlot endSlot)
    {
        if (startSlot.IsEmpty)                  // 移动无效物体
            return SlotsState.Error;
        else if (endSlot.IsEmpty)               // 有效物体移动到空位
            return SlotsState.Empty;
        else if (startSlot.IsSameHold(endSlot)) // 移动到的位置被占领,且不相同
            return SlotsState.Equal;
        else                                    // 移动到的位置具有一样的物体
            return SlotsState.Ocupy;
    }
    public static void Swap<T>(ref T item1,ref T item2)
    {
        T tmpItem = item1;
        item1 = item2;
        item2 = tmpItem;
    }
    #endregion


    #region 强制初始化
    public static void Create<T>() where T :UISlotManager
    {
        GameObject slotManagerGO = new GameObject("UISlotManager");
        UISlotManager slotManager = slotManagerGO.AddComponent<T>();
        manager = slotManager;
    }
    #endregion
}
  • 关于选择框 ‘UISlotSelectShow’ 的脚本编写:
  • 细节解释:

  内部包括了选择框显示的代码和多物体选择的代码,其中多物体选择部分暂时不会用到(和后面可数储存单元有关)。

using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class UISlotSelectShow
{
    public void Init()
    {
        if (m_SelectTrans.gameObject.activeInHierarchy)
            m_SelectTrans.gameObject.SetActive(false);
    }


    #region 选择框部分
    [Header("选择展示框部分")]
    [SerializeField] private RectTransform m_SelectTrans;
    [SerializeField] private Vector2 m_SelectOffset;

    private UIStoreSlot m_SelectSlot;
    public UIStoreSlot SelectSlot => m_SelectSlot;
    public bool IsOnSelect => m_SelectSlot != null;

    public void Select(UIStoreSlot slot)
    {
        if (m_SelectSlot == null)
            m_SelectTrans.gameObject.SetActive(true);


        m_SelectSlot = slot;

        m_SelectTrans.SetParent(slot.transform);
        m_SelectTrans.SetAsFirstSibling();
        m_SelectTrans.anchoredPosition = m_SelectOffset;
    }
    public void UnSelect(Transform outestPanel)
    {
        m_SelectSlot = null;

        //m_SelectTrans.SetParent(outestPanel);
        m_SelectTrans.gameObject.SetActive(false);
    }

    public bool SelectSame(UIStoreSlot slot)
        => m_SelectSlot == slot;
    #endregion


    #region 选择多个物体部分
    [Header("选择多个 '储存单元' 部分")]
    private List<UIStoreSlot> m_SelectSlots;
    private bool m_SelectingSlots;

    public bool IsSelectingSlots => m_SelectingSlots;

    /// <summary>
    /// 创建一个新的 '储存单元' 数组储存 selectSlots
    /// </summary>
    /// <typeparam name="T"> UIStoreSlot 继承类 </typeparam>
    /// <returns> 新的 Slot 数组 </returns>
    public T[] GetSelectSlots<T>()where T : UIStoreSlot
    {
        T[] selectSlots = new T[m_SelectSlots.Count];
        for(int i = 0; i < m_SelectSlots.Count; ++i)
            selectSlots[i] = m_SelectSlots[i] as T;

        return selectSlots;
    }

    /// <summary>
    /// 开始多个选择 '储存单元'
    /// <para> ==不安全== 不会检验是否在多选前,满不满足 IsOnSelect </para>
    /// </summary>
    /// <returns></returns>
    public void StartSelect(UIStoreSlot appendSlot)
    {
        m_SelectSlots = new List<UIStoreSlot>() { appendSlot };
        m_SelectingSlots = true;
    }
    /// <summary>
    /// 添加 selectSlot
    /// <para> ==安全== 不会重复储存已经有的 selectSlot </para>
    /// </summary>
    /// <param name="slot"></param>
    public void AppendSelect(UIStoreSlot slot)
    {
        if (!m_SelectSlots.Contains(slot))
            m_SelectSlots.Add(slot);
    }
    /// <summary>
    /// 添加 selectSlot
    /// <para> ==!注意!== 会再次储存已经有的 selectSlot </para>
    /// </summary>
    /// <param name="slot"></param>
    public void AddSelect(UIStoreSlot slot)
    {
        m_SelectSlots.Add(slot);
    }
    /// <summary>
    /// 移除 selectSlot
    /// </summary>
    /// <param name="slot"></param>
    public void RemoveSelect(UIStoreSlot slot)
    {
        m_SelectSlots.Remove(slot);
    }
    /// <summary>
    /// 置空 selectSlot 列表
    /// </summary>
    public void EndSelect()
    {
        m_SelectSlots = null;
        m_SelectingSlots = false;
        
        m_SelectSlot?.Check();
    }
    #endregion
}

4. 回退板

  • 它的操作就是直接对单元管理者回退布尔进行更改。
using UnityEngine;
using UnityEngine.EventSystems;

public class UIRebackSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    public void OnPointerEnter(PointerEventData eventData)
        => UISlotManager.manager.reback = true;
    public void OnPointerExit(PointerEventData eventData)
        => UISlotManager.manager.reback = false;
}

5. 单元容器板

  • 一个储存储存单元的容器(不止可以当背包)
using UnityEngine;
using System;

public class UISlotContainer : MonoBehaviour
{
    [SerializeField] protected UIStoreSlot[] slots;
    public int Count
    {
        get
        {
            int count = 0;
            foreach (UIStoreSlot slot in slots)
                if (!slot.IsEmpty)
                    ++count;
            return count;
        }
    }
    public bool IsEmpty => Count == 0;
    public bool IsFull => Count >= slots.Length;

    /// <summary>
    /// 将物品转移给另一个容器
    /// </summary>
    /// <param name="otherContainer"> 要转移物品给的容器 </param>
    public virtual void TransferTo(UISlotContainer otherContainer)
    {
        UISlotManager.manager.UnSelect();

        for (int i = 0, j = 0; i < slots.Length && j < otherContainer.slots.Length;)
        {
            UISlotManager.SlotsState move = UISlotManager.MoveTo(slots[i], otherContainer.slots[j]);
            switch (move)
            {
                case UISlotManager.SlotsState.Empty:
                    slots[i].ImposeTo(otherContainer.slots[j]);
                    ++i;
                    ++j;
                    break;
                case UISlotManager.SlotsState.Ocupy:
                    ++j;
                    break;
                case UISlotManager.SlotsState.Error:
                    ++i;
                    break;
                default:
                    return;
            }
        }
    }
    /// <summary>
    /// 压缩自身物品(不按排序顺序排序背包内物品)
    /// </summary>
    public virtual void TransferSelf()
    {
        UISlotManager.manager.UnSelect();

        for (int i = 0; i < slots.Length; ++i)
        {
            for (int j = i + 1; j < slots.Length;++j)
            {
                UISlotManager.SlotsState move = UISlotManager.MoveTo(slots[j], slots[i]);
                switch (move)
                {
                    case UISlotManager.SlotsState.Empty:
                        slots[j].ImposeTo(slots[i]);
                        break;
                    case UISlotManager.SlotsState.Ocupy:
                        break;
                    case UISlotManager.SlotsState.Error:
                        continue;
                    case UISlotManager.SlotsState.Equal:
                        break;
                    default:
                        return;
                }
            }
        }
    }
    /// <summary>
    /// 向空格子添加物体
    /// </summary>
    /// <param name="slot"></param>
    public void Add(params UIDragSlot[] dragSlots)
    {
        for (int i = 0, j = 0; i < slots.Length && j < dragSlots.Length; ++i)
            if (slots[i].SetHold(dragSlots[j]))
                ++j;
    }
    /// <summary>
    /// 向空格子添加物体
    /// </summary>
    /// <param name="dragSlot"></param>
    public void Add(UIDragSlot dragSlot)
    {
        for (int i = 0; i < slots.Length; ++i)
            if (slots[i].SetHold(dragSlot))
                break;
    }



#if UNITY_EDITOR
    public virtual void OnTransformChildrenChanged()
    {
        slots = new UIStoreSlot[transform.childCount];
        for (int i = 0; i < transform.childCount; ++i)
            if (transform.GetChild(i).TryGetComponent(out UIStoreSlot slot))
                slots[i] = slot;
    }

    /// <summary>
    /// 完完全全复制一个 dragSlot,数据泄露(作弊器)
    /// </summary>
    /// <param name="dragSlot"></param>
    public void Create(UIDragSlot dragSlot)
    {
        Create(dragSlot, null);
    }
    protected void Create<T>(T dragSlot, Action<T> createDo) where T : UIDragSlot
    {
        if (!IsFull)
        {
            T newSlot = Instantiate(dragSlot);
            if (!newSlot.gameObject.activeInHierarchy)
                newSlot.gameObject.SetActive(true);

            createDo?.Invoke(newSlot);

            newSlot.Start();
            Add(newSlot);
        }
    }
#endif
}

二. 脚本附着

普通操作

  • 拖动单元
    **拖动单元**

  • 储存单元
    **储存单元**

  • 单元容器板回退板
    **单元容器板**和**回退板**

  • 单元管理者
    **单元管理者**

  • 运行视频
    运行视频

更多操作

  • 转移物品按钮
    • 转移回来:
      **转移物品按钮**
    • 转移出去:
      **转移物品按钮**
  • 假排序按钮
    **假排序按钮**
  • 创建物品按钮
    **创建物品按钮**
  • 展示视频
    请添加图片描述

最后,有什么不足的请大佬们在评论区分享分享观点~

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity3D-XRInput是一个简单易懂的XR输入系统,专为Unity3D开发者设计。XRInput提供了一种集成虚拟现实和增强现实硬件设备的解决方案,帮助开发者更方便地处理虚拟现实设备的输入操作。 XRInput具有以下特点: 1. 简单易用:XRInput提供了一套简化的API接口,开发者可以轻松地获取XR设备的输入信息。无论是手柄、头戴式显示器或其他XR硬件设备,都可以通过XRInput统一管理。 2. 多平台兼容:XRInput支持大部分主流的VR和AR平台,包括Oculus Rift、HTC Vive、Windows Mixed Reality等。开发者可以无需关注具体设备的差异,只需使用XRInput即可适配多个平台。 3. 扩展性强:XRInput提供了可扩展的功能,开发者可以根据自己的需求进行定制。例如,可以添加自定义的手势识别算法,或者扩展新的输入设备。 4. 与Unity集成:XRInput与Unity3D紧密集成,无需额外的配置和插件。开发者可以直接在Unity编辑器中使用XRInput进行虚拟现实应用程序的开发。 5. 支持常见输入操作:XRInput支持常见的输入操作,如位置追踪、手势识别、触摸输入等。开发者可以根据需要处理这些输入操作,以实现更丰富的交互体验。 总之,Unity3D-XRInput是一个简单易懂的XR输入系统,为Unity开发者提供了更便捷的虚拟现实设备输入管理,帮助开发者节省时间和精力,快速开发出高质量的XR应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值