Unity编辑器扩展实践一、利用txt模板动态生成UI代码

在使用Unity3D开发过程中,随着工作时间的推移,你肯定会发现写的代码,就只有那几个模板。比如控制UI的View代码,你会发现格式都是一样的,添加引用、UI变量声明、Awake中给UI变量赋值、添加Button事件、Destroy中注销事件。可以说都可以通过一个模板写出来。这里就介绍一个我用C#写的一个利用txt模板来生成代码的工具,如果有不太好的地方,还请谅解。

之前我写了一个模板生成的代码,Unity中使用Txt模板生成View代码,但是一直觉得很不方便,自己都没用,一心想写一个好一点的来替代它。所以接下来写的这个,算Version0.0.2吧。网上也有大神写了些代码生成器,只是他们是用WriteLine写的,不适合我这种初级玩家,因为我觉得如果要改成我需要的生成器,改动的太多了,所以搞个模板,想少改些代码。

先来整理一下我的思路:

1、遍历各个子物体,预览各个子物体的对象。

2、选择具体的组件是否需要生成UI代码。

3、点击生成按钮,遍历已经选择的组件,读模板,生成对应的代码,写入对应的文本。

道理就这么简单,需要你会一点编辑器,会一点文件读写,基本上就能搞定了,这里写的代码有点不完美,将就用了,以后再优化吧。

我考虑到的注意事项:

1、C#代码不能有重复的变量名,如果你同一个子物体有很多组件需要使用,或者同一个预制上有多个同名子物体需要使用,需要处理生成的变量名。我是用变量名加一个index作为key存再字典里面,如果有同名的,将变量名后边加一个index++;

2、变量名规范,变量名不能有空格,还有一些特殊符号,注意去掉。比如你用右键创建的Scroll View的话,中间就会有一个空格;

3、模板、或者需要替换的字符串中有占位符,又有左右大括号时遇到问题。比如你需要生成要给方法体的时候,注意你的写法,如果不知道怎么解决,可以看一下C#使用String.Format常见报错 有一部分解决方案。

4、如果你跟我一样,使用EditorPrefs来记录选择了哪些组件,一定不要用EditorPrefs.DeleteAll来清理所有的数据,因为EditorPrefs不仅仅是你在使用,Unity也在用这个,如果你使用了,你会发现,你以前打开Unity可以看到那些老的工程路径不见了。

就先总结就这些吧,想到了再写,接下来上代码了。

一、组件相关的Item

因为要查找子物体,组件,子物体的显影,组件的选择,所以创建了一个类,用来管理具体的某个子物体。遍历根节点的时候递归创建,就可以在这个类里面得到一些基础信息了,如:物体名,到根节点的路径,第几个层级(用来折叠GUI的),需要的组件列表。

using System.Collections.Generic;
using UnityEngine;
namespace GenerateCodeProject
{
    public class GenerateItemData
    {
        public void InitItem(Transform tran, GenerateItemData parent = null, int siblingIndex = 0)
        {
            m_tran = tran;
            Name = m_tran.name;
            Parent = parent;
            if (Parent != null)
            {
                Index = Parent.Index + 1;
            }
            else
            {
                Index = 0;
            }
            Component[] _coms = m_tran.GetComponents<Component>();

            ItemComponentStrList.Clear();
            ItemComponentStrList.Add("GameObject");
            ItemComponentStrList.Add("Transform");

            for (int i = 0; i < _coms.Length; i++)
            {
                Component _com = _coms[i];
                if (_com.GetType().Name.Equals("PrefabLinker")
                    ||_com.GetType().Name.Equals("CanvasRenderer")
                    || _com.GetType().Name.Equals("CanvasGroup"))
                {
                    continue;
                }
                ItemComponentStrList.Add(_com.GetType().Name);
            }

            SiblingIndex = siblingIndex;
        }
        /// <summary>
        /// 当前子物体Transform
        /// </summary>
        private Transform m_tran;
        /// <summary>
        /// 物体名字
        /// </summary>
        public string Name = string.Empty;
        /// <summary>
        /// 父物体 ItemData 类
        /// </summary>
        public GenerateItemData Parent;
        /// <summary>
        /// 子物体层级(父物体为0)
        /// </summary>
        public int Index = 0;
        /// <summary>
        /// 第几个子物体(参照tran.SetAsFirstSibling)
        /// </summary>
        public int SiblingIndex = 0;

        public int ChildCount {
            get
            {
                return m_tran.childCount;
            }
        }
        private string m_showComponentKey = "ShowComponentKey_{0}_{1}";
        private string m_showChildrenKey = "ShowChildrenKey_{0}";

        private bool m_showChildrent = false;
        /// <summary>
        /// 唯一的key值,用层级index来做的。
        /// </summary>
        public string OnlyKey
        {
            get
            {
                if (Parent != null)
                {
                    return Parent.OnlyKey + "&" + SiblingIndex;
                }
                return SiblingIndex.ToString();
            }
        }
        /// <summary>
        /// 物体路径
        /// </summary>
        public string Path
        {
            get
            {
                string m_path = Name;
                if (Parent != null)
                {
                    m_path = Parent.Path + "/" + Name;
                }
                return m_path;
            }
        }

        /// <summary>
        /// 子物体上的组件
        /// </summary>
        public List<string> ItemComponentStrList = new List<string>();

        private string GetItemComponentKey(string com)
        {
            string _showComponentKey = string.Format(m_showComponentKey, OnlyKey, com);
            return _showComponentKey;
        }

        /// <summary>
        /// 获取 是否显示组件的状态
        /// </summary>
        /// <param name="com"></param>
        /// <returns></returns>
        public bool GetItemComponentShowState(string com)
        {
            string _showComponentKey = GetItemComponentKey(com);

            if (GenerateCodeManager.EditorPrefsHasKey(_showComponentKey))
            {
                return GenerateCodeManager.EditorPrefsGetBool(_showComponentKey);
            }
            return false;
        }

        /// <summary>
        /// 更新 是否显示组件的状态
        /// </summary>
        /// <param name="com"></param>
        /// <param name="state"></param>
        public void UpdateItemComponentDic(string com, bool state)
        {
            string _showComponentKey = GetItemComponentKey(com);
            GenerateCodeManager.EditorPrefsSetBool(_showComponentKey, state);
        }

        private string ShowChildrenKey
        {
            get
            {
                return string.Format(m_showChildrenKey, OnlyKey);
            }
        }
        /// <summary>
        /// 是否显示子物体,用来折叠子物体
        /// </summary>
        public bool ShowChildren
        {
            get
            {
                if (GenerateCodeManager.EditorPrefsHasKey(ShowChildrenKey))
                {
                    m_showChildrent =  GenerateCodeManager.EditorPrefsGetBool(ShowChildrenKey);
                }
                return m_showChildrent;
            }
            set {
                GenerateCodeManager.EditorPrefsSetBool(ShowChildrenKey, value);
            }
        }
    }
}

image.gif

二、代码关的Item

为了和预制的Item分开,我新建了一个类,用来管理代码的生成,这就是具体的组件了,在这个类里面获取变量名,函数名(如果是Button组件),组件名,路径,变量名Index(就是用这个避免重复变量名)。

using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace GenerateCodeProject
{
    public class CodeItemData
    {
        private string m_variableName;
        /// <summary>
        /// 变量名
        /// </summary>
        public string VariableName
        {
            get
            {
                if (Index >0)
                {
                    return "m_" + FirstCharToLower(m_variableName)+ Index;
                }
                return "m_" + FirstCharToLower(m_variableName);
            }
            set
            {
                m_variableName = value;
            }
        }

        public string EventName
        {
            get
            {
                if (ComponentType.Equals("AorButton")
                || ComponentType.Equals("Button"))
                {
                    return "OnClick" + FirstCharToUpper(m_variableName);
                }
                return "";
            }
        }
        /// <summary>
        /// 组件名
        /// </summary>
        public string ComponentType;

        /// <summary>
        /// 组件路径
        /// </summary>
        public string Path;

        /// <summary>
        /// 第几个相同的变量名
        /// </summary>
        public int Index = 0;

        private static string FirstCharToLower(string input)
        {
            if (String.IsNullOrEmpty(input))
                return input;
            input = Regex.Replace(input, @"\s", "");
            input = Regex.Replace(input, "#", "");
            string str = input.First().ToString().ToLower() + input.Substring(1);
            return str;
        }

        private static string FirstCharToUpper(string input)
        {
            if (String.IsNullOrEmpty(input))
                return input;
            input = Regex.Replace(input, @"\s", "");
            input = Regex.Replace(input, "#", "");
            string str = input.First().ToString().ToUpper() + input.Substring(1);
            return str;
        }

        /// <summary>
        /// 变量声明
        /// </summary>
        /// <returns></returns>
        public string GetVariableName()
        {
            return string.Format(Def.GetVariableString, ComponentType, VariableName);
        }
        /// <summary>
        /// 获取组件的路径
        /// </summary>
        /// <returns></returns>
        public string GetComponentPath()
        {
            if (ComponentType.Equals("GameObject"))
            {
                return string.Format(Def.GetPathStringWithSpecial, VariableName, Def.ParentGameObjectString, Path);
            }
            else if (ComponentType.Equals("Transform"))
            {
                return string.Format(Def.GetPathStringWithSpecial, VariableName, Def.ParentTranString, Path);
            }
            return string.Format(Def.GetPathGenericityString, VariableName, ComponentType, Def.ParentTranString, Path);
        }
        /// <summary>
        /// 设置点击事件
        /// </summary>
        /// <returns></returns>
        public string SetButtonEvent()
        {
            if (ComponentType.Equals("AorButton")
                || ComponentType.Equals("Button"))
            {
                return string.Format(Def.SetButtonEventString, VariableName, EventName);
            }
            return "";
        }
        /// <summary>
        /// 方法
        /// </summary>
        /// <returns></returns>
        public string GetButtonFunction()
        {
            if (ComponentType.Equals("AorButton")
               || ComponentType.Equals("Button"))
            {
                return string.Format(Def.SetButtonFunctionString, EventName);
            }
            return "";
        }
    }
}

image.gif

三、辅助代码类

为了避免代码太脏乱差,假巴意思的将一些常量、通用方法整理出来。

常量(这个需要改成自己需要的亚子):

namespace GenerateCodeProject
{
    public class Def
    {
        public static string TemplateFile =
            @"$safeitemrootname$
            $componentvariablename$
            $getcomponent$
          ";
        public static string GetPathString = "{0} = ({1}, \"{2}\");\n";
        public static string GetPathGenericityString = "{0} = {1}({2}, \"{3}\");\n";
        public static string GetPathStringWithSpecial = " {0} = {1}, \"{2}\");\n";
        public static string GetVariableString = "private {0} {1};\n";
        public static string ParentTranString = "transform";
        public static string ParentGameObjectString = "gameObject";
        public static string SetButtonEventString = "({0}, {1});\n";
        public static string SetButtonFunctionString = "private void {0}()\n{{}}\n";

    }

}

image.gif

方法:文件相关的创建模板的时候会用到,可持续化数据相关的是绘制GUI窗口显隐组件的时候用到的,代码生成相关是组后生成代码时,获取CodeItemData类。

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

namespace GenerateCodeProject
{
    public class GenerateCodeManager 
    {
        #region 文件相关
        /// <summary>
        /// 文件夹路径
        /// </summary>
        public static string DirectoryPath
        {
            get
            {
                return  Application.dataPath.Replace("Assets", "Template").Replace(@"/",@"\");
            }
        }
        /// <summary>
        /// 文件路径
        /// </summary>
        public static string TemplateFilePath
        {
            get
            {
                return DirectoryPath + @"\Template.txt";
            }
        }

        public static string TemplateFile = Def.TemplateFile;

        public static string GetGenerateScriptPath(string name)
        {
            return DirectoryPath + @"\" + name + ".lua";
        }

        public static void CheckFileExistAndCreate(string path)
        {
            if (!File.Exists(path))
            {
                //写
                FileStream _fs = new FileStream(path, FileMode.Create);
                StreamWriter _textWriter = new StreamWriter(_fs, Encoding.Default);
                _textWriter.Write(TemplateFile);

                _textWriter.Flush();
                _textWriter.Close();
                _fs.Close();
            }
        }
        #endregion

        #region 可持续化数据相关
        private static List<string> m_editorPrefsList = new List<string>();
        public static void ClearAllPrefs()
        {
            Debug.LogError(Application.dataPath);
            for (int i = 0; i < m_editorPrefsList.Count; i++)
            {
                EditorPrefs.DeleteKey(m_editorPrefsList[i]);
            }
            m_editorPrefsList.Clear();
        }

        public static void EditorPrefsSetBool(string key, bool value)
        {
            if (!m_editorPrefsList.Contains(key))
            {
                m_editorPrefsList.Add(key);
            }
            EditorPrefs.SetBool(key, value);
        }

        public static bool EditorPrefsGetBool(string key)
        {
            return EditorPrefs.GetBool(key); ;
        }

        public static bool EditorPrefsHasKey(string key)
        {
            return EditorPrefs.HasKey(key); ;
        }

        #endregion

        #region 代码生成

        public static Dictionary<string ,CodeItemData> GetCodeItemDataDic(List<GenerateItemData> dataList)
        {
            Dictionary<string, CodeItemData> _itemDataDic = new Dictionary<string, CodeItemData>();
            List<CodeItemData> _itemDataList = GetCodeItemDataList(dataList);

            for (int i = 0; i < _itemDataList.Count; i++)
            {
                CodeItemData _data = _itemDataList[i];
                while (_itemDataDic.ContainsKey(_data.VariableName))
                {
                    _data.Index += 1;
                    if (_data.Index >=10)
                    {
                        Debug.LogError("循环大于10了!");
                        break;
                    }
                }
                _itemDataDic.Add(_data.VariableName, _data);
            }
            return _itemDataDic;
        }

        public static List<CodeItemData> GetCodeItemDataList(List<GenerateItemData> dataList)
        {
            List<CodeItemData> _itemDataList = new List<CodeItemData>();
            for (int i = 0; i < dataList.Count; i++)
            {
                GenerateItemData _data = dataList[i];
                List<CodeItemData> _tempList = GetCodeItemData(_data);
                _itemDataList.AddRange(_tempList);
            }

            return _itemDataList;
        }

        public static List<CodeItemData> GetCodeItemData(GenerateItemData data)
        {
            List<CodeItemData> _itemDataList = new List<CodeItemData>();
            for (int j = 0; j < data.ItemComponentStrList.Count; j++)
            {
                string _com = data.ItemComponentStrList[j];
                if (!data.GetItemComponentShowState(_com))
                {
                    continue;
                }
                CodeItemData _codeData = new CodeItemData();
                _codeData.VariableName = data.Name;
                _codeData.ComponentType = _com;
                _codeData.Path = data.Path;
                _itemDataList.Add(_codeData);
            }

            return _itemDataList;
        }
        #endregion

    }
}

image.gif

四、绘制GUI窗口

这个脚本主要时绘制预览物体、选择具体组件的窗口(这个时最开始写的,写的有点乱,没有整理)。

如果你用EditorGUILayout.Foldout来折叠预制的话,记得用EditorGUI.indentLevel缩进你的UI。

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

namespace GenerateCodeProject
{
    public class GenerateCodeWindow : EditorWindow
    {

        [MenuItem("GenerateCode/测试测试")]
        public static void ShowMyWindow()
        {
            GenerateCodeWindow _window = EditorWindow.GetWindow<GenerateCodeWindow>("测试测试");
            _window.Show();
        }

        private Transform m_selectTransform;
        private List<Transform> m_allTransformList = new List<Transform>();

        private List<GenerateItemData> m_allItemDataList = new List<GenerateItemData>();
        private Vector2 m_scrollView = new Vector2(800, 800);
        private void OnGUI()
        {
            GUILayout.BeginHorizontal();
            if (GUILayout.Button("查看"))
            {
                m_selectTransform = GetSelectTranform();
                Repaint();
            }
            if (GUILayout.Button("清空"))
            {
                m_selectTransform = null;
                GenerateCodeManager.ClearAllPrefs();
                Repaint();
            }
            if (GUILayout.Button("生成"))
            {
                m_allItemDataList = GetAllItemDataList(m_selectTransform);
                string _path = GenerateCodeManager.GetGenerateScriptPath(Selection.activeGameObject.name);
                WriteFileWithTemplate(_path, m_allItemDataList);
            }
            if (GUILayout.Button("测试测试"))
            {
                CheckTemplate();
            }
            GUILayout.EndHorizontal();

            if (m_selectTransform == null)
            {
                return;
            }

            #region  ItemData;
            m_scrollView = GUILayout.BeginScrollView(m_scrollView);

            m_allItemDataList.Clear();
            m_allItemDataList = GetAllItemDataList(m_selectTransform);

            for (int i = 0; i < m_allItemDataList.Count; i++)
            {
                GenerateItemData _data = m_allItemDataList[i];
                EditorGUILayout.BeginHorizontal();
                string _showName = _data.Name;
                EditorGUI.indentLevel = _data.Index;
                if (_data.ChildCount > 0)
                {
                    _data.ShowChildren = EditorGUILayout.Foldout(_data.ShowChildren, _showName);
                }
                else
                {
                    EditorGUILayout.LabelField("  " + _showName);
                }
                bool _toggle = false;
                for (int _comIndex = 0; _comIndex < _data.ItemComponentStrList.Count; _comIndex++)
                {
                    string _com = _data.ItemComponentStrList[_comIndex];
                    _toggle = GUILayout.Toggle(_data.GetItemComponentShowState(_com), _com, GUILayout.Width(120));
                    _data.UpdateItemComponentDic(_com, _toggle);
                }
                EditorGUILayout.EndHorizontal();

            }

            GUILayout.EndScrollView();

            #endregion
        }

        private Transform GetSelectTranform()
        {
            return Selection.activeTransform;
        }

        private List<Transform> GetAllTransform(Transform tran)
        {
            List<Transform> _tempList = new List<Transform>();
            if (tran == null)
            {
                return _tempList;
            }
            _tempList.Add(tran);
            if (tran.childCount > 0)
            {
                for (int i = 0; i < tran.childCount; i++)
                {
                    _tempList.AddRange(GetAllTransform(tran.GetChild(i)));
                }
            }
            return _tempList;
        }

        private List<GenerateItemData> GetAllItemDataList(Transform tran, GenerateItemData parent = null, int siblingIndex = 0)
        {
            List<GenerateItemData> _tempList = new List<GenerateItemData>();

            if (tran == null)
            {
                return _tempList;
            }

            GenerateItemData _data = new GenerateItemData();
            _data.InitItem(tran, parent, siblingIndex);
            _tempList.Add(_data);
            if (!_data.ShowChildren)
            {
                return _tempList;
            }

            for (int i = 0; i < tran.childCount; i++)
            {
                _tempList.AddRange(GetAllItemDataList(tran.GetChild(i), _data, i));
            }
            return _tempList;

        }

        public string TemplatePath
        {
            get
            {
                return GenerateCodeManager.TemplateFilePath;
            }
        } 

        private void CheckTemplate()
        {
            string _testPath = GenerateCodeManager.DirectoryPath;
            if (!Directory.Exists(_testPath))
            {
                Directory.CreateDirectory(_testPath);
            }
            if (!File.Exists(GenerateCodeManager.TemplateFilePath))
            {
                GenerateCodeManager.CheckFileExistAndCreate(GenerateCodeManager.TemplateFilePath);
            }
        }
        /// <summary>
        /// 根据模板写文件
        /// </summary>
        /// <param name="path"></param>
        /// <param name="_allItemDataList"></param>
        public void WriteFileWithTemplate(string path, List<GenerateItemData> _allItemDataList)
        {
            CheckTemplate();
            //读
            StreamReader _testReader = new StreamReader(TemplatePath, Encoding.Default);
            string _text = _testReader.ReadToEnd();
            string _scriptName = Selection.activeGameObject.name;
            string _componentVariabelName = string.Empty;
            string _getComponent = string.Empty;
            string _setButtonEvent = string.Empty;
            string _getButtonFunc = string.Empty;

            Dictionary<string, CodeItemData> _itemDataDic = GenerateCodeManager.GetCodeItemDataDic(_allItemDataList);

            foreach (var item in _itemDataDic)
            {
                string _variavleName = item.Value.GetVariableName();
                string _path = item.Value.GetComponentPath();
                _componentVariabelName += _variavleName;
                _getComponent += _path;
                _setButtonEvent += item.Value.SetButtonEvent();
                _getButtonFunc += item.Value.GetButtonFunction();
            }

            _text = _text.Replace("$safeitemrootname$", _scriptName);
            _text = _text.Replace("$componentvariablename$", _componentVariabelName);
            _text = _text.Replace("$getcomponent$", _getComponent);
            _text = _text.Replace("$setbuttonevent$", _setButtonEvent);
            _text = _text.Replace("$setbuttonfunction$", _getButtonFunc);
            _testReader.Close();

            //写
            FileStream _fs = new FileStream(path, FileMode.OpenOrCreate);
            StreamWriter _textWriter = new StreamWriter(_fs, Encoding.Default);
            _textWriter.Write(_text);

            _textWriter.Flush();
            _textWriter.Close();
            _fs.Close();

            System.Diagnostics.Process.Start("explorer.exe", path);

            Debug.Log("代码生成成功!"+ path);

        }

    }
}

image.gif

最后效果就是这样的:

image
image.gif

额,这个好像不是最新的。我还改了点GUI的东西,有空在改吧。

啊,好累,就写这么多吧,有空了想到没有写到的又来补充吧,如果你恰好看到这篇博客,觉得有漏洞,记得给我说呀!

以上就是我用C#写的一个利用txt模板来生成代码的工具。如果你有更好的生成工具,期待你的分享!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容