Unity划时代热更方案 YooAsset+HybridCLR(wolong)(原huatuo)(四)

上一篇简介了YooAsset的安装,本篇我们来详细了解一下Asset的功能和使用。
之前的安装的示例项目Space ShooterAssets/Samples/Space Shooter下。

全局配置

通过右键创建配置文件(Project窗体内右键 -> Create -> YooAsset -> Create YooAsset Setting)

注意:请将配置文件放在Resources文件夹下
配置说明:Manifest File Name : 清单文件名称

在示例项目中已经帮我们建好了。
在这里插入图片描述

资源配置

学习资源配置界面。
unity界面选择YooAsset->AssetBundle Collector
在这里插入图片描述
在这里插入图片描述

界面介绍

左侧为分组列表,右侧为该分组的配置界面。

导入按钮:可以导入保存的XML文件。

导出按钮:可以将配置数据导出为XML文件。

修复按钮:在配置里的文件夹挪动位置之后,可以通过该按钮按钮来修正。

注意:该工具仅支持Unity2019.4+

公共设置

在这里插入图片描述

  • Show Packages
    是否展示资源包列表视图。

  • Show Editor Alias
    是否显示为中文模式。

  • Enable Addressable
    启用可寻址资源定位系统。

  • Unique Bundle Name
    资源包名追加PackageName作为前缀。

  • Location To Lower
    资源定位地址大小写不敏感。

  • Include Asset GUID
    资源清单里包含资源GUID信息。

资源分组

  • Active Rule
    激活规则,规则可以自定义扩展。下面是内置规则:
    • EnableGroup 启用分组。
    • DisableGroup 禁用分组。
//自定义扩展范例
public class DisableGroup : IActiveRule
{
    public bool IsActiveGroup()
    {
        return false;
    }
}
  • Grouper Name
    分组名称

  • Grouper Desc
    分组备注信息

  • Asset Tags
    资源分类标签列表,该分组下收集的资源会全部被打上该标签。
    注意:多个标签用分号隔开,例如 level1;level2;level3

搜集器

在这里插入图片描述

  • Collect Path
    收集路径,可以指定文件夹或单个资源文件。

  • Collector Type
    收集器类型:

    • MainAssetCollector 收集参与打包的主资源对象,并写入到资源清单的资源列表里(可以通过代码加载)。
    • StaticAssetCollector 收集参与打包的主资源对象,但不写入到资源清单的资源列表里(无法通过代码加载)。
    • DependAssetCollector 收集参与打包的依赖资源对象,但不写入到资源清单的资源列表里(无法通过代码加载)(当依赖资源没有被任何主资源引用的时候,则会在打包的时候自动剔除)。

StaticAssetCollector收集器和DependAssetCollector收集器适合对资源进行定制化打包策略。

示例1:一个游戏的粒子特效的纹理会非常多,通常特效制作师会把这些纹理放到一个文件夹内管理。如果我们把这些纹理打进一个AssetBundle文件内,当下次更新的时候,如果新增或改动了一个纹理,那么就要上传整个纹理的AssetBundle文件。我们可以把特效纹理通过DependAssetCollector收集器进行收集并自定义打包规则,通过文件名称的首字母进行小粒度打包,这样一个AssetBundle文件会被拆分为26个AssetBundle文件。

示例2:当我们需要严格控制某个文件夹内的依赖资源打进同一个AssetBundle文件内,那么StaticAssetCollector收集器是最佳选择,该收集器收集的资源,无论是否被其它资源引用或被多个资源引用,这些资源都会按照设定的打包规则打包,且这些资源不会被处理为share资源包。

  • AddressRule
    可寻址规则,规则可以自定义扩展。下面是内置规则:
    • AddressByFileName 以文件名为定位地址。
    • AddressByFilePath 以文件路径为定位地址。
    • AddressByGrouperAndFileName 以分组名+文件名为定位地址。
    • AddressByFolderAndFileName 以文件夹名+文件名为定位地址。
//自定义扩展范例
public class AddressByFileName : IAddressRule
{
    string IAddressRule.GetAssetAddress(AddressRuleData data)
    {
        return Path.GetFileNameWithoutExtension(data.AssetPath);
    }
}
  • PackRule
    打包规则,规则可以自定义扩展。下面是内置规则:
    • PackSeparately 以文件路径作为资源包名,每个资源文件单独打包。
    • PackDirectory 以文件所在的文件夹路径作为资源包名,该文件夹下所有文件打进一个资源包。
    • PackTopDirectory 以收集器下顶级文件夹为资源包名,该文件夹下所有文件打进一个资源包。
    • PackCollector 以收集器路径作为资源包名,收集的所有文件打进一个资源包。
    • PackGroup 以分组名称作为资源包名,收集的所有文件打进一个资源包。
    • PackRawFile 目录下的资源文件会被处理为原生资源包。
//自定义扩展范例
public class PackDirectory : IPackRule
{
    PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
    {
        //"Assets/Config/test.txt" --> "Assets/Config"
        string bundleName = Path.GetDirectoryName(data.AssetPath);
        PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
        return result;   
    }
    bool IPackRule.IsRawFilePackRule()
    {
        return false;
    }
}
  • FilterRule
    过滤规则,规则可以自定义扩展。下面是内置规则:
    • CollectAll 收集目录下的所有资源文件
    • CollectScene 只收集目录下的场景文件
    • CollectPrefab 只收集目录下的预制体文件
    • CollectSprite 只收集目录下的精灵类型的文件
//自定义扩展范例
public class CollectScene : IFilterRule
{
    public bool IsCollectAsset(FilterRuleData data)
    {
        return Path.GetExtension(data.AssetPath) == ".unity";
    }
}
  • UserData
    用户自定义数据,可以帮助定制化AddressRule和PackRule。
  • AssetTags
    资源分类标签列表,该收集器下收集的资源会全部被打上该标签。

代码示例

开发者可以通过访问收集器的实例类实现自定义需求。

例如:下面示例是通过代码关闭某个Group

using YooAsset.Editor;

private void SetGroupDsiable(string packageName, string groupName)
{
    foreach (var package in AssetBundleCollectorSettingData.Setting.Packages)
    {
        if (package.PackageName == packageName)
        {
            foreach (var group in package.Groups)
            {
                if (group.GroupName == groupName)
                {
                    group.ActiveRuleName = nameof(DisableGroup);
                    break;
                }
            }
        }
    }
}

资源构建

在这里插入图片描述
在这里插入图片描述

界面介绍

  • Build Output
    构建输出的目录,会根据Unity编辑器当前切换的平台自动划分构建结果。

  • Build Pipeline
    构建管线
    (1) BuiltinBuildPipeline: 传统的内置构建管线。
    (2) ScriptableBuildPipeline: 可编程构建管线。

  • Build Mode
    构建模式
    (1) 强制构建模式:会删除指定构建平台下的所有构建记录,重新构建所有资源包。
    (2) 增量构建模式:以上一次构建结果为基础,对于发生变化的资源进行增量构建。
    (3) 演练构建模式:在不生成AssetBundle文件的前提下,进行演练构建并快速生成构建报告和补丁清单。
    (4) 模拟构建模式:在编辑器下配合EditorSimulateMode运行模式,来模拟真实运行的环境。

  • Build Version
    构建的资源包版本。

  • Build Package
    构建的资源包名称。

  • Encryption
    加密类列表。

  • Compression
    资源包的压缩方式。

  • Output Name Style
    输出的资源包文件名称样式
    HashName:哈希值
    BundleName_HashName:资源包名+哈希值

  • Copy Buildin File Option
    首包资源文件的拷贝方式
    (1) None:不拷贝任何文件
    (2) ClearAndCopyAll:先清空已有文件,然后拷贝所有文件
    (3) ClearAndCopyByTags:先清空已有文件,然后按照资源标签拷贝文件
    (4) OnlyCopyAll:不清空已有文件,直接拷贝所有文件
    (5) OnlyCopyByTags:不清空已有文件,直接按照资源标签拷贝文件

  • 构建
    点击构建按钮会开始构建流程,构建流程分为多个节点顺序执行,如果某个节点发生错误,会导致构建失败。错误信息可以在控制台查看。

加密方法

在Editor目录下实现一个继承IEncryptionServices接口的类。

加密支持三种方式:

  • LoadFromFileOffset 通过文件偏移来解密加载。
  • LoadFromMemory 通过文件内存来解密加载。
  • LoadFromStream 通过文件流来解密加载。
// 文件偏移加密方式的示例代码
public class FileOffsetEncryption : IEncryptionServices
{
    public EncryptResult Encrypt(EncryptFileInfo fileInfo)
    {
        if (fileInfo.BundleName.Contains("_gameres_audio"))
        {
            int offset = 32;
            byte[] fileData = File.ReadAllBytes(fileInfo.FilePath);
            var encryptedData = new byte[fileData.Length + offset];
            Buffer.BlockCopy(fileData, 0, encryptedData, offset, fileData.Length);
            
            EncryptResult result = new EncryptResult();
            result.LoadMethod = EBundleLoadMethod.LoadFromFileOffset;
            result.EncryptedData = encryptedData;
            return result;
        }
        else
        {
            EncryptResult result = new EncryptResult();
            result.LoadMethod = EBundleLoadMethod.Normal;
            return result;
        }
    }
}

上面就是YooAsset主要界面部分的介绍,下面开始代码逻辑流程部分的介绍。

初始化

学习如何进行资源系统的初始化。
初始化资源系统

// 初始化资源系统
YooAssets.Initialize();

// 创建默认的资源包
var package = YooAssets.CreatePackage("DefaultPackage");

// 设置该资源包为默认的资源包,可以使用YooAssets相关加载接口加载该资源包内容。
YooAssets.SetDefaultPackage(package);

资源系统的运行模式支持三种:编辑器模拟模式,单机运行模式,联机运行模式。
在这里插入图片描述

编辑器模拟模式

在编辑器下,不需要构建资源包,来模拟运行游戏。

注意:该模式只在编辑器下起效

private IEnumerator InitializeYooAsset()
{
    var initParameters = new EditorSimulateModeParameters();
    initParameters.SimulateManifestFilePath  = EditorSimulateModeHelper.SimulateBuild("DefaultPackage");
    yield return package.InitializeAsync(initParameters);
}

单机运行模式

对于不需要热更新资源的游戏,可以使用单机运行模式。
注意:该模式需要构建资源包

private IEnumerator InitializeYooAsset()
{
    var initParameters = new OfflinePlayModeParameters();
    yield return package.InitializeAsync(initParameters);
}

联机运行模式

对于需要热更新资源的游戏,可以使用联机运行模式,该模式下初始化参数会很多。
注意:该模式需要构建资源包

  • DecryptionServices : 如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
  • QueryServices:内置资源查询服务接口。
  • RemoteServices: 远端服务器查询服务接口。
private IEnumerator InitializeYooAsset()
{
    string defaultHostServer = "http://127.0.0.1/CDN/Android/v1.0";
    string fallbackHostServer = "http://127.0.0.1/CDN/Android/v1.0";
    var initParameters = new HostPlayModeParameters();
    initParameters.QueryServices = new GameQueryServices(); //太空战机DEMO的脚本类,详细见StreamingAssetsHelper
    initParameters.DecryptionServices = new GameDecryptionServices();
    initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
    var initOperation = package.InitializeAsync(initParameters);
    yield return initOperation;
    
    if(initOperation.Status == EOperationStatus.Succeed)
    {
        Debug.Log("资源包初始化成功!");
    }
    else 
    {
        Debug.LogError($"资源包初始化失败:{initOperation.Error}");
    }
}

WebGL运行模式

针对WebGL平台的专属模式,包括微信小游戏,抖音小游戏都需要选择该模式。

注意:该模式需要构建资源包

  • DecryptionServices : WebGL平台不支持加密,可以设置为NULL。
  • QueryServices:WebSite站内资源查询服务接口。
  • RemoteServices: 远端服务器查询服务接口。
private IEnumerator InitializeYooAsset()
{
    string defaultHostServer = "http://127.0.0.1/CDN/WebGL/v1.0";
    string fallbackHostServer = "http://127.0.0.1/CDN/WebGL/v1.0";
    var initParameters = new WebPlayModeParameters();
    initParameters.QueryServices = new GameQueryServices(); //太空战机DEMO的脚本类,详细见StreamingAssetsHelper
    initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
    var initOperation = package.InitializeAsync(initParameters);
    yield return initOperation;
    
    if(initOperation.Status == EOperationStatus.Succeed)
    {
        Debug.Log("资源包初始化成功!");
    }
    else 
    {
        Debug.LogError($"资源包初始化失败:{initOperation.Error}");
    }
}

解密方法

实现一个继承IDecryptionServices接口的运行时的类。

// 文件解密的示例代码
// 注意:解密类必须配合加密类。
private class GameDecryptionServices : IDecryptionServices
{
    public ulong LoadFromFileOffset(DecryptFileInfo fileInfo)
    {
        return 32;
    }
    
    public byte[] LoadFromMemory(DecryptFileInfo fileInfo)
    {
        // 如果没有内存加密方式,可以返回空
        throw new NotImplementedException();
    }

    public Stream LoadFromStream(DecryptFileInfo fileInfo)
    {
        // 如果没有流加密方式,可以返回空
        throw new NotImplementedException();
    }
    
    public uint GetManagedReadBufferSize()
    {
        return 1024;
    }
}

源代码解析

Package.InitializeAsync()方法解析。

  • 编辑器模拟模式
    每次启动调用EditorSimulateModeHelper.SimulateBuild()方法,都会在底层执行一次模拟构建(Simulate Build)。
    如果参与构建的资源对象数量级很大的话则会有卡顿现象,可以通过直接指定已有的清单路径来避免每次都重复执行模拟构建。
  • 单机运行模式
    在初始化的时候,会直接读取内置清单文件(StreamingAssets文件夹里的文件),最后初始化缓存系统。
  • 联机运行模式
    在初始化的时候,会优先从沙盒里加载清单,如果沙盒里不存在,则会尝试加载内置清单并将其拷贝到沙盒里。最后初始化缓存系统。
    注意:如果沙盒清单和内置清单都不存在,初始化也会被判定为成功!

资源更新

学习如何进行资源文件的更新。

获取资源版本

该资源版本可以通过YooAssets提供的接口来更新,也可以通过HTTP访问游戏服务器来获取。

private IEnumerator UpdatePackageVersion()
{
    var package = YooAssets.GetPackage("DefaultPackage");
    var operation = package.UpdatePackageVersionAsync();
    yield return operation;

    if (operation.Status == EOperationStatus.Succeed)
    {
        //更新成功
        string packageVersion = operation.PackageVersion;
        Debug.Log($"Updated package Version : {packageVersion}");
    }
    else
    {
        //更新失败
        Debug.LogError(operation.Error);
    }
}

更新资源清单

对于联机运行模式,在获取到资源版本号之后,就可以更新资源清单了。

private IEnumerator UpdatePackageManifest()
{
    // 更新成功后自动保存版本号,作为下次初始化的版本。
    // 也可以通过operation.SavePackageVersion()方法保存。
    bool savePackageVersion = true;
    var package = YooAssets.GetPackage("DefaultPackage");
    var operation = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion);
    yield return operation;

    if (operation.Status == EOperationStatus.Succeed)
    {
        //更新成功
    }
    else
    {
        //更新失败
        Debug.LogError(operation.Error);
    }
}

资源包下载

在补丁清单更新完毕后,就可以更新资源文件了。

根据产品需求,可以选择更新全部资源,或者只更新部分资源。

补丁包下载接口:

  • YooAssets.CreateResourceDownloader(int downloadingMaxNumber, int failedTryAgain, int timeout)
    用于下载更新当前资源版本所有的资源包文件。

  • YooAssets.CreateResourceDownloader(string[] tags, int downloadingMaxNumber, int failedTryAgain, int timeout)
    用于下载更新资源标签指定的资源包文件。

  • YooAssets.CreateBundleDownloader(AssetInfo[] assetInfos, int downloadingMaxNumber, int failedTryAgain, int timeout)
    用于下载更新指定的资源列表依赖的资源包文件。

IEnumerator Download()
{
    int downloadingMaxNum = 10;
    int failedTryAgain = 3;
    var package = YooAssets.GetPackage("DefaultPackage");
    var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
    
    //没有需要下载的资源
    if (downloader.TotalDownloadCount == 0)
    {        
        yield break;
    }

    //需要下载的文件总数和总大小
    int totalDownloadCount = downloader.TotalDownloadCount;
    long totalDownloadBytes = downloader.TotalDownloadBytes;    

    //注册回调方法
    downloader.OnDownloadErrorCallback = OnDownloadErrorFunction;
    downloader.OnDownloadProgressCallback = OnDownloadProgressUpdateFunction;
    downloader.OnDownloadOverCallback = OnDownloadOverFunction;
    downloader.OnStartDownloadFileCallback = OnStartDownloadFileFunction;

    //开启下载
    downloader.BeginDownload();
    yield return downloader;

    //检测下载结果
    if (downloader.Status == EOperationStatus.Succeed)
    {
        //下载成功
    }
    else
    {
        //下载失败
    }
}

源代码解析

Package.UpdatePackageManifestAsync()方法解析。

  • 联机运行模式
    通过传入的清单版本,优先比对当前激活清单的版本,如果相同就直接返回成功。如果有差异就从缓存里去查找匹配的清单,如果缓存里不存在,就去远端下载并保存到沙盒里。最后加载沙盒内匹配的清单文件。

资源加载

学习资源加载的使用方法。

方法列表

  • LoadSceneAsync() 异步加载场景
  • LoadAssetSync() 同步加载资源对象
  • LoadAssetAsync() 异步加载资源对象
  • LoadSubAssetsSync() 同步加载子资源对象
  • LoadSubAssetsAsync() 异步加载子资源对象
  • LoadAllAssetsSync() 同步加载资源包内所有资源对象
  • LoadAllAssetsAsync() 异步加载资源包内所有资源对象
  • LoadRawFileSync() 同步获取原生文件
  • LoadRawFileAsync() 异步获取原生文件

统一约定

Location为资源的定位地址,也是加载资源对象的唯一标识符。

  • 在未开启可寻址模式下,location代表的是资源对象的完整路径。
// 以工程内的音频文件为例:"Assets/GameRes/Audio/bgMusic.mp3" 
package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic");
  • 在开启可寻址模式下,location代表的是资源对象可寻址地址。
// 以工程内的音频文件为例:"Assets/GameRes/Audio/bgMusic.mp3" 
// 需要在资源配置界面启用可寻址功能(Enable Addressable)。
// 配置界面的可寻址规则为AddressByFileName,那么资源定位地址填写文件名称:"bgMusic"
package.LoadAssetAsync<AudioClip>("bgMusic");

在这里插入图片描述

加载路径的匹配

// 不带扩展名的模糊匹配
package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic");

// 带扩展名的精准匹配
package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3");

异步加载范例

// 委托加载方式
void Start()
{
    AssetOperationHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3");
    handle.Completed += Handle_Completed;
}
void Handle_Completed(AssetOperationHandle handle)
{
    AudioClip audioClip = handle.AssetObject as AudioClip;
}
// 协程加载方式
IEnumerator Start()
{
    AssetOperationHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3");
    yield return handle;   
    AudioClip audioClip = handle.AssetObject as AudioClip;
}
// Task加载方式
async void Start()
{
    AssetOperationHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3");
    await handle.Task;
    AudioClip audioClip = handle.AssetObject as AudioClip;  
}

资源卸载范例

IEnumerator Start()
{
    AssetOperationHandle handle = package.LoadAssetAsync<AudioClip>("Assets/GameRes/Audio/bgMusic.mp3");
    yield return handle;
    ...
    handle.Release();
}

资源释放范例

可以在切换场景之后调用资源释放方法或者写定时器间隔时间去释放。

注意:只有调用资源释放方法,资源对象才会在内存里被移除。

private void UnloadAssets()
{
    var package = YooAssets.GetAssetsPackage("DefaultPackage");
    package.UnloadUnusedAssets();
}

预制体加载范例

IEnumerator Start()
{
    AssetOperationHandle handle = package.LoadAssetAsync<GameObject>("Assets/GameRes/Panel/login.prefab");
    yield return handle;
    GameObject go = handle.InstantiateSync();
    Debug.Log($"Prefab name is {go.name}");
}

子对象加载范例

例如:通过TexturePacker创建的图集,如果需要访问图集的精灵对象,可以通过子对象加载接口。

IEnumerator Start()
{
    SubAssetsOperationHandle handle = package.LoadSubAssetsAsync<Sprite>(location);
    yield return handle;
    var sprite = handle.GetSubAssetObject<Sprite>("spriteName");
    Debug.Log($"Sprite name is {sprite.name}");
}

资源包内所有对象加载范例

例如:我们将所有配置表打进了一个资源包里,我们想把所有配置文件一次性全部加载出来解析。

IEnumerator Start()
{
    // 注意:location只需要填写资源包里的任意资源地址。
    AllAssetsOperationHandle handle = package.LoadAllAssetsAsync<UnityEngine.TextAsset>(location);
    yield return handle;
    foreach(var assetObj in handle.AllAssetObjects)
    {    
        UnityEngine.TextAsset textAsset = assetObj as UnityEngine.TextAsset;
    }
}

场景异步加载范例

注意:当加载新的主场景的时候,会自动释放之前加载的主场景以及附加场景。

IEnumerator Start()
{
    string location = "Assets/GameRes/Scene/Login";
    var sceneMode = UnityEngine.SceneManagement.LoadSceneMode.Single;
    bool suspendLoad = false;
    SceneOperationHandle handle = package.LoadSceneAsync(location, sceneMode, suspendLoad);
    yield return handle;
    Debug.Log($"Scene name is {handle.Scene.name}");
}

原生文件加载范例

例如:wwise的初始化文件

IEnumerator Start()
{
    string location = "Assets/GameRes/wwise/init.bnk";
    RawFileOperationHandle handle = package.LoadRawFileAsync(location);
    yield return handle;
    byte[] fileData = handle.GetRawFileData();
    string fileText = handle.GetRawFileText();
    string filePath = handle.GetRawFilePath();
}

配置文件加载范例

// 自定义的配置文件
public class MyGameConfig: ScriptableObject
{
    ...
}

IEnumerator Start()
{
    string location = "Assets/GameRes/config/gameConfig.asset";
    AssetOperationHandle handle = package.LoadAssetFileAsync(location);
    yield return handle;
    MyGameConfig gameCOnfig = handle.AssetObject as MyGameConfig;
}

获取资源信息列表

通过资源标签来获取资源信息列表。

void GetAssetInfosByTag(string tag)
{
    AssetInfo[] assetInfos = package.GetAssetInfos(tag);
    foreach (var assetInfo in assetInfos)
    {
        Debug.Log(assetInfo.AssetPath);
    }
}

到此,YooAsset主要功能和使用方法讲解完毕!

下一篇将讲解YooAsset的示例工程

  • 16
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

育婴房扛把子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值