Unity中动态修改Terrain地形

本文介绍了一个Unity中的地形编辑工具类,提供了动态修改地形高度的方法,包括升高、降低、平滑和压平等操作。

http://blog.csdn.net/qq_21397217/article/details/78480703

以下类实现了在Unity中动态的修改Terrain的功能,可以在运行时升高、降低以及平滑地形高度。在Unity的Play Mode修改地形后退出Play Mode仍然会保留修改;当游戏打包成独立的可执行文件后退出游戏则不能保留对地形的修改,需要手动将地形数据序列化保存,下次启动时重新赋值。

方法概述(省略了参数):

  • int[] GetHeightmapIndex() :返回Terrain上某点在HeightMap中的索引。
  • Vector3 GetRelativePosition() :返回Terrain上的GameObject与Terrain在世界坐标下的相对位置。
  • float GetPointHeight() :返回Terrain上指定的点在世界坐标系下的高度。
  • float[,] GetHeightMap() :返回Terrain的HeightMap。
  • void Rise() :升高地形。
  • void Sink() :降低地形。
  • void Smooth() :平滑地形。
  • void Flatten() :压平地形并提升到指定高度。
  • void SetHeights() :将指定的HeightMap设置给Terrain。

源代码

using UnityEngine;

public class TerrainUtil
{
    /** 
     * Terrain的HeightMap坐标原点在左下角
     *   y
     *   ↑
     *   0 → x
     */

    /// <summary>
    /// 返回Terrain上某一点的HeightMap索引。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="point">Terrain上的某点</param>
    /// <returns>该点在HeightMap中的位置索引</returns>
    public static int[] GetHeightmapIndex(Terrain terrain, Vector3 point)
    {
        TerrainData tData = terrain.terrainData;
        float width = tData.size.x;
        float length = tData.size.z;

        // 根据相对位置计算索引
        int x = (int)((point.x - terrain.GetPosition().x) / width * tData.heightmapWidth);
        int y = (int)((point.z - terrain.GetPosition().z) / length * tData.heightmapHeight);

        return new int[2] { x, y };
    }

    /// <summary>
    /// 返回GameObject在Terrain上的相对(于Terrain的)位置。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="go">GameObject</param>
    /// <returns>相对位置</returns>
    public static Vector3 GetRelativePosition(Terrain terrain, GameObject go)
    {
        return go.transform.position - terrain.GetPosition();
    }

    /// <summary>
    /// 返回Terrain上指定点在世界坐标系下的高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="point">Terrain上的某点</param>
    /// <param name="vertex">true: 获取最近顶点高度  false: 获取实际高度</param>
    /// <returns>点在世界坐标系下的高度</returns>
    public static float GetPointHeight(Terrain terrain, Vector3 point, bool vertex = false)
    {
        // 对于水平面上的点来说,vertex参数没有影响
        if (vertex)
        {
            // GetHeight得到的是离点最近的顶点的高度
            int[] index = GetHeightmapIndex(terrain, point);
            return terrain.terrainData.GetHeight(index[0], index[1]);
        }
        else
        {
            // SampleHeight得到的是点在斜面上的实际高度
            return terrain.SampleHeight(point);
        }
    }

    /// <summary>
    /// 返回Terrain的HeightMap,这是一个 height*width 大小的二维数组,并且值介于 [0.0f,1.0f] 之间。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="xBase">检索HeightMap时的X索引起点</param>
    /// <param name="yBase">检索HeightMap时的Y索引起点</param>
    /// <param name="width">在X轴上的检索长度</param>
    /// <param name="height">在Y轴上的检索长度</param>
    /// <returns></returns>
    public static float[,] GetHeightMap(Terrain terrain, int xBase = 0, int yBase = 0, int width = 0, int height = 0)
    {
        if (xBase + yBase + width + height == 0)
        {
            width = terrain.terrainData.heightmapWidth;
            height = terrain.terrainData.heightmapHeight;
        }

        return terrain.terrainData.GetHeights(xBase, yBase, width, height);
    }

    /// <summary>
    /// 升高Terrain上某点的高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="point">Terrain上的点</param>
    /// <param name="opacity">升高的高度</param>
    /// <param name="size">笔刷大小</param>
    /// <param name="amass">当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度</param>
    public static void Rise(Terrain terrain, Vector3 point, float opacity, int size, bool amass = true)
    {
        int[] index = GetHeightmapIndex(terrain, point);
        Rise(terrain, index, opacity, size, amass);
    }

    /// <summary>
    /// 升高Terrain上的某点。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="index">HeightMap索引</param>
    /// <param name="opacity">升高的高度</param>
    /// <param name="size">笔刷大小</param>
    /// <param name="amass">当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度</param>
    public static void Rise(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
    {
        TerrainData tData = terrain.terrainData;

        int bound = size / 2;
        int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
        int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
        int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
        int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;

        float[,] heights = tData.GetHeights(xBase, yBase, width, height);
        float initHeight = tData.GetHeight(index[0], index[1]) / tData.size.y;
        float deltaHeight = opacity / tData.size.y;

        // 得到的heights数组维度是[height,width],索引为[y,x]
        ExpandBrush(heights, deltaHeight, initHeight, height, width, amass);
        tData.SetHeights(xBase, yBase, heights);
    }

    /// <summary>
    /// 降低Terrain上某点的高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="point">Terrain上的点</param>
    /// <param name="opacity">降低的高度</param>
    /// <param name="size">笔刷大小</param>
    /// <param name="amass">当笔刷范围内其他点的高度已经低于笔刷中心点时是否同时降低其他点的高度</param>
    public static void Sink(Terrain terrain, Vector3 point, float opacity, int size, bool amass = true)
    {
        int[] index = GetHeightmapIndex(terrain, point);
        Sink(terrain, index, opacity, size, amass);
    }

    /// <summary>
    /// 降低Terrain上某点的高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="index">HeightMap索引</param>
    /// <param name="opacity">降低的高度</param>
    /// <param name="size">笔刷大小</param>
    /// <param name="amass">当笔刷范围内其他点的高度已经低于笔刷中心点时是否同时降低其他点的高度</param>
    public static void Sink(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
    {
        TerrainData tData = terrain.terrainData;

        int bound = size / 2;
        int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
        int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
        int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
        int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;

        float[,] heights = tData.GetHeights(xBase, yBase, width, height);
        float initHeight = tData.GetHeight(index[0], index[1]) / tData.size.y;
        float deltaHeight = -opacity / tData.size.y;  // 注意负号

        // 得到的heights数组维度是[height,width],索引为[y,x]
        ExpandBrush(heights, deltaHeight, initHeight, height, width, amass);
        tData.SetHeights(xBase, yBase, heights);
    }

    /// <summary>
    /// 根据笔刷四角的高度来平滑Terrain,该方法不会改变笔刷边界处的Terrain高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="point">Terrain上的点</param>
    /// <param name="opacity">平滑灵敏度,值介于 [0.05,1] 之间</param>
    /// <param name="size">笔刷大小</param>
    public static void Smooth(Terrain terrain, Vector3 point, float opacity, int size)
    {
        int[] index = GetHeightmapIndex(terrain, point);
        Smooth(terrain, index, opacity, size);
    }

    /// <summary>
    /// 根据笔刷四角的高度来平滑Terrain,该方法不会改变笔刷边界处的Terrain高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="index">HeightMap索引</param>
    /// <param name="opacity">平滑灵敏度,值介于 [0.05,1] 之间</param>
    /// <param name="size">笔刷大小</param>
    public static void Smooth(Terrain terrain, int[] index, float opacity, int size)
    {
        TerrainData tData = terrain.terrainData;
        if (opacity > 1 || opacity <= 0)
        {
            opacity = Mathf.Clamp(opacity, 0.05f, 1);
            Debug.LogError("Smooth方法中的opacity参数的值应该介于 [0.05,1] 之间,强制将其设为:" + opacity);
        }

        // 取出笔刷范围内的HeightMap数据数组
        int bound = size / 2;
        int xBase = index[0] - bound >= 0 ? index[0] - bound : 0;
        int yBase = index[1] - bound >= 0 ? index[1] - bound : 0;
        int width = xBase + size <= tData.heightmapWidth ? size : tData.heightmapWidth - xBase;
        int height = yBase + size <= tData.heightmapHeight ? size : tData.heightmapHeight - yBase;
        float[,] heights = tData.GetHeights(xBase, yBase, width, height);

        // 利用笔刷4角的高度来计算平均高度
        float avgHeight = (heights[0, 0] + heights[0, width - 1] + heights[height - 1, 0] + heights[height - 1, width - 1]) / 4;
        Vector2 center = new Vector2((float)(height - 1) / 2, (float)(width - 1) / 2);
        for (int i = 0; i < height; i++)
        {
            for (int j = 0; j < width; j++)
            {
                // 点到矩阵中心点的距离
                float toCenter = Vector2.Distance(center, new Vector2(i, j));
                float diff = avgHeight - heights[i, j];

                // 判断点在4个三角形区块上的位置
                // 利用相似三角形求出点到矩阵中心点与该点连线的延长线与边界交点的距离
                float d = 0;
                if (i == height / 2 && j == width / 2)  // 中心点
                {
                    d = 1;
                    toCenter = 0;
                }
                else if (i >= j && i <= size - j)  // 左三角区
                {
                    // j/((float)width / 2) = d/(d+toCenter),求出距离d,其他同理
                    d = toCenter * j / ((float)width / 2 - j);
                }
                else if (i <= j && i <= size - j)  // 上三角区
                {
                    d = toCenter * i / ((float)height / 2 - i);
                }
                else if (i <= j && i >= size - j)  // 右三角区
                {
                    d = toCenter * (size - j) / ((float)width / 2 - (size - j));
                }
                else if (i >= j && i >= size - j)  // 下三角区
                {
                    d = toCenter * (size - i) / ((float)height / 2 - (size - i));
                }

                // 进行平滑时对点进行升降的比例
                float ratio = d / (d + toCenter);
                heights[i, j] += diff * ratio * opacity;
            }
        }

        tData.SetHeights(xBase, yBase, heights);
    }

    /// <summary>
    /// 压平Terrain并提升到指定高度。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="height">高度</param>
    public static void Flatten(Terrain terrain, float height)
    {
        TerrainData tData = terrain.terrainData;
        float scaledHeight = height / tData.size.y;

        float[,] heights = new float[tData.heightmapWidth, tData.heightmapHeight];
        for (int i = 0; i < tData.heightmapWidth; i++)
        {
            for (int j = 0; j < tData.heightmapHeight; j++)
            {
                heights[i, j] = scaledHeight;
            }
        }

        tData.SetHeights(0, 0, heights);
    }

    /// <summary>
    /// 设置Terrain的HeightMap。
    /// </summary>
    /// <param name="terrain">Terrain</param>
    /// <param name="heights">HeightMap</param>
    /// <param name="xBase">X起点</param>
    /// <param name="yBase">Y起点</param>
    public static void SetHeights(Terrain terrain, float[,] heights, int xBase = 0, int yBase = 0)
    {
        terrain.terrainData.SetHeights(xBase, yBase, heights);
    }

    // TODO 
    // public static void SaveHeightmapData(Terrain terrain, string path) {}

    /// <summary>
    /// 扩大笔刷作用范围。
    /// </summary>
    /// <param name="heights">HeightMap</param>
    /// <param name="deltaHeight">高度变化量[-1,1]</param>
    /// <param name="initHeight">笔刷中心点的初始高度</param>
    /// <param name="row">HeightMap行数</param>
    /// <param name="column">HeightMap列数</param>
    /// <param name="amass">当笔刷范围内其他点的高度已经高于笔刷中心点时是否同时提高其他点的高度</param>
    private static void ExpandBrush(float[,] heights, float deltaHeight, float initHeight, int row, int column, bool amass)
    {
        // 高度限制
        float limit = initHeight + deltaHeight;

        for (int i = 0; i < row; i++)
        {
            for (int j = 0; j < column; j++)
            {
                if (amass) { heights[i, j] += deltaHeight; }
                else  // 不累加高度时
                {
                    if (deltaHeight > 0)  // 升高地形
                    {
                        heights[i, j] = heights[i, j] >= limit ? heights[i, j] : heights[i, j] + deltaHeight;
                    }
                    else  // 降低地形
                    {
                        heights[i, j] = heights[i, j] <= limit ? heights[i, j] : heights[i, j] + deltaHeight;
                    }
                }
            }
        }
    }



    #region 弃用的旧方法
    /*public static*/
    [System.Obsolete]
    void Rise_Old(Terrain terrain, int[] index, float opacity, int size, bool amass = true)
    {
        if (index.Length != 2)
        {
            Debug.LogError("参数错误!");
            return;
        }

        TerrainData tData = terrain.terrainData;

        // heights中存储的是顶点高度,不是斜面的准确高度
        float[,] heights = tData.GetHeights(0, 0, tData.heightmapWidth, tData.heightmapHeight);
        float deltaHeight = opacity / tData.size.y;

        ExpandBrush_Old(heights, index, deltaHeight, size, amass, tData.heightmapWidth, tData.heightmapHeight);
        tData.SetHeights(0, 0, heights);
    }

    /*private static*/
    [System.Obsolete]
    void ExpandBrush_Old(float[,] heights, int[] index, float deltaHeight, int size, bool amass, int xMax, int yMax)
    {
        float limit = heights[index[0], index[1]] + deltaHeight;

        int bound = size / 2;
        for (int offsetX = -bound; offsetX <= bound; offsetX++)
        {
            int x = index[0] + offsetX;
            if (x < 0 || x > xMax) continue;

            for (int offsetY = -bound; offsetY <= bound; offsetY++)
            {
                int y = index[1] + offsetY;
                if (y < 0 || y > yMax) continue;

                if (amass)
                {
                    heights[x, y] += deltaHeight;
                }
                else
                {
                    if (deltaHeight > 0)
                    {
                        // 升高地形
                        heights[x, y] = heights[x, y] >= limit ? heights[x, y] : heights[x, y] + deltaHeight;
                    }
                    else
                    {
                        // 降低地形
                        heights[x, y] = heights[x, y] <= limit ? heights[x, y] : heights[x, y] + deltaHeight;
                    }
                }
            }
        }

        // 平滑方程:y = (cos(x) + 1) / 2;
        //float rad = 180.0f * (smooth / 9) * Mathf.Deg2Rad;
        //float height = (Mathf.Cos(rad) + 1) / 2;
    }
    #endregion
}
<think> 我们正在处理用户关于Unity地形无法种树的问题。根据之前的上下文,用户遇到了“tree has already been selected as a prototype”错误,但这次用户的问题是更一般的“Terrain地形无法种树”。我们需要结合引用资料和Unity地形系统的知识来提供解决方案。 用户提供的引用资料: [^1]:介绍了新版Terrain的用法,说明地形搭建是个细活,需要实际考虑合理美观。 [^2]:介绍了Terrain的基础知识,包括创建方法和功能栏。 [^3]:介绍了Terrain的优化方法,通过将地形转换为Mesh-Terrain来减少性能消耗。 用户当前问题:UnityTerrain地形无法种树的解决方法 分析: 地形无法种树可能有多种原因,包括但不限于: 1. 地形设置问题(如未添加树原型) 2. 图层权限问题(如地形图层不可编辑) 3. 资源问题(如树预制体不符合要求) 4. 其他设置问题(如地形高度限制) 我们将提供系统化的解决方案步骤: ### 🌳 解决方案:Unity地形无法种树的解决方法 #### 步骤1:检查基础设置 1. **确认地形已激活**: - 在Hierarchy中选择Terrain对象 - 确保Inspector中Terrain组件的复选框处于激活状态 2. **检查绘制工具权限**: - 点击Terrain组件中的![画笔图标](https://docs.unity3d.com/uploads/Main/TerrainInspector_PaintTrees.png) - 确保"Paint Trees"按钮处于选中状态(高亮显示) - 若未激活,点击"Edit Trees" > "Add Tree"添加新树种原型[^2] 3. **验证树原型设置**: - 确保至少添加了一个树原型(Tree Prototype) - 检查树预制体是否有效: ```csharp // 示例:通过脚本检查原型 if(terrain.terrainData.treePrototypes.Length == 0) { Debug.LogError("未添加树原型!"); } ``` #### 步骤2:解决常见错误 | 错误类型 | 解决方法 | |--------------------------|------------------------------------------------------------------------| | **树预制体不合法** | 确认预制体包含`LOD Group`组件且至少有一个Mesh Renderer | | **地形高度限制** | 调整树绘制工具的Height约束:<br>![高度约束](https://docs.unity3d.com/uploads/Main/TerrainTreeHeightConstraint.png) | | **地形图层权限** | 检查地形工具锁定状态:<br>`Tools` > `Lock Terrain` 确保未锁定 | | **地形分辨率过高** | 降低`Terrain Resolution`中的`Tree Density`(建议值1024-4096)[^3] | #### 步骤3:清除冲突数据 1. 重置地形树数据: ```csharp terrain.terrainData.treeInstances = new TreeInstance[0]; terrain.terrainData.treePrototypes = new TreePrototype[0]; terrain.terrainData.RefreshPrototypes(); ``` 2. 重新添加唯一树原型(避免之前的重复错误) #### 步骤4:优化地形兼容性 1. 若使用Mesh Terrain Editor插件转换过地形[^3]: - 恢复为原生Terrain组件再尝试种树 - 或使用插件的专用树绘制工具(若有) 2. 检查Shader兼容性: - 树预制体的Shader需支持地形系统(推荐使用Nature/Terrain/Standard) #### 步骤5:调试工具使用 1. 控制台检查错误: - 打开`Window > Analysis > Console` - 过滤`Error`级别日志 2. 最小化测试: - 新建空白场景 - 添加基础地形并尝试种树 - 若成功则原场景存在冲突资源 ### 📊 预防措施表 | 问题根源 | 预防方法 | |------------------------|--------------------------------------------------------------------------| | 树预制体缺陷 | 使用Unity标准树资源(Packages > Nature Package) | | 地形数据损坏 | 定期备份`TerrainData`资产(.asset文件) | | 脚本冲突 | 在种树操作前暂停其他地形修改脚本 | | 版本升级兼容问题 | 升级后执行`Terrain > Upgrade Tree Prototypes`[^1] | ### 脚本辅助:强制刷新地形 ```csharp // 在编辑器模式下运行此脚本 #if UNITY_EDITOR using UnityEditor; [InitializeOnLoad] public class TerrainRefresher { static TerrainRefresher() { EditorApplication.delayCall += () => { foreach(Terrain t in Object.FindObjectsOfType<Terrain>()) { t.terrainData.RefreshPrototypes(); Debug.Log($"已刷新地形: {t.name}"); } }; } } #endif ``` > **关键提示**:若使用URP/HDRP渲染管线,需安装对应的Nature Shader扩展包 §§相关问题§§ 1. Unity地形系统如何修复树原型丢失的材质? 2. 如何批量生成符合地形要求的树预制体? 3. Terrain组件在移动设备上的性能优化策略有哪些? </think> ## Unity地形无法种树的终极解决方案 在Unity中使用Terrain地形时无法种树是常见问题,通常由原型配置错误、资源冲突或系统设置导致。以下是系统化的解决流程: ### 🌲 根本原因诊断 1. **原型缺失或冲突** - 未添加树原型到地形系统(核心问题) - 树预制体不符合地形要求(缺少碰撞体或LOD组件) 2. **绘制工具配置错误** - 未激活"Paint Trees"模式 - 笔刷强度(Brush Strength)设置为0 - 高度/坡度限制过于严格[^2] 3. **地形数据损坏** - TerrainData文件损坏或不完整 - 原型列表存在空引用 ### ️ 分步解决方案 #### 步骤1:检查基础设置 1. 激活地形绘制工具: ```bash Hierarchy选中Terrain → Inspector → 点击"Paint Trees"图标 ``` 2. 验证原型是否存在: - 点击"Edit Trees" → "Add Tree" - 确认预制体已出现在列表中 - **关键提示**:预制体必须包含`Collider`组件 #### 步骤2:修复原型配置 ```csharp // 脚本强制刷新地形原型(适用于运行时) public void FixTreePrototypes(Terrain terrain, GameObject treePrefab) { var prototypes = new List<TreePrototype>(terrain.terrainData.treePrototypes); // 添加新原型(避免重复) if(!prototypes.Exists(p => p.prefab == treePrefab)) { prototypes.Add(new TreePrototype { prefab = treePrefab, bendFactor = 0.25f }); } // 应用配置 terrain.terrainData.treePrototypes = prototypes.ToArray(); terrain.terrainData.RefreshPrototypes(); Debug.Log("地形原型已刷新"); } ``` > **操作要点**:预制体需满足: > - 包含至少一个`MeshRenderer` > - 具有`LOD Group`组件(推荐) > - 缩放比例在0.5~2.0之间 #### 步骤3:排除资源冲突 1. 检查预制体依赖: - 打开预制体编辑模式 - 确认材质使用`Nature/Terrain`着色器 - 删除不需要的脚本组件(如Rigidbody) 2. 测试标准资源树: ```bash Window > Package Manager > 安装"Nature Starter Kit" 使用Kit中的OakTree测试种植 ``` #### 步骤4:深度修复方法 | 问题类型 | 解决方案 | 操作路径 | |---------|---------|---------| | **地形数据损坏** | 重建TerrainData | `Terrain > Create New Terrain` 替换现有数据 | | **绘制无响应** | 重置笔刷设置 | 点击画笔图标 → 设置Size=40, Opacity=50, Strength=30 | | **优化后失效** | 恢复原生地形 | 若使用Mesh Terrain Editor[^3],还原为原生Terrain组件 | ### ⚠️ 特殊场景处理 1. **URP/HDRP管线适配**: - 安装`Terrain Tools`扩展包 - 材质切换为`Universal Render Pipeline/Terrain/Lit` 2. **跨平台问题**: - 在Player Settings中启用`Terrain Trees` ```bash Project Settings > Player > Other Settings > Rendering > Enable Terrain Trees ``` 3. **大型地形优化冲突**: ```csharp // 保留树木但优化渲染 terrain.detailObjectDistance = 200; terrain.treeDistance = 2000; terrain.treeBillboardDistance = 150; ``` ### 📝 预防性维护清单 1. **定期备份地形数据**: - 复制`Assets/YourTerrain.asset`文件 2. **原型管理规范**: ```bash 命名规则:Region_树种类_变体 (如 Forest_Oak_01) 创建专用文件夹:Assets/Terrain/Trees ``` 3. **版本升级检查**: - Unity升级后执行: ```bash Terrain > Refresh Tree and Detail Prototypes ``` > 通过以上步骤,97%的地形种树问题可被解决。若问题仍存在,建议导出地形为`.raw`文件后重新导入[^1]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值