Unity_建造系统及保存加载

这篇具有很好参考价值的文章主要介绍了Unity_建造系统及保存加载。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Unity_建造系统及保存加载

主要是尝试类似RTS的建造系统, 以及通过序列化/反序列化保存和加载游戏数据, 测试版本Unity2020.3


建造物体

移动

生成使用透明材质的的模型到场景, 通过射线检测获取鼠标位置并使模型位置跟随移动

private Camera mCamera;//场景相机
public GameObject PrefabModel;//预制体模型
private GameObject createModel;//用于创建的场景模型

void Start()
{
	//获取场景相机
	GameObject gameObject=GameObject.Find("Main Camera");
	mCamera=gameObject.GetComponent<Camera>();

	createModel = Instantiate(PrefabModel);//生成模型到场景
}

void Update()
{
	/*从摄像机向鼠标方向发射射线,获取鼠标位置*/
	Ray ray = mCamera.ScreenPointToRay(Input.mousePosition);
	RaycastHit hit;
	LayerMask mask = 1 << (LayerMask.NameToLayer("Floor"));//遮罩图层,使射线只检测Floor图层

	if (Physics.Raycast(ray, out hit,Mathf.Infinity,mask.value))
	{
		Debug.DrawLine(ray.origin, hit.point,Color.green);//在编辑器视图中绘制射线
		createModel.transform.position = hit.point;//使模型位置跟随鼠标坐标
	}
}

预制体模型的一些处理

测试时发现默认情况下的预制体模型实例化到鼠标位置后会弹起来, 检查发现模型轴点位置是在碰撞体积内部的, 怀疑是产生了和地板模型的碰撞导致了,所以调整抬高了一点碰撞体积, 让模型轴点在碰撞体积以外, 再进行生成测试就正常了, 如果生成有问题可以参考:

Unity_建造系统及保存加载

旋转

用鼠标滚轮控制模型旋转角度

void Update()
{
	if (Input.GetAxis("Mouse ScrollWheel")!=0)//鼠标滚轮
	{
		//旋转模型角度
		newcreateModel.transform.Rotate(0,Input.GetAxis("Mouse ScrollWheel") *Time.deltaTime*20000,0,Space.World);//Space.World为使用全局坐标系
	}
}

设置材质

固定模型到场景中后把模型材质从透明恢复为正常材质

public Material AlbedoMat;//正常材质

...

createModel.GetComponent<MeshRenderer>().sharedMaterial = AlbedoMat;//设置模型为正常材质

碰撞/重叠检测

给预制体模型添加Collider碰撞体和Rigidbody刚体组件并使用触发器来检测是否与其他已经固定的模型有重叠导致穿模, 如果有重叠则不能固定到场景并把模型材质变红提示

[参考的]Unity碰撞和触发

Unity_建造系统及保存加载

//触发开始时执行一次
public void OnTriggerEnter(Collider collider){
	Debug.Log("与其他模型有重叠,不可建造");
}

//触发过程中一直重复执行
public void OnTriggerStay(Collider collider){
	Debug.Log("与其他模型有重叠,不可建造");
}

//触发结束时执行一次
public void OnTriggerExit(Collider collider){
	Debug.Log("无重叠,可以建造");
}

数据保存/加载

查资料时都管这个叫序列化和反序列化, 听起来好高大上的样子, Unity自带了JsonUtility用于处理 json数据(JsonUtility文档)

测试用到的主要是获取场景上所有已固定的建筑模型的类型、位置和角度并保存为json文件, 在重新加载场景后可以加载保存的数据并根据数据恢复保存时的状态

保存数据

/*用于保存数据的类*/
[Serializable]//应用可序列化属性
public class BuildStateSave
{
	public List<string> modelType = new List<string>();//模型类型
	public List<double> posX = new List<double>();//X位置
	public List<double> posY = new List<double>();//Y位置
	public List<double> posZ = new List<double>();//Z位置
	public List<double> rotX = new List<double>();//X方向
	public List<double> rotY = new List<double>();//Y方向
	public List<double> rotZ = new List<double>();//Z方向
	
	/*把数据转换为json字符串*/
	public string SaveToString()
	{
		return JsonUtility.ToJson(this,true);//[prettyPrint]为true时返回的字符串保持可读格式,为false时大小最小
	}
}

/*保存按钮*/
public void SaveSence()
{
	GameObject[] buildCubes =GameObject.FindGameObjectsWithTag("Build");//获取所有已建造对象
	
	BuildStateSave buildState = new BuildStateSave();
	
	for (int i = 0; i < buildCubes.Length; i++)
	{
		buildState.modelType.Add(buildCubes[i].name);
		buildState.posX.Add(buildCubes[i].transform.position.x);
		buildState.posY.Add(buildCubes[i].transform.position.y);
		buildState.posZ.Add(buildCubes[i].transform.position.z);
		buildState.rotX.Add(buildCubes[i].transform.rotation.eulerAngles.x);
		buildState.rotY.Add(buildCubes[i].transform.rotation.eulerAngles.y);
		buildState.rotZ.Add(buildCubes[i].transform.rotation.eulerAngles.z);
	}
	
	string filePath = Application.streamingAssetsPath + "/BuildSave.json";

	if (File.Exists(filePath))//判断文件是否存在
	{
		File.Delete(filePath);//删除这个文件
	}

	//找到当前路径
	FileInfo file = new FileInfo(filePath);

	//判断有没有文件,有则打开文件,没有则创建后打开
	StreamWriter sw = file.CreateText();
	
	//数据转换为字符串并写入文件
	sw.WriteLine(buildState.SaveToString());
	
	sw.Close();//关闭文件
	sw.Dispose();
}

执行保存后路径下的json文件中可以看到保存的信息数据

Unity_建造系统及保存加载

加载数据

/*用于加载数据的类*/
[System.Serializable]//应用可序列化属性
public class BuildStateLoad
{
	public List<string> modelType = new List<string>();//模型类型
	public List<double> posX = new List<double>();//X位置
	public List<double> posY = new List<double>();//Y位置
	public List<double> posZ = new List<double>();//Z位置
	public List<double> rotX = new List<double>();//X方向
	public List<double> rotY = new List<double>();//Y方向
	public List<double> rotZ = new List<double>();//Z方向

	/*把json字符串转换为变量数据*/
	public BuildStateLoad CreateFromJSON(string jsonString)
	{
		return JsonUtility.FromJson<BuildStateLoad>(jsonString);
	}
}

/*加载按钮*/
public void LoadSence()
{
	/*从json文件读取数据*/
	FileStream fs = new FileStream(Application.streamingAssetsPath + "/BuildSave.json", FileMode.Open, FileAccess.Read, FileShare.None);
	StreamReader sr = new StreamReader(fs, System.Text.Encoding.Default);
	if (null == sr) return;
	string str = sr.ReadToEnd();
	sr.Close();

	/*字符串数据反序列化到变量中*/
	BuildStateLoad buildInfo = new BuildStateLoad();
	buildInfo=buildInfo.CreateFromJSON(str);

	for (int i = 0; i < buildInfo.modelType.Count; i++)
	{
		/*生成模型到场景*/
		switch (buildInfo.modelType[i])
		{
			case "Model1":
				createModel = Instantiate(PrefabModel1);
				break;
			case "Model2":
				createModel = Instantiate(PrefabModel2);
				break;
		}
		
		/*重命名*/
		createModel.name = buildInfo.modelType[i];
		
		/*位置*/
		createModel.transform.position = new Vector3((float)buildInfo.posX[i],(float)buildInfo.posY[i],(float)buildInfo.posZ[i]);
		
		/*旋转*/
		createModel.transform.rotation = Quaternion.Euler(new Vector3((float)buildInfo.rotX[i],(float)buildInfo.rotY[i],(float)buildInfo.rotZ[i]));
		
		/*固定到场景*/
		createModel.GetComponent<BoxCollider>().isTrigger=false;//关闭触发器恢复碰撞
		createModel.GetComponent<Rigidbody>().useGravity=true;//恢复刚体组件的重力
	}
}

执行加载后获取保存的数据

Unity_建造系统及保存加载


完整代码

主要逻辑脚本, 挂载在一个场景空对象上

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

public class BuildTest : MonoBehaviour
{
    private Camera mCamera;//场景相机

    public GameObject boxPrefabModel;//预制体模型
    public GameObject flak88PrefabModel;//预制体模型
    
    private GameObject newcreateModel;//用于创建的场景模型
    private GameObject loadcreateModel;//用于加载的场景模型
    
    public Material transparentMat;//透明材质
    public Material unInitMat;//阻挡材质
    
    public Material boxAlbedoMat;//Box正常材质
    public Material flak88AlbedoMat;//88炮正常材质

    private bool iSBuilding = false;//是否为建造模式
    public bool unInitPrefab = false;//是否重叠穿模

    private string selectBuildType;//记录当前选择建造的物体类型
    
    void Start()
    {
        /*获取相机*/
        GameObject gameObject=GameObject.Find("Main Camera");
        mCamera=gameObject.GetComponent<Camera>();
    }
    
    void Update()
    {
        if (iSBuilding)
        {
            /*从摄像机向鼠标方向发射射线,获取鼠标位置*/
            Ray ray = mCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            LayerMask mask = 1 << (LayerMask.NameToLayer("Floor"));
            
            /*Raycast:
             [1]射线方向
             [2]碰撞到的对象信息
             [3]射线长度,Mathf.Infinity为无限远
             [4]遮罩图层,不检测其他图层的物体
             */
            if (Physics.Raycast(ray, out hit,Mathf.Infinity,mask.value))
            {
                Debug.DrawLine(ray.origin, hit.point,Color.green);//在编辑器视图中绘制射线
                
                /*模型位置跟随鼠标坐标*/
                newcreateModel.transform.position = hit.point;

                if (unInitPrefab)//若位置与已生成物体重叠,修改显示材质
                {
                    /*设置模型材质为不可生成状态*/
                    MeshRenderer[] Mat = newcreateModel.GetComponentsInChildren<MeshRenderer>();//获取当前对象及所有子对象上的材质组件并更换材质
                    for (int i = 0; i < Mat.Length; i++)
                    {
                        Mat[i].sharedMaterial = unInitMat;
                    }
                }
                else
                {
                    /*设置模型材质为透明*/
                    MeshRenderer[] Mat = newcreateModel.GetComponentsInChildren<MeshRenderer>();//获取当前对象及所有子对象上的材质组件并更换材质
                    for (int i = 0; i < Mat.Length; i++)
                    {
                        Mat[i].sharedMaterial = transparentMat;
                    }
                    
                    if (Input.GetMouseButtonDown(0))//点击鼠标左键
                    {
                        /*固定对象到场景中*/
                        newcreateModel.GetComponent<BoxCollider>().isTrigger=false;//关闭触发器恢复碰撞
                        newcreateModel.GetComponent<Rigidbody>().useGravity=true;//恢复刚体组件的重力
                        //newcreateModel.layer = 20;//设置图层用于接收爆炸力影响

                        switch (selectBuildType)
                        {
                            case "Box":
                                newcreateModel.GetComponent<MeshRenderer>().sharedMaterial = boxAlbedoMat;//设置旧模型为正常材质
                                newcreateModel = Instantiate(boxPrefabModel);//生成新模型到场景
                                break;
                            case "Flak88":
                                /*设置旧模型为正常材质*/
                                for (int i = 0; i < Mat.Length; i++)
                                {
                                    Mat[i].sharedMaterial = flak88AlbedoMat;
                                }
                                
                                newcreateModel = Instantiate(flak88PrefabModel);//生成新模型到场景
                                break;
                        }
                        
                        newcreateModel.name = selectBuildType;//重命名对象
                    }
                }
                
                if (Input.GetAxis("Mouse ScrollWheel")!=0)//滑动鼠标滚轮
                {
                    //旋转模型角度
                    newcreateModel.transform.Rotate(0,Input.GetAxis("Mouse ScrollWheel") *Time.deltaTime*20000,0,Space.World);//加上Space.World为基于全局坐标系变换
                }
            }
        }

        //按下鼠标右键退出建造模式
        if (Input.GetMouseButtonDown(1))
        {
            iSBuilding = false;
            SetCursor(true);
            Destroy(newcreateModel);
            
            /*退出建造模式时如果是重叠状态会导致再次进入建造模式时也是重叠状态,这里在退出时强制取消重叠状态*/
            if (unInitPrefab)
            {
                unInitPrefab = false;
            }
        }
    }
    
    /// <summary>
    /// 设置鼠标状态
    /// </summary>
    /// <param name="">true显示,false隐藏</param>
    public void SetCursor(bool _cursorState)
    {
        //隐藏鼠标
        Cursor.visible = _cursorState;
    }

    /*进入Box建造模式按钮*/
    public void StartBoxBuild()
    {
        selectBuildType = "Box";
        iSBuilding = true;
        SetCursor(false);
        
        /*生成模型到场景*/
        newcreateModel = Instantiate(boxPrefabModel);
        
        /*设置模型材质为透明*/
        newcreateModel.GetComponent<MeshRenderer>().sharedMaterial = transparentMat;
        
        /*重命名对象便于区分*/
        newcreateModel.name = selectBuildType;
    }
    
    /*进入Flak88建造模式按钮*/
    public void StartFlak88Build()
    {
        selectBuildType = "Flak88";
        iSBuilding = true;
        SetCursor(false);
        
        /*生成模型到场景*/
        newcreateModel = Instantiate(flak88PrefabModel);
        
        /*设置模型材质为透明*/
        MeshRenderer[] Mat = newcreateModel.GetComponentsInChildren<MeshRenderer>();

        for (int i = 0; i < Mat.Length; i++)
        {
            Mat[i].sharedMaterial = transparentMat;
        }
        
        /*重命名*/
        newcreateModel.name = selectBuildType;
    }
    
    /*保存按钮*/
    public void ClickSaveGame()
    {
        GameObject[] buildCubes =GameObject.FindGameObjectsWithTag("Build");//寻找对象
        
        BuildStateSave buildState = new BuildStateSave();
        
        for (int i = 0; i < buildCubes.Length; i++)
        {
            buildState.modelType.Add(buildCubes[i].name);
            buildState.posX.Add(buildCubes[i].transform.position.x);
            buildState.posY.Add(buildCubes[i].transform.position.y);
            buildState.posZ.Add(buildCubes[i].transform.position.z);
            buildState.rotX.Add(buildCubes[i].transform.rotation.eulerAngles.x);
            buildState.rotY.Add(buildCubes[i].transform.rotation.eulerAngles.y);
            buildState.rotZ.Add(buildCubes[i].transform.rotation.eulerAngles.z);
        }
        
        string filePath = Application.streamingAssetsPath + "/BuildSave.json";

        if (File.Exists(filePath))//判断文件是否存在
        {
            File.Delete(filePath);//删除这个文件
        }

        //找到当前路径
        FileInfo file = new FileInfo(filePath);

        //判断有没有文件,有则打开文件,没有则创建后打开
        StreamWriter sw = file.CreateText();
        
        //数据转换为字符串并写入文件
        sw.WriteLine(buildState.SaveToString());
        
        sw.Close();//关闭文件
        sw.Dispose();
    }

    /*重新加载场景按钮*/
    public void ReLoadGame()
    {
        Application.LoadLevel(Application.loadedLevel);//重置场景
    }
    
    /*加载按钮*/
    public void ClickLoadGame()
    {
        /*从json文件读取数据*/
        FileStream fs = new FileStream(Application.streamingAssetsPath + "/BuildSave.json", FileMode.Open, FileAccess.Read, FileShare.None);
        StreamReader sr = new StreamReader(fs, System.Text.Encoding.Default);
        if (null == sr) return;
        string str = sr.ReadToEnd();
        sr.Close();

        /*字符串数据反序列化到变量中*/
        BuildStateLoad buildInfo = new BuildStateLoad();
        buildInfo=buildInfo.CreateFromJSON(str);

        for (int i = 0; i < buildInfo.modelType.Count; i++)
        {
            /*生成模型到场景*/
            switch (buildInfo.modelType[i])
            {
                case "Box":
                    loadcreateModel = Instantiate(boxPrefabModel);
                    break;
                case "Flak88":
                    loadcreateModel = Instantiate(flak88PrefabModel);
                    break;
            }
            
            /*重命名*/
            loadcreateModel.name = buildInfo.modelType[i];
            
            /*位置*/
            loadcreateModel.transform.position = new Vector3((float)buildInfo.posX[i],(float)buildInfo.posY[i],(float)buildInfo.posZ[i]);
            
            /*旋转*/
            loadcreateModel.transform.rotation = Quaternion.Euler(new Vector3((float)buildInfo.rotX[i],(float)buildInfo.rotY[i],(float)buildInfo.rotZ[i]));
            
            /*固定到场景*/
            loadcreateModel.GetComponent<BoxCollider>().isTrigger=false;//关闭触发器恢复碰撞
            loadcreateModel.GetComponent<Rigidbody>().useGravity=true;//恢复刚体组件的重力
        }
    }

    /*用于保存数据的类*/
    [Serializable]//应用可序列化属性,不受支持的字段以及私有字段、静态字段和应用了NonSerialized属性的字段会被忽略
    public class BuildStateSave
    {
        /*
         泛型集合List<T>
         大小可按需动态增加,不会强行对值类型进行装箱和拆箱,或对引用类型进行向下强制类型转换,性能提高
         */
        public List<string> modelType = new List<string>();//模型类型
        public List<double> posX = new List<double>();//X位置
        public List<double> posY = new List<double>();//Y位置
        public List<double> posZ = new List<double>();//Z位置
        public List<double> rotX = new List<double>();//X方向
        public List<double> rotY = new List<double>();//Y方向
        public List<double> rotZ = new List<double>();//Z方向
        
        /*把列表数据转换为json字符串*/
        public string SaveToString()
        {
            return JsonUtility.ToJson(this,true);//[prettyPrint]为true时保持可读格式,为false时最小大小
        }
    }
    
    /*用于加载数据的类*/
    [System.Serializable]//应用可序列化属性,不受支持的字段以及私有字段、静态字段和应用了NonSerialized属性的字段会被忽略
    public class BuildStateLoad
    {
        public List<string> modelType = new List<string>();//模型类型
        public List<double> posX = new List<double>();//X位置
        public List<double> posY = new List<double>();//Y位置
        public List<double> posZ = new List<double>();//Z位置
        public List<double> rotX = new List<double>();//X方向
        public List<double> rotY = new List<double>();//Y方向
        public List<double> rotZ = new List<double>();//Z方向

        /*把json字符串转换为变量数据*/
        public BuildStateLoad CreateFromJSON(string jsonString)
        {
            return JsonUtility.FromJson<BuildStateLoad>(jsonString);
        }
    }
}

用于碰撞触发检测的脚本, 挂载在预制体模型上文章来源地址https://www.toymoban.com/news/detail-443906.html

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

public class 碰撞检测 : MonoBehaviour
{
    private BuildTest testScript;
    
    private void Start()
    {
        /*获取脚本对象*/
        GameObject gameObject=GameObject.Find("脚本挂载");
        testScript = gameObject.GetComponent<BuildTest>();
    }

    //触发开始时执行一次
    public void OnTriggerEnter(Collider collider){
        testScript.unInitPrefab = true;
    }
 
    //触发过程中一直重复执行
    public void OnTriggerStay(Collider collider){
        testScript.unInitPrefab = true;
    }
 
    //触发结束时执行一次
    public void OnTriggerExit(Collider collider){
        testScript.unInitPrefab = false;
    }
}

到了这里,关于Unity_建造系统及保存加载的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【unity实战】实现类似英雄联盟的buff系统

    参考原视频链接 【视频】:https://www.bilibili.com/video/BV1Xy4y1N7Cb 注意 :本文为学习笔记记录,推荐支持原作者,去看原视频自己手敲代码理解更加深入 当今大多数游戏都拥有一些形式的Buff系统,利用这种系统可以增强或削弱游戏角色的特定属性。在Unity中,我们可以使用脚本

    2024年02月05日
    浏览(47)
  • 类似微信预览缩放保存插件previewImage.js

    很好用的预览缩放保存插件。 插件源码如下:     (function(){         let insideObject = {             // \\\'inputId\\\' : \\\'\\\'    //  用于页面多图片预览时绑定相应预览图片             \\\'inputName\\\':\\\'plantIconPath\\\',  // 用于表单提交             \\\'imageId\\\':\\\'preview_img\\\',    // 用于预览图

    2024年02月11日
    浏览(43)
  • 基于threejs加载大型BIM模型的优化尝试

    轻量化引擎,该合并的合并,该共享的共享,材质光影等等效果都很难再提升的时候,我们总不能转到隔壁的去渲染技术栈去吧? 最近几个月,陆陆续续做了很多的尝试,先把这些方案的思路记录下来,欢迎大佬给予点评,如果这里有坑,请偷偷告知我一声,避免踩雷,就当

    2024年01月21日
    浏览(65)
  • locust快速入门--使用locust-plugins保存类似jmeter的csv数据

    将locust测试的数据保存为类似jmeter一样的csv文件。 利用locust-plugins的功能,将数据保存为类似jmeter一样的csv文件 每次结束测试时不需要退出locust程序,就可以将本次测试的数据进行保存 安装locust插件库 pip install locust-plugins 引入插件库,使用提供jmeter方法,实现csv文件保存。

    2024年01月23日
    浏览(43)
  • Unity 网格建造

    游戏中常见的建造方式,目前在做的游戏中需要。 但搜出来的教程比较少,我又是个英文盲,没找到合适的方法,于是瞎研究了一个,说实话也挺简单的。 整体感觉还行,演示: 先定义几个变量 首先根据 可建造区域大小 和 可建造区域起始点 创建网格,放到允许建造区域

    2024年02月13日
    浏览(30)
  • Unity的AssetBundle系统来动态加载FBX模型

    在Unity中,可以使用C#脚本和Unity的AssetBundle系统来动态加载FBX模型。以下是一个简单的示例,演示如何动态加载FBX模型: 准备FBX模型 首先,准备一个或多个FBX模型,并将它们导入到Unity项目中。确保每个FBX模型都有一个独立的游戏对象,并且已经被正确地设置为“Static”或“

    2024年02月06日
    浏览(41)
  • Unity 建造者模式(实例详解)

    说明 在Unity中,建造者模式(Builder Pattern)是一种创建型设计模式,它通过分离对象构建过程的复杂性,允许您以更灵活和可扩展的方式创建不同变体的对象。这种模式尤其适用于需要构造具有多个可选或必须组件的对象,并且希望客户端代码无需了解内部构造细节的情况下

    2024年01月23日
    浏览(35)
  • Unity 之 Addressable可寻址系统 -- 将Resources加载资源方式修改为Addressable加载 -- 实战(一)

    加载方式: Resources 使用同步加载方式;Resources 加载资源时,应用程序将会被阻塞,直到资源加载完成,这可能会导致应用程序出现卡顿或挂起的情况。 Addressables 使用异步加载方式。这意味着使用 Unity 而使用 Addressables 加载资源时,应用程序可以继续运行,而不会出现卡顿

    2024年02月05日
    浏览(39)
  • Unity 之 Addressable可寻址系统 -- 资源加载和释放 -- 进阶(二)

    概述:本篇文章从资源加载的方式和具体示例演示,为大家介绍可寻址资源系统的资源加载和资源释放。 同步异步相关概念: 同步:是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行

    2024年02月02日
    浏览(43)
  • Unity 之 Addressable可寻址系统 -- 资源远程加载 | 资源预下载 -- 进阶(三)

    概述:实现方式是使用Unity的可寻址系统结合云资源分发(AA+CCD)的形式。本篇文章就来为讲解CCD的使用介绍,以及AA+CCD使用的示例。 在Hub界面的游戏云选项,可以看到官网介绍入口: CCD:全称Cloud Content Delivery,译为:云端资源分发。 Unity 推出首个用于实时游戏更新的端到端

    2024年01月16日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包