Unity杂文——Editor的Tree

原文地址

简介

本文介绍了一种用于编辑器开发过程中树形结构数据进行渲染的辅助脚本。

TreeElementGUI

TreeElementGUI是树形结构元素渲染的基类,包含了节点深度、父节点、子节点、节点名字、节点ID等属性,并且提供了无参构造函数和有参构造函数。

<details>
<summary> TreeElementGUI </summary>

/// <summary>
/// 树结构的元素
/// </summary>
public abstract class TreeElementGUI
{
    private int m_ID;
    private string m_Name;
    private int m_Depth;
    [NonSerialized] private TreeElementGUI m_Parent;
    [NonSerialized] private List<TreeElementGUI> m_Children;

    /// <summary>
    /// 深度
    /// </summary>
    public int Depth
    {
        get => m_Depth;
        set => m_Depth = value;
    }

    /// <summary>
    /// 父节点
    /// </summary>
    public TreeElementGUI Parent
    {
        get => m_Parent;
        set => m_Parent = value;
    }

    /// <summary>
    /// 子节点
    /// </summary>
    public List<TreeElementGUI> Children
    {
        get => m_Children;
        set => m_Children = value;
    }
    
    /// <summary>
    /// 是否有子节点
    /// </summary>
    public bool HasChildren => m_Children != null && m_Children.Count > 0;

    /// <summary>
    /// 节点名字
    /// </summary>
    public string Name
    {
        get => m_Name;
        set => m_Name = value;
    }

    /// <summary>
    /// 节点ID
    /// </summary>
    public int Id
    {
        get => m_ID;
        set => m_ID = value;
    }

    /// <summary>
    /// 无参构造函数
    /// </summary>
    protected TreeElementGUI() :this(-1, -1, "")
    {
        
    }

    /// <summary>
    /// 有参构造函数
    /// </summary>
    /// <param name="id"></param>
    /// <param name="depth"></param>
    /// <param name="name"></param>
    protected TreeElementGUI(int id, int depth, string name)
    {
        m_Name = name;
        m_ID = id;
        m_Depth = depth;
    }

    public abstract void OnGUI(Rect rect, int columnIndex);
    public abstract bool IsMatchSearch(string search);
}

</details>

TreeViewItemGUI<T>

TreeViewItemGUI<T>是节点元素显示的类,继承自TreeViewItem,并且包含了一个泛型参数T,其中T必须是TreeElementGUI的子类。TreeViewItemGUI<T>包含了一个Data属性,用于获取节点元素的数据,同时提供了OnGUI和IsMatchSearch方法,用于在UI上绘制节点元素和进行搜索匹配。

<details>
<summary> TreeViewItemGUI<T> </summary>

/// <summary>
/// 树结构编辑器显示
/// </summary>
/// <typeparam name="T"></typeparam>
public class TreeViewItemGUI<T> : TreeViewItem where T : TreeElementGUI
{
    private readonly T m_Data;

    public T Data => m_Data;

    public TreeViewItemGUI(int id, int depth, string displayName, T data) : base(id, depth, displayName)
    {
        m_Data = data;
    }

    public void OnGUI(Rect rect, int columnIndex)
    {
        m_Data.OnGUI(rect, columnIndex);
    }

    public bool IsMatchSearch(string search)
    {
        return m_Data.IsMatchSearch(search);
    }
}

</details>

TreeGUIUtility

TreeGUIUtility是一个用于编辑器开发过程中树形结构数据进行渲染的辅助脚本。该脚本包含了一些常用的方法,可以帮助开发者处理树形结构数据。

其中,TreeToList<T>方法可以将树形结构数据转换为列表形式,Find<T>方法可以在树形结构数据中查找符合条件的节点元素,ListToTree<T>方法可以将列表形式的数据转换为树形结构数据,ValidateDepthValues<T>方法可以检查列表中的深度值是否合法,UpdateDepthValues<T>方法可以更新树形结构数据中的深度值,FindCommonAncestorsWithinList<T>方法可以查找列表中共同的祖先节点。

<details>
<summary> TreeGUIUtility<T> </summary>

public static class TreeGUIUtility
{
    public static void TreeToList<T>(T root, IList<T> result) where T : TreeElementGUI
    {
        if (result == null)
            throw new NullReferenceException("The input 'IList<T> result' list is null");
        result.Clear();

        var stack = new Stack<T>();
        stack.Push(root);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            result.Add(current);

            if (current.Children != null && current.Children.Count > 0)
            {
                for (var i = current.Children.Count - 1; i >= 0; i--)
                {
                    stack.Push((T)current.Children[i]);
                }
            }
        }
    }
    
    public static T Find<T>(T root, Func<T, bool> comparer) where T : TreeElementGUI
    {
        var stack = new Stack<T>();
        stack.Push(root);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if(root != current && comparer(current))
            {
                return current;
            }
            if (current.Children != null && current.Children.Count > 0)
            {
                for (var i = current.Children.Count - 1; i >= 0; i--)
                {
                    stack.Push((T)current.Children[i]);
                }
            }
        }

        return null;
    }

    /// <summary>
    /// List转成树结构
    /// </summary>
    /// <param name="list"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T ListToTree<T>(IList<T> list) where T : TreeElementGUI
    {
        // 验证深度的值
        ValidateDepthValues(list);

        // 清理状态
        foreach (var element in list)
        {
            element.Parent = null;
            element.Children = null;
        }

        // 设置子节点和父节点
        for (var parentIndex = 0; parentIndex < list.Count; parentIndex++)
        {
            var parent = list[parentIndex];
            var alreadyHasValidChildren = parent.Children != null;
            if (alreadyHasValidChildren)
                continue;

            var parentDepth = parent.Depth;
            var childCount = 0;

            // Count children based depth value, we are looking at children until it's the same depth as this object
            for (var i = parentIndex + 1; i < list.Count; i++)
            {
                if (list[i].Depth == parentDepth + 1)
                    childCount++;
                if (list[i].Depth <= parentDepth)
                    break;
            }

            // Fill child array
            List<TreeElementGUI> childList = null;
            if (childCount != 0)
            {
                childList = new List<TreeElementGUI>(childCount); // Allocate once
                childCount = 0;
                for (var i = parentIndex + 1; i < list.Count; i++)
                {
                    if (list[i].Depth == parentDepth + 1)
                    {
                        list[i].Parent = parent;
                        childList.Add(list[i]);
                        childCount++;
                    }

                    if (list[i].Depth <= parentDepth)
                        break;
                }
            }

            parent.Children = childList;
        }

        return list[0];
    }

    /// <summary>
    /// 检查List的深度值
    /// </summary>
    /// <param name="list"></param>
    /// <typeparam name="T"></typeparam>
    /// <exception cref="ArgumentException"></exception>
    public static void ValidateDepthValues<T>(IList<T> list) where T : TreeElementGUI
    {
        if (list.Count == 0)
            throw new ArgumentException("list should have items, count is 0, check before calling ValidateDepthValues", nameof(list));

        if (list[0].Depth != -1)
            throw new ArgumentException("list item at index 0 should have a depth of -1 (since this should be the hidden root of the tree). Depth is: " + list[0].Depth, nameof(list));

        for (var i = 0; i < list.Count - 1; i++)
        {
            var depth = list[i].Depth;
            var nextDepth = list[i + 1].Depth;
            if (nextDepth > depth && nextDepth - depth > 1)
                throw new ArgumentException(string.Format("Invalid depth info in input list. Depth cannot increase more than 1 per row. Index {0} has depth {1} while index {2} has depth {3}", i, depth, i + 1, nextDepth));
        }

        for (var i = 1; i < list.Count; ++i)
            if (list[i].Depth < 0)
                throw new ArgumentException("Invalid depth value for item at index " + i + ". Only the first item (the root) should have depth below 0.");

        if (list.Count > 1 && list[1].Depth != 0)
            throw new ArgumentException("Input list item at index 1 is assumed to have a depth of 0", nameof(list));
    }


    /// <summary>
    /// 更新深度值
    /// </summary>
    /// <param name="root"></param>
    /// <typeparam name="T"></typeparam>
    /// <exception cref="ArgumentNullException"></exception>
    public static void UpdateDepthValues<T>(T root) where T : TreeElementGUI
    {
        if (root == null)
            throw new ArgumentNullException(nameof(root), "The root is null");

        if (!root.HasChildren)
            return;

        var stack = new Stack<TreeElementGUI>();
        stack.Push(root);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if (current.Children != null)
            {
                foreach (var child in current.Children)
                {
                    child.Depth = current.Depth + 1;
                    stack.Push(child);
                }
            }
        }
    }

    /// <summary>
    /// 判断是否是子节点
    /// </summary>
    /// <param name="child"></param>
    /// <param name="elements"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    static bool IsChildOf<T>(T child, IList<T> elements) where T : TreeElementGUI
    {
        while (child != null)
        {
            child = (T)child.Parent;
            if (elements.Contains(child))
                return true;
        }
        return false;
    }

    /// <summary>
    /// 查找共同的祖先节点
    /// </summary>
    /// <param name="elements"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static IList<T> FindCommonAncestorsWithinList<T>(IList<T> elements) where T : TreeElementGUI
    {
        if (elements.Count == 1)
            return new List<T>(elements);

        var result = new List<T>(elements);
        result.RemoveAll(g => IsChildOf(g, elements));
        return result;
    }
}

</details>

TreeGUIModel<T>

TreeGUIModel<T>是一个用于管理树形结构数据的类,包含了增加、移除、移动、清空、更新等方法,可以帮助开发者更方便地处理树形结构数据。

<details>
<summary> TreeGUIModel<T> </summary>

public class TreeGUIModel<T> where T : TreeElementGUI, new()
{
    private T m_Root;
    private int m_MaxID;
    private bool m_IsDirty;

    public T Root => m_Root;

    public event Action<T> added;
    public event Action<T> removed;

    public int Count => m_Root.Children.Count;
    public bool IsDirty => m_IsDirty;

    public TreeGUIModel()
    {
        m_Root = new T
        {
            Id = GenerateUniqueID(), Depth = -1, Name = $"{typeof(T).Name} - Root",
            Children = new List<TreeElementGUI>()
        };
        m_IsDirty = true;
    }

    /// <summary>
    /// 根据id查找元素
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public T Find(int id)
    {
        return (T)m_Root.Children.FirstOrDefault(element => element.Id == id);
    }

    /// <summary>
    /// 自动生成唯一ID
    /// </summary>
    /// <returns></returns>
    public int GenerateUniqueID()
    {
        return ++m_MaxID;
    }

    /// <summary>
    /// 获得所有的子节点
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public IList<int> GetAncestors(int id)
    {
        var parents = new List<int>();
        var item = Find(id);
        if (item != null)
        {
            while (item.Parent != null)
            {
                parents.Add(item.Parent.Id);
                item = (T)item.Parent;
            }
        }
        return parents;
    }

    /// <summary>
    /// 获得有子节点的所有子节点
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public IList<int> GetDescendantsThatHaveChildren(int id)
    {
        var searchFromThis = Find(id);
        return searchFromThis != null ? GetParentsBelowStackBased(searchFromThis) : new List<int>();
    }

    /// <summary>
    /// 获得基于栈的所有子节点
    /// </summary>
    /// <param name="searchFromThis"></param>
    /// <returns></returns>
    private IList<int> GetParentsBelowStackBased(TreeElementGUI searchFromThis)
    {
        var stack = new Stack<TreeElementGUI>();
        stack.Push(searchFromThis);

        var parentsBelow = new List<int>();
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if (current.HasChildren)
            {
                parentsBelow.Add(current.Id);
                foreach (var T in current.Children)
                {
                    stack.Push(T);
                }
            }
        }

        return parentsBelow;
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elementID"></param>
    public void RemoveElements(int elementID)
    {
        var elements = m_Root.Children.Where(element => element.Id == elementID).Cast<T>().ToArray();
        RemoveElements(elements);
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elementIDs"></param>
    public void RemoveElements(IList<int> elementIDs)
    {
        var elements = m_Root.Children.Where(element => elementIDs.Contains(element.Id)).Cast<T>().ToArray();
        RemoveElements(elements);
    }

    /// <summary>
    /// 移除元素
    /// </summary>
    /// <param name="elements"></param>
    public void RemoveElements(IList<T> elements)
    {
        var commonAncestors = TreeGUIUtility.FindCommonAncestorsWithinList(elements);

        foreach (var element in commonAncestors)
        {
            element.Parent.Children.Remove(element);
            element.Parent = null;
            removed?.Invoke(element);
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="elements"></param>
    /// <param name="parent"></param>
    /// <param name="insertPosition"></param>
    /// <param name="isNew"></param>
    /// <exception cref="ArgumentNullException"></exception>
    public void AddElements(IList<T> elements, TreeElementGUI parent, int insertPosition, bool isNew = false)
    {
        if (elements == null)
            throw new ArgumentNullException(nameof(elements), "elements is null");
        if (elements.Count == 0)
            throw new ArgumentNullException(nameof(elements), "elements Count is 0: nothing to add");
        if (parent == null)
            throw new ArgumentNullException(nameof(parent), "parent is null");

        parent.Children ??= new List<TreeElementGUI>();

        parent.Children.InsertRange(insertPosition, elements);
        foreach (var element in elements)
        {
            element.Parent = parent;
            element.Depth = parent.Depth + 1;
            TreeGUIUtility.UpdateDepthValues(element);
            if(isNew)
            {
                added?.Invoke(element);
            }
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="root"></param>
    /// <param name="isNew"></param>
    public void AddElement(T root, bool isNew = false)
    {
        root.Id = GenerateUniqueID();
        root.Depth = -1;
        root.Parent = m_Root;
        m_Root.Children.Add(root);
        if(isNew)
        {
            added?.Invoke(root);
        }

        SetDirty();
    }

    /// <summary>
    /// 增加元素
    /// </summary>
    /// <param name="element"></param>
    /// <param name="parent"></param>
    /// <param name="insertPosition"></param>
    /// <param name="isNew"></param>
    public void AddElement(T element, T parent, int insertPosition, bool isNew = false)
    {
        parent.Children ??= new List<TreeElementGUI>();
        parent.Children.Insert(insertPosition, element);
        element.Parent = parent;

        TreeGUIUtility.UpdateDepthValues(parent);

        if (isNew)
        {
            added?.Invoke(element);
        }

        SetDirty();
    }

    /// <summary>
    /// 移动元素
    /// </summary>
    /// <param name="parentElement"></param>
    /// <param name="insertionIndex"></param>
    /// <param name="elements"></param>
    /// <exception cref="ArgumentException"></exception>
    public void MoveElements(TreeElementGUI parentElement, int insertionIndex, List<TreeElementGUI> elements)
    {
        if (insertionIndex < 0)
            throw new ArgumentException("Invalid input: insertionIndex is -1, client needs to decide what index elements should be reparented at");

        // Invalid reparenting input
        if (parentElement == null)
            return;

        // We are moving items so we adjust the insertion index to accomodate that any items above the insertion index is removed before inserting
        if (insertionIndex > 0)
            insertionIndex -= parentElement.Children.GetRange(0, insertionIndex).Count(elements.Contains);

        // Remove draggedItems from their parents
        foreach (var draggedItem in elements)
        {
            draggedItem.Parent.Children.Remove(draggedItem);    // remove from old parent
            draggedItem.Parent = parentElement;                 // set new parent
        }

        parentElement.Children ??= new List<TreeElementGUI>();

        // Insert dragged items under new parent
        parentElement.Children.InsertRange(insertionIndex, elements);

        TreeGUIUtility.UpdateDepthValues(Root);

        SetDirty();
    }

    /// <summary>
    /// 标记为脏
    /// </summary>
    private void SetDirty()
    {
        m_IsDirty = true;
    }

    /// <summary>
    /// 清理
    /// </summary>
    public void Clear()
    {
        m_Root.Children.Clear();
        SetDirty();
    }

    /// <summary>
    /// 更新
    /// </summary>
    internal void Update()
    {
        m_IsDirty = true;
    }
}

</details>

TreeViewWithGUIModel<T>

TreeViewWithGUIModel<T>是一个抽象类,继承自TreeView,用于在编辑器中渲染树形结构数据。该类包含了一些常用的方法,可以帮助开发者处理树形结构数据。

该类还包含了一些属性,用于控制树形结构数据的外观和行高等。

TreeViewWithGUIModel<T>类的子类可以通过实现抽象方法来自定义树形结构数据的渲染和行为。

<details>
<summary> TreeViewWithGUIModel<T> </summary>

public abstract class TreeViewWithGUIModel<T> : TreeView where  T : TreeElementGUI, new()
{
    protected TreeGUIModel<T> m_TreeModel;
    private readonly List<TreeViewItem> m_Rows = new List<TreeViewItem>(100);
    public bool ShowAlternatingRowBackgrounds
    {
        get => showAlternatingRowBackgrounds;
        set => showAlternatingRowBackgrounds = value;
    }

    public bool ShowBorder
    {
        get => showBorder;
        set => showBorder = value;
    }

    public float RowHeight
    {
        get => rowHeight;
        set => rowHeight = value;
    }

    protected TreeViewWithGUIModel(TreeViewState state, TreeGUIModel<T> model) : base(state)
    {
        Init(model);
    }

    protected TreeViewWithGUIModel(TreeViewState state, MultiColumnHeader multiColumnHeader, TreeGUIModel<T> model) : base(state, multiColumnHeader)
    {
        Init(model);
        multiColumnHeader.sortingChanged += OnSortingChanged;

    }
    private void Init(TreeGUIModel<T> model)
    {
        m_TreeModel = model;
    }

    private void OnSortingChanged(MultiColumnHeader _multiColumnHeader)
    {
        SortIfNeeded(rootItem, m_Rows);
    }

    private void SortIfNeeded(TreeViewItem root, List<TreeViewItem> rows)
    {
        if( null == multiColumnHeader) return;

        if ( rows.Count <= 1)
            return;

        if (multiColumnHeader.sortedColumnIndex == -1)
        {
            return; // No column to sort for (just use the order the data are in)
        }

        // Sort the roots of the existing tree items
        rootItem.children = SortByMultipleColumns(rows);
        TreeToList(root, rows);
        Repaint();
    }

    public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
    {
        if (root == null)
            throw new NullReferenceException("root");
        if (result == null)
            throw new NullReferenceException("result");

        result.Clear();

        if (root.children == null)
            return;

        var stack = new Stack<TreeViewItem>();
        for (var i = root.children.Count - 1; i >= 0; i--)
            stack.Push(root.children[i]);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            result.Add(current);

            if (current.hasChildren && current.children[0] != null)
            {
                for (var i = current.children.Count - 1; i >= 0; i--)
                {
                    stack.Push(current.children[i]);
                }
            }
        }
    }
    protected override TreeViewItem BuildRoot()
    {
        return null == m_TreeModel.Root
            ? new TreeViewItemGUI<T>(0, -1, "Root", null)
            : new TreeViewItemGUI<T>(m_TreeModel.Root.Id, -1, m_TreeModel.Root.Name, m_TreeModel.Root);
    }

    protected override bool DoesItemMatchSearch(TreeViewItem item, string search)
    {
        var target = (TreeViewItemGUI<T>)item;
        return target.IsMatchSearch(search);
    }

    protected override void RowGUI(RowGUIArgs args)
    {
        var item = (TreeViewItemGUI<T>) args.item;
        if (null == multiColumnHeader)
        {
            item.OnGUI(args.rowRect, 0);
        }
        else
        {
            var columns = args.GetNumVisibleColumns();
            for (var i = 0; i < columns; i++)
            {
                var rt = args.GetCellRect(i);
                CenterRectUsingSingleLineHeight(ref rt);
                item.OnGUI(rt, args.GetColumn(i));
            }
        }
    }

    protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        m_Rows.Clear();
        if (m_TreeModel.Root == null)
        {
            return m_Rows;
        }

        if (hasSearch)
        {
            Search(m_TreeModel.Root, searchString, m_Rows);
        }
        else if (m_TreeModel.Root.HasChildren)
        {
            AddChildrenRecursive(root, m_TreeModel.Root, 0, m_Rows);
        }
        SortIfNeeded(root, m_Rows);
        return m_Rows;
    }

    private void AddChildrenRecursive(TreeViewItem root, T parent, int depth, IList<TreeViewItem> newRows)
    {
        foreach (var treeElement in parent.Children)
        {
            var child = (T) treeElement;
            var item = new TreeViewItemGUI<T>(child.Id, depth, child.Name, child);
            newRows.Add(item);
            root.AddChild(item);

            if (child.HasChildren)
            {
                if (IsExpanded(child.Id))
                {
                    AddChildrenRecursive(item, child, depth + 1, newRows);
                }
                else
                {
                    item.children = CreateChildListForCollapsedParent();
                }
            }
        }
    }

    private void Search(T searchFromThis, string search, List<TreeViewItem> result)
    {
        if (string.IsNullOrEmpty(search))
            throw new ArgumentException("Invalid search: cannot be null or empty", nameof(search));

        var stack = new Stack<T>();
        foreach (var element in searchFromThis.Children)
            stack.Push((T)element);

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            // Matches search?
            if (current.IsMatchSearch(search))
            {
                result.Add(new TreeViewItemGUI<T>(current.Id, 0, current.Name, current));
            }

            if (current.Children != null && current.Children.Count > 0)
            {
                foreach (var element in current.Children)
                {
                    stack.Push((T)element);
                }
            }
        }
        SortSearchResult(result);
    }

    public override IList<TreeViewItem> GetRows()
    {
        return m_Rows;
    }

    protected virtual void SortSearchResult(List<TreeViewItem> rows)
    {
        // sort by displayName by default, can be overriden for multColumn solutions
        rows.Sort((x, y) => EditorUtility.NaturalCompare(x.displayName, y.displayName));
    }

    protected virtual List<TreeViewItem> SortByMultipleColumns(List<TreeViewItem> children)
    {
        return children;
    }


    public Rect DoLayout(params GUILayoutOption[] options)
    {
        if(m_TreeModel.IsDirty)
        {
            m_TreeModel.Update();
            Reload();
        }

        GUILayout.BeginVertical();
        var rect = GUILayoutUtility.GetRect(GUIContent.none,
                                           GUIStyle.none,
                                           options);
        OnGUI(rect);
        GUILayout.EndVertical();

        return rect;
    }
}

</details>

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

推荐阅读更多精彩内容