1. 基本介绍

1. MenuItem
  1. 使用 MenuItem 特性,让静态函数作为一个菜单栏功能

[MenuItem(“Tools/mytest”)]

  1. 第三个参数层级,可控制显示的顺序。不同级别的参数最小值和最大值之间最小为11
  2. 添加快捷键
  • %-CTRL
  • # -Shift
  • & -Alt

[MenuItem( “Tools/New Option %#a” )] 即Alt+Shift+A
[MenuItem( “Tools/Item2 _g” )] 即G

  1. 特殊路径
  • Assets/[] -添加到“Assets”菜单下,同时也显示在右键单击项目视图时弹出的菜单中。
  • Asset/Create/[] - 添加在“Assets ->Create”子菜单中。
  • CONTEXT/[任意组件名]/[] - 菜单项将出现在给定组件的右键单击菜单中。
[MenuItem("CONTEXT/Rigidbody/Do Something")]
public static void DoSomething (MenuCommand command) {Rigidbody body= command.context  as  Rigidbody ;body.mass =  5 ;
}
  • GameObject/[] 菜单项会出现在层级的右键菜单中。但你需要给定一个层级 < 50

[MenuItem(“GameObject/mytest2”, false, 12)]

  1. 限定
    通过第二个参数来限定该按钮是否启用
// 效果
[MenuItem("Assets/SetMass", false, 12)]
static void SetMass()
{Rigidbody rigidbody = Selection.gameObjects[0].GetComponent<Rigidbody>();rigidbody.mass = 5;}// 约束
[MenuItem("Assets/SetMass", true)]
static bool SetMassOption()
{return Selection.gameObjects[0].GetComponent<Rigidbody>() != null;
}
2. AddComponentMenu

添加组件到“添加组件”菜单下。

3. ContextMenu、ContextMenuItem

在继承MonoBehaviour时:

特性 描述
ContextMenu 添加到自身的右键菜单中
ContextMenuItem 添加到菜单列表
public class MyObject : MonoBehaviour
{[ContextMenuItem("+1", "AddValue")]public int value;public string data;[ContextMenu("重置")]void ResetData(){value = 0;data = "";}void AddValue(){value++;}
}
4. ScriptableWizard

继承该类可生成一个点击后关闭并重置的面板,可进行方便的对象创建修改操作

using UnityEngine;
using UnityEditor;public class MyObjectEditor : ScriptableWizard
{// 参数public string changeName = "未命名";// 创建按钮到编辑器,并设置按钮名称以及标题[MenuItem("Tools/CreateWizard")]public static void CreateWizard(){// 一个点击后关闭并重置的面板DisplayWizard<MyObjectEditor>("统一修改", "修改", "第二个按钮");}// 点击后关闭界面并执行private void OnWizardCreate(){// 修改选取对象的名字foreach (var obj in Selection.gameObjects){// 操作记录到unity记录中,以便ctrl+z撤回Undo.RecordObject(obj, "修改名字");obj.name = changeName;}// 在界面显示提示信息,仅在未关闭时显示ShowNotification(new GUIContent(Selection.gameObjects.Length + "个对象被修改了"));}// 点击另一个按钮,这不会关闭界面private void OnWizardOtherButton() => OnWizardCreate();// 仅在初始化和值改变时变化private void OnWizardUpdate() => HelpInfor();// 选中物体时调用private void OnSelectionChange() => HelpInfor();// 用于提示信息void HelpInfor(){if (Selection.gameObjects.Length == 0){// 错误信息errorString = "请选择一个对象!";helpString = "";}else{// 帮助信息errorString = "";helpString = "您选择了" + Selection.gameObjects.Length + "个对象";}}
}
5. [UnityEditor.InitializeOnLoadMethod]

每次Unity编译都会运行一次

2. 编译器绘图

1. 绘图方法
  1. GUILayout
方法 描述
Label 写入一个文本
BeginVertical 开始垂直绘图
BeginHorizontal 开始水平绘图
BeginScrollView 开始滚动视图
Button 按钮控件,放在IF语句中判断点击
  1. EditorGUILayout
方法 描述
TextField 文本框
EnumPopup 枚举选项
IntField 整数选项
ObjectField(object, typeof, false) 一个Unity对象
PropertyField 一个SerializedObject类中的对象
2. 示例:窗口形式

主面板

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;public class AudioWindowEditor : EditorWindow
{static SaveData data;string path = "Assets/Resources/MainData.asset";string text;string search = "";static GUIStyle textStyle;static AudioWindowEditor window;[MenuItem("Tools/test")]public static void CreateWindow(){// 固定大小窗口// Rect rect = new Rect(0, 0, 800, 600);// AudioWindowEditor window = GetWindowWithRect(typeof(AudioWindowEditor), rect) as AudioWindowEditor;if (window == null){window = GetWindow<AudioWindowEditor>("数据管理");textStyle = new GUIStyle("HeaderLabel");textStyle.fontSize = 24;}window.Show();}/// <summary>/// 每秒调用十次/// </summary>private void OnInspectorUpdate(){}private void OnGUI(){// 设置标题和其显示的值,将其值返回// text = EditorGUILayout.TextField("输入文字", text);if (data == null) data = AssetDatabase.LoadAssetAtPath<SaveData>(path);if (data == null){// new SaveData()可替换为 CreateInstance<SaveData>()AssetDatabase.CreateAsset(new SaveData(), path);AssetDatabase.SaveAssets();data = AssetDatabase.LoadAssetAtPath<SaveData>(path);}var settings = new SerializedObject(data);GUI.skin.label.fontSize = 16;GUILayout.Label("基本内容");EditorGUILayout.PropertyField(settings.FindProperty("key"), new GUIContent("名称"));// 第一栏// GUILayout.BeginHorizontal("Wizard Box");search = EditorGUILayout.TextField("", search, "SearchTextField");// GUILayout.EndHorizontal();// 第二栏GUILayout.BeginVertical("Wizard Box", GUILayout.MaxHeight(1));GUILayout.BeginHorizontal(new GUIStyle("HelpBox"){padding = new RectOffset(10, 0, 4, 4)});GUILayout.Label("名称", new GUIStyle("SettingsHeader"){fontSize = 16,alignment = TextAnchor.MiddleLeft,});GUILayout.Label("值", new GUIStyle("SettingsHeader"){fontSize = 16,alignment = TextAnchor.MiddleLeft,});GUILayout.EndHorizontal();KeyValue removeOne = null;KeyValue moveUp = null;KeyValue moveDown = null;// 这其实是错误的写法,不应该直接操纵数据源,而是通过settings.FindProperty("datas")arraySize和settings.FindProperty("datas").GetArrayElementAtIndex(i)for (int i = 0; i < data.keyvalues.Count; i++){var dataSlot = data.keyvalues[i];if (dataSlot.key == null) dataSlot.key = "";if (dataSlot.key.ToLower().Contains(search.ToLower())){GUILayout.Space(2);GUILayout.BeginHorizontal("HelpBox");dataSlot.key = EditorGUILayout.TextField(dataSlot.key);dataSlot.value = (GameObject)EditorGUILayout.ObjectField(dataSlot.value, typeof(GameObject), false);if (i != 0){if (GUILayout.Button("↑", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) moveUp = dataSlot;}else GUILayout.Button("", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18));if (i != data.keyvalues.Count - 1){if (GUILayout.Button("↓", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) moveDown = dataSlot;}else GUILayout.Button("", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18));if (GUILayout.Button("-", GUILayout.MaxWidth(24), GUILayout.MaxHeight(18))) removeOne = dataSlot;GUILayout.EndHorizontal();}}if (removeOne != null) data.keyvalues.Remove(removeOne);if (moveUp != null){int index = -1;for (int i = 0; i < data.keyvalues.Count; i++) if (data.keyvalues[i] == moveUp) index = i;if (index != -1){var move = moveUp;data.keyvalues.Remove(moveUp);data.keyvalues.Insert(index - 1, move);}}if (moveDown != null){int index = -1;for (int i = 0; i < data.keyvalues.Count; i++) if (data.keyvalues[i] == moveDown) index = i;if (index != -1){var move = moveDown;data.keyvalues.Remove(moveDown);data.keyvalues.Insert(index + 1, move);}}if (GUILayout.Button("+")){data.keyvalues.Add(new KeyValue());}GUILayout.EndVertical();// 应用修改settings.ApplyModifiedPropertiesWithoutUndo();}// 修改asset文件[MenuItem("Tools/CardDataArraysLoad")]public static void CardDataArraysLoad(){string path = "Assets/Configs/CardDataArrayConfig.asset";CardDataArrayConfig config = AssetDatabase.LoadAssetAtPath<CardDataArrayConfig>(path);config.baseCardDatas = new CardData[baseCardDatas.Count];EditorUtility.SetDirty(config);AssetDatabase.SaveAssets();AssetDatabase.Refresh();}
}

数据类

using UnityEngine;
using System.Collections.Generic;public class SaveData : ScriptableObject
{public string key;public List<KeyValue> keyvalues;
}[System.Serializable]
public class KeyValue
{public string key;public GameObject value;
}
3. 示例:项目设置中添加项形式
using UnityEditor;
using UnityEngine;public class MainDataEditor
{static MainData data;[SettingsProvider]public static SettingsProvider MyCustom(){string path = "Assets/Resources/Data/MainData.asset";var provider = new SettingsProvider("MainData", SettingsScope.Project){// 默认情况下,如果未提供标签,则路径的最后一个标记将用作显示名称。label = "主线资源库",// 填充搜索关键字// 创建SettingsProvider并将其绘图(IMGUI)功能初始化guiHandler = (searchContext) =>{if (data == null) data = AssetDatabase.LoadAssetAtPath<MainData>(path);if (data == null){// new MainData()可替换为 CreateInstance<MainData>()AssetDatabase.CreateAsset(new MainData(), path);AssetDatabase.SaveAssets();data = AssetDatabase.LoadAssetAtPath<MainData>(path);}var settings = new SerializedObject(data);GUI.skin.label.fontSize = 16;GUILayout.Label("基本扩展");EditorGUILayout.PropertyField(settings.FindProperty("blood"), new GUIContent("溅血特效"));EditorGUILayout.PropertyField(settings.FindProperty("spark"), new GUIContent("火星特效"));EditorGUILayout.PropertyField(settings.FindProperty("sharp"), new GUIContent("锋利特效"));// 应用修改settings.ApplyModifiedPropertiesWithoutUndo();}};return provider;}
}

数据类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;[CreateAssetMenu(fileName = "MainData", menuName = "Data/MainData", order = 1)]
public class MainData : ScriptableObject
{/// <summary>/// 溅血特效/// </summary>public GameObject blood;/// <summary>/// 火星特效/// </summary>public GameObject spark;/// <summary>/// 锋利特效/// </summary>public GameObject sharp;/// <summary>/// 结束的线/// </summary>public GameObject endLine;/// <summary>/// 结束光环/// </summary>public GameObject endNova;/// <summary>/// 孔明灯/// </summary>public GameObject KongMingLight;/// <summary>/// 枪线/// </summary>public GameObject weaponLine;public TileBase[] tileBases;public AudioClip[] audioClips;/// <summary>/// 背包/// </summary>public Bag bag;/// <summary>/// 提示信息/// </summary>public Infor infor;public Dialog dialog;/// <summary>/// 失败面板/// </summary>public Fail failPanel;public Fail winPanel;public LoadScene loadScenePanel;public Sprite[] move_Sprs;public WeaponData[] weapons;/// <summary>/// 保存值/// </summary>public SaveData saveData;public Player player;/// <summary>/// 物品列表/// </summary>public Good[] goods;
}

3. 实例化自定义-PropertyDrawer

数据类

using System;
using UnityEngine;[Serializable]
public class MyCustomVariable
{public string name;public int key;public float value;public bool flag;public KeyCode input;public Color color;public GameObject obj;public Vector3 dir;
}

使用PropertyDrawer实例化到一行中

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;//给MyCustomVariable指定编译器实例化渲染方式
[CustomPropertyDrawer(typeof(MyCustomVariable))]
public class MyCustomVariableDrawer : PropertyDrawer
{public override void OnGUI(Rect position, SerializedProperty property, GUIContent label){//重载BeginPropertyEditorGUI.BeginProperty(position,label,property);  //绘制单元格头的标签名,不需要可以去掉position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);//Unity默认的每个属性字段都会占用一行,我们这里希望一条自定义Property占一行//要是实现这个要求我们分三步: 1. 取消缩进  2. 设置PropertyField 3.还原缩进//不要缩进子字段,由我们自定义间距var indent = EditorGUI.indentLevel;EditorGUI.indentLevel = 0;//计算要用到的属性显示rect   Rect(x,y,width,height)x,y是左顶点var nameRect = new Rect(position.x, position.y, 50, position.height);var keyRect = new Rect(position.x + 60, position.y, 40, position.height);var flagRect = new Rect(position.x + 110, position.y, 20, position.height);var inputRect = new Rect(position.x + 140, position.y, 60, position.height);var colorRect = new Rect(position.x + 210, position.y, 50, position.height);var dirRect = new Rect(position.x + 270, position.y, 100, position.height);var objRect = new Rect(position.x + 380, position.y, position.width - 390, position.height);//绘制字段 - 将GUIContent.none传递给每个字段,以便绘制它们而不是用标签//属性绘制器不支持布局来创建GUI;//因此,您必须使用的类是EditorGUI而不是EditorGUILayout。这就是为什么要给每个属性指定RectEditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);EditorGUI.PropertyField(keyRect, property.FindPropertyRelative("key"), GUIContent.none);EditorGUI.PropertyField(flagRect, property.FindPropertyRelative("flag"), GUIContent.none);EditorGUI.PropertyField(inputRect, property.FindPropertyRelative("input"), GUIContent.none);EditorGUI.PropertyField(colorRect, property.FindPropertyRelative("color"), GUIContent.none);EditorGUI.PropertyField(dirRect, property.FindPropertyRelative("dir"), GUIContent.none);EditorGUI.PropertyField(objRect, property.FindPropertyRelative("obj"), GUIContent.none);//将缩进还原EditorGUI.indentLevel = indent;EditorGUI.EndProperty();}//设定单元格高度,默认返回为默认高度public override float GetPropertyHeight(SerializedProperty property, GUIContent label){return base.GetPropertyHeight(property, label);}
}

列表元素的编译器扩展-PropertyDrawer

using System;
using Cinemachine.Editor;
using Framework.GameObjectBinder.Views.Variables;
using UnityEditor;
using UnityEditor.Rendering;
using UnityEditorInternal;
using UnityEngine;[CustomPropertyDrawer(typeof(MyCustomVaribleArray))]
public class MyCustomVariableArrayDrawer : PropertyDrawer
{private ReorderableList list;public override void OnGUI(Rect position, SerializedProperty property, GUIContent label){//获取要渲染的列表property = property.FindPropertyRelative("variables");//设定动作函数if (list == null){list = new ReorderableList(property.serializedObject, property, true, true, true, true){multiSelect = true,elementHeight = 22,drawElementCallback = DrawElement,drawHeaderCallback = DrawHeader,// onAddCallback = onAddDropdownCallback = OnAddElement,onRemoveCallback = OnRemoveElement,drawElementBackgroundCallback = DrawElementBackground};}else{list.serializedProperty = property;}list.DoList(position);}public override float GetPropertyHeight(SerializedProperty property, GUIContent label){// 基础高度var height = base.GetPropertyHeight(property, label) + 60;// 计算列表var variables = property.FindPropertyRelative("variables");for (var i = 0; i < variables.arraySize; i++)height += EditorGUI.GetPropertyHeight(variables.GetArrayElementAtIndex(i)) + 5;return height;}private void DrawElement(Rect rect, int index, bool isActive, bool isFocused){var variables = list.serializedProperty;if (index < 0 || index >= variables.arraySize)return;var variable = variables.GetArrayElementAtIndex(index);var x = rect.x;var y = rect.y + 2;var width = rect.width - 20;var height = rect.height - 4;var variableRect = new Rect(x, y, width, height);EditorGUI.PropertyField(variableRect, variable, GUIContent.none);var buttonRect = new Rect(variableRect.xMax + 5, y + 1, 18, 18);if (GUI.Button(buttonRect, new GUIContent("+"))){Debug.Log("点击了按钮");}}private void DrawHeader(Rect rect){GUI.Label(rect, "数据列表");}private void OnAddElement(Rect rect, ReorderableList list){var variables = list.serializedProperty;//当前列表数量var index = variables.arraySize > 0 ? variables.arraySize : 0;//生成多选菜单var menu = new GenericMenu();//根据枚举项,决定生成菜单选项名称foreach (KeyCode variableType in Enum.GetValues(typeof(KeyCode))){var type = variableType;menu.AddItem(new GUIContent(variableType.ToString()), false,context =>{AddVariable(variables, index, type);}, null);}menu.ShowAsContext();}//根据选项,生成元素protected virtual void AddVariable(SerializedProperty variables, int index, KeyCode type){if (index < 0 || index > variables.arraySize)return;variables.serializedObject.Update();//在在列表底插入元素,相当于追加元素variables.InsertArrayElementAtIndex(index);//获取到最新添加的元素,并根据选项修改枚举值var variableProperty = variables.GetArrayElementAtIndex(index);variableProperty.FindPropertyRelative("input").SetEnumValue(type);//写入数据到实例化对象variables.serializedObject.ApplyModifiedProperties();//清除焦点GUI.FocusControl(null);}private void OnRemoveElement(ReorderableList list){var variables = list.serializedProperty;var selectedIndices = list.selectedIndices;//如果没选择任何表单元素,那么删除最后一个if (selectedIndices.Count == 0){AskRemoveVariable(variables, variables.arraySize - 1);}//删除所有选中的对象for (var i = selectedIndices.Count - 1; i >= 0; i--){var index = selectedIndices[i];list.Deselect(index);AskRemoveVariable(variables, index);}}protected virtual void AskRemoveVariable(SerializedProperty variables, int index){if (variables == null || index < 0 || index >= variables.arraySize)return;var variable = variables.GetArrayElementAtIndex(index);var name = variable.FindPropertyRelative("name").stringValue;// 某项被填入值时,弹出提示避免直接清除if (string.IsNullOrEmpty(name)){RemoveVariable(variables, index);return;}if (EditorUtility.DisplayDialog("Confirm delete",string.Format("Are you sure you want to delete the item named \"{0}\"?", name), "Yes", "Cancel"))RemoveVariable(variables, index);}protected virtual void RemoveVariable(SerializedProperty variables, int index){if (index < 0 || index >= variables.arraySize)return;variables.serializedObject.Update();//清除表单元素variables.DeleteArrayElementAtIndex(index);//写入数据到实例化对象variables.serializedObject.ApplyModifiedProperties();GUI.FocusControl(null);}private void DrawElementBackground(Rect rect, int index, bool isActive, bool isFocused){ReorderableList.defaultBehaviours.DrawElementBackground(rect, index, isActive, false, true);}
}

Unity-编辑器扩展(Editor)相关推荐

  1. Unity编辑器扩展之EditorWindow

    Unity编辑器扩展之EditorWindow 继承这个类的编辑器脚本可以用来创建一个编辑器窗口,类似Inspector窗口 若要在这个类中添加一些控件,可以使用GUI和GUILayout控件,还可以 ...

  2. 【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出

    书接上回:[Unity编辑器扩展](一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用_TopGames的博客-CSDN博客 工具使用预览: 工具目标: 1. 实现将p ...

  3. 【Unity编辑器扩展】(三)PSD转UGUI Prefab, 一键拼UI解放美术/程序(完结)

    工具效果: 第一步,把psd图层转换为可编辑的节点树,并自动解析UI类型.自动绑定UI子元素: 第二步, 点击"生成UIForm"按钮生成UI预制体 (若有UI类型遗漏可在下拉菜单 ...

  4. Unity编辑器扩展 UI控件篇

    前摇 :认识编辑器扩展的必要性 由于各种各样的原因,无论是移动端亦或是主机/PC端,进几年的发行的游戏体量是越来越大.通常来说大体量的游戏开发需要一套很成熟完善的工作流,亦或说有很强的工业化的能力,像 ...

  5. Unity 编辑器扩展菜单

    Unity 编辑器扩展菜单 目录 Unity 编辑器扩展菜单 一.MenuItem 特性 菜单栏扩展 1.创建多级菜单 2.创建可使用快捷键的菜单项 3.创建可被勾选的菜单项 4.检查菜单是否使用 5 ...

  6. Unity编辑器扩展: 程序化打图集工具

    开始前的声明:该案例中图集所使用图片资源均来源于网络,仅限于学习使用 一.前言 关于编辑器扩展相关的知识,在前面的两篇内容中做了详细的描述,链接地址: 第一篇 :Unity编辑器扩展 UI控件篇 第二 ...

  7. 【Unity编辑器扩展实践】、查找所有引用该图片的预制体

    上一篇Unity编辑器扩展实践二.通过代码查找所有预制中已经查到到所有的预制体了. 然后我们就可以用这些预制体做一些其他的操作了,比如查找该预制的资源引用.可以直接遍历预制,找到预制里面的所有Imag ...

  8. 【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用

    [Unity编辑器扩展](二)PSD转UGUI Prefab, 图层解析和碎图导出_psd导入unity_TopGames的博客-CSDN博客 [Unity编辑器扩展](三)PSD转UGUI Pref ...

  9. Unity编辑器扩展——在Editor下动态添加监听事件

    对于Unity下例如按钮这种我们可以为它的监听事件手动进行赋值 如果需要通过编辑器扩展来动态为这些监听事件赋值应该如何去实现呢? 首先需要确认的是这些监听事件必须要符合一定的规则才能添加上: 1.必须 ...

  10. Unity编辑器扩展Texture显示选择框

    学习NGUI插件的时候,突然间有一个问题为什么它这些属性可以通过弹出窗口来选中呢? 而我自己写的组件只能使用手动拖放的方式=.=. Unity开发了组件Inspector视图扩展API,如果我们要写插 ...

最新文章

  1. 我的读论文经验总结!
  2. 通达信板块监控指标_【精选指标】通达信创业板涨停变色主图指标,助你股海捉龙擒牛!...
  3. 普大喜奔:沁恒单片机免费样品申请开始啦!
  4. openCV 图像相加,位运算,协方差,绝对值,比较
  5. 案例开发分析 || ​​​​​​​Scheduler组件
  6. [YTU]_2921( Shape系列-7)
  7. phpcms后台进入地址(包含No permission resources错误)
  8. java相除保留两位小数_Java:Java快速入门
  9. Android深入浅出之Binder机制(转)
  10. STM32F103mini教程学习总结与心得(二)---->串口通信
  11. Tensorflow基于mnist数据集实现AlexNet
  12. c语言 如何调用void函数,在C中从main调用void*函数
  13. 【MATLAB】通信信号调制通用函数 — 带通滤波器
  14. Swagger自动生成接口文档
  15. 【中级计量经济学】Lecture 8 虚拟变量回归
  16. Unity3D | FPS游戏_敌人相关
  17. 如何缓解自己紧张焦虑的情绪?
  18. 高速串行计算机扩展总线标准,高速串行计算机扩展总线标准Bosch Sensortec开发出BMP384...
  19. 苹果笔记本计算机管理员删除,如何删除一个管理员?
  20. xHiveAI-A311D:AI开发套件

热门文章

  1. 自动驾驶争得CES 2018头彩:百度Apollo2.0试乘,Lyft干脆直接提供自动驾驶出租车! | 焦点
  2. 最近所学网络知识的一个总结
  3. Winform 中 TextBox 换行问题解析(转)
  4. 机器视觉尺寸检测基础
  5. 服务监控之promethues+grafana,直接送你上大师,这还不上热门吗
  6. baumol wolfe matlab,MATLAB计算方法在选煤和动力配煤中的应用
  7. 2020-8-30 《明日方舟》python抽卡代码
  8. m4s格式转换mp3_下载的mp3音频怎么转换wav格式
  9. 河南景区免门票致数千游客滞留 未制定应急预案-河南-景区-免门票
  10. 3422. 【NOIP2013模拟】水叮当的舞步 (Standard IO)