脚本-简单脚本

news2025/2/27 21:44:45

脚本执行顺序

默认脚本是无序执行,也就是哪个脚本先执行,哪个脚本后执行是未知的。

也可以手动指定执行顺序,

在如上图所示中,可以点击 加号 按钮,然后添加脚本的顺序,数值越大,执行顺序越靠后。

消息函数

Awake 初始化执行,仅执行一次,在Start方法前,脚本被禁用后依旧会执行,

Start 初始化执行,仅执行一次,脚本被禁用后不会执行

Update , 帧更新,每帧调用一次,

OnEnable, 脚本组件启用时调用一次,

OnDisable, 脚本组件禁用时调用一次,

消息调用

此处的消息调用和消息函数不一样,看起来名字相同而已。

在target物体上挂了一个Script2的脚本,在r物体上挂了一个SimpleExample的脚本。

SimpleExample中代码如下所示,

GameObject target = null;
// Start is called before the first frame update
void Start()
{
    Application.targetFrameRate = 60;
    target = GameObject.Find("/empty/target");
}

// Update is called once per frame
void Update()
{
    if (Input.GetKeyDown(KeyCode.UpArrow))
    {
        target.SendMessage("musicVolume", 20f);
    }
    else if (Input.GetKeyDown(KeyCode.DownArrow))
    {
        target.SendMessage("musicVolume", -1f);
    }
}

获取到target物体后,通过SendMessage就可以通过反射调用Script2脚本中的musicVolume方法,这个方法如下所示,

public void musicVolume(float volume)
{
    audioSource.volume = volume;
}

此 SendMessage(methodName,Object args); 两个参数中,第一个是方法名,第二个是Object参数,那么也就意味着可以传入一个数组等。

此处不知道为何,第二个参数填0的时候就报错。

target.SendMessage("musicVolume", 0f);

物体增加脚本

右击,Create->C# Script , 新建一个*.cs的文件,然后双击在Visual Studio中打开。

在GameObject上挂载上这个脚本 Add Component --> Script

脚本原始就是这些代码。

简单信息打印

void Start()
{
    GameObject go = this.gameObject;
    Debug.Log("info:"+go);
    Debug.Log("info:" + go.name);
    Debug.Log("info:" + go.transform.position);
}

this代表的是SimpleExample这个CS脚本,this.gameObject自然就是它挂载的GameObject了。

transform.position打印的是世界坐标系的坐标,只有用localPosition打印的才是本地坐标系的坐标,也就是相对坐标。

本地坐标系和世界坐标系说明

但和界面上本地坐标系和世界坐标系不同在于,世界坐标指的是在Scene中的坐标,而localPosition是相对父物体的坐标。

物体移动

方法一

不断的修改localPosition的数值,

void Update()
{
    Vector3 localPosition = this.transform.localPosition;
    localPosition.x += 0.01f;
    this.transform.localPosition = localPosition;
}

this.transform 是 this.gameObject.transform的缩写。

方法二

Translate(x,y,z)方法,此处的x,y,z是增量值,即 x+= ? ,y+=? , z+=?

void Update()
{
    this.transform.Translate(0.01f,0,0);
}

这两种方法都可以让物体在X轴上每帧移动0.01的距离。

点击Translate()方法后,反编译发现,其实也是设置世界坐标。

此处会执行else中语句。

TransformDirection_Injected就无法看到了,但从注释可以看出,应该是将本地坐标系转换为世界坐标。

在移动时有两个坐标系参考,一个是世界坐标系,一个是自身的坐标系。

参考

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/liudazhuangdeshan/yp3bqv/konq1e#WHHfU

所以Translate()方法还有第四个参数,Space.World和Space.Self,分别表示参考世界坐标系,和自身坐标系。

朝物体移动

朝物体移动首先需要转向,用到LookAt方法,这个方法是将物体旋转,使Z轴指向目标物体

target = GameObject.Find("target");
this.transform.LookAt(target.transform); //Z轴指向目标物体

朝物体移动后还需要判断和物体的距离,

Vector3 positionSelf = this.transform.position;
Vector3 positionTarget = this.target.transform.position;
Vector3 distanceV = positionSelf - positionTarget;
float distance = distanceV.magnitude; //判断距离
if (distance > 2)
{
    this.transform.Translate(0,0,0.1f,Space.Self);
}

物体旋转

方法一

Vector3 localAngles = this.transform.localEulerAngles;
localAngles.y += 0.5f;
this.transform.localEulerAngles = localAngles;

通过调整localEulerAngles这个角度,能让物体不断的旋转。

方法二

用Rotate方法,

this.transform.Rotate(0, 0.5f, 0, Space.Self);

脚本参数

在脚本中设置全局变量即可,Tooltip是提示,当鼠标悬浮时可以显示提示文字。

[ Tooltip("Y轴移动速度")]
public float speed = 0.3f;

鼠标按键

Input.GetMouseButtonDown() 鼠标按下

Input.GetMouseButtonUp() 鼠标抬起

Input.GetMouseButton() 正在按鼠标 鼠标状态的探测

0 鼠标左键

1 鼠标右键

2 鼠标中键

if (Input.GetMouseButtonDown(0))
{
    Debug.Log("按下鼠标左键");
}
else if (Input.GetMouseButtonDown(1))
{
    Debug.Log("按下鼠标右键");
}
else if (Input.GetMouseButtonDown(2))
{
    Debug.Log("按下鼠标中键");
}

鼠标按键问题点

鼠标按下是个全局事件,也就是说并不是像 html页面一样,给某个元素设置一个 @click 事件,所以如何判断鼠标点击了哪个物体?

在3D游戏中,看起来是3D的,但其实屏幕是2D,也就是说其实只存在x,y两个坐标系就可以判断位置在屏幕中的范围。

屏幕左下角是(0,0)坐标,从左下角开始向上和向右。

Vector3 mousePos = Input.mousePosition;//鼠标点击在屏幕中位置

上面就可以获取到鼠标在屏幕中点击的位置。

Vector3 objWorldPos = this.transform.position;
Vector3 objScreenPos = Camera.main.WorldToScreenPoint(objWorldPos);

上面两行代码,获取物体的三维坐标,并将三维坐标转换为屏幕坐标,此时objScreenPos就只有x,y是有效值,z并没有参考价值了。

int screenWidth = Screen.width;
int screenHeight = Screen.height;

上述两行代码可以获取屏幕的宽高,

Vector3 objWorldPos = this.transform.position;
Vector3 objScreenPos = Camera.main.WorldToScreenPoint(objWorldPos);

Vector3 mouseWorldPos = Input.mousePosition;
Vector3 mouseScreenPos = Camera.main.WorldToScreenPoint(mouseWorldPos);

Vector3 distanceV = objScreenPos-mouseScreenPos;
float distance = distanceV.magnitude;


if (objScreenPos.x < 0 || objScreenPos.x > Screen.width 
    || objScreenPos.y < 0 || objScreenPos.y > Screen.height)
{

}

键盘事件

Input.GetKeyDown() 按键事件,按下键

Input.GetKeyUp() 按键事件,抬起键

Input.GetKey() 按键状态,某个键正被按着

if (Input.GetKey(KeyCode.W))
{
    this.transform.Translate(0,0,0.3f,Space.Self);
}else if (Input.GetKey(KeyCode.S))
{
    this.transform.Translate(0, 0, -0.3f, Space.Self);
}

一般可以用Input.GetKey来让物体移动,其中KeyCode的值可以在unity的api文档中找到。

AudioSource组件调用

给r这个GameObject挂上Audio Source组件,然后就可以通过脚本获取组件,用组件来控制播放的开始和暂停等。

AudioSource audioSource = null;

// Start is called before the first frame update
void Start()
    {
        Application.targetFrameRate = 60;
        //获取此脚本上GameObject的AudioSource组件
        audioSource = GetComponent<AudioSource>();

    }

// Update is called once per frame
void Update()
    {
        if (Input.GetMouseButtonDown(0))//鼠标左击
        {
            if (audioSource.isPlaying)//正在播放
            {
                audioSource.Pause();
            }
            else
            {
                audioSource.Play();
            }
        }
    }

GetComponent<AudioSource>(); 是 this.gameObject.GetComponent<>的缩写,也可以缩写为this.GetComponent<>。

通过完整的写法可以知道,如果把this改为其他物体的GameObject,那么就能获取到其他物体上挂载的组件。

代码播放音效

如上图所示,不给AudioSource文件指定音乐文件,而是在脚本中指定音乐文件,然后播放。

public AudioClip clip;

AudioSource audioSource;

// Start is called before the first frame update
void Start()
{
    Application.targetFrameRate = 60;
    this.audioSource = GetComponent<AudioSource>();
}

// Update is called once per frame
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        audioSource.PlayOneShot(clip);
    }
    
}

PlayOneShot方法常用来播放游戏枪声音效。

物体获取

方法一

在脚本参数中有设置一个speed的参数,在unity编辑器界面就可以设置数值。

那么也可以设置一个变量是GameObject,AudioSource等。

在target上挂载AudioSource组件,在r上挂载脚本,把target作为一个赋值传入。

public GameObject target = null;   

    AudioSource audioSource = null;

    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;
        //target上的组件
        audioSource = target.GetComponent<AudioSource>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))//鼠标左击
        {
            if (audioSource.isPlaying)//正在播放
            {
                audioSource.Pause();
            }
            else
            {
                audioSource.Play();
            }
        }
    }

方法二

 GameObject target = GameObject.Find("target");
audioSource = target.GetComponent<AudioSource>();

直接利用GameObject的Find方法

如果Find方法里面的单词写错了,会报下面的空指针异常。

GameObject target = GameObject.Find("/empty/target");
 GameObject target = GameObject.Find("empty/target");

如果像这种挂载在其他节点下,那么就需要加上路径,上面两种路径都可以找到target。

父物体获取

target = GameObject.Find("/empty/target");
GameObject empty = target.transform.parent.gameObject;

如上代码所示,获取target的父物体empty。

从上面代码也可以看出,维持父子间关系的是transform类,只有获取transform类才能获取到parent,此时获取的也只是父节点的transform,要继续获取GameObject还得加上parent.gameObject才行。

如果维持父子间关系的是GameObject类,那么就可以直接 target.parent,就能获取父节点的GameObject对象了。

父物体设置

如图所示,运行时将target父节点设置为r物体。

target = GameObject.Find("/empty/target");
target.transform.SetParent(this.transform);

当设置为SetParent(null)时,就是放在根节点下。

脚本获取

其实脚本获取和物体获取,组件获取一样,脚本也是组件的一种。

如何获取其他物体上挂载的脚本?

Script2.cs脚本挂载到target上,在SimpleExample中获取Scripts脚本。

Scripts2 scripts2 = null;
// Start is called before the first frame update
void Start()
{
    Application.targetFrameRate = 60;
    GameObject target = GameObject.Find("/empty/target");
    scripts2 = target.GetComponent<Scripts2>();
}

如上述所示,其实和获取组件一样,都是通过GameObject。

void Update()
{
    scripts2.musicPlay();
    if (Input.GetKeyDown(KeyCode.UpArrow))
    {
        scripts2.volume++;
    }else if (Input.GetKeyDown(KeyCode.DownArrow))
    {
        scripts2.volume--;
    }
}

在SimpleExample.cs的Update()方法中,就可以调用Script2.cs中public的方法和变量了。

Script2.cs中代码如下,

AudioSource audioSource = null;
    public float volume = 0f;//音量

    // Start is called before the first frame update
    void Start()
    {
        audioSource = this.GetComponent<AudioSource>();
        audioSource.volume = volume;
    }

    // Update is called once per frame
    void Update()
    {
       audioSource.volume = volume;
    }

    public void musicPlay()
    {
        if (Input.GetMouseButtonDown(0))//鼠标左击
        {
            if (audioSource.isPlaying)//正在播放
            {
                audioSource.Pause();
            }
            else
            {
                audioSource.Play();
            }
        }
    }

物体激活和隐藏

对应到界面就是这个小勾。

 GameObject target = GameObject.Find("/empty/target");
if (target.activeSelf)
{
    target.SetActive(false);
}

物体处于激活状态,就隐藏。

修改物体材质

材质使用Meshranderer中material修改即可。

public Material[] materials;

int nowIndex = 0;

// Start is called before the first frame update
void Start()
{
    Application.targetFrameRate = 60;
}

// Update is called once per frame
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        nowIndex = ++nowIndex%materials.Length;
        MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
        meshRenderer.material = materials[nowIndex];
    }
    
}

定时器

有两种定时器,只执行一次的定时器Invoke和重复执行的InvokeRepeat,有点像javascript中的setTimeout和setInterval方法。

Invoke

注意Invoke里是反射,第二个参数单位是秒。

而且放在Start()方法中即可,放在Update中就是重复执行了,Update本身就是重复执行的方法。

此处的this代表的是Script组件,也就是说获取了其他GameObject上的Script组件后,也可以调用其他脚本里的方法。

void Start()
{
    Application.targetFrameRate = 60;
    this.Invoke("repeatFunction", 2f);
}

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

private void repeatFunction()
{
    Debug.Log("repeatFuntion:" + Time.time);
}

InvokeRepeating

第二个参数是脚本加载1秒后开始执行第一次,

第三个参数是每隔2秒执行一次。

this.InvokeRepeating("repeatFunction", 1f, 2f);

//判断是否要执行,可以取消定时任务
if (this.IsInvoking("repeatFunction"))
{
    this.CancelInvoke("repeatFunction");
}

获取线程号

using System.Threading;
int threadId = Thread.CurrentThread.ManagedThreadId;

物体向量

长度

使用magnitude即可。

Vector2 vector2 = new Vector2(2, 3);
float magnitude2 = vector2.magnitude;

Vector3 vector3 = new Vector3(2,3,3); 
float magnitude3 = vector3.magnitude;

单位向量

使用normalized将此向量变为长度为1的单位向量,长度为1即

 

Vector3 vector3 = new Vector3(2,3,3); 
Vector3 v3Normal = vector3.normalized;

常用向量简写

Vector3.zero (0,0,0)

Vector3.up (0,1,0)

Vector3.right (1,0,0)

Vector3.forward (0,0,1)

向量运算

Vector3 a = new Vector3(1,1,1);

Vector3 b = new Vector3(2,2,2);

两个向量相加就是 a+b = (3,3,3) 分别x,y,z相加。

相减就是 a-b = (-1,-1,-1) 分别x,y,z相减。

向量乘以常量 a*2 = (2,2,2) x,y,z分别乘以这个常量。

距离

Vector3 a = new Vector3(2,3,3);
Vector3 b = new Vector3(2, 3, 3);

Vector3 c = b - a;
float distance = c.magnitude;//两个向量点位置之间的距离

float distance = Vector3.distance(a,b); //这是封装的方法,求距离的

动态创建实例

使用 Object.Instantiate,第一个参数是一个Object对象,即预制体,第二个是挂载到的父节点,是一个Transform对象。

public class SimpleExample : MonoBehaviour
{
    [ Tooltip("预制体")]
    public GameObject prefab;

    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;
  
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
           GameObject go01 = Object.Instantiate(prefab,null);
           GameObject go02 = Object.Instantiate(prefab);
           go01.transform.position = Vector3.zero;
           go02.transform.position = Vector3.zero;
        }
    }
}

上面第二个参数null,或者为空都是挂载到根节点下。

销毁实例

使用 Object.Destroy(销毁对象,延迟销毁时间); 时间单位秒,第一个参数通常传GameObject

public class SimpleExample : MonoBehaviour
{
    [Tooltip("预制体")]
    public GameObject prefab;

    private GameObject gameObject;

    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (gameObject != null) { 
                Object.Destroy(gameObject);  //销毁实例
            }
            else
            {
                gameObject = Object.Instantiate(prefab, null);
                gameObject.transform.position = Vector3.zero;
            }
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/118052.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

为什么团队执行力差,管理者应该如何解决?

没有执行力&#xff0c;一切都是空谈。团队执行力差可能是这些原因&#xff1a; 1、目标不明确&#xff0c;项目方向不清晰。 2、责任不清&#xff0c;不知道干啥&#xff0c;也不知道为什么而干。 3、技能不熟练&#xff0c;没有明确的完成标准。 4、分配任务&#xff0c;…

【JavaScript】飞机大战

文章目录一、效果演示设计思路二、鼠标版飞机大战代码展示1.HTML结构代码2.CSS样式代码3.JavaScript代码js.js文件plane.js文件三、键盘版飞机大战代码展示1.HTML结构代码2.CSS样式代码3.JavaScript代码四、代码资源分享一、效果演示 利用html&#xff0c;css&#xff0c;js制…

华为云服务器上部署war包(虚拟机也同样适用)

目录linux部署war包安装jdk关闭防火墙简单粗暴&#xff08;推荐虚拟机使用&#xff09;复杂但安全&#xff08;推荐服务器使用&#xff09;安装tomcat部署war包linux部署war包 安装jdk 执行命令查看可安装java版本 yum -y list java*执行命令安装jdk8 yum install -y java-…

12. 爬虫训练场项目,jinja2 模板继承,项目继续迭代

本篇博客我们将前端模板的通用部分进行抽离&#xff0c;便于整理管理&#xff0c;使用的是 jinja2 中模板继承相关技术。 文章目录Flask 模板引擎块&#xff08;Block&#xff09;更细的块拆解完善 general 目录和 school 目录 HTML 文件宏&#xff08;Macro&#xff09;Flask …

章节六:RASA NLU组件介绍--特征生成器

目录一、前言二、特征生成器MitieFeaturizerSpacyFeaturizerConveRTFeaturizerLanguageModelFeaturizerRegexFeaturizerCountVectorsFeaturizerLexicalSyntacticFeaturizer一、前言 RASA在处理对话时&#xff0c;整体流程是pipeline结构&#xff0c;自然语言理解&#xff08;N…

SpringBoot操作Redis

目录 1.IDE创建一个maven项目 2、 添加redis启动器 3.修改配置文件application.properties 4.在测试类中测试 SpringBoot操作Hash&#xff08;哈希&#xff09; SpringBoot操作List集合类型 SpringBoot操作Set集合类型 SpringBoot操作ZSet集合类型 1.IDE创建一个maven项…

dubbo(尚硅谷)学习笔记2

我们现在来做dubbo和springboot整合&#xff1a; 我们先来创建一个springboot项目&#xff1a; 然后把serviceimpl层拷贝过来。 因为我们这个也需要用到公用接口和实体类&#xff0c;所以还是需要导入一下这个依赖&#xff1a; 同样的我们也需要创建一个服务的消费者&#xf…

设计模式之美总结(行为型篇)

title: 设计模式之美总结&#xff08;行为型篇&#xff09; date: 2022-12-26 17:25:29 tags: 设计模式 categories:设计模式 cover: https://cover.png feature: false 文章目录1. 观察者/发布订阅模式&#xff08;Observer Design Pattern/Publish-Subscribe Design Pattern…

Unity2D像素游戏开发——Aseprite简单人物绘画+动画制作导出精灵表示例

目录 前言 什么是帧&#xff1f; 什么是Aseprite&#xff1f; 运行环境 正文 示例&#xff1a;绘制人物 制作多帧动画 微调 导出精灵表 总结 作品欣赏 附一个下载链接&#xff1a; 前言 什么是帧&#xff1f; 我们看到的动画都是由一张张图片连续播放而成的&#…

scipy

scipy.interpolate插值方法 import numpy as np def func(x, y):return x*(1-x)*np.cos(4*np.pi*x) * np.sin(4*np.pi*y**2)**2grid_x, grid_y np.mgrid[0:1:100j, 0:1:200j]rng np.random.default_rng() points rng.random((1000, 2)) values func(points[:,0], points[:…

高颜值蓝牙耳机有哪些?音质好颜值高的蓝牙耳机推荐

喜欢安静的人们&#xff0c;相信都会有一副蓝牙耳机吧&#xff0c;作为我们生活当中必不可少的数码产品&#xff0c;除了手机以外&#xff0c;蓝牙耳机几乎也是使用率很高的&#xff0c;它通过蓝牙连接&#xff0c;非常方便&#xff0c;下面是小编精心挑选的四款蓝牙耳机。 一…

告别“限速”,个人网盘进入云时代

配图来自Canva可画 在数字经济广泛渗透的条件下&#xff0c;个人网盘市场也得到了长足发展。而在5G和AI的加持下&#xff0c;个人网盘不断进行技术融合和迭代&#xff0c;云盘已然成为互联网用户以及智能设备存储的基本服务&#xff0c;而其应用场景也顺理成章地开始向各个细分…

window11 node.js 安装与下载

最近电脑莫名其妙的被一些恶意流氓软件捆绑了&#xff0c;今天我直接给恢复出厂设置了。顺便记录一下软件的安装步骤。 1. 先去官网下载 官网地址 ① 进入到官网后如下图所示 ②根据自己电脑选择合适的版本下载&#xff08;我是wiindows 64位 &#xff09; ③ 双击安装包点击…

道路交通警示牌数据集以及训练好的YOLO模型权重文件

道路交通警示牌yolo模型1.交通标志数据集的介绍2.训练出权重文件1.交通标志数据集的介绍 交通标志&#xff08;国外的交通标志&#xff09;数据集是经过标注过的数据集&#xff0c;包括77个类别&#xff1b;标注类别如下&#xff1a; ‘200m’, ‘50-100m’, ‘Ahead-Left’, …

如何写好一份数据分析报告?

数据分析报表怎么做&#xff1f;这是一个很笼统的问题&#xff0c;所以这篇尝试从数据分析报表的3个方面来说下&#xff0c;准备了3天&#xff0c;内容较长&#xff0c;心急的小伙伴先看索引&#xff1a; 数据分析报表的原则数据分析报表的数据来源数据分析报表的可视化展示 0…

【按钮的两种状态 Objective-C语言】

一、继续上一篇文章的按钮案例 1.先说思路: 1)先把最上面的图片按钮实现了 我们拽1个按钮,给它一个背景图,加一个文字“点我啊” 当你鼠标按下去的时候,换成另1个背景图 当你鼠标按下去的时候,按钮的背景图变了,并且上面的文字也变了,变成“摸我干啥” 当你鼠标抬起…

Doris-集成其他系统(四)

目录0、准备1、Spark 读写 Doris1.1 准备 Spark 环境1.2 使用 Spark Doris Connector1.2.1 SQL 方式读写数据1.2.2 DataFrame 方式读写数据&#xff08;batch&#xff09;1.2.3 RDD 方式读取数据1.2.4 配置和字段类型映射1.3 使用 JDBC 的方式&#xff08;不推荐&#xff09;2、…

京东零售大数据云原生架构实践

通常谈到大数据&#xff0c;想到的是大数据平台、Hadoop生态或者数据湖技术&#xff0c;关注于大数据存储、大数据计算方向上的技术发展与应用&#xff1b;谈到云原生&#xff0c;想到的是微服务架构、容器化或者SRE&#xff08;Site Reliability Engineer&#xff09;运维范畴…

圣诞节快乐,程序员们!

一、前言 为了参加圣诞创意大赛&#xff0c;拖着阳过的病体&#xff0c;在咳嗽的间隔时间变长之后&#xff0c;发个帖子沾点节日气氛。前段时间参加了大模型训练营&#xff0c;趁着热度&#xff0c;刷一下AIGC的氛围。 二、创意名 因为生病了&#xff0c;所以就懒&#xff0…

【Pygamre实战】2023人气超高的模拟经营类游戏:梦想小镇代码版火爆全场,免费体验分享下载哦~

前言 梦想还是要有的&#xff0c;万一实现了呢&#xff1f;&#xff01;今天小编就来用代码实现自己专属的城市——特大都市&#xff1a; 梦想小镇启航。顾名思义&#xff0c;梦想小镇是梦想花开之地。自己当市长不香嘛&#xff01; 所有文章完整的素材源码都在&#x1f447;…