PlayMaker简单实例(2):角色与摄影机的运动控制

角色控制是游戏设计中必不可少的一个设计环节,这一节我们讲一讲如何制作基本的角色运动控制交互逻辑。

因为是简单实例教程,所以一律不涉及角色动画控制,只谈运动控制。


Demo演示:Simple Movement Control


常见的运动控制交互设计有这么几种:

  1. 方向控制类
    1. 使用单方向控制角色在特定平面内自由运动,角色始终面朝运动方向
    2. 使用双方向分别控制角色在特定平面内自由运动的位置以及运动方向(“双摇杆”射击)
    3. 使用单方向控制角色在棋盘格内非自由运动移动
  2. 目的地控制类
    1. 设置目的地让角色自动运动过去(“点击移动”式)

与运动控制息息相关的是摄影机运动控制,通常会有这么几种形式:

  1. 大鸟瞰视角摄影机不运动
  2. 固定视角摄影机(正俯视、斜俯视、正平视)跟随或半跟随角色
  3. 第三人称可变视角跟随摄影机
  4. 第一人称主视角摄影机

在Unity3D中,控制游戏物体的运动主要有这样几种实现方法:

  1. 完全依靠物理解算,基本不控制:比如我们在PlayMaker简单实例(1):PM_Cube中最后做的发射小方块的例子,设置力量、初始速度,然后就都交给物理引擎去计算了。
  2. 完全通过transform参数控制位移和旋转:比如我们让游戏物体永远跟随鼠标运动。
  3. 借助物理引擎进行部分控制:
    1. 对Rigidbody持续施加动力(force)或持续设定速度(velocity);
    2. 对Character Controller设置运动(move);
    3. 对NavMesh Agent设置运动(move)或设置目的地(set destination)。

完全依靠物理解算不适合用来进行准确操控,完全通过位移和旋转参数进行控制很难处理碰撞问题,所以最后一种方式是我们常用的。方向控制类通常借助Rigidbody或者Character Controller,目的地控制类通常借助NavMesh Agent。


范例01:单方向运动控制

分析:

单方向运动控制是持续不断给运动物体指明其运动方向和速度,且运动物体始终面向运动方向的一种控制方法,我们用键盘方向键、手柄摇杆、甚至用鼠标指针位置来为物体指引方向。

“速度”本质上是一个矢量,其方向代表了运动的方向,其长度代表了运动的快慢。

如果是键盘或者手柄的话,使用input axis就可以获取一个二维的矢量输入作为方向指示,再添加一个自定义的speed属性就可以了;如果是鼠标,则可以用鼠标相对于运动物体的位置作为其方向指示,鼠标离运动物体的距离长短作为速度指示。

准备场景

依旧沿用PM_Cube的项目文件,新建一个名叫Movement的场景。创建地面,并拼一个简易的Player角色出来。

Player角色由一个方块身体,一个方块头部,再加一个黑球以指示正面方向。其实用什么模型都可以,我习惯于用一个Player空物体作为顶级节点,把其他的视觉元素都放在Player下方。三个视觉元素的Collider组件我都删掉了,不需要它们进行碰撞解算,然后在Player上添加了一个Box Collider组件,调整到合适位置。

FSM_Movement

Player添加Fsm,改名为FSM_Movement

State 1中添加一个Get Axis Vector的行为以获得Axis Input,再添加一个Set Velocity的行为给Player设置速度。

出现这个提示代表我们缺少Rigidbody组件,Set Velocity只能作用于Rigidbody,点击提示自动添加。

Get Axis Vector中,默认已经把Horizontal AxisVertical Axis填好了,这个名称是和Input面板中的设置一一对应的,大家可以从菜单Edit > Project Settings > Input查看。设置Store Vector参数为一个新的变量input axis,让Get Axis Vector把输入矢量储存到这个变量中。

比如我们按下AWSD键,由于AD被map到了Horizontal AxisWD被map到了Vertical Axis,因此会造成Horizontal AxisVertical Axis的输入值从0快速变成1或者-1(W和D代表正向,为1,A和S代表反向,为-1),然后由于Get Axis Vector中Map To Plane设置的是“XZ”,所以输出矢量的X值会取得Horizontal Axis值,输出矢量的Z值会取得Vertical Axis值,输出矢量的Y为0。

Set Velocity中可以直接将input axis变量赋给Vector参数,然后设置运动坐标系(Space)为World,勾选上Every Frame选项。这样,游戏每时每刻都会监控Horizontal Axis和Vertical Axis的输入,并以此调整游戏物体的速度。

Get Axis VectorMultiplier参数会将输出矢量放大,所以我们也可以为Multiplier设置一个新变量speed,并在Variable面板中设置speed为10,并使其在Inspector中可见。

测试场景,Player在场景中翻滚,这是因为我们给了速度,但物体很轻,地面摩擦力造成其重心不稳容易倾倒(毕竟是刚体物理解算啊)。

一个解决方法是对其Rigidbody的旋转予以约束,不让其沿着x轴和z轴旋转。

另一个办法是让地面变得没有摩擦力。新建一个Physic Material,命名为slippery,拖到场景中赋给Ground地面物体,然后修改slippery的运动摩擦(Dynamic Friction)和静态摩擦(Static Friction)为0。

我这里选择第一种方法,也就是约束Rigidbody的x轴和z轴旋转。同时我把Playerspeed值修改为5,10米/秒的运动速度对于一个1米25高的Player来说有点太快了。

接下来添加Player旋转的控制。

在Action Browser里输入“rotate”,会出现很多一些关于旋转的Action,但这都不是我们会用到的。实际上,适合我们需求的Action的名称并不包含“rotate”,而是包含“look at”。所以说,对于常用的Action命令还是要记忆一下,否则连关键字都打不出来就不好了。

在Transform类别下有很多种“Look At”,看名字知道带“2d”字眼的肯定是用于二维游戏制作,而带“Smooth”字眼的很有可能是平滑过渡,而不带“Smooth”的则可能是突然变化。

选择Look At或者Smooth Look At都可以。

Look At

Look At
Game Object的正面(Z轴正方向)立刻指向目标物体(Target Object)或目标坐标(Target Position),如果Keep Vertical被勾选,则保证该物体仅绕自身纵轴旋转,否则使用Up Vector中设置的方向作为上方。
勾选Draw Debug Line的话,会在场景视图中显示一条Debug Line Color中所指定颜色的直线以方便判断是否正确。

Smooth Look At

Smooth Look At
Game Object的正面(Z轴正方向)按一定速度(Speed)转向指向目标物体(Target Object)或目标坐标(Target Position),如果Keep Vertical被勾选,则保证该物体仅绕自身纵轴旋转,否则使用Up Vector中设置的方向作为上方。
当游戏物体正向与目标所在方向达到Finish Tolerance允许的接近程度时,触发Finish Event中指定的事件。
勾选Debug时,会在场景视图中显示调试用指向箭头。

现在的问题是如何获得这个Target Object或者Target Position。提供一个思路给大家参考:

如果将Player自身位置作为坐标原点的话,这个方向实际就是我们Input Axis所指向的方向。所以Target Position = Player Position + Input Axis。在Get Axis Vector中我们已经得到了一个放大过的input axis变量了,所以这里直接使用这个值。

Set Velocity下方添加一个Get Position和一个Vector3 Add。在Get Position中获取Player的位置,储存在player position中,然后在Vector3 Add中把input axis加给player position,现在这个player position值就是我们的目标点位置。

PlayMaker中用Action来进行数学计算就是这么混乱,习惯了就好了。

注意,这个Get PositionVector3 Add都是每帧运行的。

最后,将这个player position值设置成Smooth Look AtTarget Position

更简单一点的办法是使用Smooth Look At Direction行为,直接把input axis指定给Target Direction就好了。

摄影机跟随

通常这样的操作方式都会采用俯视摄影机跟随的方式来设置视角。

最简单的一个做法其实是把主摄像机当做Player的子物体就好了,但这样做没有扩展性,比如以后Player加上跳跃的话,相机视角就会跟着跳起来。

于是更常见的操作是实时根据Player位置去设置主摄像机的位置,直接给Player Position加上一个偏移量(offset)就好了。

选择Main Camera,添加Fsm(修改名称为“FSM_Follow”)

State 1是用来做初始设置的,State 2才是真正用来设置相机跟随的状态。

用专门的初始状态来做预设定是PlayMaker的常见做法,比如这个例子里面我们需要在游戏场景中寻找一个叫做“Player”的游戏物体,这个操作只需要在游戏初始化的时候做一次就行了,所以放在专门的State里面,执行完毕就转换到其他State,以后就不用反复执行这种查找操作了。

Find Game Object

Find Game Object常常被用作在场景中根据名称(Object Name)来寻找特定游戏物体,同时还可以按照特定标签(With Tag)来过滤。找到的游戏物体可以保存(Store)在一个变量中(这个例子里面我新建了一个player变量)。

但要记住的是,如果场景中同时有多个同名游戏物体(比如实时生成的克隆物体就都是一样的名字),Find Game Object只会返回所找到的第一个游戏物体,而不会把所有的物体信息都保留下来。所以一定要确保场景中不会出现同名的查找对象。

State 2中:

  • 添加Get Position以读取player物体的位置信息,储存在player position中;
  • 然后使用Vector3 Operator来计算player position变量和一个新建的camera offset变量的相加结果,并储存在一个新建的变量camera position中;
  • 最后添加Set Position把变量camera position变量指定给当前物体(也就是Main Camera)。

Vector3 Operator可以对两个Vector3类型的变量做很多操作:相加、相减、获取夹角、获取距离等等,并将结果储存于第三个变量。我们以后可能会经常用到这个行为。

具体这个例子中,也可以直接用Vector3 Add来做这个相加的计算,但Vector3 Add不能指定第三个变量来储存结果,而是直接改变第一个变量值,所以如果使用Vector3 Add的话,后面的Set Position就需要使用player position这个变量了(现在的player position变量所代表的位置,已经不是Player所处的位置了,而是偏移之后的位置。

运行测试,在测试状态下可以实时修改camera offset值(因为之前已经设置其在Inspector中可见了),实时调整合适的相机位置。

一个比较理想的相机位置是Y = 10,Z = -5,然后相机自身的Rotation X = 60。

(红色界面表示当前处于运行模式)

要注意,运行模式下所做的修改都在退出运行模式后都不会得到保存。一个不是特别完美的解决方案是在点击相应组件(Component)的设置按钮(小齿轮图标),然后选择Copy Component,等退出运行模式以后再同样点击“设置”,选择Paste Component Values,这样就可以把运行模式下该组件的参数信息应用到正常模式下了。


范例02:“双摇杆”式运动控制

分析:

“双摇杆”是单方向控制的升级版,单独将运动物体的面对方向拿出来用第二个摇杆或者鼠标指针来予以控制。

摇杆的话,还是使用input axis就可以了;鼠标的话,大多利用鼠标相对于运动物体的位置矢量做指示,让物体始终朝向这个鼠标的方向。

鼠标是在摄影机平面运动的,要用鼠标来指示运动物体的朝向需要将鼠标平面坐标转换成三维坐标,因此需要使用Raycast。

如果角色有动画,“双摇杆”式控制就要根据两个方向之间的角度来切换运动动画,比如人往前走往后走以及侧身走的动画都是不一样的。

Rigidbody + 双摇杆

我们可以在上一个范例的基础上将其改造成“双摇杆”式运动控制。

Player保存成一个prefab,更名为“Player_A”,这时候场景中的Player也被自动更名为“Player_A”了。
断开场景中Player_A与prefab的联接(菜单GameObject > Break Prefab Instance),再将其名称改回“Player”。

在FSM_Movement中,为State 1添加一个Mouse Pick行为。

Mouse Pick可以帮助我们获取鼠标指针下的游戏物体的相关信息,我们这里需要获得的是鼠标指针下地面物体上对应点的三维空间位置。

所以我们设置Mouse PickStore Point为新建变量mouse point,设置Layer Mask为1,并在新出现的Element 0参数中选择“Add Layer...”以新建一个叫做“Ground”的“层”,并选择这个Ground层为Element 0参数的值。

当然我们也可以直接在工具栏的Layer设置中直接选择Edit Layers...,或者选择Ground物体之后在其Layer设置中选择Add Layer...,都可以打开Layer编辑面板。

别忘了,不管用什么办法打开层编辑面板添加的新层,都需要将Ground物体指定到这个层中才会起作用。初学者经常指定了新层,然后设置了层遮罩,却忘记将游戏物体指定到新层中。

做这样的设置是为让Mouse Pick行为中发射的ray只探测Ground层中的物体,这样避免探测到我们的主角、房屋、甚至UI物体,造成混乱。

此外,这里这样做是比较简化的设计,默认鼠标永远都会探测到地面物体,永远都会有数值返回给Store Point
假如鼠标探测不到任何有效的物体,会将Store Point数值设置为<<0, 0, 0>>,出现跳帧现象。
想偷懒的话,就把地面设大一点,或者干脆设置一个很大的专门的地面物体不显示,只用来接受探测。

Mouse Pick需要被设置成每帧运行。

获得了鼠标位置点以后,就可以将mouse point变量赋给一个Look At行为的Target Position参数,让Player永远面向这个位置。

使用Smooth Look At也可以,但鼠标的移动和角色的转向之间就有一点延迟效果了,如果需要这种效果可以选择Smooth Look At行为。

我将原来的Smooth Look At Direction前面的小√去掉了,这样这个行为就不会起作用,大家也可以将它删掉。

运行测试,感觉使用Smooth Look At的效果更顺滑一些,觉得转向太慢可以将Speed值设置大一些。为了显示方便,我勾选了Smooth Look AtDebug选项,并做了一个小球当做指示物体(再添加一个Set Position,用来指示小球的位置为mouse point即可)。

最终完整的Action结构如下图:

Character Controller + 双摇杆

Character Controller是Unity3D提供的一个专门用于角色控制的组件:

  • Slope Limit:角色能够爬上多大角度的坡
  • Step Offset:多高(小于设定值)的平台会被认为是台阶
  • Skin Width:两个角色的碰撞体(Capsule Collider)之间能够相互穿透多大距离
  • Min Move Distance:可以移动的最小距离(用来避免轻微抖动)
  • Center:角色的Capsule Collider中心高度
  • Radius:角色的Capsule Collider的截面半径
  • Height:角色的Capsule Collider的高度

Character Controller实际上就是一个不可见的Capsule物体,角色模型则被放置在这个Capsule物体内部,随之而动。

下面我们把上面的例子改造成由Character Controller组件来控制的角色。

将上面例子中的Player做成Player_B.prefab,然后断开场景中Player的Prefab联接,名称改回Player

删除掉Box Collider组件和Rigidbody组件,添加Character Controller组件,设置好Capsule碰撞体的大小和位置(按上图)。

然后在FSM_Movement中,删除Set Velocity行为,换成Controller Simple Move行为,把input axis变量指定给Move Vector参数,然后把speed变量指定给Speed参数。

Controller Simple Move给Character Controller指定一个方向Move Vector,并设定一个速度Speed,Character Controller就可以匀速运动了。

角色指向部分的设置不需要改变。

(折叠的Action都没有修改)

运行测试,发现角色一跳一跳的,但从场景视图来看,角色本身的运动是平滑的,问题貌似发生在摄影机的跟随交互逻辑中。

但是,摄影机的逻辑很清晰啊:获取Player位置,添加offset,然后设置Camera位置。

真正的问题出在Character Controller组件的设置里。Min Move Distance = 0.001的默认设置让角色的真实运动和其Position属性并不是完全一致的,这样摄影机在运动刚开始的几帧里面就会比角色快。修改Min Move Distance参数为0就可以保持同步了。

另一个解决方案是在Player内添加一个空物体Camera Point,然后让摄影机不指定Player而指定这个Camera Point为其跟随对象。

运行测试场景,一切正常。

在场景中新建一个Cube物体当做障碍物,Player可以和障碍物的Collider相互碰撞。这是因为Character Controller组件自己就可以被当成是一个Capsule Collider。只不过以后要做碰撞检测的时候需要使用专门的Controller Collision行为。

为了不要频繁地修改Player名字,我把Main Camera的Fsm中的State 1中的Find Game Object修改了一下,不指定名字,而是指定Tag,意思是让其寻找场景中有Player这个Tag的物体,通常这个物体都只会有一个。

这样一来,只要我将Player的Tag指定为Player,不论它具体名称是什么,都能被这个行为找到。

为场景物体或者Prefab添加Tag都是在Inspector中左上角进行添加的,如果需要增加新的Tag,点击Add Tag...,跟添加新的Layer是一样的操作。

把这个Player保存成Player_C.prefab


范例03:“点击移动”式运动控制

分析:

“点击移动”是给定一个目的地,然后让运动物体自动移动过去。如果不考虑障碍物的因素,这个目标很容易达成,但如果场景中有障碍物,“点击移动”就变得很复杂了。

这是因为如果希望运动物体绕开障碍物,就必须让其具有一定的智能,在游戏设计中我们称之为“寻路系统”。Unity3D内置一个比较简单的“寻路系统”:Navigation,主要由Nav Mesh和Nav Agent两个部分组成。Nav Mesh生成一个可以行走的范围区域,Nav Agent为运动物体计算出一条可行的路线。

要实现“点击移动”,需要将Player设置成一个Nav Agent,然后鼠标点击实际上是告诉这个Nav Agent你的目的地在哪里。

Nav Agent不是Rigidbody,不能使用Set Velocity这样的行为,但Nav Agent自身也提供了很多方便的函数给我们使用。

要在PlayMaker中运用这套“寻路系统”,需要加载一套专门的命令库。

老规矩先准备场景。断开Player的prefab联接,然后删掉Character Controller组件和Fsm组件,并在场景中用大方块拼一些障碍物出来。

为Player添加Fsm,建立如下Graph:

State 1中添加Get Mouse Button Down,设置当鼠标左键按下时,触发事件LMB Down

State 2中添加Mouse Pick,设置从鼠标位置对Ground层中的物体发射Ray,将获取的点位置储存在mouse point中,这个mouse point就是我们设置的“目的地”。

再添加一个Move Towards,设置Target Positionmouse point,设置Finish EventFINISHED,就可以实现“点击鼠标左键后Player移动到相应点的位置”的需求了。

但是,Player是沿着直线运动的,不会回避障碍物。

Move Towards是一个不依赖任何物理学计算纯粹改变物体位移属性的Action。它会在物体靠近目的地(距离 < Finish Distance参数值)时触发Finish Event中所指定的事件以跳转到其他状态。

一旦跳转状态,Move Towards就不再起效果了,物体会立刻停止移动。

删掉这个Move Towards,下面我们来建立一个简单寻路系统。

首先给Player添加一个Nav Mesh Agent组件。

  • Agent Type:设置Agent类型,主要是控制Agent能跨上多高的台阶,爬上多陡的坡
  • Base Offset:Agent离地高度
  • Speed:运动速度
  • Angular Speed:转身速度
  • Acceleration:最快加速度(值越大,启动越快)
  • Stopping Distance:接近目标距离多近时停止运动
  • Auto Braking:如果勾选,Agent在接近目标时会自动减速
  • Radius:Agent碰撞体的截面半径
  • Height:Agent碰撞体的高度
  • Quality:回避障碍物的计算精度,这个质量设置越高越耗费CPU资源,设置成None就不会回避障碍物而是直接撞上去了
  • Priority:计算资源不够时优先度低的Agent会降低回避障碍质量
  • Auto Traverse Off Mesh:如果需要自行处理角色在Off Mesh Link处的行为的话,不勾选此选项
  • Auto Repath:让Agent到达分路径末端时会自动重新计算路径
  • Area Mask:可以设置部分区域不允许该Agent通过

这个组件中可以通过RadiusHeight参数设置Agent的碰撞体大小。与Character Controller不同,Nav Mesh Agent的碰撞体是圆柱体。

pm_movement036.png
pm_movement037.png

从菜单Window > Navigation打开Navigation面板,

选择地面,勾选Navigation Static,并将其设置成Walkable

选择所有的障碍物,勾选Navigation Static,并将其设置成Not Walkable

到Bake面板中点击Bake按钮进行Nav Mesh的烘焙。

  • Agent Radius:这个值越高,障碍物边缘不可行走的空档就越大,最好设置成所有Agent中最小的那个的半径值
  • Agent Height:这个值越高,半空中有障碍物的区域就越可能不可走过,最好设置成所有Agent中最高的那个的高度
  • Max Slope:最大可行走坡度
  • Step Height:最大可行走台阶高度
  • Drop Height:如果这个值为正,就在小于这个高度的两块Nav Mesh之间创建“掉落”类型的Off Mesh Link
  • Jump Distance:如果这个值为正,就在小于这个距离的两块Nav Mesh之间创建“跳跃”类型的Off Mesh Link

按照上述设置而烘焙成的Nav Mesh如下图所示:

蓝色区域就是可以行走的区域。

要注意,参与创建Nav Mesh的可行走表面和不可行走表面都需要设置成Navigation Static,一旦被设置成Navigation Static,该物体就不能再移动了,也不能播放动画。

在场景中选择Player,继续设置Fsm。

想要Nav Mesh Agent运动,可以直接给它设置一个目标,它就会自动寻找合适路线走过去。我们在State 2中的Mouse Pick后面添加一个Set Agent Destination,将Destination参数指定为mouse point变量。

注意,Set Agent Destination这个Action并不包含在PlayMaker的初始安装文件中,需要自己去官方WIKI下载适合版本的PathFinding.unitypackage安装以后才能正常使用。

此外,如果希望能够在5.6版本中就使用最新2017.1的Navigation系统,可以去Github上相关页面去下载核心脚本,解压后放进工程文件夹就可以了。

运行场景,我们可以看到Player现在可以自己找路了。


最终的Demo场景我添加了一个Manager,让每次载入场景的时候都会随机选择不同的Player.prefab来载入,这一部分的具体制作过程就不写了,搞成随机的主要原因还是我懒得分成4个不同的场景而已。

项目工程文件(不包括PlayMaker插件及其uGUI扩展Action包)


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

推荐阅读更多精彩内容