[3D游戏编程]打飞碟游戏 HIT UFO

游戏简介

在打飞碟游戏中,玩家可以通过点击飞出的飞碟得分,目标是争取获得更高的分。

游戏规则

  • 游戏有 n 个 round,每个 round 都包括10 次 trial。
  • 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
  • 每个 trial 的飞碟有随机性,总体难度随 round 上升。
  • 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。

项目实现要求

  • 必须使用对象池管理飞碟对象。
  • 建议使用 ScriptableObject 配置不同的飞碟
  • 建议使用物理引擎管理飞碟飞行路径。

项目实现效果

计分规则

加分项加分
飞碟为蓝色,大小为3加1分
飞碟为绿色,大小为2加2分
飞碟为红色,大小为1加3分
飞碟速度为x(都是偶数)原有基础上加x/2分

类图

截图及视频

游戏开始界面

运动学模式

物理模式 游戏结算页面

视频展示

HIT UFO

具体代码

SSDirector

public class SSDirector : System.Object
{
    // singlton instance
    private static SSDirector _instance;

    public MainController currentController { get; set; }


    // get instance anytime anywhare!
    public static SSDirector getInstance()
    {
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }

}

Singleton

这是为实现场景单实例写的Singleton单实例模板类

using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    protected static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = (T)FindObjectOfType(typeof(T));
                if (instance == null)
                {
                    Debug.LogError("An instance of " + typeof(T) +
                    " is needed in the scene, but there is none.");
                }
            }
            return instance;
        }
    }
}

DiskFactory

该类主要实现了飞碟的生成和回收,实现了对象池的任务,通过使用两个列表:已使用和未使用的飞碟,使得飞碟对象不需要反复生成和销毁。

using System.Collections.Generic;
using UnityEngine;

public class DiskFactory : MonoBehaviour
{
    private List<Disk> dirtyDisk; // 正在被使用的disk
    private List<Disk> freeDisk; // 没有被使用的disk
    // Start is called before the first frame update
    void Start()
    {
        dirtyDisk = new List<Disk>();
        freeDisk = new List<Disk>();
    }

    // 获取飞碟
    public GameObject GetDisk(Ruler ruler)
    {
        GameObject disk;

        // 如果freeDisk中有能用的就直接用,没有就新创建
        int diskCount = freeDisk.Count;
        if (diskCount == 0)
        {
            disk = GameObject.Instantiate(
                Resources.Load<GameObject>("Prefabs/Disk"), Vector3.zero, Quaternion.identity);
            disk.AddComponent(typeof(Disk));
        }
        else
        {
            disk = freeDisk[diskCount - 1].gameObject;
            freeDisk.Remove(freeDisk[diskCount - 1]);
        }

        // 设置速度、颜色、大小、飞入方向
        disk.GetComponent<Disk>().speed = ruler.speed;
        disk.GetComponent<Disk>().color = ruler.color;
        disk.GetComponent<Disk>().size = ruler.size;

        // 给飞碟上颜色
        if (ruler.color == "red")
        {
            disk.GetComponent<Renderer>().material.color = Color.red;
        }
        else if (ruler.color == "green")
        {
            disk.GetComponent<Renderer>().material.color = Color.green;
        }
        else
        {
            disk.GetComponent<Renderer>().material.color = Color.blue;
        }

        // 绘制飞碟大小
        disk.transform.localScale = new Vector3(ruler.size, 0.2f, ruler.size);

        // 选择飞碟飞入屏幕的起始位置
        disk.transform.position = ruler.beginPos;

        // 设置飞碟显示
        disk.SetActive(true);

        // 将飞碟加入使用队列
        dirtyDisk.Add(disk.GetComponent<Disk>());

        return disk;
    }

    // 飞碟回收方法,将不使用的飞碟从使用队列放到空闲队列中
    public void FreeDisk(GameObject disk)
    {
        foreach (Disk d in dirtyDisk)
        {
            if (d.gameObject.GetInstanceID() == disk.GetInstanceID())
            {
                disk.SetActive(false);
                dirtyDisk.Remove(d);
                freeDisk.Add(d);
                break;
            }

        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Disk

该类定义了飞碟所需要的参数。

using UnityEngine;

public class Disk : MonoBehaviour
{
    public int size; // 大小
    public string color; // 颜色
    public int speed; // 发射速度
}

ScoreRecorder

该类定义了加分规则。

public class ScoreRecorder
{
    public int score; // 游戏分数

    public ScoreRecorder()
    {
        score = 0;
    }

    // 记录分数,根据点击中的飞碟的大小,速度计算得分 
    public void Record(Disk disk)
    {
        // 飞碟大小与颜色相对应,最小为红色,然后黄色,最后蓝色
        // 颜色为红色得3分,颜色为黄色得2分,颜色为蓝色得1分
        int diskSize = disk.size;
        if(diskSize == 1)
        {
            score += 3;
        }
        else if(diskSize == 2)
        {
            score += 2;
        }
        else
        {
            score += 1;
        }
        // 加速度的分
        score += disk.speed / 2;

    }

    // 重置分数,设为0
    public void Reset()
    {
        score = 0;
    }
}

RoundController

该类确定了运动类型的使用,初始化基本参数(游戏进行多少round、每trail多少个飞碟),定义了飞碟的生成、释放。

该类中的LaunchDisk,是整个项目的核心算法,实现了对每个飞碟属性的设置。

该类的Update函数同样是整个项目的核心算法,实现了对发射时间间隔的控制、游戏是否结束的判断等等。

该类中有一个FreeAllFactoryDisk函数,是用于在切换运动类型时,将前一种类型的飞碟全部清除。这样做的目的是,两种运动类型的飞碟属性是不同的,所以不能让上一种类型的飞碟影响现在的游戏。

using UnityEngine;

public class RoundController : MonoBehaviour
{
    private IActionManager actionManager; // 选择飞碟的运动类型
    private ScoreRecorder scoreRecorder; // 记分器
    private MainController mainController;
    private Ruler ruler; // 飞碟获取规则

    void Start()
    {
        // 一开始飞碟的运动类型默认为运动学运动
        actionManager = gameObject.AddComponent<CCActionManager>();
        gameObject.AddComponent<PhysisActionManager>();
        scoreRecorder = new ScoreRecorder();
        mainController = SSDirector.getInstance().currentController;
        gameObject.AddComponent<DiskFactory>();
        InitRuler();
    }
    // 初始化ruler
    void InitRuler()
    {
        ruler.trialNum = 0;
        ruler.roundNum = 0;
        ruler.roundSum = 2;
        ruler.sendTime = 0;
        ruler.roundDisksNum = new int[10];
        generateRoundDisksNum();
    }

    // 生成每trial同时发出的飞碟数量的数组,同时发出飞碟个数不超过4
    public void generateRoundDisksNum()
    {
        for (int i = 0; i < 10; ++i)
        {
            ruler.roundDisksNum[i] = Random.Range(0, 4) + 1;
        }
    }

    public void Reset()
    {
        InitRuler();
        scoreRecorder.Reset();
    }

    public void Record(Disk disk)
    {
        scoreRecorder.Record(disk);
    }

    public int GetScores()
    {
        return scoreRecorder.score;
    }

    public void SetRoundSum(int roundSum)
    {
        ruler.roundSum = roundSum;
    }

    // 设置游戏模式,同时支持物理运动模式和动力学运动模式
    public void SetPlayDiskMode(bool isPhysis)
    {
        if (isPhysis)
        {
            actionManager = Singleton<PhysisActionManager>.Instance as IActionManager;
        }
        else
        {
            actionManager = Singleton<CCActionManager>.Instance as IActionManager;
        }
    }

    // 发射飞碟
    public void LaunchDisk()
    {
        // 使飞碟飞入位置尽可能分开,从不同位置飞入使用的数组
        int[] beginPosY = new int[5] { 0, 0, 0, 0, 0 };

        for (int i = 0; i < ruler.roundDisksNum[ruler.trialNum]; ++i)
        {
            int random; 
            // 如果是第一轮,速度为2-4;否则是2-6
            if (ruler.roundNum == 1)
                random = (Random.Range(0, 2) + 1) * 2;
            else
                random = (Random.Range(0, 3) + 1) * 2;
            ruler.speed = random;
            
            // 飞碟颜色与大小
            random = Random.Range(0, 3) + 1;
            ruler.size = random;
            if (random == 1)
            {
                ruler.color = "red";
            }
            else if (random == 2)
            {
                ruler.color = "green";
            }
            else
            {
                ruler.color = "blue";
            }

            // 飞碟飞入的方向
            random = Random.Range(0, 2);
            if (random == 1)
            {
                ruler.direction = new Vector3(3, 0.3f, 0);
            }
            else
            {
                ruler.direction = new Vector3(-3, 0.3f, 0);
            }

            // 确定不同飞碟的飞入位置
            do
            {
                random = Random.Range(0, 4) + 1;
            } while (beginPosY[random] != 0);
            beginPosY[random] = 1;
            ruler.beginPos = new Vector3(-ruler.direction.x * 4, 1.5f * random, 0);

            // 根据ruler从工厂中生成一个飞碟
            GameObject disk = Singleton<DiskFactory>.Instance.GetDisk(ruler);

            // 设置飞碟的飞行动作
            actionManager.PlayDisk(disk, ruler.speed, ruler.direction);
        }
    }

    // 释放工厂飞碟
    public void FreeFactoryDisk(GameObject disk)
    {
        Singleton<DiskFactory>.Instance.FreeDisk(disk);
    }

    // 释放所有工厂飞碟
    public void FreeAllFactoryDisk()
    {
        GameObject[] obj = FindObjectsOfType(typeof(GameObject)) as GameObject[];
        foreach (GameObject g in obj)
        {
            if (g.name == "Disk(Clone)")
            {
                Singleton<DiskFactory>.Instance.FreeDisk(g);
            }
        }
    }

    void Update()
    {
        if (mainController.GetGameState() == 1)
        {
            ruler.sendTime += Time.deltaTime;
            // 每隔2s发送一次飞碟(trial)
            if (ruler.sendTime > 2)
            {
                ruler.sendTime = 0;
                // 当次数trial等于0时,说明进入下一个回合,回合加一
                if (ruler.trialNum == 0)
                    ruler.roundNum++;
                // 如果未到设定回合数
                if (ruler.roundNum <= ruler.roundSum)
                {
                    // 发射飞碟,次数trial增加
                    mainController.SetUserInterfaceTip("");
                    LaunchDisk();
                    ruler.trialNum++;
                    // 当次数trial等于10时,说明一个回合已经结束,重新生成飞碟数组
                    if (ruler.trialNum == 10)
                    {
                        ruler.trialNum = 0;
                        generateRoundDisksNum();
                    }
                }
                // 否则游戏结束,提示重新进行游戏
                else
                {
                    mainController.SetUserInterfaceTip("Click Restart and Play Again!");
                    mainController.SetGameState(2);
                }
                // 设置回合数和trial数目的提示
                if(ruler.roundNum > ruler.roundSum)
                    mainController.SetUserInterfaceRoundNum(ruler.roundSum);
                else
                    mainController.SetUserInterfaceRoundNum(ruler.roundNum);
                mainController.SetUserInterfaceTrialNum(ruler.trialNum);
            }
        }
    }
}

IActionManager

提供了PlayDisk的接口方法,分别由CCActionManager和PhysisActionManager实现。

using UnityEngine;

public interface IActionManager
{
    void PlayDisk(GameObject disk, float speed, Vector3 direction);
}

ISSActionCallback

飞碟运动完成后的回调接口,当飞碟飞行完成之后,就会调用这个接口里的SSActionEvent方法,完成对飞碟对象的销毁,该方法分别由CCActionManager和PhysisActionManager实现。

using UnityEngine;

public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    // 回调函数
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null);
}

CCActionManager和PhysisActionManager

提供了PlayDisk方法、SSActionEvent方法的实现。

using UnityEngine;

public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    CCPlayDiskAction PlayDiskAction; // 飞碟空中动作

    public void PlayDisk(GameObject disk, float speed, Vector3 direction)
    {
        PlayDiskAction = CCPlayDiskAction.GetSSAction(direction, speed);
        RunAction(disk, PlayDiskAction, this);
    }

    // 回调函数
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Competed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        // 结束飞行后回收飞碟
        Singleton<RoundController>.Instance.FreeFactoryDisk(source.gameObject);
    }
}
using UnityEngine;

public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager
{
    PhysisPlayDiskAction PlayDiskAction; // 飞碟空中动作

    public void PlayDisk(GameObject disk, float speed, Vector3 direction)
    {
        PlayDiskAction = PhysisPlayDiskAction.GetSSAction(direction, speed);
        RunAction(disk, PlayDiskAction, this);
    }

    // 回调函数
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Competed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        // 结束飞行后回收飞碟
        Singleton<RoundController>.Instance.FreeFactoryDisk(source.gameObject);
    }
}

SSActionManager

是CCActionManager和PhysisActionManager的基类。

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class SSActionManager : MonoBehaviour
{

    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingAdd = new List<SSAction>();
    private List<int> waitingDelete = new List<int>();

    // Update is called once per frame
    protected void Update()
    {
        foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
        waitingAdd.Clear();

        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID()); // release action
            }
            else if (ac.enable)
            {
                ac.Update(); // update action
            }
        }

        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Object.Destroy(ac);
        }
        waitingDelete.Clear();
    }

    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

SSAction

是CCPlayDiskAction和PhysisPlayDiskAction的基类。

using UnityEngine;

public class SSAction : ScriptableObject
{
    public bool enable = true; 
    public bool destroy = false; 

    public GameObject gameObject { get; set; } // 附着游戏对象
    public Transform transform { get; set; } // 游戏对象的的运动
    public ISSActionCallback callback { get; set; } // 回调函数

    protected SSAction() {}

    // Use this for initialization
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }

    // Update is called once per frame
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

CCPlayDiskAction和PhysisPlayDiskAction

实现了运动学类型和物理类型动作的实现,重写了Start和Update。

using UnityEngine;

public class CCPlayDiskAction : SSAction
{
    float gravity; // 垂直速度
    float speed; // 水平速度
    Vector3 direction;  // 方向
    float time; // 时间

    public static CCPlayDiskAction GetSSAction(Vector3 direction, float speed)
    {
        CCPlayDiskAction action = ScriptableObject.CreateInstance<CCPlayDiskAction>();
        action.gravity = 9.8f;
        action.time = 0;
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        gameObject.GetComponent<Rigidbody>().isKinematic = true;
    }

    public override void Update()
    {
        time += Time.deltaTime;
        transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
        transform.Translate(direction * speed * Time.deltaTime);
        // 飞碟到达底部动作结束,回调
        if (this.transform.position.y < -5)
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
    }
}

using UnityEngine;

public class PhysisPlayDiskAction : SSAction
{
    float speed; // 水平速度
    Vector3 direction; // 飞行方向

    public static PhysisPlayDiskAction GetSSAction(Vector3 direction, float speed)
    {
        PhysisPlayDiskAction action = ScriptableObject.CreateInstance<PhysisPlayDiskAction>();
        action.speed = speed;
        action.direction = direction;
        return action;
    }

    public override void Start()
    {
        gameObject.GetComponent<Rigidbody>().isKinematic = false;
        // 水平初速度
        gameObject.GetComponent<Rigidbody>().velocity = speed * direction;
    }

    public override void Update()
    {
        // 飞碟到达底部动作结束,回调
        if (this.transform.position.y < -5)
        {
            this.destroy = true;
            this.enable = false;
            this.callback.SSActionEvent(this);
        }
    }
}

MainController

该类负责控制RoundController和UserInterface,每当RoundController监测到游戏事件发生,MainController就将状态改变传入UserInterface,使得游戏界面改变。

using UnityEngine;
using UnityEngine.SocialPlatforms.Impl;

public class MainController : MonoBehaviour
{
    private RoundController roundController; // 回合控制器
    private UserInterface userInterface; 
    private int gameRound; // 游戏回合
    private int gameState; // 0为游戏准备开始,1为游戏正在进行,2为游戏结束

    void Start()
    {
        SSDirector.getInstance().currentController = this;
        roundController = gameObject.AddComponent<RoundController>();
        userInterface = gameObject.AddComponent<UserInterface>();
        gameState = 0;
        gameRound = 2;
    }
    // 必要的Get函数
    public int GetGameRound()
    {
        return gameRound;
    }
    public int GetGameState()
    {
        return gameState;
    }
    // 必要的Set函数
    public void SetGameState(int state)
    {
        gameState = state;
    }
    public void SetRoundSum(int roundSum)
    {
        if (roundSum < 2)
            return;
        gameRound = roundSum;
        roundController.SetRoundSum(roundSum);
    }
    // 与其他类相关的Get、Set函数
    public void SetPlayDiskMode(bool isPhysis)
    {
        roundController.SetPlayDiskMode(isPhysis);
    }
    public void SetUserInterfaceTip(string tip)
    {
        userInterface.SetTip(tip);
    }
    
    public void SetUserInterfaceScore(int score)
    {
        userInterface.SetScore(score);
    }

    public void SetUserInterfaceRoundNum(int round)
    {
        userInterface.SetRoundNum(round);
    }
    public void SetUserInterfaceTrialNum(int trial)
    {
        userInterface.SetTrialNum(trial);
    }

    public int GetScore()
    {
        return roundController.GetScores();
    }
    public void Restart()
    {
        userInterface.Init();
        roundController.Reset();
    }
    public void Hit(Vector3 position)
    {
        //Debug.Log("PRESS");
        Camera camera = Camera.main;
        Ray ray = camera.ScreenPointToRay(position);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit))
        {
            if (hit.collider.gameObject.GetComponent<Disk>() != null)
            {
                //把击中的飞碟移出屏幕,触发回调释放
                hit.collider.gameObject.transform.position = new Vector3(0, -6, 0);
                // 记录飞碟得分
                roundController.Record(hit.collider.gameObject.GetComponent<Disk>());
                // 显示当前得分
                userInterface.SetScore(roundController.GetScores());
            }
        }
    }

    // 释放所有工厂飞碟
    public void FreeAllFactoryDisk()
    {
        roundController.FreeAllFactoryDisk();
    }

    void Update()
    {
        if(gameState == 1)
        {
            if (Input.GetButtonDown("Fire1"))
            {
                Debug.Log("PRESS");
                Hit(Input.mousePosition);
                userInterface.SetScore(GetScore());
            }
        }
    }
}

UserInterface

该类用于显示游戏界面,包括按钮、标题等等。

using UnityEngine;
using System.Collections;

public class UserInterface : MonoBehaviour
{
    private MainController mainController;
    private int score;
    private string tip;
    private string roundNum;
    private string trialNum;

    void Start()
    {
        Init();
        mainController = SSDirector.getInstance().currentController;
    }
    // 一些必要的set函数
    public void SetTip(string tip)
    {
        this.tip = tip;
    }

    public void SetScore(int score)
    {
        this.score = score;
    }

    public void SetRoundNum(int round)
    {
        roundNum = "回合: " + round;
    }

    public void SetTrialNum(int trial)
    {
        if (trial == 0) trial = 10;
        trialNum = "Trial: " + trial;
    }

    public void Init()
    {
        score = 0;
        tip = "";
        roundNum = "";
        trialNum = "";
    }

    public void AddTitle()
    {
        GUIStyle titleStyle = new GUIStyle();
        titleStyle.normal.textColor = Color.black;
        titleStyle.fontSize = 50;

        GUI.Label(new Rect(Screen.width / 2 - 80, 20, 60, 100), "Hit UFO", titleStyle);
    }

    public void ShowHomePage()
    {
        GUIStyle labelStyle = new GUIStyle();
        labelStyle.normal.textColor = Color.black;
        labelStyle.fontSize = 30;
        GUI.Label(new Rect(450, 250, 100, 50), "回合: " + mainController.GetGameRound(), labelStyle);
        if (GUI.Button(new Rect(420, 150, 160, 80), "开始游戏\n(默认为2回合)"))
        {
            mainController.Restart();
            mainController.SetGameState(1);
        }
        if (GUI.Button(new Rect(420, 253, 30, 30), "+"))
        {
            Debug.Log(mainController.GetGameRound());
            mainController.SetRoundSum(mainController.GetGameRound() + 1);
        }
        if(GUI.Button(new Rect(542, 253, 30, 30), "-"))
        {
            Debug.Log(mainController.GetGameRound());
            mainController.SetRoundSum(mainController.GetGameRound() - 1);
        }

    }

    public void ShowGamePage()
    {
        // 游戏信息显示
        GUIStyle labelStyle = new GUIStyle();
        labelStyle.normal.textColor = Color.black;
        labelStyle.fontSize = 30;

        GUI.Label(new Rect(750, 10, 100, 50), "得分: " + score, labelStyle);
        GUI.Label(new Rect(310, 170, 50, 200), tip, labelStyle);
        GUI.Label(new Rect(750, 60, 100, 50), roundNum, labelStyle);
        GUI.Label(new Rect(750, 110, 100, 50), trialNum, labelStyle);

        // 退出游戏
        if (GUI.Button(new Rect(10, 10, 100, 50), "退出游戏"))
        {
            mainController.FreeAllFactoryDisk();
            mainController.Restart();
            mainController.SetGameState(0);
        }

        // 选择模式
        if (GUI.Button(new Rect(10, Screen.height - 115, 110, 50), "运动学模式"))
        {
            mainController.FreeAllFactoryDisk();
            mainController.SetPlayDiskMode(false);
            mainController.Restart();
            mainController.SetGameState(1);
        }
        if (GUI.Button(new Rect(10, Screen.height - 55, 110, 50), "物理模式"))
        {
            mainController.FreeAllFactoryDisk();
            mainController.SetPlayDiskMode(true);
            mainController.Restart();
            mainController.SetGameState(1);
        }
    }

    public void ShowRestart()
    {
        ShowGamePage();
        if (GUI.Button(new Rect(440, 230, 100, 60), "Restart"))
        {
            mainController.FreeAllFactoryDisk();
            mainController.Restart();
            mainController.SetGameState(1);
        }
    }
    public void Show()
    {
        // 游戏开始界面
        if (mainController.GetGameState() == 0)
        {
            ShowHomePage();
        }
        else if (mainController.GetGameState() == 1)
        {
            ShowGamePage();
        }
        else
        {
            ShowRestart();
        }
    }
    void OnGUI()
    {
        AddTitle();
        Show();
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值