Unity 之 Pure版Entity?Component?System?(ECS) ?官方Rotation示例解析

又有一段時間沒有寫博客了,真是不爽~~~ 趁著最近兩天沒事,趕緊補上一篇,這次開始寫一篇Pure ECS版本示例解析,對上次Hybrid版本Unity之淺析 Entity Component System (ECS)的補充,使用的是官方案例的Rotation場景。


有說錯或不準確的地方歡迎留言指正


Unity版本 208.2.3f1 Entities 版本 preview.8

ECS雖然現在已經實裝,但還在實驗階段,筆者在開發的過程中也遇到了一些IDE卡頓,Unity編輯器崩潰的情況。這個情況相信在Unity后續版本中會得到改善。

這么多問題為什么還要用呢?那就是計算速度快?。?!真的很快,筆者這垃圾筆記本此場景創建20W個Cube還能保持在20幀左右,所以可見一斑。

主要參考官方文檔地址

對應工程文件下載


  • 2018/08/29更新 添加 [BurstComplie]特性 以后如果你打開Burst Complier的話,下面的代碼會在編譯的時候被Burst Compiler優化,運行速度更快,目前Burst只是運行在編輯器模式下,之后正式出了會支持編譯

效果展示

下面筆者會逐步創建示例中的場景,使用Unity版本2018.2.3f1 ,基本配置請參考Unity之淺析 Entity Component System (ECS)

首選需要準備的資源為:

  • Unity對應Logo模型
  • 一個在場景中對應的Logo Object
  • 一個產卵器,生產指定Cube按照規定半徑隨機分布

創建Unity對應Logo模型

在hierarchy中創建一個gameObject命名為RotationLogo然后添加組下組件,這些組件都是ECS自帶的

  • GameObjectEntity 必帶組件,沒有的話ECS系統不識別
  • PositionComponent 組件對應傳統模式中 transform.position
  • CopyInitialTransformFromGameObjectComponent 初始化TransformMatrix中的數據
  • TransformMatrix 指定應該存儲一個4x4矩陣。這個矩陣是根據位置的變化自動更新的【直譯官方文檔】
  • MeshInstanceRendererComponent可以理解為原來的Mesh Filter與Mesh Renderer結合體,且大小不受tranform中Scale數值控制
  • MoveSpeedComponent也是官方自帶組件,因為ECS主要是面向數據編程,此組件僅僅代表一個運行速度的數據

注意:MeshInstanceRendererComponent中需要Mesh是指定使用哪個網格,對應的Material需要勾選Enable GPU Instancing


創建一個產卵器,生產指定Cube按照規定半徑隨機分布

在hierarchy中創建一個gameObject命名為RotatingCubeSpawner然后添加如下組件,這些組件都是ECS自帶的,這里沒有使用TransformMatrix 組件,因為TransformMatrix 組件需要配合其他組件或系統使用,例如MeshInstanceRenderer,這里RotatingCubeSpawner僅僅是一個產卵觸發,所以不需要。

創建腳本 SpawnRandomCircleComponent ,然后添加到RotatingCubeSpawner上

using System;
using Unity.Entities;
using UnityEngine;


/// <summary>
/// 使用ISharedComponentData可顯著降低內存
/// </summary>
[Serializable]
public struct SpawnRandomCircle : ISharedComponentData//使用ISharedComponentData可顯著降低內存
{
    //預制方塊
    public GameObject prefab;
    public bool spawnLocal;
    //生成的半徑
    public float radius;
    //生成物體個數
    public int count;
}

/// <summary>
/// 包含方塊的個數個生成半徑等
/// </summary>
public class SpawnRandomCircleComponent : SharedComponentDataWrapper<SpawnRandomCircle> { }

在傳統模式中,我們能把腳本掛到gameObejc上是因為繼承了MonoBehaviour,但是在Pure ECS版本中,如需要的數據掛在對應的Object上,創建的類需要繼承SharedComponentDataWrapperComponentDataWrapper,包含的數據(struct)需要繼承ISharedComponentDataIComponentData
這里大家可能有疑問了,既然都能創建掛載為什么出現兩個類?使用SharedComponentDataWrapper與ISharedComponentData可顯著降低內存,創建100個cube和一個cube的消耗內存的差異幾乎為零。如使用的數據僅僅是讀取,或很少的改變,且在同Group中(后續示例中有展示),使用SharedComponentData是一個不錯的選擇。


接下來開始編寫Logo模型旋轉所需的額外數據

按照示例顯示,Logo圖標在一個指定的位置以規定的半徑旋轉,在Logo一定范圍的cube會觸發旋轉效果

創建如下數據添加到Object上

旋轉中心點和對應半徑的數據
using System;
using Unity.Entities;
using Unity.Mathematics;

/// <summary>
/// 轉動Logo的中心點和轉動半徑
/// </summary>
[Serializable]
public struct MoveAlongCircle : IComponentData
{
    //Logo對應的中心點
    public float3 center;
    //Logo對應的半徑
    public float radius;
    //運行時間
    //[NonSerialized]
    public float t;
}
/// <summary>
/// 轉動Logo的中心點和轉動半徑
/// </summary>
public class MoveAlongCircleComponent : ComponentDataWrapper<MoveAlongCircle> { }
Logo碰撞方塊后給予方塊重置的速度數據
using System;
using Unity.Entities;

/// <summary>
/// Logo碰撞方塊后給予方塊重置的速度
/// </summary>
[Serializable]
public struct RotationSpeedResetSphere : IComponentData
{
    //方塊重置的速度
    public float speed;
}
/// <summary>
/// 方塊旋轉的速度
/// </summary>
public class RotationSpeedResetSphereComponent : ComponentDataWrapper<RotationSpeedResetSphere> { }
觸發方塊旋轉的半徑數據
using System;
using Unity.Entities;


[Serializable]
public struct Radius : IComponentData
{
    //觸發方塊旋轉的半徑
    public float radius;
}
/// <summary>
/// 觸發方塊旋轉的半徑
/// </summary>
public class RadiusComponent : ComponentDataWrapper<Radius> { }

話不多說,接下來要讓Logo嗨起來! 哦不對,讓Logo轉起來。。。。

下面是Logo旋轉的全部邏輯代碼,筆者會逐步為大家解析

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;


//Logo運動相關邏輯
public class MoveAlongCircleSystem : JobComponentSystem
{
    // Logo運動相關邏輯中需要用到的數據
    struct MoveAlongCircleGroup
    {
        //Logo位置
        public ComponentDataArray<Position> positions;
        //旋轉的中心點和半徑數據
        public ComponentDataArray<MoveAlongCircle> moveAlongCircles;
        //旋轉速度數據
        [ReadOnly] public ComponentDataArray<MoveSpeed> moveSpeeds;
        //固定寫法
        public readonly int Length;
    }
    //注入數據 Inject自帶特性
    [Inject] private MoveAlongCircleGroup m_MoveAlongCircleGroup;


    [BurstCompile]
    struct MoveAlongCirclePosition : IJobParallelFor//Logo位置旋轉更新邏輯,可以理解為傳統模式中的Update
    {
        /// <summary>
        /// 位置數據
        /// </summary>
        public ComponentDataArray<Position> positions;
        /// <summary>
        /// 中心點及半徑數據
        /// </summary>
        public ComponentDataArray<MoveAlongCircle> moveAlongCircles;
        /// <summary>
        /// 運行速度
        /// </summary>
        [ReadOnly] public ComponentDataArray<MoveSpeed> moveSpeeds;
        /// <summary>
        /// 運行時間
        /// </summary>
        public float dt;

        /// <summary>
        /// 并行執行for循環 i 根據length計算 打印的一直是0
        /// </summary>
        /// <param name="i"></param>
        public void Execute(int i)
        {
            //Debug.Log(i); //打印的一直是0 雖然可以打印,但是會報錯,希望官方會出針對 ECS 的 Debug.Log

            //運行時間
            float t = moveAlongCircles[i].t + (dt * moveSpeeds[i].speed);
            //位置偏移量
            float offsetT = t + (0.01f * i);
            float x = moveAlongCircles[i].center.x + (math.cos(offsetT) * moveAlongCircles[i].radius);
            float y = moveAlongCircles[i].center.y;
            float z = moveAlongCircles[i].center.z + (math.sin(offsetT) * moveAlongCircles[i].radius);

            moveAlongCircles[i] = new MoveAlongCircle
            {
                t = t,
                center = moveAlongCircles[i].center,
                radius = moveAlongCircles[i].radius
            };
            //更新Logo的位置
            positions[i] = new Position
            {
                Value = new float3(x, y, z)
            };
        }
    }

    //數據初始化
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var moveAlongCirclePositionJob = new MoveAlongCirclePosition();
        moveAlongCirclePositionJob.positions = m_MoveAlongCircleGroup.positions;
        moveAlongCirclePositionJob.moveAlongCircles = m_MoveAlongCircleGroup.moveAlongCircles;
        moveAlongCirclePositionJob.moveSpeeds = m_MoveAlongCircleGroup.moveSpeeds;
        moveAlongCirclePositionJob.dt = Time.deltaTime;
        return moveAlongCirclePositionJob.Schedule(m_MoveAlongCircleGroup.Length, 64, inputDeps);
    }
}

解析一

其中這段code 指的是需要聲明一個Group 【可以理解為傳統模式中組件的集合】,這里含有Logo運動相關邏輯中需要用到的數據,注入m_MoveAlongCircleGroup,可以使在unity運行時unity自動尋找符合此數據集合的物體,然后把對應的數據都注入到m_MoveAlongCircleGroup中。這樣我們也就變相的找到了Logo物體

解析二

struct MoveAlongCirclePosition : IJobParallelFor代碼塊中的Execute,可以理解為傳統模式中的Update,不過是并行執行的。相關邏輯就是計算運行時間、運算位置并賦值。

以為這就完了,并沒有,看下面

解析三

想要把MoveAlongCirclePosition中的變量和我們找到的物體聯系起來,且在Job系統中并行執行就需要JobHandle OnUpdate。他的作用是把我們包裝起來的業務邏輯【就是Execute】放到Job系統執行【多核心并行計算】,并且把找到的物體和MoveAlongCirclePosition中的變量關聯起來。

下面我們要讓產卵器動起來

準備產卵器中預制體

在hierarchy中創建一個gameObject命名為RotatingCube然后添加如下組件

除官方自帶組件外添加額外組件RotationSpeedComponent和RotationAccelerationComponent,分別代表cube實時的旋轉速度和cube速度衰減的加速度

實時的旋轉速度 數據
using System;
using Unity.Entities;

/// <summary>
/// 方塊自身速度
/// </summary>
[Serializable]
public struct RotationSpeed : IComponentData
{
    public float Value;
}
public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { }

速度衰減的加速度 數據
using System;
using Unity.Entities;
/// <summary>
/// 方塊的加速度 -1 速度逐漸變慢
/// </summary>
[Serializable]
public struct RotationAcceleration : IComponentData
{
    public float speed;
}
public class RotationAccelerationComponent : ComponentDataWrapper<RotationAcceleration> { }

然后把預制體拖拽到指定的產卵器中,設置好數據

產卵Cube全部Code

using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

//產卵器系統相關邏輯
public class SpawnRandomCircleSystem : ComponentSystem
{
    //對應產卵器的組件集合
    struct Group
    {
        //含有產卵所需的 個數、半徑、預制體數據
        [ReadOnly] public SharedComponentDataArray<SpawnRandomCircle> Spawner;
        //產卵器位置數據
        public ComponentDataArray<Position> Position;
        //產卵器對應的 GameObject Entity 實體
        public EntityArray Entity;
        //因為目前產卵器只有一個,所以其 Length 數值為 1
        public readonly int Length;
    }
    //注入組件集合
    [Inject] Group m_Group;


    protected override void OnUpdate()
    {

        while (m_Group.Length != 0)
        {
            var spawner = m_Group.Spawner[0];
            var sourceEntity = m_Group.Entity[0];
            var center = m_Group.Position[0].Value;

            //根據產卵的個數聲明對應個數的 entities 數組
            var entities = new NativeArray<Entity>(spawner.count, Allocator.Temp);
            //實例化cube
            EntityManager.Instantiate(spawner.prefab, entities);
            //創建對應的position數組(個數等于cube創建個數)
            var positions = new NativeArray<float3>(spawner.count, Allocator.Temp);

            if (spawner.spawnLocal)
            {
                //計算出每一個Cube對應的Position位置 使用 ref 填充
                GeneratePoints.RandomPointsOnCircle(new float3(), spawner.radius, ref positions);

                //遍歷Position賦值
                for (int i = 0; i < spawner.count; i++)
                {
                    var position = new LocalPosition
                    {
                        Value = positions[i]
                    };
                    //為每一個Entity賦值
                    EntityManager.SetComponentData(entities[i], position);
                    //因為選擇的是spawnLocal,所以要為對應的 entity添加 TransformParent(類似于原來的 transform.SetParent)
                    EntityManager.AddComponentData(entities[i], new TransformParent { Value = sourceEntity });
                }
            }
            else
            {
                GeneratePoints.RandomPointsOnCircle(center, spawner.radius, ref positions);
                for (int i = 0; i < spawner.count; i++)
                {
                    var position = new Position
                    {
                        Value = positions[i]
                    };
                    EntityManager.SetComponentData(entities[i], position);
                }
            }

            entities.Dispose();
            positions.Dispose();

            EntityManager.RemoveComponent<SpawnRandomCircle>(sourceEntity);

            //實例化 & AddComponent和RemoveComponent調用使注入的組無效,
            //所以在我們進入下一個產卵之前我們必須重新注入它們
            UpdateInjectedComponentGroups();
        }
    }
}
解析一

看到 ComponentSystem我們就可以知道里面的主要業務邏輯是基于Hybrid版ECS實現的,還是老套路,聲明組件集合(產卵器),然后注入變量m_Group中

解析二

在這一段代碼塊中我們可以看到,因為Length==1(一個產卵器),所以后進入到while循環中執行對應的業務邏輯,當然在最后Length會為0,后續會提到原因。會根據產卵的個數聲明對應個數的 entities 數組。使用EntityManager.Instantiate實例化Cube,創建對應的position數組(個數等于cube創建個數)。使用EntityManager.Instantiate最明顯的特點是創建的Cube在hierarchy視圖中是沒有的。

解析三

使用GeneratePoints.RandomPointsOnCircle設置對應的隨機位置(工程中有提供)。區分使用Local Position主要是這兩地方,用EntityManager.AddComponentData把對應的父物體數據添加進去,類似于原來的 transform.SetParent。

LocalPosition中有數據
LocalPosition中不含有數據
解析四

這一部分也是使Length的值變為0的關鍵,把無用的數據entities與positions進行釋放。移除對應的產卵器再重新注入。換句話說就是destory產卵器。


然后我們創建一個能讓Cube自轉的sysytem,類似于

自轉系統Code

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
    {
        //Time.deltaTime
        public float dt;

        public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
        {
            rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.axisAngle(math.up(), speed.Value * dt));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new RotationSpeedRotation() { dt = Time.deltaTime };
        return job.Schedule(this, 64, inputDeps);
    }
}

解析一

在自轉系統中我們沒有指定對應的Group(組件系統集合),而且執行的Execute代碼塊所繼承接口IJobParallelFor代替為IJobProcessComponentData,IJobProcessComponentData文檔中的解釋筆者并是不是很理解,但根據測試的結果筆者認為是使用ref關鍵字搜索全部的Rotation組件,然后把自身的RotationSpeed數值賦值進去。因為如果在Logo上添加Rotation與RotationSpeed組件,Logo物體也會進行旋轉(賦值相關代碼下面會有講解)。


觸發Cube旋轉系統

全部Code

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;

//在RotationSpeedSystem前運行
[UpdateBefore(typeof(RotationSpeedSystem))]
public class RotationSpeedResetSphereSystem : JobComponentSystem
{
    /// <summary>
    /// Logo對應的entity group
    /// </summary>
    struct RotationSpeedResetSphereGroup
    {
        //Logo給予Cube速度對應的數據
        [ReadOnly] public ComponentDataArray<RotationSpeedResetSphere> rotationSpeedResetSpheres;
        //Logo對應的旋轉半徑
        [ReadOnly] public ComponentDataArray<Radius> spheres;
        //Logo對應的位置
        [ReadOnly] public ComponentDataArray<Position> positions;
        public readonly int Length;
    }
    //注入Logo組件集合
    [Inject] RotationSpeedResetSphereGroup m_RotationSpeedResetSphereGroup;

    /// <summary>
    /// 方塊的entity group
    /// </summary>
    struct RotationSpeedGroup
    {
        //方塊自身的旋轉速度
        public ComponentDataArray<RotationSpeed> rotationSpeeds;
        //方塊的位置
        [ReadOnly] public ComponentDataArray<Position> positions;
        //固定寫法 數值等于Cube的個數
        public readonly int Length;
    }
    //注入Cube組件集合
    [Inject] RotationSpeedGroup m_RotationSpeedGroup;

    [BurstCompile]
    struct RotationSpeedResetSphereRotation : IJobParallelFor
    {
        /// <summary>
        /// 方塊的速度
        /// </summary>
        public ComponentDataArray<RotationSpeed> rotationSpeeds;
        /// <summary>
        /// 方塊的坐標
        /// </summary>
        [ReadOnly] public ComponentDataArray<Position> positions;

        //下面都是Logo上面的組件
        [ReadOnly] public ComponentDataArray<RotationSpeedResetSphere> rotationSpeedResetSpheres;
        [ReadOnly] public ComponentDataArray<Radius> spheres;
        [ReadOnly] public ComponentDataArray<Position> rotationSpeedResetSpherePositions;

        public void Execute(int i)//i 0-9  這個i值取對應 Schedule 中設置的 arrayLength 的數值 此Code中設置的為 m_RotationSpeedGroup.Length
        {
            //UnityEngine.Debug.Log($"長度{i}");
            //方塊的中心點
            var center = positions[i].Value;

            for (int positionIndex = 0; positionIndex < rotationSpeedResetSpheres.Length; positionIndex++)
            {
                //計算圓球與方塊的距離 ,小于指定具體傳入速度
                if (math.distance(rotationSpeedResetSpherePositions[positionIndex].Value, center) < spheres[positionIndex].radius)
                {
                    rotationSpeeds[i] = new RotationSpeed
                    {
                        Value = rotationSpeedResetSpheres[positionIndex].speed
                    };
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var rotationSpeedResetSphereRotationJob = new RotationSpeedResetSphereRotation
        {
            rotationSpeedResetSpheres = m_RotationSpeedResetSphereGroup.rotationSpeedResetSpheres,
            spheres = m_RotationSpeedResetSphereGroup.spheres,
            rotationSpeeds = m_RotationSpeedGroup.rotationSpeeds,
            rotationSpeedResetSpherePositions = m_RotationSpeedResetSphereGroup.positions,
            positions = m_RotationSpeedGroup.positions
        };

        return rotationSpeedResetSphereRotationJob.Schedule(m_RotationSpeedGroup.Length, 32, inputDeps);
    }
}

解析一

用的還是前面的老套路,與以往不同是在RotationSpeedResetSphereSystem上添加的[UpdateBefore(typeof(RotationSpeedSystem))]特性,他負責確保RotationSpeedResetSphereSystem在RotationSpeedSystem前執行,可以理解為手動的控制執行順序


最后一步就是Cube速度衰減系統

全部Code

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Burst;
using Unity.Mathematics;
using UnityEngine;

public class RotationAccelerationSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedAcceleration : IJobProcessComponentData<RotationSpeed, RotationAcceleration>
    {
        public float dt;
        //對Cube自身的RotationSpeed進行衰減處理
        public void Execute(ref RotationSpeed speed, [ReadOnly]ref RotationAcceleration acceleration)
        {
            speed.Value = math.max(0.0f, speed.Value + (acceleration.speed * dt));
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var rotationSpeedAccelerationJob = new RotationSpeedAcceleration { dt = Time.deltaTime };
        return rotationSpeedAccelerationJob.Schedule(this, 64, inputDeps);
    }
}

解析一

使用的也是IJobProcessComponentData接口,整體和自旋轉系統基本一致。

打完收工?。。≌婺岈斃踾~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容