Unity中的通用对象池

本文介绍了Unity中如何实现一个通用的对象池,以解决频繁创建和销毁对象导致的性能问题。通过一个简单的射击子弹示例,阐述了内存池的设计思路,包括从使用者角度的需求、内存池单元结构、单元组结构、内存池结构以及组件化内存池的实现。最后展示了如何在实际游戏中使用内存池管理子弹,以达到资源高效复用和内存控制的目的。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文为博主原创文章,欢迎转载。请保留博主链接http://blog.youkuaiyun.com/andrewfan
Unity中的通用对象池

  本节通过一个简单的射击子弹的示例来介绍Transform的用法。子弹射击本身很容易制作,只要制作一个子弹Prefab,再做一个发生器,使用发生器控制按频率产生子弹,即克隆子弹Prefab,然后为每个子弹写上运动逻辑就可以了。这本该是很简单的事情。不过问题来了,发射出去后的子弹如何处理?直接Destroy吗?这太浪费了,要知道Unity的Mono内存是不断增长的。就是说出了Unity内部的那些网格、贴图等等资源内存(简单说就是继承自UnityEngine下的Object的那些类),而继承自System下的Object的那些代码产生的内存即是Mono内存,它只增不减。同样,你不断Destroy你的Unity对象也是要消耗性能去进行回收,而子弹这种消耗品实在产生的太快了,我们必需加以控制。
  那么,我们如何控制使得不至于不断产生新的内存呢?答案就是自己写内存池。自己回收利用之前创建过的对象。所以这个章节的内容,我们将重点放在写一个比较好的内存池上。就我自己来讲,在写一份较为系统的功能代码之前,我考虑的首先不是这个框架是该如何的,而是从使用者的角度去考虑,这个代码如何写使用起来才会比较方便,同样也要考虑容易扩展、通用性强、比较安全、减少耦合等等。
本文最后结果显示如下:

3.4.1、从使用者视角给出需求

  首先,我所希望的这个内存池的代码最后使用应该是这样的。

Bullet a = Pool.Take(); //从池中立刻获取一个单元,如果单元不存在,则它需要为我立刻创建出来。返回一个Bullet脚本以便于后续控制。注意这里使用泛型,也就是说它应该可以兼容任意的脚本类型。
Pool.restore(a);//当使用完成Bullet之后,我可以使用此方法回收这个对象。注意这里实际上我已经把Bullet这个组件的回收等同于某个GameObject(这里是子弹的GameObject)的回收。
  使用上就差不多是这样了,希望可以有极其简单的方法来进行获取和回收操作。
3.4.2、内存池单元结构

  最简单的内存池形式,差不多就是两个List,一个处于工作状态,一个处于闲置状态。工作完毕的对象被移动到闲置状态列表,以便于后续的再次获取和利用,形成一个循环。我们这里也会设计一个结构来管理这两个List,用于处理同一类的对象。
  接下来是考虑内存池单元的形式,我们考虑到内存池单元要尽可能容易扩展,就是可以兼容任意数据类型,也就是说,假设我们的内存池单元定为Pool_Unit,那么它不能影响后续继承它的类型,那我们最好使用接口,一旦使用类,那么就已经无法兼容Unity组件,因为我们自定义的Unity组件全部继承自MonoBehavior。接下来考虑这个内存单元该具有的功能,差不多有两个基本功能要有:

restore();//自己主动回收,为了方便后续调用,回收操作最好自己就有。
getState();//获取状态,这里是指获取当前是处于工作状态还是闲置状态,也是一个标记,用于后续快速判断。因为接口中无法存储单元,这里使用变通的方法,就是留给实现去处理,接口中要求具体实现需要提供一个状态标记。
  综合内存池单元和状态标记,给出如下代码:
  



namespace AndrewBox.Pool
{
  public interface Pool_Unit
  {
      Pool_UnitState state();
      void setParentList(object parentList);
      void restore();
  }
  public enum Pool_Type
  {
      Idle,
      Work
  }
  public class Pool_UnitState
  {
      public Pool_Type InPool
      {
          get;
          set;
      }
  }
}

3.4.3、单元组结构

  接下来考虑单元组,也就是前面所说的针对某一类的单元进行管理的结构。它内部有两个列表,一个工作,一个闲置,单元在工作和闲置之间转换循环。它应该具有以下功能:
创建新单元;使用抽象方法,不限制具体创建方法。对于Unity而言,可能需要从Prefab克隆,那么最好有方法可以从指定的Prefab模板复制创建。
获取单元;从闲置表中查找,找不到则创建。
回收单元;将其子单元进行回收。
  综合单元组结构的功能,给出如下代码:
 



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pool_UnitList<T> where T:class,Pool_Unit
    {
        protected object m_template;
        protected List<T> m_idleList;
        protected List<T> m_workList;
        protected int m_createdNum = 0;
        public Pool_UnitList()
        {
            m_idleList = new List<T>();
            m_workList = new List<T>();
        }



        /// <summary>
        /// 获取一个闲置的单元,如果不存在则创建一个新的
        /// </summary>
        /// <returns>闲置单元</returns>
        public virtual T takeUnit<UT>() where UT:T
        {
            T unit;
            if (m_idleList.Count > 0)
            {
                unit = m_idleList[0];
                m_idleList.RemoveAt(0);
            }
            else
            {
                unit = createNewUnit<UT>();
                unit.setParentList(this);
                m_createdNum++;
            }
            m_workList.Add(unit);
            unit.state().InPool = Pool_Type.Work;
            OnUnitChangePool(unit);
            return unit;
        }
        /// <summary>
        /// 归还某个单元
        /// </summary>
        /// <param name="unit">单元</param>
        public virtual void restoreUnit(T unit)
        {
            if (unit!=null && unit.state().InPool == Pool_Type.Work)
            {
                m_workList.Remove(unit);
                m_idleList.Add(unit);
                unit.state().InPool = Pool_Type.Idle;
                OnUnitChangePool(unit);
            }
        }
        /// <summary>
        /// 设置模板
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="template"></param>
        public void setTemplate(object template)
        {
            m_template = template;
        }
        protected abstract void OnUnitChangePool(T unit);
        protected abstract T createNewUnit<UT>() where UT : T;
    }
}

3.4.4、内存池结构

  内存池是一些列单元组的集合,它主要使用多个单元组具体实现内存单元的回收利用。同时把接口尽可能包装的简单,以便于用户调用,因为用户只与内存池进行打交道。另外,我们最好把内存池做成一个组件,这样便于方便进行初始化、更新(目前不需要,或许未来你需要执行某种更新操作)等工作的管理。这样,我们把内存池结构继承自上个章节的BaseBehavior。获得如下代码:
  


using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pool_Base<UnitType, UnitList> : BaseBehavior
        where UnitType : class,Pool_Unit
        where UnitList : Pool_UnitList<UnitType>, new()
    {
        /// <summary>
        /// 缓冲池,按类型存放各自分类列表
        /// </summary>
        private Dictionary<Type, UnitList> m_poolTale = new Dictionary<Type, UnitList>();

        protected override void OnInitFirst()
        {
        }

        protected override void OnInitSecond()
        {

        }

        protected override void OnUpdate()
        {

        }

        /// <summary>
        /// 获取一个空闲的单元
        /// </summary>
        public T takeUnit<T>() where T : class,UnitType
        {
            UnitList list = getList<T>();
            return list.takeUnit<T>() as T;
        }

        /// <summary>
        /// 在缓冲池中获取指定单元类型的列表,
        /// 如果该单元类型不存在,则立刻创建。
        /// </summary>
        /// <typeparam name="T">单元类型</typeparam>
        /// <returns>单元列表</returns>
        public UnitList getList<T>() where T : UnitType
        {
            var t = typeof(T);
            UnitList list = null;
            m_poolTale.TryGetValue(t, out list);
            if (list == null)
            {
                list = createNewUnitList<T>();
                m_poolTale.Add(t, list);
            }
            return list;
        }
        protected abstract UnitList createNewUnitList<UT>() where UT : UnitType;
    }
}

3.4.5、组件化
  目前为止,上述的结构都没有使用到组件,没有使用到UnityEngine,也就是说它们不受限使用于Unity组件或者普通的类。当然使用起来也会比较麻烦。由于我们实际需要的内存池单元常常用于某种具体组件对象,比如子弹,那么我们最好针对组件进一步实现。也就是说,定制一种适用于组件的内存池单元。同时也定制出相应的单元组,组件化的内存池结构。
  另外,由于闲置的单元都需要被隐藏掉,我们在组件化的内存池单元中需要设置两个GameObject节点,一个可见节点,一个隐藏节点。当组件单元工作时,其对应的GameObject被移动到可见节点下方(当然你也可以手动再根据需要修改它的父节点)。当组件单元闲置时,其对应的GameObject也会被移动到隐藏节点下方。
  综合以上,给出以下代码:



using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Pool
{

    public class Pool_Comp:Pool_Base<Pooled_BehaviorUnit,Pool_UnitList_Comp>
    {
        [SerializeField][Tooltip("运行父节点")]
        protected Transform m_work;
        [SerializeField][Tooltip("闲置父节点")]
        protected Transform m_idle;

        protected override void OnInitFirst()
        {
            if (m_work == null)
            {
                m_work = CompUtil.Create(m_transform, "work");
            }
            if (m_idle == null)
            {
                m_idle = CompUtil.Create(m_transform, "idle");
                m_idle.gameObject.SetActive(false);
            }
        }

        public void OnUnitChangePool(Pooled_BehaviorUnit unit)
        {
            if (unit != null)
            {
                var inPool=unit.state().InPool;
                if (inPool == Pool_Type.Idle)
                {
                    unit.m_transform.SetParent(m_idle);
                }
                else if (inPool == Pool_Type.Work)
                {
                    unit.m_transform.SetParent(m_work);
                }
            }
        }
        protected override Pool_UnitList_Comp createNewUnitList<UT>()
        {
            Pool_UnitList_Comp list = new Pool_UnitList_Comp();
            list.setPool(this);
            return list;
        }


    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Pool
{
    public class Pool_UnitList_Comp : Pool_UnitList<Pooled_BehaviorUnit>
    {
        protected Pool_Comp m_pool;
        public void setPool(Pool_Comp pool)
        {
            m_pool = pool;
        }
        protected override Pooled_BehaviorUnit createNewUnit<UT>() 
        {
            GameObject result_go = null;
            if (m_template != null && m_template is GameObject)
            {
                result_go = GameObject.Instantiate((GameObject)m_template);
            }
            else
            {
                result_go = new GameObject();
                result_go.name = typeof(UT).Name;
            }
            result_go.name =result_go.name + "_"+m_createdNum;
            UT comp = result_go.GetComponent<UT>();
            if (comp == null)
            {
                comp = result_go.AddComponent<UT>();
            }
            comp.DoInit();
            return comp;
        }

        protected override void OnUnitChangePool(Pooled_BehaviorUnit unit)
        {
            if (m_pool != null)
            {
                m_pool.OnUnitChangePool(unit);
            }
        }
    }
}
using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AndrewBox.Pool
{
    public abstract class Pooled_BehaviorUnit : BaseBehavior, Pool_Unit
    {
        //单元状态对象
        protected Pool_UnitState m_unitState = new Pool_UnitState();
        //父列表对象
        Pool_UnitList<Pooled_BehaviorUnit> m_parentList;
        /// <summary>
        /// 返回一个单元状态,用于控制当前单元的闲置、工作状态
        /// </summary>
        /// <returns>单元状态</returns>
        public virtual Pool_UnitState state()
        {
            return m_unitState;
        }
        /// <summary>
        /// 接受父列表对象的设置
        /// </summary>
        /// <param name="parentList">父列表对象</param>
        public virtual void setParentList(object parentList)
        {
            m_parentList = parentList as Pool_UnitList<Pooled_BehaviorUnit>;
        }
        /// <summary>
        /// 归还自己,即将自己回收以便再利用
        /// </summary>
        public virtual void restore()
        {
            if (m_parentList != null)
            {
                m_parentList.restoreUnit(this);
            }
        }

    }
}

3.4.6、内存池单元具体化
接下来,我们将Bullet具体化为一种内存池单元,使得它可以方便从内存池中创建出来。



using UnityEngine;
using System.Collections;
using AndrewBox.Comp;
using AndrewBox.Pool;

public class Bullet : Pooled_BehaviorUnit 
{
    [SerializeField][Tooltip("移动速度")]
    private float m_moveVelocity=10;
    [SerializeField][Tooltip("移动时长")]
    private float m_moveTime=3;
    [System.NonSerialized][Tooltip("移动计数")]
    private float m_moveTimeTick;
    protected override void OnInitFirst()
    {
    }

    protected override void OnInitSecond()
    {
    }

    protected override void OnUpdate()
    {
        float deltaTime = Time.deltaTime;
        m_moveTimeTick += deltaTime;
        if (m_moveTimeTick >= m_moveTime)
        {
            m_moveTimeTick = 0;
            this.restore();
        }
        else
        {
            var pos = m_transform.localPosition;
            pos.z += m_moveVelocity * deltaTime;
            m_transform.localPosition = pos;
        }
    }
}

3.4.7、内存池的使用
最后就是写一把枪来发射子弹了,这个逻辑也相对简单。为了把内存池做成单例模式并存放在单独的GameObject,我们还需要另外一个单例单元管理器的辅助,一并给出。



using UnityEngine;
using System.Collections;
using AndrewBox.Comp;
using AndrewBox.Pool;

public class Gun_Simple : BaseBehavior 
{

    [SerializeField][Tooltip("模板对象")]
    private GameObject m_bulletTemplate;
    [System.NonSerialized][Tooltip("组件对象池")]
    private Pool_Comp m_compPool;
    [SerializeField][Tooltip("产生间隔")]
    private float m_fireRate=0.5f;
     [System.NonSerialized][Tooltip("产生计数")]
    private float m_fireTick;
    protected override void OnInitFirst()
    {
        m_compPool = Singletons.Get<Pool_Comp>("pool_comps");
        m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);
    }

    protected override void OnInitSecond()
    {

    }

    protected override void OnUpdate()
    {
        m_fireTick -= Time.deltaTime;
        if (m_fireTick < 0)
        {
            m_fireTick += m_fireRate;
            fire();
        }
    }
    protected void fire()
    {
        Bullet bullet =  m_compPool.takeUnit<Bullet>();
        bullet.m_transform.position = m_transform.position;
        bullet.m_transform.rotation = m_transform.rotation;
    }
}
using AndrewBox.Comp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace AndrewBox.Comp
{
    /// <summary>
    /// 单例单元管理器
    /// 你可以创建单例组件,每个单例组件对应一个GameObject。
    /// 你可以为单例命名,名字同时也会作为GameObject的名字。
    /// 这些产生的单例一般用作管理器。
    /// </summary>
    public static class Singletons
    {
        private static Dictionary<string, BaseBehavior> m_singletons = new Dictionary<string, BaseBehavior>();
        public static T Get<T>(string name) where T:BaseBehavior
        {

            BaseBehavior singleton = null;
            m_singletons.TryGetValue(name, out singleton);
            if (singleton == null)
            {
                GameObject newGo = new GameObject(name);
                singleton = newGo.AddComponent<T>();
                m_singletons.Add(name, singleton);
            }
            return singleton as T;
        }
        public static void Destroy(string name)
        {
            BaseBehavior singleton = null;
            m_singletons.TryGetValue(name, out singleton);
            if (singleton != null)
            {
                m_singletons.Remove(name);
                GameObject.DestroyImmediate(singleton.gameObject);
            }
        }
        public static void Clear()
        {
            List<string> keys = new List<string>();
            foreach (var key in m_singletons.Keys)
            {
                keys.Add(key);
            }
            foreach (var key in keys)
            {
                Destroy(key);
            }
        }

    }
}

3.4.8、总结
最终,我们写出了所有的代码,这个内存池是通用的,而且整个游戏工程,你几乎只需要这样的一个内存池,就可以管理所有的数量众多且种类繁多的活动单元。而调用处只有以下几行代码即可轻松管理。


        m_compPool = Singletons.Get<Pool_Comp>("pool_comps");//创建内存池
        m_compPool.getList<Bullet>().setTemplate(m_bulletTemplate);//设置模板
        Bullet bullet =  m_compPool.takeUnit<Bullet>();//索取单元
        bullet.restore(); //回收单元

最终当你正确使用它时,你的GameObject内存不会再无限制增长,它将出现类似的下图循环利用。

本文为博主原创文章,欢迎转载。请保留博主链接http://blog.youkuaiyun.com/andrewfan

<think>好的,我现在需要帮用户生成一段适用于Unity中的对象池代码。首先,我得回忆一下对象池的基本概念和用途。对象池主要是为了减少频繁创建和销毁对象带来的性能开销,这在游戏开发中特别重要,比如处理子弹、敌人等需要大量生成和回收的对象。 接下来,我需要确定用户的需求。他们可能想要一个通用对象池,可以在不同项目中复用,或者针对特定对象的优化版本。用户没有具体说明,所以最好提供一个基础但灵活的实现,方便扩展。 然后,考虑Unity的架构。对象池通常需要一个管理类,负责存储和管理池中的对象。这些对象应该是GameObject,并且可能需要初始化和回收的方法。可能需要使用Queue来管理可用对象,因为队列的先进先出特性适合对象池的存取。 还需要考虑对象的预制体,池子的大小,是否支持动态扩展。用户可能希望设置初始大小,当池子为空时,能够动态创建新实例,或者返回null,这需要设计参数来控制。 另外,对象池应该提供获取和回收对象的方法。获取时,如果池中有可用对象,就取出并激活;没有的话,根据设置决定是否创建新对象。回收时,将对象禁用并放回池中。 还要注意内存管理,避免内存泄漏。确保回收的对象正确重置状态,比如位置、旋转、刚体速度等,防止重复使用时的残留数据。 然后,参考常见的Unity对象池实现,可能会用到泛型或者非泛型类。这里可能需要使用MonoBehaviour单例模式,方便在场景中管理。或者使用静态类,但单例更灵活,可以附加到游戏管理器上。 接下来,开始编写代码结构。定义一个ObjectPool类,包含队列、预制体、父节点(用于组织场景中的对象)、初始大小等字段。在Awake或Start方法中初始化池子,预生成实例。 编写Get方法,从队列中取出对象,激活并返回。如果队列为空,实例化新对象(如果允许动态创建)。Recycle方法则禁用对象,并放回队列。 还需要处理对象的初始化逻辑,比如获取组件,可能需要使用GetComponent<T>(),所以可能让对象预制体带有特定组件,或者在获取时添加组件。或者让用户自己处理初始化,对象池只负责管理激活状态。 测试一下边界情况,比如池子为空且不允许动态创建时返回null,或者处理多次回收同一对象的问题。可能需要添加检查,确保对象不会被重复回收。 最后,提供一个使用示例,比如在射击游戏中生成子弹,演示如何从对象池获取子弹,发射后回收。这样用户更容易理解如何集成到自己的项目中。 确保代码符合Unity的API规范,比如使用Instantiate和Destroy,正确设置父对象,避免场景中的混乱。同时,代码注释要清晰,方便用户理解每个部分的作用。 可能还需要考虑多线程安全,不过在Unity中大部分操作在主线程,可能不需要。但如果有异步操作,需要加锁,但一般情况下可以忽略。 总结一下,对象池的关键点包括:管理可用对象的集合,提供获取和回收接口,处理对象的激活和禁用,预加载初始对象,动态扩展选项,以及正确的初始化和清理逻辑。确保代码简洁高效,易于集成和使用。</think>以下是为Unity设计的通用对象池实现代码(含使用说明): ```csharp using System.Collections.Generic; using UnityEngine; /// <summary> /// 通用对象池(单例模式) /// </summary> public class ObjectPool : MonoBehaviour { public static ObjectPool Instance; [System.Serializable] public class Pool { public string tag; // 对象标识 public GameObject prefab; // 预制体 public int initSize; // 初始数量 public Transform parent; // 父节点(可选) } public List<Pool> pools; // 对象池配置 private Dictionary<string, Queue<GameObject>> poolDictionary; void Awake() { Instance = this; InitializePools(); } // 初始化所有对象池 private void InitializePools() { poolDictionary = new Dictionary<string, Queue<GameObject>>(); foreach (Pool pool in pools) { Queue<GameObject> objectPool = new Queue<GameObject>(); for (int i = 0; i < pool.initSize; i++) { GameObject obj = CreateNewObject(pool); objectPool.Enqueue(obj); } poolDictionary.Add(pool.tag, objectPool); } } // 从池中获取对象 public GameObject GetFromPool(string tag, Vector3 position, Quaternion rotation) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogError($"对象池未找到标签为 {tag} 的配置"); return null; } Queue<GameObject> poolQueue = poolDictionary[tag]; if (poolQueue.Count == 0) { // 动态扩展池大小 Pool poolConfig = pools.Find(p => p.tag == tag); GameObject newObj = CreateNewObject(poolConfig); return PrepareObject(newObj, position, rotation); } GameObject obj = poolQueue.Dequeue(); return PrepareObject(obj, position, rotation); } // 回收对象到池中 public void ReturnToPool(string tag, GameObject obj) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogError($"对象池未找到标签为 {tag} 的配置"); return; } ResetObject(obj); poolDictionary[tag].Enqueue(obj); } // 创建新对象 private GameObject CreateNewObject(Pool pool) { GameObject obj = Instantiate(pool.prefab); obj.SetActive(false); if (pool.parent != null) obj.transform.SetParent(pool.parent); return obj; } // 准备使用对象 private GameObject PrepareObject(GameObject obj, Vector3 position, Quaternion rotation) { obj.transform.position = position; obj.transform.rotation = rotation; obj.SetActive(true); // 重置可能存在的Rigidbody状态 Rigidbody rb = obj.GetComponent<Rigidbody>(); if (rb != null) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; } return obj; } // 重置对象状态 private void ResetObject(GameObject obj) { obj.SetActive(false); obj.transform.localPosition = Vector3.zero; obj.transform.localRotation = Quaternion.identity; } } ``` 使用步骤: 1. **配置对象池** ```csharp // 在Inspector面板配置: // 示例配置子弹对象池 [SerializeField] private GameObject bulletPrefab; [SerializeField] private Transform bulletParent; // 在ObjectPool组件的pools列表中添加: // tag: "Bullet" // prefab: 子弹预制体 // initSize: 20 // parent: bulletParent(可选) ``` 2. **获取对象** ```csharp // 生成子弹 GameObject bullet = ObjectPool.Instance.GetFromPool( "Bullet", spawnPoint.position, spawnPoint.rotation ); // 获取后可以设置子弹参数 if (bullet != null) { bullet.GetComponent<Bullet>().Initialize(damage, speed); } ``` 3. **回收对象** ```csharp // 当子弹需要销毁时 void OnCollisionEnter(Collision collision) { ObjectPool.Instance.ReturnToPool("Bullet", gameObject); } ``` 功能特点: - 支持多类型对象池配置 - 动态扩展池大小 - 自动重置对象物理状态 - 对象分类存储管理 - Inspector可视化配置 - 防止重复创建/销毁的性能开销 注意事项: 1. 所有需要池化的预制体必须配置tag 2. 建议通过ReturnToPool回收对象,而不是Destroy 3. 回收时会自动重置位置和旋转 4. 通过parent参数可以保持场景层级整洁 5. 适合高频创建/销毁的物体(子弹、特效、NPC等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值