【学习资料整理】XLua相关知识点

Xlua与C#相互调用,先简单分为C#调用lua代码和lua调用C#代码,由于之前项目用的华佗热更,Lua热更没有项目经验,这里只做一些浅显的个人理解,有错误还望大牛指正!!!

Xlua与C#相互调用

C#调用lua代码

1.在C#中编写lua语句

使用lua解析器编写

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

public class Lesson1_luaEnv : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //lua解析器对象
        LuaEnv env = new LuaEnv();
        //参数为字符串,执行lua语句
        env.DoString("print('nihao')");
        //手动清除没有释放的对象,相当于GC
        //env.Tick();

        //销毁lua解析器
        //env.Dispose();

        //执行lua脚本
        env.DoString("require('Main')");
    }


}

2.重定向文件

LuaEnv env = new LuaEnv();
env.AddLoader(MyCustomLoaderPath);
 private byte[] MyCustomLoaderPath(ref string filePath){//todo}

addloader是一个委托,添加一个路径函数到委托里面即可

3.编写lua管理器

思路:保证lua解析器的唯一性,并且提供一些api重定向文件,使解析器按照自定义路径访问lua脚本

/// <summary>
/// lua管理器
/// 提供 lua解析器
/// </summary>
public class LuaMgr :BaseManager<LuaMgr>
{
    //执行Lua语言的函数
    //释放垃圾
    //销毁
    //重定向
    private LuaEnv luaEnv;


    /// <summary>
    /// 得到Lua中的_G
    /// </summary>
    public LuaTable Global
    {
        get
        {
            return luaEnv.Global;
        }
    }

    public void Init()
    {
        //已经初始化了 别初始化 直接返回
        if (luaEnv != null)
            return;
        //初始化
        luaEnv = new LuaEnv();
        //加载lua脚本 重定向,先访问指定文件夹中是否有lua文件,再尝试指定AB包文件中是否有.txt文件,都没有再访问默认路径
        luaEnv.AddLoader(MyCustomLoader);
        luaEnv.AddLoader(MyCustomABLoader);
    }
    private byte[] MyCustomLoader(ref string filePath)
    {      
        //拼接lua文件所在的路径
        string path = Application.dataPath + "/LuaScripts/" + filePath + ".lua";  
        //如果对应的文件存在
        if (File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
    }

 private byte[] MyCustomABLoader(ref string filePath)
    {
        Debug.Log("进入AB包加载 重定向函数");
        //从AB包中加载lua文件
        //加载AB包
        string path = Application.streamingAssetsPath + "/lua";
        AssetBundle ab = AssetBundle.LoadFromFile(path);

        //加载Lua文件 返回
        TextAsset tx = ab.LoadAsset<TextAsset>(filePath + ".lua");
        //加载Lua文件 byte数组
        return tx.bytes;
        }

....///其他函数,包括执行某个lua脚本函数,释放垃圾,销毁解析器等,可参考在C#中编写lua语句自行编写

4.执行lua中的存储在_G中的变量和函数

由于存储在_G表中属于全局的,直接使用lua管理器中的Global属性获取对应的全局变量或者函数

LuaMgr.GetInstance().Init();
//执行某个lua脚本
LuaMgr.GetInstance().DoLuaFile("Test");
 //获取lua脚本中的变量值
var testnumber = LuaMgr.GetInstance().Global.Get<int>("testnumber");
print("testnumber:"+testnumber);

var testbool = LuaMgr.GetInstance().Global.Get<bool>("testbool");
print("testbool:" + testbool);
//这里使用set并不会更改对应lua文件中对应变量的值,可以简单理解为更改缓存的值
 LuaMgr.GetInstance().Global.Set("testnumber", 22);

对应的lua文件

//Test.lua
testnumber=1
testbool=true
testFloat=1.2
testString="123"
print(testnumber,testbool,testFloat,testString)

对于执行lua文件中对应的function,可以参考路径重定向,其实就是声明一个委托变量,然后通过Global.Get添加存储在_G表中对应lua function,只要参数类型返回值类型能够对应上就ok了,只要根据这个原理,其他类型的函数都可以完成调用

	[CSharpCallLua]
	public delegate void CustomCall();
	[CSharpCallLua]
    public delegate int CustomCall3(int a, out int b, out bool c, out string d, out int e);
    [CSharpCallLua]
    public delegate int CustomCall4(int a, ref int b, ref bool c, ref string d, ref int e);

    [CSharpCallLua]
    public delegate void CustomCall5(string a, params int[] args);//变长参数的类型 是根据实际情况来定的
  void Start()
    {
        LuaMgr.GetInstance().Init();
        LuaMgr.GetInstance().DoLuaFile("Main");   //Main执行require("Test")

       //也可以使用UnityAction Action等委托
		//甚至也可以使用Xlua提供的LuaFunction
        var call=LuaMgr.GetInstance().Global.Get<CustomCall>("testfun");
        call();

        //多返回值
        //使用 out 和 ref 来接收
        CustomCall3 call3 = LuaMgr.GetInstance().Global.Get<CustomCall3>("testfun3");
        int b;
        bool c;
        string d;
        int e;
        Debug.Log("第一个返回值:" + call3(100, out b, out c, out d, out e));
        Debug.Log(b + "_" + c + "_" + d + "_" + e);
        
		CustomCall4 call4 = LuaMgr.GetInstance().Global.Get<CustomCall4>("testfun3");
        int b1 = 0;
        bool c1 = true;
        string d1 = "";
        int e1 = 0;
        Debug.Log("第一个返回值:" + call4(200, ref b1, ref c1, ref d1, ref e1));
        Debug.Log(b1 + "_" + c1 + "_" + d1 + "_" + e1);
        //变长参数
        CustomCall5 call5 = LuaMgr.GetInstance().Global.Get<CustomCall5>("testfun4");
        call5("123", 1, 2, 3, 4, 5, 566, 7, 7, 8, 9, 99);
        }

对应的lua文件

//Test.lua
--无参无返回
testfun=function()
   print("无参无返回")
end


--多返回
testfun3=function(a)
   print("多返回参数")
   return a,a+1,true
end

--边长参数
testfun4=function(a,...)
   print("变长参数")
   print(a)
   arg={...}
   for K,v in pairs(arg) do
      print(v)
   end
end

5.C#调用lua中的"list",“Dictionary”

lua上的列表和字典本质上都是table,由于是动态语言,弱语言类型,所以使用限制很薄弱,对于我们清楚类型的lua table,我们在C#可以指定类型,对于无法确定指定类型的lua table,我们可以使用C#中的Object

void Start()
    {
        LuaMgr.GetInstance().Init();
        LuaMgr.GetInstance().DoLuaFile("Main"); //main.lua执行require("Test")


        //同一类型List
        List<int> list = LuaMgr.GetInstance().Global.Get<List<int>>("testList");
        Debug.Log("*******************List************************");
        for (int i = 0; i < list.Count; ++i)
        {
            Debug.Log(list[i]);
        }
        //不指定类型 object
        List<object> list3 = LuaMgr.GetInstance().Global.Get<List<object>>("testList2");
        Debug.Log("*******************List object************************");
        for (int i = 0; i < list3.Count; ++i)
        {
            Debug.Log(list3[i]);
        }
         Debug.Log("*******************Dictionary************************");
        Dictionary<string, int> dic = LuaMgr.GetInstance().Global.Get<Dictionary<string, int>>("testDic");
        foreach (string item in dic.Keys)
        {
            Debug.Log(item + "_" + dic[item]);
        }
        Debug.Log("*******************Dictionary object************************");
        Dictionary<object, object> dic3 = LuaMgr.GetInstance().Global.Get<Dictionary<object, object>>("testDic2");
        foreach (object item in dic3.Keys)
        {
            Debug.Log(item + "_" + dic3[item]);
        }
 }

对应的lua文件

//Test.lua
testList={1,2,3,4,5,6,7,8}
testList2={1,"1213",3,true,5,nil,7.2,8}

testDic={
["1"]=1,
["2"]=2,
["3"]=3
}

testDic2={
   ["1"]=1,
   [true]=2,
   [false]=true,
   ["123"]=false
}

5.类、接口、Xlua中的luatable映射lua中的table

1.类映射
声明一个类,成员对应lua中指定的table 需要注意的是,C#类成员变量名需和映射的lua文件对应table中成员变量名一致

C#类
//成员变量以及函数和lua脚本中对应数量可以不一致
[CSharpCallLua]
public class CsharpCallLua {
    public int testInt;
    public bool testBool;
    public float testFloat;
    public string testString;
    public UnityAction testFun;
    public CallluaInClass testInClass;

}

//Test.lua
testClass={
   testInt=2,
   testBool=true,
   testFloat=1.2,
   testString="123",

   testFun=function()
      print("121231")
   end,
   testInClass={
      testInInt=99,
   }
}

然后老套路,使用Global.Get实现映射

CsharpCallLua obj = new CsharpCallLua();
 obj = LuaMgr.GetInstance().Global.Get<CsharpCallLua>("testClass");

        Debug.Log(obj.testInt);
        Debug.Log(obj.testBool);
        Debug.Log(obj.testFloat);
        Debug.Log(obj.testString);
        Debug.Log(obj.testInClass.testInInt);
        obj.testFun();

接口映射和类映射相似,无非把成员变量改为成员属性即可
LuaTable直接使用Global属性get对应lua table直接映射

		LuaTable table = LuaMgr.GetInstance().Global.Get<LuaTable>("testClass");
        Debug.Log(table.Get<int>("testInt"));
        Debug.Log(table.Get<LuaFunction>("testFun").Call());
        table.Dispose();

lua调用C#代码

为了简介文章,以下所使用的C#代码都在同一个mono文件LuaCallCsharp中

1.lua使用C#类

想要在lua脚本中直接使用unity引擎相关的,使用规则为CS.命名空间.类名
例如CS.UnityEngine.GameObject为UnityEngine.GameObject;
CS.UnityEngine.Debug为UnityEngine.Debug;

//C#测试脚本
public class test1 { 

    public void Speak(string str)
    {
        Debug.Log("test1:" + str);
    }
}

namespace M
{
    public class test2
    {
        public void Speak(string str)
        {
            Debug.Log("test2:" + str);
        }

    }
}

public class LuaCallCsharp : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
       
    }

}

如果想在lua调用该Mono脚本中的test1或者test2类,使用他们的成员函数,我们首先需要动态生成一个预制体,然后挂载该脚本,最后引用命名空间类名使用对应类

--lua脚本
--类
--cs.命名空间.类名
GameObject=CS.UnityEngine.GameObject;
Debug=CS.UnityEngine.Debug;
Vector3=CS.UnityEngine.Vector3;


local obj1=GameObject();
local obj2=GameObject("测试物体");


--成员变量用. 成员方法用:
local obj4=GameObject.Find("测试物体")
Debug.Log(obj4.name);
Debug.Log(obj4.transform.position);
obj4.transform:Translate(Vector3.forward);
Debug.Log(obj4.transform.position)


local t=CS.test1()
t:Speak("说话")


local t2=CS.M.test2()
t2:Speak("吃饭")


local obj5=GameObject("LuaCallCsharp加脚本测试")
obj5:AddComponent(typeof(CS.LuaCallCsharp));

2.lua使用C# 枚举

在C#中定义枚举

public enum E_MyEnum
{
    Idle,
    Move,
    Atk
}

lua代码如下

--枚举
PrimitiveType=CS.UnityEngine.PrimitiveType
GameObject=CS.UnityEngine.GameObject
E_MyEnum=CS.E_MyEnum
--这里是使用GameObject中的静态方法生成一个立方体,所以用.而不是:调用
local obj1=GameObject.CreatePrimitive(PrimitiveType.Cube);




local c=E_MyEnum.Idle;
print(c)

--枚举转换,可以通过索引和枚举变量中的状态名进行转换
local a=E_MyEnum.__CastFrom(1)
print(a)


local b=E_MyEnum.__CastFrom("Atk")
print(b)

3.lua使用C# 数组 list 字典

在C#定义一些数组 list 字典等类型

public class Lesson3 {
    public int[] array = new int[5] {1,2,3,4,5 };
    public List<int> list = new List<int>();
    public Dictionary<int, string> dic = new Dictionary<int, string>();
}

lua文件中的知识点与前两课相似,无非就是新增一些lua api 不做解释

--数组 list dic
print("**************array*****************")
local obj =CS.Lesson3()

print(obj.array.Length)
--访问指定元素
print(obj.array[1])


--遍历
for i=0,obj.array.Length-1 do
    print(obj.array[i])
end

--在lua中创建一个数组
local array2=CS.System.Array.CreateInstance(typeof(CS.System.Int32),11)
print(array2.Length)
print(array2[0])


print("**************LIST*****************")
obj.list:Add(2)
obj.list:Add(4)
obj.list:Add(8)

--长度
print(obj.list.Count)
print(obj.list)
for i=0,obj.list.Count-1 do
    print(obj.list[i])
end

--在lua中创建list
--旧版
local list2=CS.System.Collections.Generic["List`1[System.String]"]()
print(list2)
list2:Add(2)
print(list2.Count)

--新版本 Xlua>V2.1.12
local List_string= CS.System.Collections.Generic.List(CS.System.String)
local list3=List_string()
list3:Add("5555555555")
print(list3[0])




print("**************Dictionary*****************")
obj.dic:Add(1,"你好")
print(obj.dic[1])


for k,V in pairs(obj.dic) do
    print(k,v)
end

local dic_Vector3=CS.System.Collections.Generic.Dictionary(CS.System.String,CS.UnityEngine.Vector3)
local dic2=dic_Vector3()
dic2:Add("121",CS.UnityEngine.Vector3.right)

for k,V in pairs(dic2) do
    print(k,v)
end 
--特殊
print(dic2:get_Item("121"));
dic2:set_Item("123",CS.UnityEngine.Vector3.zero)
print(dic2:get_Item("121"));

4.lua使用C# ref和out的方法

首先回顾之前所学知识,成员方法在lua中使用:调用,静态方法使用.调用,拓展方法也使用冒号调用,但是拓展方法所在的类需要加上特性[XLua.LuaCallCSharp],
在C#中 ref主要是引用 out则用于多放回参数
先声明对应的函数

public class lesson5
{
    public int RefFun(int a,ref int b ,ref int c,int d)
    {
        b = a + d;
        c = a - d;
        return 100;
    }

    public int OutFun(int a, out int b, out int c, int d)
    {
        b = a;
        c = d;
        return 200;
    }

    public int RefOutFun(int a,out int b,ref int c)
    {
        b = a * 10;
        c = a * 20;
        return 300;
    }

}

从上面可以简单看出,RefFun,OutFun,RefOutFun都可以最大接收3个返回参数,因为ref需要提前初始化变量,out则返回参数
对应lua文件和打印结果去下

--ref 需要对应的占位符
--out 不需要传参 也就是不需要传占位
local L5=CS.lesson5();

local a,b,c,d=L5:RefFun(1,2,3,4);
print(a,b,c,d);
--LUA: 100	5	-3	nil


local a,b,c,d=L5:OutFun(1,4);
print(a,b,c,d);
--LUA: 200	1	4	nil

local a,b,c,d=L5:RefOutFun(1,4);
print(a,b,c,d);
-- 300,10,20,nil
--LUA: 300	10	20	nil

5.lua使用C# 中的函数重载

我们知道,C#是强类型语言,lua是弱类型语言,那么两种语言在执行函数重载时有所不同,lua本身不支持函数重载,C#支持重载,但我们是使用lua文件去调用C# 所以应该符合的规则是C# 按照道理说应该支持重载 实际上有所不同,我们仔细思考一下,使用lua文件调用C#中已经写好的函数,那么lua需要传入实参,但由于lua是弱语言,他并不能区分int float double的区别,因此在C#中 如果我们声明两个重载是int 和float类型,那么在lua中传实参就会出现问题 如果是int和string类型,则不会,如果我们非要用int和float的重载,Xlua也提供了解决方法

--Xlua提供了反射机制去解决这种问题
local m1=typeof(Lesson6):GetMethod("Calc",{typeof(CS.System.Int32)})
local m2=typeof(Lesson6):GetMethod("Calc",{typeof(CS.System.Single)})


local f1=xlua.tofunction(m1)
local f2=xlua.tofunction(m2)
--第一个参数为对象,如果是静态方法,则省略
print(f2(obj,10.2))

6.lua使用C# 中的委托和事件

其实学到这里我们基本可以知道,lua调用C#的各种知识点无非就是两种语言使用相同类型的不同约束,例如使用:调用成员方法,而C#是直接.就可以使用,重载中强弱类型的使用限制等,在C#委托中,我们给委托添加函数的时候,可以使用+= 添加函数,对于只添加一个函数的委托,我们还可以使用=,而在lua中,并不支持复合运算符,也就是+=不可以被使用,所以在lua中向委托添加函数,则只能用=,如果后续想继续添加函数,则使用A=A+B的形式添加。
对于事件,事件在类外只能使用+=去添加函数不能使用=,这直接封死了所有可以添加函数的情况,Xlua给我们提供了解决方法,将在lua中事件的使用(本来是.调用)改成了类似于成员函数的调用,并且通过传参解决添加和删除函数的问题

对于委托和时间的清空,委托直接赋值nil解决,而事件则在C#类声明事件清空函数,在lua中通过调用成员方法清空事件

public class Lesson7
{
    public UnityAction del;
    public event UnityAction eventAction;

    public void DoEvent()
    {
        if (eventAction != null) eventAction();
    }


    public void ClearEvent()
    {
        if (eventAction != null) eventAction = null;
    }
}
--lua代码
local obj=CS.Lesson7()
--C#类中的委托主要是添加lua中的函数的

local fun=function()
    print("Lua函数fun")
end

--Lua没有复合运算符 不能+=
--第一个函数应该用C#委托中的=,往后的函数可以用A=A+B
obj.del=fun
obj.del=obj.del+fun
--也可以用类似lamda的写法
obj.del=obj.del+function()
   print("临时申明的函数")
end

obj.del()

print("************取消注册函数***********")
obj.del=obj.del-fun
obj.del=obj.del-fun
obj.del()
--清空所有存储的函数
obj.del=nil
print("*********清空所有注册函数**********")
--添加测试
obj.del=fun
obj.del()



-------------事件------------
print("*********事件**********")
local fun2=function()
    print("事件加的函数")
end

--需要把事件当成函数使用
--有点类似于成员方法的使用
obj:eventAction("+",fun2)
obj:eventAction("+",fun2)
obj:DoEvent()


--事件取消
print("*********事件取消**********")
obj:eventAction("-",fun2)
obj:DoEvent()

--事件清除
print("*********事件清除**********")
--清除事件不能直接设置为空
--原因在于C#中的事件在事件外不能直接清空
--可以在C#事件对应类里面添加方法清空事件
obj:ClearEvent();
obj:DoEvent();

7.lua使用C# 调用二维数组

使用数组的成员方法调用

public class lesson8 {

    public int[,] array = new int[2, 3] { {1,2,3 }, {9,9,9} };

}
print("***************lua调用C# 二维数组*************")

local obj=CS.lesson8()

--获取二维数组长度
print("行:"..obj.array:GetLength(0))
print("列:"..obj.array:GetLength(1))

--获取某一个元素,以下两种都不对,基于C#习惯可能会这样用
--但lua并不支持这两种方式访问数组
--print(obj.array[0,0])
--print(obj.array[0][0])


--使用C#数组提供的访问数组的成员方法访问
print(obj.array:GetValue(0,0))

--遍历二维数组
for i=0,obj.array:GetLength(0)-1 do 
    for j=0,obj.array:GetLength(1)-1 do
        print(obj.array:GetValue(i,j))
    end
end

8.lua中的nil 和C#中的Null比较

注释写的比较清楚,也不是很难,不做过多解释

print("***************lua调用C# C#中的Null和Lua中nil比较*************")
--需求:往场景上物体添加一个脚本,如果存在就不在,不存在脚本就加
GameObject=CS.UnityEngine.GameObject;
Rigidbody=CS.UnityEngine.Rigidbody;
lesson9=CS.lesson9;

local obj=GameObject("测试加刚体物体")
local rig=obj:GetComponent(typeof(Rigidbody));

--rig是一个C#的对象 无法与lua中的空 也就是nil比较
--如果要比较 有以下几种方法
--1.使用C#对象中的Equals(nil)比较
--rig:Equals(nil)
--2.声明一个全局方法,使得可以判空
--例如ISNull(obj)
--3.可以在C#声明一个判空的扩展方法 前提对象是继承Object类的

-- if rig:Equals(nil) then
--     print("需要加刚体")
--     obj:AddComponent(typeof(Rigidbody))
-- end


-- if ISNull(rig) then
--     print("需要加刚体")
--     obj:AddComponent(typeof(Rigidbody))
-- end


if rig:isNull() then
    print("需要加刚体")
    obj:AddComponent(typeof(Rigidbody))
end  

9.lua调用C#中的协程

注释写的也比较清楚,不做过多解释,这里只做一小部分解释,util=require(“util”)可以是Xlua中util.lua文件在当前lua脚本的同级目录下,即同个文件夹下,也可以是添加默认路径中的指定路径,即通过修改lua的package.path路径,在重定向文件路径搜索不到文件时,搜索默认的指定路径,这样就可以不用拖动XLua中util.lua的位置,这两种做法均可

print("*************lua 调用C#协程************")
--Xlua提供的工具表
util=require("util")

GameObject =CS.UnityEngine.GameObject
WaitForSeconds=CS.UnityEngine.WaitForSeconds

local obj=GameObject("Test Coroutine")
local mono =obj:AddComponent(typeof(CS.LuaCallCsharp))


--希望被开启的协程函数
fun=function()
  local a=1;
  while true do
    --不能直接使用C#中的yield return
    --使用lua中的协程返回
    coroutine.yield(WaitForSeconds(1))
    print(a)
    a=a+1
    if a>10 then
        mono:StopCoroutine(b)
    end
  end
end


--不能直接使用C#中的协程开启方法
-- mono:StartCoroutine(fun)
--使用Xlua提供的工具表
b=mono:StartCoroutine(util.cs_generator(fun))

10.lua调用C# 给系统变量加特性

当我们需要在lua中调用C#的某些事件,但该事件无法被添加[XLua.CSharpCallLua]特性,可以使用如下方法去添加特性

[XLua.CSharpCallLua]
  public static List<Type> lu = new List<Type>()
  {
      typeof(UnityAction<float>)
  };

11.lua调用C# 泛型

呼呼终于到最后一个了 注释写的也挺多的 直接看注释吧…

public class lesson12 { 
    public interface ITest { }

    public class father
    {

    }

    public class child : father, ITest { }


    public void TestFun1<T>(T a,T b)where T: father
    {
        Debug.Log("有参有约束的函数");
    }

    public void TestFun2<T>(T a)
    {
        Debug.Log("有参无约束的函数");
    }


    public void TestFun3<T>() where T: father
    {
        Debug.Log("无参有约束的函数");
    }


    public void TestFun4<T>(T a) where T : ITest
    {
        Debug.Log("有参有约束的函数,但参数不是类");
    }
}
print("*********泛型函数*********")
local lesson12=CS.lesson12;
local obj=lesson12();
local father=lesson12.father();
local child=lesson12.child();

--lua仅支持有参数有约束的泛型函数
obj:TestFun1(father,child);
obj:TestFun1(child,father);

-- --lua不支持没有约束的泛型函数
-- obj:TestFun2(child)
-- obj:TestFun2(father)

-- --lua不支持无参数有约束的函数
-- obj:TestFun3()

-- --lua不支持非类的约束
-- obj:TestFun4(child)


--用XLua提供的方法有一些约束
--如果打包是以Mono打包 则都可以使用
--如果打包是以il2cpp 引用类型都可以使用
--值类型 除非C#已经调用过同类型的泛型参数,否则无法使用


--Xlua提供了一些方法,让不支持的泛型支持
--1.得到通用函数
local testfun2=xlua.get_generic_method(CS.lesson12,"TestFun2")

--2.设置泛型类型再使用
local testfun2_T=testfun2(CS.System.Object)
--调用
testfun2_T(obj,child)

local testfun3=xlua.get_generic_method(CS.lesson12,"TestFun3")
local testfun3_T=testfun3(CS.lesson12.father)
testfun3_T(obj)

local testfun4=xlua.get_generic_method(CS.lesson12,"TestFun4")
local testfun4_T=testfun4(CS.lesson12.ITest)
testfun4_T(obj,child)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值