GameFrameWork框架(Unity3D)使用笔记(八) 实现场景加载进度条

前言:

        游戏在转换场景的时候,需要花费时间来加载相关的资源。而这个过程往往因为游戏场景的规模和复杂度以及玩家电脑配置的原因花费一小段时间(虽然这个项目里用不到)。

        所以,如果这一小段时间,画面就卡在这里,啥也做不了,玩家也不知道啥时候能加载好。这个等待的时间实际上非常地影响玩家的使用体验。

        目前大多数游戏在转换关卡这种时候都会有个加载界面,显示加载进度。这样玩家可以对啥时候能加载好有个心理预估(判断要不要因为加载太久浪费时间不如卸载游戏(开个玩笑))。

        一般加载场景显示进度条的方法搜搜就有了,就是利用Unity自带的异步加载函数SceneManager.LoadSceneAsync()加载场景,并且通过AsyncOperation跟踪加载进度,从而设置进度条之类的。

        不过,在GameFramework框架下,加载场景的模块被进一步封装,那怎么在UGF下实现加载的进度条就是本篇的主要内容。


一、实现过程讲解

        我看过一些非GF的加载场景的方案,大多数都是:对于从场景a-->场景b的过程,将其变为从场景a-->场景c-->场景b

        其中,场景c里面主要就只有一个加载界面,主要用来显示进度条等内容。这样的话,从a->c可以非常快速地跳转(因为c中就只有个UI所以即便配置不高也能很快跳转)然后玩家在c中观看进度条的时候,后台异步加载场景b,加载完毕后立刻转到场景b。

        但是,在GF框架下,有个不同的地方。就是GF框架预制体所在的Launcher场景是一直存在的,并且框架的UI统一在这个场景里管理。

        所以在GF里实现进度条的功能,我的方案是直接在ProcedureChangeScene流程里面加载新场景的同时显示进度条UI,并且在加载完成后关闭UI。

        此外,由于一些原因(参考这一篇:http://t.csdn.cn/65FDe),想要测试这个进度条的真正效果,需要把当前工程Build再运行测试。


一、拼制UI

 在场景里面拼个进度条UI:

然后写UI的逻辑脚本,这里实现控制UI进度条的函数方便之后调用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace ShadowU
{
    public class LoadingForm: UGuiForm
    {
        public GameObject mask;     //进度条的遮罩
        public Text loadingText;    //加载中的文字

        protected override void OnClose(bool isShutdown, object userData)
        {
            base.OnClose(isShutdown, userData);
        }

        protected override void OnInit(object userData)
        {
            base.OnInit(userData);
        }

        protected override void OnOpen(object userData)
        {
            base.OnOpen(userData);
            loadingText.text = "加载中.  .  .  .  .  .  ";
        }

        protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(elapseSeconds, realElapseSeconds);
            //控制加载中文字后的句号浮动
            string temp = loadingText.text;
            temp.Insert(3, temp[temp.Length - 1].ToString()).Remove(temp.Length - 1);
            loadingText.text = temp;
        }
        void SetProcess(float a)
        {
            a = Mathf.Clamp(a,0f,1.0f);//将数值控制在0-1
            mask.transform.SetLocalScaleX(a);
        }
    }
}

接上脚本,设置好public参数,做成预制体:


二、修改ProcedureChangeScene流程代码

         ProcedureChangeScene开始的时候,加载出Loading界面,同时开始异步加载新场景。

        通过LoadSceneUpdateEvent事件来更新加载界面进度条的进度。

        最后,在场景加载完成后关闭Loading界面即可。

        新的ProcedureChangeScene代码如下:


using GameFramework.Fsm;
using GameFramework.Event;
using GameFramework.DataTable;
using UnityGameFramework.Runtime;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;

namespace ShadowU
{
    public class ProcedureChangeScene : ProcedureBase
    {
        private const int MenuSceneId = 1;   //菜单场景的ID 

        private bool m_ChangeToMenu = false; //是否转换为菜单
        private bool m_IsChangeSceneComplete = false; //转换场景是否结束

        private LoadingForm m_LoadingForm;    //加载界面
        public override bool UseNativeDialog
        {
            get
            {
                return true;
            }
        }

        protected override void OnEnter(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner)
        {
            base.OnEnter(procedureOwner);

            Debug.Log("ChangeScene!");
            m_IsChangeSceneComplete = false; //如果为true的话 则在update中会切换流程

            //打开加载界面
            GameEntry.UI.OpenUIForm(AssetUtility.GetUIFormAsset("LoadingForm"), "Default", 1, this);

            //注册事件
            GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId,OnLoadSceneSuccess);
            GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //隐藏所有实体
            GameEntry.Entity.HideAllLoadingEntities();
            GameEntry.Entity.HideAllLoadedEntities();

            //卸载所有场景
            string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames();

            foreach (string sn in loadedSceneAssetNames)
                GameEntry.Scene.UnloadScene(sn);

            //还原游戏速度
            GameEntry.Base.ResetNormalGameSpeed();

            
            //获取下一个场景的ID
            int sceneId = procedureOwner.GetData<VarInt32>("NextSceneId");
            m_ChangeToMenu = sceneId == MenuSceneId; //判断是否是转到菜单
            IDataTable<DRScene> dtScene = GameEntry.DataTable.GetDataTable<DRScene>();
            DRScene drScene = dtScene.GetDataRow(sceneId); //获取数据表行
            if(drScene == null)
            {
                Log.Warning("Can not load scene '{0}' from data table.", sceneId.ToString());
                return;
            }
            GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName),Constant.AssetPriority.SceneAsset,this);
        }



        protected override void OnLeave(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, bool isShutdown)
        {
            base.OnLeave(procedureOwner, isShutdown);

            GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
            GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //关闭UI
            if (m_LoadingForm != null)
            {
                m_LoadingForm.Close(isShutdown);
                m_LoadingForm = null;
            }
        }

        protected override void OnUpdate(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);


            if (!m_IsChangeSceneComplete)   
            {
                return;  //还没完成场景切换
            }

            if (m_ChangeToMenu)
            {
                ChangeState<ProcedureMenu>(procedureOwner);   //菜单
            }else
            {
                ChangeState<ProcedureMain>(procedureOwner);   //游戏运行流程
            }
        }
        
        private void OnLoadSceneSuccess(object sender,GameEventArgs e)
        {
            LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e;
            if(ne.UserData != this)
            {
                return;
            }
            Log.Info("Load scene '{0}' OK.", ne.SceneAssetName);
            m_IsChangeSceneComplete = true;
        }
        private void OnLoadSceneFailure(object sender, GameEventArgs e)
        {
            LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage);
        }

        private void OnLoadSceneUpdate(object sender, GameEventArgs e)
        {
            LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2"));
            //更新加载界面的进度条
            if(m_LoadingForm != null)
                m_LoadingForm.SetProcess(ne.Progress);
        }

        private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e)
        {
            LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName, ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
        }
        private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
        {
            OpenUIFormSuccessEventArgs ne = (OpenUIFormSuccessEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            m_LoadingForm = (LoadingForm)ne.UIForm.Logic;
        }
    }
}

 三、测试效果

        直接编辑器模式运行的话看场景加载并不是异步的,所以要测试这个进度条的真正效果需要打包运行。

        关于打包的过程之后的文章里会讲,这里就先跳过了。

        测试:

 进度条的功能是完成了!

但是。。。又出了新的问题:菜单界面在加载界面之上。

不过编辑器模式运行并没有这个问题:

    

但是没有问题只是看上去的。。。如果我立即暂停后观察,其实这个模式下也会两个UI同屏,只不过相对来说时间比较短暂。我经过一段时间分析后,想到UGuiForm里面在关闭和打开界面的时候都有淡入淡出的逻辑,于是作出如下假设:

加载界面看到菜单界面并不是渲染层级先后的问题,而是它们都在淡入或淡出过程中。所以说,他们都处于半透明状态所以能都看得见。

(因为基于这个假设能解决问题那我就不去证明我这个假设到底对不对了嘿嘿~) 

那解决办法就很灵活了。比如可以判断取消打开加载界面的淡入效果。不过我觉得这样改结构会很不美观。

于是我打算这样处理:让加载界面至少停留一秒。同时减小淡入淡出的时间。

在UGuiForm.cs里面修改淡入淡出时间

修改一下ProcedureChangeScene的代码(增加了和loadingTimer变量有关的部分):


using GameFramework.Fsm;
using GameFramework.Event;
using GameFramework.DataTable;
using UnityGameFramework.Runtime;
using UnityEngine;
using UnityEngine.SceneManagement;
using System;

namespace ShadowU
{
    public class ProcedureChangeScene : ProcedureBase
    {
        private const int MenuSceneId = 1;   //菜单场景的ID 

        private bool m_ChangeToMenu = false; //是否转换为菜单
        private bool m_IsChangeSceneComplete = false; //转换场景是否结束

        private LoadingForm m_LoadingForm;    //加载界面
        private float loadingTimer;           //加载界面停留的计时器
        public override bool UseNativeDialog
        {
            get
            {
                return true;
            }
        }

        protected override void OnEnter(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner)
        {
            base.OnEnter(procedureOwner);

            Debug.Log("ChangeScene!");
            m_IsChangeSceneComplete = false; //如果为true的话 则在update中会切换流程

            //打开加载界面
            GameEntry.UI.OpenUIForm(AssetUtility.GetUIFormAsset("LoadingForm"), "Default", 1, this);
            loadingTimer = 1.0f;         //加载界面至少停留一秒

            //注册事件
            GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId,OnLoadSceneSuccess);
            GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //隐藏所有实体
            GameEntry.Entity.HideAllLoadingEntities();
            GameEntry.Entity.HideAllLoadedEntities();

            //卸载所有场景
            string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames();

            foreach (string sn in loadedSceneAssetNames)
                GameEntry.Scene.UnloadScene(sn);

            //还原游戏速度
            GameEntry.Base.ResetNormalGameSpeed();

            
            //获取下一个场景的ID
            int sceneId = procedureOwner.GetData<VarInt32>("NextSceneId");
            m_ChangeToMenu = sceneId == MenuSceneId; //判断是否是转到菜单
            IDataTable<DRScene> dtScene = GameEntry.DataTable.GetDataTable<DRScene>();
            DRScene drScene = dtScene.GetDataRow(sceneId); //获取数据表行
            if(drScene == null)
            {
                Log.Warning("Can not load scene '{0}' from data table.", sceneId.ToString());
                return;
            }
            GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName),Constant.AssetPriority.SceneAsset,this);
        }



        protected override void OnLeave(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, bool isShutdown)
        {
            base.OnLeave(procedureOwner, isShutdown);

            GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
            GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
            GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
            GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
            GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId,OnOpenUIFormSuccess);

            //关闭UI
            if (m_LoadingForm != null)
            {
                m_LoadingForm.Close(isShutdown);
                m_LoadingForm = null;
            }
        }

        protected override void OnUpdate(IFsm<GameFramework.Procedure.IProcedureManager> procedureOwner, float elapseSeconds, float realElapseSeconds)
        {
            base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);


            if (!m_IsChangeSceneComplete)   
            {
                return;  //还没完成场景切换
            }

            m_LoadingForm.SetProcess(1);

            if(loadingTimer > 0)  
            {
                loadingTimer -= elapseSeconds;//更新计时器
                return;
            }

            if (m_ChangeToMenu)
            {
                ChangeState<ProcedureMenu>(procedureOwner);   //菜单
            }else
            {
                ChangeState<ProcedureMain>(procedureOwner);   //游戏运行流程
            }
        }
        
        private void OnLoadSceneSuccess(object sender,GameEventArgs e)
        {
            LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e;
            if(ne.UserData != this)
            {
                return;
            }
            Log.Info("Load scene '{0}' OK.", ne.SceneAssetName);
            m_IsChangeSceneComplete = true;
        }
        private void OnLoadSceneFailure(object sender, GameEventArgs e)
        {
            LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage);
        }

        private void OnLoadSceneUpdate(object sender, GameEventArgs e)
        {
            LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2"));
            //更新加载界面的进度条
            if(m_LoadingForm != null)
                m_LoadingForm.SetProcess(ne.Progress);
        }

        private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e)
        {
            LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName, ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
        }
        private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
        {
            OpenUIFormSuccessEventArgs ne = (OpenUIFormSuccessEventArgs)e;
            if (ne.UserData != this)
            {
                return;
            }

            m_LoadingForm = (LoadingForm)ne.UIForm.Logic;
        }
    }
}

此外还有个细节:就是进度条到90就不动了。其实场景加载到90%的时候就已经加载完了,最后的10%就在于有没有把场景显示出来。那为了看起来不膈应,我上面的代码里在加载完场景后手动把进度条设置为100:

打包看下最终效果:

 效果挺理想的,那就这样了!

        

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在前面的笔记中,我们已经实现了选项卡的基本功能,可以切换不同的选项卡内容。但是,为了让选项卡更美观和实用,我们需要进一步完善选项卡的功能。 ## 1. 添图标 我们可以为每个选项卡添一个图标,这样可以更直观地表示每个选项卡的功能。实现方法如下: 首先,我们需要准备好图标。这里我们以三个选项卡为例,分别对应“文件”,“编辑”和“视图”功能。我们可以在 Unity 的 Assets 窗口中创建一个文件夹,然后将三个图标拖拽到该文件夹中。 接下来,在 OnGUI() 函数中,为每个选项卡添一个图标。具体实现代码如下: ```csharp // 定义三个纹理变量 private Texture2D fileTexture; private Texture2D editTexture; private Texture2D viewTexture; // 在 Start() 函数中纹理 void Start() { fileTexture = Resources.Load<Texture2D>("fileIcon"); editTexture = Resources.Load<Texture2D>("editIcon"); viewTexture = Resources.Load<Texture2D>("viewIcon"); } // 在 OnGUI() 函数中为每个选项卡添图标 void OnGUI() { GUI.skin = skin; // 绘制选项卡标签栏 GUILayout.BeginHorizontal(); if (GUILayout.Toggle(selectedTab == 0, new GUIContent(fileTexture, "文件"), "tabLeft")) selectedTab = 0; if (GUILayout.Toggle(selectedTab == 1, new GUIContent(editTexture, "编辑"), "tabMid")) selectedTab = 1; if (GUILayout.Toggle(selectedTab == 2, new GUIContent(viewTexture, "视图"), "tabRight")) selectedTab = 2; GUILayout.EndHorizontal(); // 绘制选项卡内容 switch (selectedTab) { case 0: DrawFileTab(); break; case 1: DrawEditTab(); break; case 2: DrawViewTab(); break; } } ``` 在上面的代码中,我们首先定义了三个纹理变量,然后在 Start() 函数中了这三个纹理。在 OnGUI() 函数中,我们为每个选项卡添了一个图标,并使用 GUIContent 类型的参数来设置图标和鼠标悬停时显示的文本。 运行程序,我们可以看到每个选项卡都有了自己的图标。 ## 2. 设置选项卡大小 默认情况下,选项卡的大小是根据标签文本的长度来自动调整的。但是,有时候我们希望选项卡的大小是固定的,这样可以使选项卡更美观。实现方法如下: 首先,我们需要定义一个变量来保存选项卡的宽度。例如,我们希望每个选项卡的宽度都是 100 像素,那么可以定义一个变量如下: ```csharp private int tabWidth = 100; ``` 然后,在 OnGUI() 函数中为每个选项卡设置宽度。具体实现代码如下: ```csharp // 在 OnGUI() 函数中为每个选项卡设置宽度 void OnGUI() { GUI.skin = skin; // 计算选项卡的总宽度 int totalWidth = tabWidth * 3 + 6; // 绘制选项卡标签栏 GUILayout.BeginHorizontal(GUILayout.Width(totalWidth)); if (GUILayout.Toggle(selectedTab == 0, new GUIContent(fileTexture, "文件"), "tabLeft", GUILayout.Width(tabWidth))) selectedTab = 0; if (GUILayout.Toggle(selectedTab == 1, new GUIContent(editTexture, "编辑"), "tabMid", GUILayout.Width(tabWidth))) selectedTab = 1; if (GUILayout.Toggle(selectedTab == 2, new GUIContent(viewTexture, "视图"), "tabRight", GUILayout.Width(tabWidth))) selectedTab = 2; GUILayout.EndHorizontal(); // 绘制选项卡内容 switch (selectedTab) { case 0: DrawFileTab(); break; case 1: DrawEditTab(); break; case 2: DrawViewTab(); break; } } ``` 在上面的代码中,我们首先计算了选项卡标签栏的总宽度,然后使用 GUILayout.BeginHorizontal() 函数设置了标签栏的宽度。接下来,我们使用 GUILayout.Width() 函数为每个选项卡设置宽度。 运行程序,我们可以看到每个选项卡的宽度都是固定的。 ## 3. 支持拖拽 有时候,我们希望能够通过拖拽的方式来切换选项卡,这样可以使用户体验更流畅。实现方法如下: 首先,我们需要定义一个变量来保存当前鼠标是否按下。例如,我们可以定义一个布尔型变量如下: ```csharp private bool isMouseDown; ``` 然后,在 OnGUI() 函数中检测鼠标是否按下,并根据鼠标位置计算选项卡索引。具体实现代码如下: ```csharp // 在 OnGUI() 函数中检测鼠标是否按下,并根据鼠标位置计算选项卡索引 void OnGUI() { GUI.skin = skin; // 计算选项卡的总宽度 int totalWidth = tabWidth * 3 + 6; // 绘制选项卡标签栏 GUILayout.BeginHorizontal(GUILayout.Width(totalWidth)); if (Event.current.type == EventType.MouseDown && Event.current.button == 0) { isMouseDown = true; } if (Event.current.type == EventType.MouseUp && Event.current.button == 0) { isMouseDown = false; int index = (int)(Event.current.mousePosition.x / tabWidth); if (index != selectedTab) { selectedTab = index; } } if (GUILayout.Toggle(selectedTab == 0, new GUIContent(fileTexture, "文件"), "tabLeft", GUILayout.Width(tabWidth))) selectedTab = 0; if (GUILayout.Toggle(selectedTab == 1, new GUIContent(editTexture, "编辑"), "tabMid", GUILayout.Width(tabWidth))) selectedTab = 1; if (GUILayout.Toggle(selectedTab == 2, new GUIContent(viewTexture, "视图"), "tabRight", GUILayout.Width(tabWidth))) selectedTab = 2; GUILayout.EndHorizontal(); // 绘制选项卡内容 switch (selectedTab) { case 0: DrawFileTab(); break; case 1: DrawEditTab(); break; case 2: DrawViewTab(); break; } } ``` 在上面的代码中,我们首先检测鼠标是否按下。如果鼠标左键按下,我们将 isMouseDown 变量设置为 true。接下来,我们检测鼠标左键是否释放。如果是,我们将 isMouseDown 变量设置为 false,并根据鼠标位置计算选项卡索引。如果选项卡索引不等于当前选项卡索引,则切换选项卡。 运行程序,我们可以使用鼠标拖拽的方式切换选项卡。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

咖啡咖_CoffCa

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

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

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

打赏作者

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

抵扣说明:

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

余额充值