[Unity官方教程]Tanks!单机双人坦克大战源码和素材

这是Unity官方案例Tanks的素材,读者可以自行取阅。
链接:https://pan.baidu.com/s/1PSAZeT5zQOQJXNxzP9qs1A
提取码:a57u

前言

本篇文章是我在观看了官方教程后写的脚本,相较于官方的更为详细,方便你们拿来直接查看引用。而且是适用于新版本的Unity。我这一版本的Unity是Version 2019.2.9f1 Persional

建议多阅读观看官网的文档和教程,一方面他们的代码更加的规范,另一方面游戏开发的思想要更加的好。一个很明显的区别在于,官方的代码,每个模块之间相互独立,各司其职缺一不可。但又不会在一个脚本里塞入过多的内容显得冗杂多余,整体上层次分明,架构清楚。各个类之间高内聚,低耦合。比国内所谓的一些学院教的要好得多,解释的也更加的清楚。

官方中英机翻视频教程点这里。个人建议不要开倍速一点点看完,适应一下老师的语速,而不要知其然不知其所以然。那么,废话不多说,直接上代码。英语水平有限,注释能看懂就行,还望见谅。

脚本

GameManager类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    // Properties
    public int m_NumRoundsToWin = 5; // number of rounds per start
    public float m_StartDelay = 3f; // time delay ecah start
    public float m_EndDelay = 3f; // time delay each end

    private int m_RoundNumber; // round in which you are


    // Reference
    public CameraContorller m_CameraContorl;
    public Text m_MessageText;
    public GameObject m_TankPrefab;
    public TankManager[] m_Tanks;

    private WaitForSeconds m_StartWait;
    private WaitForSeconds m_EndWait;
    private TankManager m_RoundWinner;
    private TankManager m_GameWinner;

    private void Start()
    {
        m_StartWait = new WaitForSeconds(m_StartDelay);
        m_EndWait = new WaitForSeconds(m_EndDelay);

        SpawnAllTanks();
        SetCameraTargets();

        StartCoroutine(GameLoop());
    }

    private void SpawnAllTanks()
    {
        // Spawn all the tanks for player, set their numbers from 1 to 2, and then call the function SetUp() to draw color for tanks.

        for (int i = 0; i <m_Tanks.Length; ++i)
        {
            m_Tanks[i].m_Instance =
                Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position, m_Tanks[i].m_SpawnPoint.rotation);
            m_Tanks[i].m_PlayerNumber = i + 1;
            m_Tanks[i].SetUp();
        }
    }

    private void SetCameraTargets()
    {
        // Assign all the tank's transform to the GameraControl script.

        Transform[] targets = new Transform[m_Tanks.Length];

        for (int i = 0; i < targets.Length; ++i)
        {
            targets[i] = m_Tanks[i].m_Instance.transform;
        }

        m_CameraContorl.m_Targets = targets;
    }

    private IEnumerator GameLoop()
    {
        yield return StartCoroutine(RoundStarting());
        yield return StartCoroutine(RoundPlaying());
        yield return StartCoroutine(RoundEnding());

        if (m_GameWinner != null)
        {
                SceneManager.LoadScene(0);
        }
        else
        {
            StartCoroutine(GameLoop());
        }
    }

    private IEnumerator RoundStarting()
    {
        // When the game firstly starts or back to the scene(0) again, we should reset all tanks.
        // And then, we disable all tanks' control so that the player couldn't contorl them.
        // At the same time, we set the camera to the right position and size that calculated by the function in the CameraControl script.
        // Finally, we show the text "Round" and we wait for a while so that it won't flash by.

        ResetAllTanks();
        DisableTankControl();

        m_CameraContorl.SetStartPositionAndSize();

        m_RoundNumber++; // plus 1 to the round count

        m_MessageText.text = "ROUND " + m_RoundNumber;

        yield return m_StartWait;
    }

    private IEnumerator RoundPlaying()
    {
        // Ok now, we need to give the palyers control of their tank, or they will be angry.
        // So, we enable all tanks' control, and change the text to null.
        // You don't expect a big line blocking the palyers' view.
        // When two players fight out a winner, we can return the funcion.
        // Of course, their is nothing we could return.

        EnableTankControl();

        m_MessageText.text = string.Empty;

        while (!OneTankLeft())
        {
            yield return null;
        }
    }

    private IEnumerator RoundEnding()
    {
        // A winner came out. Winner whould like to run around for celebrating, but we don't think is a wise move.
        // So we disable tanks' control again, and we set winner for this round to null.
        // Because it still stores the reference of the winner of the last round.
        // And then, we need to find the reference of the winner of this round and we judge wether he is the game winner.
        // Again we show some "text" and wait for a while, then we get into the next round or start game again.

        DisableTankControl();

        m_RoundWinner = null;

        m_RoundWinner = GetRoundWinner();

        if (m_RoundWinner != null)
            m_RoundWinner.m_Wins++;

        m_GameWinner = GetGameWinner();

        string message = EndMessage();
        m_MessageText.text = message;

        yield return m_EndWait;
    }

    private bool OneTankLeft()
    {
        // Determine if any tanks are dead.

        int numTanksLeft = 0;

        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            if (m_Tanks[i].m_Instance.activeSelf)
                numTanksLeft++;
        }

        return numTanksLeft <= 1;
    }

    private TankManager GetRoundWinner()
    {
        // Go through all the tanks, if find a TankManager who's Instance is active, then return it, or else return null.

        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            if (m_Tanks[i].m_Instance.activeSelf)
                return m_Tanks[i];
        }

        return null;
    }

    private TankManager GetGameWinner()
    {
        // Go through all the tanks, if find a TankManager who's m_Wins is equal to 5, then return it, or else return null.

        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            if (m_Tanks[i].m_Wins == m_NumRoundsToWin)
                return m_Tanks[i];
        }

        return null;
    }

    private string EndMessage()
    {
        string message = "DARW!";

        if (m_RoundWinner != null)
            message = m_RoundWinner.m_ColoredPlayerText + " WINS THE ROUND!";

        message += "\n\n\n\n";

        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            message += m_Tanks[i].m_ColoredPlayerText + ": " + m_Tanks[i].m_Wins + " WINS\n";
        }

        if (m_GameWinner != null)
            message = m_GameWinner.m_ColoredPlayerText + " WIN THE GAME!";

        return message;
    }

    private void ResetAllTanks()
    {
        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            m_Tanks[i].Reset();
        }
    }

    private void DisableTankControl()
    {
        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            m_Tanks[i].DisableTankControl();
        }
    }

    private void EnableTankControl()
    {
        for (int i = 0; i < m_Tanks.Length; ++i)
        {
            m_Tanks[i].EnableTankControl();
        }
    }
}

TankManager类
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class TankManager
{
    // Properties
    [HideInInspector] public int m_PlayerNumber;
    [HideInInspector] public int m_Wins;
    [HideInInspector] public string m_ColoredPlayerText;

    // Reference
    public Color m_PlayerColor;
    public Transform m_SpawnPoint;

    private TankMovement m_Movement;
    private TankShooting m_Shooting;
    private GameObject m_CanvasGameObject;

    [HideInInspector] public GameObject m_Instance;
 
    public void SetUp()
    {
        m_Movement = m_Instance.GetComponent<TankMovement>();
        m_Shooting = m_Instance.GetComponent<TankShooting>();
        m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas>().gameObject;

        m_Movement.m_PlayerNumber = m_PlayerNumber;
        m_Shooting.m_PlayerNumber = m_PlayerNumber;

        m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER " + m_PlayerNumber + "</color>";

        MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer>();

        for (int i = 0; i < renderers.Length; ++i)
        {
            renderers[i].material.color = m_PlayerColor;
        }
    }

    public void DisableTankControl()
    {
        // Turn off the script and turn off the canvas.

        m_Movement.enabled = false;
        m_Shooting.enabled = false;

        m_CanvasGameObject.SetActive(false);
    }

    public void EnableTankControl()
    {
        // Turn on the script and turn on the canvas.

        m_Movement.enabled = true;
        m_Shooting.enabled = true;

        m_CanvasGameObject.SetActive(true);
    }

    public void Reset()
    {
        // Set the Instance back to it's spawn point.

        m_Instance.transform.position = m_SpawnPoint.position;
        m_Instance.transform.rotation = m_SpawnPoint.rotation;

        // all of the tank's apart from the winner will be off, and we need to reset all of them
        // so we need to turn everything off first befor we can turn it back on again
        m_Instance.SetActive(false);
        m_Instance.SetActive(true);
    }
}
TankMovement类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TankMovement : MonoBehaviour
{
    // Properties
    public int m_PlayerNumber = 1;
    public float m_MoveSpeed = 12f;
    public float m_TurnSpeed = 180f;
    public float m_PitchRange = 0.2f;

    private float m_MovementInputValue;
    private float m_TurnInputValue;
    private float m_OriginalPitch;
    private string m_MovementAxisName;
    private string m_TurnAxisName;
    

    // Reference
    public AudioSource m_MovementAudio;
    public AudioClip m_EngineIdling;
    public AudioClip m_EngineDriving;

    private Rigidbody m_RigidBody;

    private void Awake()
    {
        m_RigidBody = GetComponent<Rigidbody>();
    }

    private void OnEnable()
    {
        // This function is called when this script is turned on.

        m_RigidBody.isKinematic = false;

        // Initialize inputvalue
        m_MovementInputValue = 0f;
        m_TurnInputValue = 0f;
    }

    private void OnDisable()
    {
        m_RigidBody.isKinematic = true;
    }

    private void Start()
    {
        // Get diffrent axisname input name for diffrent player.

        m_MovementAxisName = "Vertical" + m_PlayerNumber;
        m_TurnAxisName = "Horizontal" + m_PlayerNumber;

        // For now the originalPith's value equal to 1
        m_OriginalPitch = m_MovementAudio.pitch;
    }

    private void Update()
    {
        // Store the Player's input and make sure the audio for the engine is playing.

        m_MovementInputValue = Input.GetAxis(m_MovementAxisName);
        m_TurnInputValue = Input.GetAxis(m_TurnAxisName);

        EngineAudio();
    }

    private void EngineAudio()
    {
        // Play the correct audio clip based on whether or not the tank is moving and what audio is currently playing.

        if (Mathf.Abs(m_MovementInputValue) < 0.1f && Mathf.Abs(m_TurnInputValue) < 0.1f)
        {
            if (m_MovementAudio.clip == m_EngineDriving)
            {
                m_MovementAudio.clip = m_EngineIdling; // change the clip

                float lowPitch = m_OriginalPitch - m_PitchRange;
                float highPitch = m_OriginalPitch + m_PitchRange;
                m_MovementAudio.pitch = Random.Range(lowPitch, highPitch); // set the pitch between 0.8 and 1.2

                m_MovementAudio.Play();
            }
        }
        else
        {
            if (m_MovementAudio.clip == m_EngineIdling)
            {
                m_MovementAudio.clip = m_EngineDriving;
                
                float lowPitch = m_OriginalPitch - m_PitchRange;
                float highPitch = m_OriginalPitch + m_PitchRange;
                m_MovementAudio.pitch = Random.Range(lowPitch, highPitch); // set the pitch between 0.8 and 1.2

                m_MovementAudio.Play();
            }
        }
    }

    private void FixedUpdate()
    {
        // MOve ang turn the tank.

        Move();
        Turn();
    }

    private void Move()
    {
        // Adjust the position of the tank based on the player's input.

        Vector3 movement = transform.forward * m_MovementInputValue * m_MoveSpeed * Time.deltaTime;

        m_RigidBody.MovePosition(m_RigidBody.position + movement);
    }

    private void Turn()
    {
        // Adjust the position of the tank based on the player's input.

        float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;

        Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f);
        m_RigidBody.MoveRotation(m_RigidBody.rotation * turnRotation);
    }
}

CameraControl类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraContorller : MonoBehaviour
{
    // Properties
    public float m_DampTime = 0.2f; // time that the camera reach to the target will take
    public float m_ScreenEdgeBuff = 4f;
    public float m_MinSize = 6.5f;

    private float m_ZoomSpeed;


    // Reference
    [HideInInspector] public Transform[] m_Targets;

    private Camera m_Camera;
    private Vector3 m_MoveVelocity;
    private Vector3 m_DesiredPosition; // the position we want camera reach to

    private void Awake()
    {
        m_Camera = GetComponentInChildren<Camera>();
    }

    private void FixedUpdate()
    {
        Move();
        Zoom();
    }

    private void Move()
    {
        // Calculate the desiredposition.

        FindAveragePosition();

        // smooth the CamearRig's follow motion
        transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime);
    }

    private void FindAveragePosition()
    {
        // Get the average position among all the tanks and the let the CameraRig reach to the position.

        Vector3 averagePos = new Vector3();
        int numTargets = 0;

        // check if the target' gameObject is active, wo don't need to zoom in on a deactivated tank
        for (int i = 0; i < m_Targets.Length; ++i)
        {
            
            if (!m_Targets[i].gameObject.activeSelf)
                continue;

            averagePos += m_Targets[i].position; // plus all tanks' position and count the total of active tank
            ++numTargets;
        }

        if (numTargets > 0)
            averagePos /= numTargets;

        averagePos.y = transform.position.y; // Keep the CameraRig's height and Camera's height at a level

        m_DesiredPosition = averagePos;
    }

    private void Zoom()
    {
        // Get the size and make Camera size reach to it smoothly.

        float requiredSize = FindRequiredSize();
        m_Camera.orthographicSize = Mathf.SmoothDamp(m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime);
    }

    private float FindRequiredSize()
    {
        // Calculate the size.

        Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition);

        float size = 0f;

        /* Go through all the tanks, find out all the size it could be.
        Pick the largest one, then the tanks are all definitely going to be on the screen. */
        for (int i = 0; i < m_Targets.Length; ++i)
        {
            if (!m_Targets[i].gameObject.activeSelf)
                continue;

            Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position);
            Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos;

            /* For the Camera view, when tank is at Y axix, the Camera size is equal to 
            absolute Y value of the Vector of the position of the tank, and itequal
            to X value / Camera.aspect while tank is at X axis.
            Finally we could find the furthest value.  */
            size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.y));
            size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.x) / m_Camera.aspect);
        }

        size += m_ScreenEdgeBuff;
        size = Mathf.Max(size, m_MinSize); // set minimum size so that it won't zoom in so much

        return size;
    }

    public void SetStartPositionAndSize()
    {
        // Set the scene to the right size and right position at every round.

        FindAveragePosition();
        transform.position = m_DesiredPosition;
        m_Camera.orthographicSize = FindRequiredSize();
    }
}

TankShooting类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TankShooting : MonoBehaviour
{
    // Properties
    public int m_PlayerNumber = 1;
    public float m_MinLaunchForce = 15f;
    public float m_MaxLaunchForce = 30f;
    public float m_MaxChargeTime = 0.75f;

    private float m_CurrentLaunchForce;
    private float m_ChargeSpeed;
    private bool m_Fired; // record wether the player shoot the shell or not
    private string m_FireButtom;


    // Reference
    public Rigidbody m_Shell;
    public Transform m_FireTransform;
    public Slider m_AimSlider;
    public AudioSource m_ShootingAudio;
    public AudioClip m_ChargingClip;
    public AudioClip m_FireClip;

    private void OnEnable()
    {
        m_CurrentLaunchForce = m_MinLaunchForce;
        m_AimSlider.value = m_MinLaunchForce;
    }

    private void Start()
    {
        m_FireButtom = "Fire" + m_PlayerNumber;

        m_ChargeSpeed = (m_MaxLaunchForce - m_MinLaunchForce) / m_MaxChargeTime;
    }

    private void Update()
    {
        // Track the current state of the fire button and make decision based on the current launch force.

        m_AimSlider.value = m_MinLaunchForce;

        if (m_CurrentLaunchForce >= m_MaxLaunchForce && !m_Fired)
        {
            // at max charge, not yet fired
            m_CurrentLaunchForce = m_MaxLaunchForce;
            Fire();
        }
        else if (Input.GetButtonDown(m_FireButtom))
        {
            // once we pressed fire buttom for the first time
            m_Fired = false;
            m_CurrentLaunchForce = m_MinLaunchForce;

            m_ShootingAudio.clip = m_ChargingClip;
            m_ShootingAudio.Play();
        }
        else if (Input.GetButton(m_FireButtom) && !m_Fired)
        {
            // Holding the fire button, not yet fired
            m_CurrentLaunchForce += m_ChargeSpeed * Time.deltaTime;

            m_AimSlider.value = m_CurrentLaunchForce;
        }
        else if (Input.GetButtonUp(m_FireButtom) && !m_Fired)
        {
            // we relased the button, having not fired yet
            Fire();
        }
    }

    private void Fire()
    {
        // Instantiate and launch the shell.

        m_Fired = true;

        Rigidbody shellInstance = Instantiate(m_Shell, m_FireTransform.position, m_FireTransform.rotation) as Rigidbody;

        shellInstance.velocity = m_CurrentLaunchForce * m_FireTransform.forward;

        m_ShootingAudio.clip = m_FireClip;
        m_ShootingAudio.Play();

        m_CurrentLaunchForce = m_MinLaunchForce; // safety catch
    }
}

TankHealth类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TankHealth : MonoBehaviour
{
    // Properties
    public float m_StartingHealth = 100f;

    private float m_CurrentHealth;
    private bool m_Dead;


    // Reference
    public Slider m_Slider;
    public Image m_FillImage;
    public Color m_FullHealthColor = Color.green;
    public Color m_ZeroHealthColor = Color.red;
    public GameObject m_ExplosionPrefab;

    private AudioSource m_ExplosionAudio;
    private ParticleSystem m_ExplosionParticles;

    private void Awake()
    {
        // We can get the reference of ParticleSystem by creating an ExplosionPrefab instance.

        m_ExplosionParticles = Instantiate(m_ExplosionPrefab).GetComponent<ParticleSystem>();
        m_ExplosionAudio = m_ExplosionParticles.GetComponent<AudioSource>();

        /* To avoid that always keep the particle system int the memory, keep them in the scene,
        we put it straight in to the game as soon as it starts, we swithc it off straight away
        and we assign our references for when we need them. */
        m_ExplosionParticles.gameObject.SetActive(false);
    }

    private void OnEnable()
    {
        m_CurrentHealth = m_StartingHealth;
        m_Dead = false;

        SetHealthUI();
    }

    public void TankDamage(float amount)
    {
        // Adjust the tank's current healt, update the UI based on the new health and check whether or not the tank is dead.

        m_CurrentHealth -= amount;

        SetHealthUI();

        if (m_CurrentHealth <= 0f && !m_Dead)
        {
            OnDeath();
        }
    }

    private void SetHealthUI()
    {
        // Adjust the value and colour of the slider, to make sure it looks appropriate.

        m_Slider.value = m_CurrentHealth;

        m_FillImage.color = Color.Lerp(m_ZeroHealthColor, m_FullHealthColor, m_CurrentHealth / m_StartingHealth);
    }

    private void OnDeath()
    {
        // Play the effect for the death of the tank and deactive it.

        m_Dead = true;

        // move the particle system to the place where the playe died and turn on it
        m_ExplosionParticles.transform.position = transform.position;
        m_ExplosionParticles.gameObject.SetActive(true);

        m_ExplosionParticles.Play();

        m_ExplosionAudio.Play();

        gameObject.SetActive(false);
    }
}

ShellExplosion类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShellExplosion : MonoBehaviour
{
    //Properties
    public float m_MaxDamage = 100f;
    public float m_ExplosionForce = 1000f;
    public float m_MaxLifeTime = 2f;
    public float m_ExplosionRadius = 5f;


    // Reference
    public LayerMask m_TankMask;
    public ParticleSystem m_ExplosionParticles;
    public AudioSource m_ExplosionAudio;

    private void Start()
    {
        Destroy(gameObject, m_MaxLifeTime);
    }

    private void OnTriggerEnter(Collider other)
    {
        // Find all the tanks in an area around the shell and damage them.

        Collider[] colliders = Physics.OverlapSphere(transform.position, m_ExplosionRadius, m_TankMask);

        for (int i = 0; i < colliders.Length; i++)
        {
            Rigidbody targetRigidbody = colliders[i].GetComponent<Rigidbody>();

            if (!targetRigidbody)
                continue;

            targetRigidbody.AddExplosionForce(m_ExplosionForce, transform.position, m_ExplosionRadius);

            // addressing a particular instance of TankHealth script so we can talk to it and reduce health with it
            TankHealth targetHealth = targetRigidbody.GetComponent<TankHealth>();

            if (!targetHealth)
                continue;

            float damage = CalculateDamage(targetRigidbody.position);

            targetHealth.TankDamage(damage);
        }

        /* When the shell is destoryed, its child is destoryed either, either is ParticleSystem;
        So we need to unparent them, so that they could still play even if the shell is destoyed.
        After that, they will explode and ducouple themsevles. */
        m_ExplosionParticles.transform.parent = null;

        m_ExplosionParticles.Play();

        m_ExplosionAudio.Play();

        Destroy(m_ExplosionParticles.gameObject, m_ExplosionParticles.main.duration);
        Destroy(gameObject); // remove the shell as well
    }

    private float CalculateDamage(Vector3 targetPosition)
    {
        // Calculate the amout of damage a target should takes based on it's position.

        Vector3 explosionToTarget = targetPosition - transform.position;

        float explosionDistance = explosionToTarget.magnitude;

        float relativeDistance = (m_ExplosionRadius - explosionDistance) / m_ExplosionRadius;

        float damage = relativeDistance * m_MaxDamage;

        /* The damage starting off at 1 in the center going down to 0 at the edge and then they'll be
        negative beyond the edge. We don't want that, so we choose it from 0 and negative damage. */
        damage = Mathf.Max(0f, damage);

        return damage;
    }
}

UIDirectionController类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIDirectionContorller : MonoBehaviour
{
    // Properties
    public bool m_UseRelativeRotation = true;


    // Reference
    private Quaternion m_RelativeRotation;

    private void Start()
    {
        m_RelativeRotation = transform.parent.localRotation;
    }

    private void Update()
    {
        if (!m_UseRelativeRotation)
            transform.rotation = m_RelativeRotation;
    }
}

一定要自己写一遍哦~~~

  • 0
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值