unity 3D RPG教程(七)

目录

声明

31:Transition 实现同场景内传送

32:Different Scene 跨场景传送

33: Save Data 保存数据

34:Main Menu 制作主菜单

35:SceneFader 场景转换的渐入渐出

36:Build & Run打包及运行


声明

本教程学习均来自U3D中文课堂麦扣老师

31:Transition 实现同场景内传送

创建代码SceneController:

using UnityEngine.SceneManagement;

public class SceneController : Singleton<SceneController>//泛型单例模式
{
    
}

TransitionPoint:当进入传送门并且按下按键E的时候将所有的参数都传送给SceneController

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.E) && canTrans == true)
        {
            //TODO:SceneController传送
        }
    }

这就意味着要在SceneController里创建一个函数方法,并且加载一个TransitionPoint的参数,这样就获得所有的变量了

 SceneController:其中加载场景我们用异步加载的方法

 设置传送的位置和角度的方法:

SceneController:

public class SceneController : Singleton<SceneController>//泛型单例模式
{
    GameObject player;
    public void TransitionToDestination(TransitionPoint transitionPoint)//传送方法
    {
        switch(transitionPoint.transitionType)//判断同场景传送还是不同场景传送
        {
            case TransitionPoint.TransitionType.SameScene:
                break;
            case TransitionPoint.TransitionType.DifferentScene:
                break;
        }
    }

    IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景,目标点
    {
        player = GameManager.Instance.playerStats.gameObject;
        player.transform.SetPositionAndRotation();
    }
}

接下来要写一个方法,从所有的DesinationPoint里去找到跟这个destination匹配的标签,返回这个物体的TransitionDestination:

    private TransitionDestination GetDestination(TransitionDestination.DestinationTag destinationTag)//得到目标点的 TransitionDestination
    {
        var entrance = FindObjectsOfType<TransitionDestination>();//获得所有的 TransitionDestination
        for (int i=0;i < entrance.Length;i++)
        {
            if(entrance[i].destinationTag == destinationTag)
            {
                return entrance[i];
            }
        }
        return null;
    }

补充完代码SceneController:

public class SceneController : Singleton<SceneController>//泛型单例模式
{
    GameObject player;
    public void TransitionToDestination(TransitionPoint transitionPoint)//传送方法
    {
        switch(transitionPoint.transitionType)//判断同场景传送还是不同场景传送
        {
            case TransitionPoint.TransitionType.SameScene://同场景传送
                StartCoroutine(Transition(SceneManager.GetActiveScene().name,transitionPoint.destinationTag));
                break;
            case TransitionPoint.TransitionType.DifferentScene://不同场景传送

                break;
        }
    }

    IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景、目标点
    {
        player = GameManager.Instance.playerStats.gameObject;
        player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
        yield return null;
    }

    private TransitionDestination GetDestination(TransitionDestination.DestinationTag destinationTag)//得到目标点的 TransitionDestination
    {
        var entrance = FindObjectsOfType<TransitionDestination>();//获得所有的 TransitionDestination
        for (int i=0;i < entrance.Length;i++)
        {
            if(entrance[i].destinationTag == destinationTag)
            {
                return entrance[i];
            }
        }
        return null;
    }
}

在 TransitionPoint的Update中调用传送方法:

TransitionPoint:

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.E) && canTrans == true)
        {
            //TODO:SceneController传送
            SceneController.Instance.TransitionToDestination(this);
        }
    }

回到unity,发现传送门的collider太大阻挡了射线Player无法进入

 在MouseManager中制作一个对门的指针修改:

    void SetCursorTexture() //设置指针的贴图
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        if(Physics.Raycast(ray,out hitInfo))
        {
            //切换鼠标贴图
            switch(hitInfo.collider.gameObject.tag)
            {
                case "Ground":
                    Cursor.SetCursor(target, new Vector2(16, 16),CursorMode.Auto); //偏移(16,16)
                    break;
                case "Enemy":
                    Cursor.SetCursor(attack, new Vector2(16, 16), CursorMode.Auto); //偏移(16,16)
                    break;
                case "Portal":
                    Cursor.SetCursor(doorway, new Vector2(16, 16), CursorMode.Auto); //偏移(16,16)
                    break;
            }
        }
    }
    void MouseControl()//返回鼠标左键点击返回值
    {
        if(Input.GetMouseButtonDown(0)&&hitInfo.collider != null)
        {
            if(hitInfo.collider.gameObject.CompareTag("Ground"))
            {
                OnMouseClicked?.Invoke(hitInfo.point); //当前OnMouseClicked事件如果不为空,将点击到地面上的坐标传回给这个事件(执行所有加入到onMouseClicked的函数方法)
            }
            if (hitInfo.collider.gameObject.CompareTag("Enemy"))
            {
                OnEnemyClicked?.Invoke(hitInfo.collider.gameObject); //当前OnEnemyClicked事件如果不为空,将点击到敌人的gameObject传回给这个事件(执行所有加入到OnEnemyClicked的函数方法)
            }
            if (hitInfo.collider.gameObject.CompareTag("Attackable"))
            {
                OnEnemyClicked?.Invoke(hitInfo.collider.gameObject); //当前OnEnemyClicked事件如果不为空,将点击到敌人的gameObject传回给这个事件(执行所有加入到OnEnemyClicked的函数方法)
            }
            if (hitInfo.collider.gameObject.CompareTag("Portal"))
            {
                OnMouseClicked?.Invoke(hitInfo.point); //当前OnMouseClicked事件如果不为空,将点击到地面上的坐标传回给这个事件(执行所有加入到onMouseClicked的函数方法)
            }
        }
    }

 为传送门添标签Portal:

发现人物卡在外面了,因为传送门的Trigger的范围特别大,阻挡了鼠标发出来的射线的方向,停留在Trigger范围的外边了,打开Prefab,

因为Trigger可以阻挡射线,我们适度将Trigger高度降低,范围也降低一些,

 现在Player可以到达传送门了,但是不能传送到目标点,原因是需要关闭Agent

SceneController:

    IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景、目标点
    {
        player = GameManager.Instance.playerStats.gameObject;
        playerAgent = player.GetComponent<NavMeshAgent>();
        playerAgent.enabled = false;//传送时关闭NavMeshAgent
        player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
        playerAgent.enabled = true;//传送完开启NavMeshAgent
        yield return null;
    }

32:Different Scene 跨场景传送

创建一个新场景Room,注意用地图导航烘焙

 SceneController:补充传送方法:

    public void TransitionToDestination(TransitionPoint transitionPoint)//传送方法
    {
        switch(transitionPoint.transitionType)//判断同场景传送还是不同场景传送
        {
            case TransitionPoint.TransitionType.SameScene://同场景传送
                StartCoroutine(Transition(SceneManager.GetActiveScene().name,transitionPoint.destinationTag));
                break;
            case TransitionPoint.TransitionType.DifferentScene://不同场景传送
                StartCoroutine(Transition(transitionPoint.sceneName, transitionPoint.destinationTag));
                break;
        }
    }

    IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景、目标点
    {
        if(SceneManager.GetActiveScene().name != sceneName)//不同场景传送
        {
            yield return SceneManager.LoadSceneAsync(sceneName);
        }
        else//同场景传送
        {
            player = GameManager.Instance.playerStats.gameObject;
            playerAgent = player.GetComponent<NavMeshAgent>();
            playerAgent.enabled = false;//传送时关闭NavMeshAgent
            player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
            playerAgent.enabled = true;//传送完开启NavMeshAgent
            yield return null;
        }
    }

现在怎么把人物放到那个位置呢?

如果我们在每个场景里都添加人物的话,这个其实不是我们想要的一种方式,因为做游戏的的时候我们需要在某个点将我们的人物生成出来,而且我们会为它配套加载我们之前保存过的数据,我们以后会有人物的血量、经验值、装备、背包物品、快捷栏、任务、对话、所有的内容记录,我们希望加载下一个场景的时候仍然保存这些数据,所以每次我们转换场景的时候,我们希望每次重新加载场景的时候把人物重新生成出来,然后把这些所有的数据重新加载回去,所以需要生成我们的Player了

拿到Player的Prefab:

    public GameObject playerPrefab;
  IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景、目标点
    {
        //TODO:保存数据

        if(SceneManager.GetActiveScene().name != sceneName)//不同场景传送
        {
            yield return SceneManager.LoadSceneAsync(sceneName);
            yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
            yield break;
        }
        else//同场景传送
        {
            player = GameManager.Instance.playerStats.gameObject;
            playerAgent = player.GetComponent<NavMeshAgent>();
            playerAgent.enabled = false;//传送时关闭NavMeshAgent
            player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
            playerAgent.enabled = true;//传送完开启NavMeshAgent
            yield return null;
        }
    }

返回Unity进入传送门:

发现没有将场景加载进Build Setting中,加载进来。

 发现并没有生成人物,原因是没有Manager比如SceneManager,所以让它加载场景的时候不要删除这些物体,保证SceneController仍然存在,这样就会把人物生成出来了

SceneController:

    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(this);
    }

同样修改MouseManager和GameManager,这样就不用在每个场景当中去拖拽这些Manager了

现在可以生成Player了,但是无法移动,需要把所以的地板设置为Ground

 现在鼠标贴图变了,但还不能移动

原因是PlayerController中

 人物启动的时候为Action添加了方法,当场景切换人物消失需要将这个方法从Action取消订阅:

PlayerController:

    private void Start()
    {
        MouseManager.Instance.OnMouseClicked += MoveToTarget; //onMouseClicked事件添加注册 MoveToTarget()方法
        MouseManager.Instance.OnEnemyClicked += EventAttack;

        GameManager.Instance.RigisterPlayer(characterStats);//注册GameManager
    }

    private void OnDisable()//当场景切换人物消失需要将这2个方法从Action取消订阅
    {
        if (!MouseManager.IsInitialized) return;
        MouseManager.Instance.OnMouseClicked -= MoveToTarget; 
        MouseManager.Instance.OnEnemyClicked -= EventAttack;
    }

Game Manager:

人物回到Game场景相机不在跟随:在Game Manager脚本中获得相机,然后实现跟随人物

    private CinemachineFreeLook followCamera;//相机
    public void RigisterPlayer(CharacterStats player)//反向注册
    {
        playerStats = player;

        followCamera = FindObjectOfType<CinemachineFreeLook>();
        if(followCamera != null)
        {
            followCamera.Follow = playerStats.transform.GetChild(2);
            followCamera.LookAt = playerStats.transform.GetChild(2);
        }
    }

33: Save Data 保存数据

保存ScriptableObject:之前在CharacterSatas里创建了一个Template Data然后生成出来到Character Data,我们生成场景的时候也是生成人物,人物生成出来之后也拿到一份新的Character Data,然后我们将这个数据改成原来存储的,就实现了加载的方法。

创建代码SaveManager:

public class SaveManager : Singleton<SaveManager>
{
    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(this);
    }

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.S))
        {
            SavePlayerData();//保存Player数据
            Debug.Log("保存Player数据");
        }
        if(Input.GetKeyDown(KeyCode.L))
        {
            LoadPlayerData();//加载Player数据
            Debug.Log("加载Player数据");
        }
    }

    public void SavePlayerData()//保存Player数据
    {
        Save(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
    }
    public void LoadPlayerData()//加载Player数据
    {
        Load(GameManager.Instance.playerStats.characterData, GameManager.Instance.playerStats.characterData.name);
    }

    public void Save(object data,string key)//保存数据
    {
        var jsonData = JsonUtility.ToJson(data,true);
        PlayerPrefs.SetString(key, jsonData);
        PlayerPrefs.Save();
    }

    public void Load(object data, string key)//加载数据
    {
        if(PlayerPrefs.HasKey(key))
        {
            JsonUtility.FromJsonOverwrite(PlayerPrefs.GetString(key),data);
        }
    }
}

补充SceneController的传送协程

    IEnumerator Transition(string sceneName,TransitionDestination.DestinationTag destinationTag)//传送目标场景、目标点
    {
        //TODO:保存数据
        SaveManager.Instance.SavePlayerData();//保存Player数据

        if (SceneManager.GetActiveScene().name != sceneName)//不同场景传送
        {
            yield return SceneManager.LoadSceneAsync(sceneName);
            if(SceneManager.GetActiveScene().name != "Game")
            {
                yield return Instantiate(playerPrefab, GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
            }            
            //读取数据
            SaveManager.Instance.LoadPlayerData();//加载Player数据
            yield break;
        }
        else//同场景传送
        {
            player = GameManager.Instance.playerStats.gameObject;
            playerAgent = player.GetComponent<NavMeshAgent>();
            playerAgent.enabled = false;//传送时关闭NavMeshAgent
            player.transform.SetPositionAndRotation(GetDestination(destinationTag).transform.position, GetDestination(destinationTag).transform.rotation);
            playerAgent.enabled = true;//传送完开启NavMeshAgent
            yield return null;
        }
    }

34:Main Menu 制作主菜单

创建一个Main场景为主菜单,添加Text和3个Button ,Player组件只留下Animator,创建MainMenu脚本挂载在MenuCanvas上面

实现3个按钮的脚本:

SceneController:实现进入第一个场景

    public void TransitionToFirstLevel()
    {
        StartCoroutine(LoadLevel("Game"));
    }
    IEnumerator LoadLevel(string scene)
    {
        if(scene != "")
        {
            yield return SceneManager.LoadSceneAsync(scene);
            yield return player = Instantiate(playerPrefab,GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation);

            //保存数据
            SaveManager.Instance.SavePlayerData();
            yield break;
        }
    }

GameManager:找到入口点

    public Transform GetEntrance()
    {
        foreach(var entrance in FindObjectsOfType<TransitionDestination>())
        {
            if(entrance.destinationTag == TransitionDestination.DestinationTag.ENTER)
            {
                return entrance.transform;
            }
        }
        return null;
    }

MainMenu:实现NewGame()

using UnityEngine.UI;

public class MainMenu : MonoBehaviour
{
    Button newGameBtn;
    Button continueBtn;
    Button QuitBtn;

    private void Awake()
    {
        newGameBtn = transform.GetChild(1).GetComponent<Button>();
        continueBtn = transform.GetChild(2).GetComponent<Button>();
        QuitBtn = transform.GetChild(3).GetComponent<Button>();

        newGameBtn.onClick.AddListener(NewGame);
        continueBtn.onClick.AddListener(ContinueGame);
        QuitBtn.onClick.AddListener(QuitGame);
    }

    void NewGame()
    {
        PlayerPrefs.DeleteAll();
        //转换场景
        SceneController.Instance.TransitionToFirstLevel();
    }

    void ContinueGame()
    {
        //转换场景,读取进度

    }

    private void QuitGame()
    {
        //退出游戏
        Application.Quit();
        Debug.Log("退出游戏");
    }
}

实现Continue Game:

在SaveManager中保存一下Player所在的场景:

    string sceneName = "";//保存场景名字
    public string SceneName { get { return PlayerPrefs.GetString(sceneName); } }

    public void Save(object data,string key)//保存数据
    {
        var jsonData = JsonUtility.ToJson(data,true);//将Data写为jsonData
        PlayerPrefs.SetString(key, jsonData);//将jsonData写入key
        PlayerPrefs.SetString(sceneName,SceneManager.GetActiveScene().name);//保存场景名字
        PlayerPrefs.Save();
    }

SceneController:跳转到Player所在的场景

    public void TransitionToLoadGame()//Continue Game
    {
        StartCoroutine(LoadLevel(SaveManager.Instance.SceneName));
    }

PlayerController:拿到自己存储的数据

    private void OnEnable()//人物启用的时候注册订阅
    {
        GameManager.Instance.RigisterPlayer(characterStats);//注册GameManager
    }
    private void Start()
    {
        MouseManager.Instance.OnMouseClicked += MoveToTarget; //onMouseClicked事件添加注册 MoveToTarget()方法
        MouseManager.Instance.OnEnemyClicked += EventAttack;

        SaveManager.Instance.LoadPlayerData();//拿到自己存储的数据
    }

 SaveManager:

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Escape))//返回主场景
        {
            SceneController.Instance.TransitionToMain();
        }
        if(Input.GetKeyDown(KeyCode.S))
        {
            SavePlayerData();//保存Player数据
            Debug.Log("保存Player数据");
        }
        if(Input.GetKeyDown(KeyCode.L))
        {
            LoadPlayerData();//加载Player数据
            Debug.Log("加载Player数据");
        }
    }

SceneController:

    public void TransitionToMain()
    {
        StartCoroutine(LoadMain());
    }
    IEnumerator LoadMain()
    {
        yield return SceneManager.LoadSceneAsync("Main");
        yield break;
    }

MainMenu:

    void ContinueGame()
    {
        //转换场景,读取进度
        SceneController.Instance.TransitionToLoadGame();
    }

35:SceneFader 场景转换的渐入渐出

使用TimeLine实现场景的动画

 创建一个空物体

 

设置好动画

 MainMenu控制动画的播放:

using UnityEngine.UI;
using UnityEngine.Playables;

public class MainMenu : MonoBehaviour
{
    Button newGameBtn;

    PlayableDirector director;//TimeLine动画

    private void Awake()
    {
        //拿到3个按钮
        newGameBtn = transform.GetChild(1).GetComponent<Button>();
        //按下按钮触发的事件
        newGameBtn.onClick.AddListener(PlayTimeLine);

        director = FindObjectOfType<PlayableDirector>();

        director.stopped += NewGame;//当TimeLine动画播放完之后调用NewGame函数
    }

    void PlayTimeLine()
    {
        director.Play();
    }

    void NewGame(PlayableDirector obj)//参数用不到
    {
        PlayerPrefs.DeleteAll();
        //转换场景
        SceneController.Instance.TransitionToFirstLevel();
    }

}

 播放动画时关闭EventSystem

 添加淡入淡出:

创建Canvas:Fade Canvas,创建一个Image铺满画布,为画布添加Canvas Group组件

 创建脚本Scene Fader:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SceneFader : MonoBehaviour
{
    CanvasGroup canvasGroup;
    public float fadeInDuration;//渐入时间
    public float fadeOutDuration;//渐出时间
    private void Awake()
    {
        canvasGroup = GetComponent<CanvasGroup>();

        DontDestroyOnLoad(gameObject);//转化场景不销毁渐入渐出效果
    }

    public IEnumerator FadeOutIn()
    {
        yield return FadeOut(fadeOutDuration);
        yield return FadeIn(fadeInDuration);
    }

    public IEnumerator FadeOut(float time)
    {
        while(canvasGroup.alpha < 1)
        {
            canvasGroup.alpha += Time.deltaTime / time;
            yield return null;
        }
    }
    public IEnumerator FadeIn(float time)
    {
        while(canvasGroup.alpha != 0)
        {
            canvasGroup.alpha -= Time.deltaTime / time;
            yield return null;
        }
    }

}

 将Fade Canvas保存为预制体,

在SceneController里拿到预制体并调用函数Scene Fader方法:

    public SceneFader sceneFader;

    IEnumerator LoadLevel(string scene)
    {
        SceneFader fade = Instantiate(sceneFader);
            
        if(scene != "")
        {
            yield return StartCoroutine(fade.FadeOut(2.5f));//渐出场景
            yield return SceneManager.LoadSceneAsync(scene);
            yield return player = Instantiate(playerPrefab,GameManager.Instance.GetEntrance().position, GameManager.Instance.GetEntrance().rotation);

            //保存数据
            SaveManager.Instance.SavePlayerData();
            yield return StartCoroutine(fade.FadeIn(2.5f));//渐入场景
            yield break;
        }
    }

将预制体拖拽赋值到SceneController里

 现在就能实现场景渐出渐入了

可以将portal传送门的Collider关闭,防止转换场景时指针变成传送门图标

MouseManager:默认鼠标图标为指针

    void SetCursorTexture() //设置指针的贴图
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        if(Physics.Raycast(ray,out hitInfo))
        {
            //切换鼠标贴图
            switch(hitInfo.collider.gameObject.tag)
            {
                case "Ground":
                    Cursor.SetCursor(target, new Vector2(16, 16),CursorMode.Auto); //偏移(16,16)
                    break;
                case "Enemy":
                    Cursor.SetCursor(attack, new Vector2(16, 16), CursorMode.Auto); //偏移(16,16)
                    break;
                case "Portal":
                    Cursor.SetCursor(doorway, new Vector2(16, 16), CursorMode.Auto); //偏移(16,16)
                    break;
                default:
                    Cursor.SetCursor(arrow, new Vector2(16, 16), CursorMode.Auto); //偏移(16,16)
                    break;
            }
        }
    }

 SceneController:添加接口结束广播

public class SceneController : Singleton<SceneController>,IEndGameObserver//泛型单例模式//接口:接受广播
    


    public void EndNotify()
    {
        if(fadeFinshed)
        {
            fadeFinshed = false;
            StartCoroutine(LoadMain());
        }
    }

SceneFader:每次入后都删除Fade Canvas

    public IEnumerator FadeIn(float time)
    {
        while(canvasGroup.alpha != 0)
        {
            canvasGroup.alpha -= Time.deltaTime / time;
            yield return null;
        }

        Destroy(gameObject);
    }

36:Build & Run打包及运行

设置:

 Scripting  Backend设置为IL2CPP,好处如下:

 设置好图标:

 打包之前可以清除存档

 

 点击Build即可打包

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Unity3D RPG游戏框架是一种用于开发角色扮演游戏的一套工具和框架。它提供了一系列功能和组件,使开发者能够快速建立一个具有角色控制、任务系统、战斗系统等功能的游戏。 首先,Unity3D RPG游戏框架提供了强大的角色控制功能。开发者可以轻松地创建角色,并对其进行动画、物理和碰撞等控制。框架还提供了角色属性和状态管理的机制,使开发者能够定义和管理角色的生命值、能力值和状态等。 其次,Unity3D RPG游戏框架支持任务系统的开发。开发者可以创建各种类型的任务,如主线任务、支线任务和日常任务等,并为每个任务定义任务目标、奖励和任务进度等。框架还提供了任务的触发和完成的事件回调,使开发者能够灵活地控制任务的逻辑和流程。 此外,Unity3D RPG游戏框架还包括战斗系统的实现。开发者可以创建各种类型的敌人和怪物,并为其定义属性、技能和行为等。框架提供了各种战斗机制,如近战攻击、远程攻击和技能释放等。同时,框架还支持战斗AI的设计和开发,使敌人和怪物能够智能地进行战斗。 此外,Unity3D RPG游戏框架还提供了一些额外的功能和工具,如用户界面、物品系统和声音管理等。开发者可以使用这些功能来增强游戏的可玩性和趣味性。 总之,Unity3D RPG游戏框架是一个功能强大的工具和框架,它能够帮助开发者快速建立一个完整的RPG游戏。无论是开发者的经验水平还是游戏的规模,都可以借助这个框架来实现自己的创意和想法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值