快速开发实用工具---VRTK

VRTK是由一些大神对SteamVR进行一定的优化后封装出来的便捷快速VR开发工具,下面一步一步来了解这个神插件。
我也不知道从哪说起好,所以干脆以它自带的案例来分析了解它吧。

Paste_Image.png

DEMO 1 场景切换、简单获取手柄并监控按钮

第一个场景001_CameraRig_VRPlayArea,它的功能是按下手柄任意键切换到下一个场景。你需要做的准备工作是准备好切换的下一个场景,也就是要先创景一个场景,然后在场景中添加VRTK的VR摄像机和SceneChanger脚本。这里说一点,当前版本的VRTK的prefabs包里没有拉出来就可以用的VR摄像头,要自己做,先创建一个gameObject,改名为VRTK_SDKManager,然后添加VRTK_SDKManager脚本,然后添加预置物SDKSetups然后VRTK_SDKManager下面有个AutoPopulate按钮,按下后就会自动组合成一个VR摄像机了。

图片01.png
图片02.png
图片03.png

添加SceneChanger场景切换脚本。

图片04.png

最后一步就是到Build Settings哪里把场景添加上去。

图片05.png

然后来讲一下场景切换是怎么运作的,SceneChanger脚本里有两个个核心类:
VRTK_DeviceFinder类:用于在场景中寻找左右手柄、头显、返回硬件编号或手柄,头显的gameobject。

GameObject rightHand = VRTK_DeviceFinder.GetControllerRightHand(true);

VRTK_SDK_Bridge类:这是插件封装了 SteamVR 的比较底层的;APIVRTK_SDK_Bridge.IsTriggerPressedOnIndex(controllerIndex) 。输入硬件的编号,返回是否按下的布尔值。

VRTK_SDK_Bridge.GetControllerButtonState(SDK_BaseController.ButtonTypes.Trigger, SDK_BaseController.ButtonPressTypes.Press, controllerReference);

重要的API:
获得左右手柄的游戏物体
VRTK_DeviceFinder.GetControllerRightHand();
VRTK_DeviceFinder.GetControllerLiftHand();
获得左右手柄对应的硬件编号
VRTK_DeviceFinder.GetControllerIndex(rightHand)

图片6.png
图片07.png

DEMO 2 发射激光与手柄的事件输入

Toolkit把所有手柄事件都封装到VRTK_ControllerEvents里,针对VR中常见的交互动作: UI点击,抓住物体,触发物体功能等,自定义手柄按键,绑定这些动作,下面对脚本逐块代码块进行讲解。

Inspector面板上的可见参数:

· Pointer Toggle Button:用于控制一束激光指示线开/关,相当于鼠标的指向功能。
· Pointer Set Button:用于设置激光指向的目标标记,触发目标的功能,相当于鼠标点击。
· Grab Toggle Button:用于控制抓取游戏中的物体,配合使用VRTK插件集成的抓取,投掷功能.
· Use Toggle Button:用于触发目标事件,比如把手柄放在门把手上,按下这个按键,触发门打开的功能,手柄放在电灯开关上,按下按键,触发电灯开发的功能(这个和pointerSetButton区别在于后者需要激光落在目标上,才能触发目标上的功能)。
· UI Click Button::用于触发UI Canvas 目标的事件。
· Menu Toggle Button:用于点击弹出游戏内置按钮,游戏目录显示开关。
· Axis Fidelity:按键按下的强度,完全按下是1,松开时是0, 现在只有Trigger按键(手部后面扳机)有此属性
· triggerClickThreShold: 扳机要到多大程度,才能使用扳机的触发功能. 如果是0.5 ,则表示只要按下一半的程度就可以触发,如果是1,则必须完全按下才能触发.

图片08.png

VRTK_ControllerEvents里一些重要变量的解释:

· public bool triggerPressed - 当trigger被扣下一半左右时为真.
· public bool triggerTouched - 当trigger被扣下一点点时为真.
· public bool triggerHairlinePressed - 当trigger比任何之前扣下的程度多时为真.
· public bool triggerClicked - 当trigger完全扣下时为真.
· public bool triggerAxisChanged - 当trigger位置改变时为真.
· public bool applicationMenuPressed - 当application menu被按下时为真.
· public bool touchpadPressed - 当touchpad被按下时为真.
· public bool touchpadTouched - 当touchpad被触碰时为真.
· public bool touchpadAxisChanged - 当touchpad触碰位置改变时为真.
· public bool gripPressed - 当grip被按下时为真.
· public bool pointerPressed - 当别名为pointer的按钮被按下时为真.
· public bool grabPressed - 当别名为grab的按钮被按下时为真.
· public bool usePressed - 当别名为use的按钮被按下时为真.
· public bool uiClickPressed - 当别名为UI click的按钮被按下时为真.
· public bool menuPressed - 当别名为menu的按钮被按下时为真.

图片09.png

声明事件装载参数ControllerInteractionEventArgs :

图片10.png

脚本里包含的事件:

· TriggerPressed - 当trigger被扣下一半左右时发送事件.
· TriggerReleased - 当Trigger从扣下一半的状态释放后发送事件.
· TriggerTouchStart - 当trigger被扣下一点点时发送事件.
· TriggerTouchEnd - 当trigger完全没有被扣下时发送事件.
· TriggerHairlineStart - 当trigger扣下的程度超过了当前的hairline阈值时发送事件.
· TriggerHairlineEnd - 当tringger释放程度超过了当前的hairline阈值时发送事件.
· TriggerClicked - 当trigger在clicked之前扣下的过程中发送事件.
· TriggerUnclicked - 当trigger不再一直处于clicked状态时发送事件.
· TriggerAxisChanged - 当trigger扣下的量发生变化时发送事件.
· ApplicationMenuPressed - 当application menu被按下时发送事件.
· ApplicationMenuReleased - 当application menu被释放时发送事件.
· GripPressed - 当grip被按下时发送事件.
· GripReleased - 当grip被释放时发送事件.
· TouchpadPressed - 当touchpad被按下的时候发送事件(比触摸的按压程度大).
· TouchpadReleased - 当touchpad从被按下(非触碰)的状态下释放时发送事件.
· TouchpadTouchStart - 当touchpad被触摸时发送事件 (不是点击或者摁下).
· TouchpadTouchEnd - 当touchpad不再被触摸时发送事件.
· TouchpadAxisChanged - 当touchpad被触摸的点改变时发送事件.
· AliasPointerOn - 当pointer toggle(别名)被按下的时候发送事件.
· AliasPointerOff - 当pointer toggle(别名)被释放的时候发送事件.
· AliasPointerSet - 当pointer set(别名)被释放时发送事件.
· AliasGrabOn - 当grab toggle(别名)被按下的时候发送事件.
· AliasGrabOff - 当grab toggle(别名)被释放的时候发送事件.
· AliasUseOn - 当use toggle(别名)被按下的时候发送事件.
· AliasUseOff - 当use toggle(别名)被释放时发送事件.
· AliasMenuOn - 当menu toggle(别名)被按下时发送事件.
· AliasMenuOff - 当menu toggle(别名)被释放时发送事件.
· AliasUIClickOn - 当UI click(别名)被按下时发送事件.
· AliasUIClickOff - 当UI click(别名)被释放时发送事件.

事件和bool状态变量有着对应的关系,通常一个bool状态变量会对应至少两个按钮事件

按钮的别名:

图片11.png

这个工具类给Vive手柄一些常用的操作取一些别名,和实际的按钮建立映射,例如:
public ButtonAlias menuToggleButton = ButtonAlias.Application_Menu;
这个menuToggleButton与SteamVR中的SteamVR_Controller.ButtonMask.ApplicationMenu
对应,当这个按钮被按下时,别名按钮对应的事件(如果有绑定)也会发送。

和SteamVR相关的全局变量

1、private uint controllerIndex; —— controllerIndex - 手柄的索引值,通过trackedController.index获取
2、private SteamVR_TrackedObject trackedController;—— controllerIndex - 手柄的索引值,通过trackedController.index获取
3、private SteamVR_Controller.Device device;—— device - 设备类,通过此类获取实际中手柄的各种数据
4、private Vector2 touchpadAxis = Vector2.zero;—— touchpadAxis - 全局变量,touchpad的坐标
5、private Vector2 triggerAxis = Vector2.zero;—— triggerAxis - 全局变量,trigger的坐标
6、private float hairTriggerDelta;——
7、private Vector3 controllerVelocity = Vector3.zero;—— controllerVelocity - 手柄运动的速度
8、private Vector3 controllerAngularVelocity = Vector3.zero;—— controllerAngularVelocity - 手柄旋转的角速度

事件发送方法:

以OnTriggerPressed方法为例,其他都和这个差不多

public virtual void OnTriggerPressed(ControllerInteractionEventArgs e)
{
    if (TriggerPressed != null)
    {
        TriggerPressed(this, e);//发送事件,通知绑定此事件的脚本,执行具体的逻辑,但是此处是真正最后调用的地方
    }
}

事件的参数装载方法SetButtonEvent

该方法返回一个ControllerInteractionEventArgs参数;

private ControllerInteractionEventArgs SetButtonEvent(ref bool buttonBool, bool value, float buttonPressure)
{
    buttonBool = value;
    ControllerInteractionEventArgs e;
    e.controllerIndex = controllerIndex;
    e.buttonPressure = buttonPressure;
    e.touchpadAxis = device.GetAxis();//调用SteamVR API获取当前的touchpad二维坐标
    e.touchpadAngle = CalculateTouchpadAxisAngle(e.touchpadAxis);//计算二维坐标在圆形表盘上对应的角度
    return e;
}

通过传入ref bool buttonBool,可以在对ControllerInteractionEventArgs进行装填的同时,把事件对应的按钮bool状态进行更新,例如TriggerPressed和TriggerReleased事件对应的按钮bool状态是triggerPressed,当发送TriggerPressed事件时要同时更新triggerPressed为true;发送TriggerReleased事件时要同时更新triggerPressed为false

初始化代码

private void Awake()
{
    trackedController = GetComponent();
    gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
}
private void Start()
{
    //获取当前脚本attach的Controller的index
    controllerIndex = (uint)trackedController.index;
    if (controllerIndex < uint.MaxValue)
    {
        //获取设备
        device = SteamVR_Controller.Input((int)controllerIndex);
    }
}

一般头显对应的index为0,两个手柄分别为0和1

别名按钮事件发送

private void EmitAlias(ButtonAlias type, bool touchDown, float buttonPressure, ref bool buttonBool)
…
        if (pointerToggleButton == type)
        {
            if (touchDown)
            {
                pointerPressed = true;
                OnAliasPointerOn(SetButtonEvent(ref buttonBool, true, buttonPressure));
            }
            else
            {
                pointerPressed = false;
                OnAliasPointerOff(SetButtonEvent(ref buttonBool, false, buttonPressure));
            }
        }
…

根据type判断是哪个别名按钮,最后一个参数buttonBool对应的是非别名的按钮bool状态,例如这个pointerToggleButton,发送事件时要把touchpadPressed状态更新,而更新为true还是false要根据touchDown的值来判断,上面的OnAliasPointerOn等方法和OnTriggerPressed
  值得注意的是,不同的别名对应的可能是相同的按钮,例如pointerToggleButton和pointerSetButton都是ButtonAlias.Touchpad_Press。

禁用事件

private void OnDisable()
{
    //在0.1s内调用DisableEvents(),禁用所有事件发送
    Invoke("DisableEvents", 0.1f);
}
///
/// 禁用,还原,但是保存touchpad和trigger的坐标等
///
private void DisableEvents()
{
    if (triggerPressed)
    {
        OnTriggerReleased(SetButtonEvent(ref triggerPressed, false, 0f));
        EmitAlias(ButtonAlias.Trigger_Press, false, 0f, ref triggerPressed);
    }
    ...
    triggerAxisChanged = false;
    touchpadAxisChanged = false;
    controllerIndex = (uint)trackedController.index;
    if (controllerIndex < uint.MaxValue)
    {
        device = SteamVR_Controller.Input((int)controllerIndex);
        Vector2 currentTriggerAxis = device.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger);
        Vector2 currentTouchpadAxis = device.GetAxis();
        // 保存当前的touchpad和trigger的设置.
        touchpadAxis = new Vector2(currentTouchpadAxis.x, currentTouchpadAxis.y);
        triggerAxis = new Vector2(currentTriggerAxis.x, currentTriggerAxis.y);
        hairTriggerDelta = device.hairTriggerDelta;
    }
}

这个方法应该就是将所有事件对应的按钮bool状态置为false,同时保存touchpad和trigger上的坐标信息,但是为什么要重新获取一次device呢?

Update()方法

private void Update()
{
    controllerIndex = (uint)trackedController.index;
    //Only continue if the controller index has been set to a sensible number
    //SteamVR 在未找到Controller时会把index置为uint最大的值
    if (controllerIndex >= uint.MaxValue)
    {
        return;
    }
    device = SteamVR_Controller.Input((int)controllerIndex);
    Vector2 currentTriggerAxis = device.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger);
    Vector2 currentTouchpadAxis = device.GetAxis();
    //Trigger Pressed
    if (device.GetPressDown(SteamVR_Controller.ButtonMask.Trigger))
    {
        //发送事件,设triggerPressed为true,同时发送Trigger_Press对应的别名按钮事件
        OnTriggerPressed(SetButtonEvent(ref triggerPressed, true, currentTriggerAxis.x));
        EmitAlias(ButtonAlias.Trigger_Press, true, currentTriggerAxis.x, ref triggerPressed);
    }
    else if (device.GetPressUp(SteamVR_Controller.ButtonMask.Trigger))
    {
        OnTriggerReleased(SetButtonEvent(ref triggerPressed, false, 0f));
        EmitAlias(ButtonAlias.Trigger_Press, false, 0f, ref triggerPressed);
    }
    ...
    // 保存当前trigger和touchpad状态.
    touchpadAxis = new Vector2(currentTouchpadAxis.x, currentTouchpadAxis.y);
    triggerAxis = new Vector2(currentTriggerAxis.x, currentTriggerAxis.y);
    hairTriggerDelta = device.hairTriggerDelta;
}

这里要说一下,这脚本每帧监听一次SteamVR中的控制柄状态,而当监听到状态后,执行一次自己的委托事件(ControllerInteractionEventHandler),并且执行一次EmitAlias也就是别名按钮事件(ButtonAlias type, bool touchDown, float buttonPressure, ref bool buttonBool)。EmitAlias方法是用来判断并分发事件给AliasXX(oN/oFF)的,AliasXX(oN/oFF)这些委托方法就是上面提过的Inspector面板中的

图片12.png

· Pointer Toggle Button: 这个按钮用于控制一束激光指示线开/关.
· Pointer Set Button: 这个按钮用于设置指示线的目标标记.
· Grab Toggle Button: 这个按钮用于控制抓取游戏中的物体.
· Use Toggle Button: 这个按钮用于使用游戏中的物体.
· UI Click Button: 这个按钮用于点击UI元素.
· Menu Toggle Button:这个按钮用于点击弹出游戏内置按钮.

使用实例

例如在VRTK_ControllerEvents_ListenerExample中

GetComponent().TriggerPressed += new ControllerInteractionEventHandler(DoTriggerPressed);
private void DoTriggerPressed(object sender, ControllerInteractionEventArgs e)
{
    DebugLogger(e.controllerIndex, "TRIGGER", "pressed", e);
}

获取到当前Controller绑定的VRTK_ControllerEvents脚本,为它的TriggerPressed绑定DoTriggerPressed方法,在VRTK_ControllerEvents脚本中,每一帧会检测trigger是否被按下,如果按下,则发送事件

OnTriggerPressed(SetButtonEvent(ref triggerPressed, true, currentTriggerAxis.x));

然后在OnTriggerPressed方法里执行TriggerPressed(this, e);
  此时Example中的DoTriggerPressed(this,e)被真正调用,而VRTK_ControllerEvents_ListenerExample脚本中无需在update中写代码,只需要在初始化的时候绑定事件就可以了。

PS:引自http://www.youzivr.cn/jisu/Unity3D/2267.html

DEMO 3 手柄的激光指针

第三个场景主要添加的脚本是VRTK_Pointer和VRTK_StraightPointerRenderer,它们都在Pointer文件夹里。

图片13.png

这里有一点要说到,在第二个demo中其实也有用到的,就是另外创建一个空物体,并在空物体下面添加两个空物体以代替两个手柄的事件引用操作,分别添加VRTK_ControllerEvents脚本,然后在VR摄像机[VRTK_SDKManager]下Script Aliases栏下分别绑定新创建的两个手柄引用,这样的好处就是能更简洁地管理手柄的触发事件,添加功能,这样更便于管理并且不会破坏[CameraTig]物体的Prefab。

图片14.png
图片15.png

回过头来我们来了解一下VRTK_Pointer和VRTK_StraightPointerRenderer这两个脚本。
VRTK_Pointer继承于VRTK_DestinationMaker,VRTK_StraightPointerRenderer继承于VRTK_BasePointerRenderer;这是新版本的激光指针脚本,以前是只有一个脚本而已,新版本分成了两个,据我的了解,VRTK_StraightPointerRenderer是负责创建和激光属性的调节,VRTK_Pointer是负责对激光触发的功能进行管理。

图片16.png
VRTK_Pointer

Destination Marker Settings 目的地标记设定
Enable Teleport(启动传送):如果勾选了,在目标设置事件中的teleport标志位就设为true,所以传送脚本就知道是否要行动到新的目标。如果这个选项没有勾选的话,控制器光束启动但是不会触发位移。
Target List Policy:设置限制位移的物体,如果有物体的Tag被设置为在VRTK_Policy List中设置的Tag之一的话,那么将无法传送到该物体上。
Pointer Activation Settings 指针激活设定
Pointer Renderer:获取要控制的VRTK_Straight Pointer Renderer渲染器。
Activation Button:激活或禁用指针按钮。
Hold Button To Activate:勾上时需要一直按按钮才能激活射线,否则按一下按钮激活按一下按钮就不激活。??
Activation On Enable:??
Activation Delay:激活延迟??
Pointer Selection Settings 指针选择设定??
妈蛋查来查去都不知道这些属性什么意思,还是先了解各个Demo的功能和使用方法,细节以后再补上了!!

VRTK_Straight Pointer Renderer

Playarea Cursor :游玩区光标和碰撞的尺寸,旧版中只是设置它的X和Y,现在新版中有个专门用来控制游玩区域的脚本VRTK_PlayAreaCursor,这里选的就是它。
Direction Indicator:方向指示器,有个VRTK_Pointer Direction Indicator脚本,跟上面一样要加到手柄上然后选择它。
Custom Raycast:有个VRTK_Custom Raycast脚本,要添加这个脚本

反正这个脚本就是实现从手柄发出激光射线,并且可以拓展出各种功能。

DEMO 4 传送

主角是VRTK_BasicTeleport脚本,和上面VRTK_Pointer和VRTK_Straight Pointer Renderer脚本联用,要有VRTK_BasicTeleport才能实现瞬移功能,具体操作很简单,在手柄上添加了VRTK_Pointer和VRTK_Straight Pointer Renderer脚本后,在手柄的同一父物体下创建一个空物体命名为PlayArea,然后添加VRTK_BasicTeleport脚本就行了,还有就是如果想用曲线做瞬移,只要把VRTK_Straight Pointer Renderer脚本换成VRTK_Bezier Pointer Renderer。

图片17.png
VRTK_BasicTeleport

Blink To Color:传送时的黑屏颜色,模拟眨眼,过渡更自然。
Blink Transition Speed:黑屏的褪色速度,根据你的需要设置,为0的话就不会有褪色闪烁效果。
Distance Blink Delay:黑屏时间延迟,随传送距离远近成正比的黑屏时间。假如传送的位置比较远,则黑屏时间更久。数值越大,黑屏时间越长,数值为0时,不受距离远近影响。
Headset Position Compensation:以头部为基准,作为传送的位置。未打钩,则是玩家可玩区域的中心位置作为传送的目标点。可玩区域不能和游戏环境交叉,最大化玩家活动空间。
Target List Policy:可以添加 VRTK_TagOrScriptPolicyList 脚本,其中添加多个禁止传送的物体标签。
Nav Mesh Limit Distance:navemesh 边缘限制,在 navemesh 上传送,如果在 navemesh 的边界,则禁止传送,数值是离边界的距离范围。

DEMO 5 抓取物体

主要用到的脚本是VRTK_Interact Touch、VRTK_Interact Grab和VRTK_Interactable Object。前面两个是放到手柄上,后面那个是放到可抓取物体上的。

VRTK_Interact Touch
图片18.png

Custom Collider Container:一个可选的GameObject,它包含复合碰撞器来表示触碰对象。 如果这是空的,那么在运行时会自动生成碰撞器,以配合SDK默认控制器。

VRTK_Interact Grab

Grab Settings 抓取设定
Grab Button:设置抓取触发按钮。
Grab Precognition:提前预判抓取物体。如果物体以快速的速度移动,那么由于人的反应时间,很难及时按下抓取按钮来捕捉物体。 这里的较高的数字意味着在控制器触摸对象之前可以按下抓取按钮,值越大,可提前的抓取时间越长,并且当碰撞发生时,如果抓住按钮仍然被按下,则抓取动作将成功。
Throw Multiplier:用来乘以对象被扔出时速度的量。在放大 CameraRig 来模拟能够把物体扔的更远时是很有用的。
Create Rigid Body When Not Touching:在碰到物体时才创建 RigidBody。当抓取按钮按下时,如果控制器没有碰到一个可交互的对象,那么添加到控制器上的刚体就允许控制器去推动周围其他的刚体对象。
Controller Attach Point:设置控制器上的一个将抓住的物体固定住刚体点。 如果为空,它将被设置为SDK默认值。
Controller EventsInteract Touch就是当前控制器上的VRTK_Controller Events和VRTK_Interact Grab脚本,这里会自动获取,不需要设置。

VRTK_Interactable Object
图片19.png

Disable When Idle:如果这被监控到对象没有被交互时,可以禁用交互对象脚本。 这将消除可交互对象对每个帧的潜在呼叫数量。
Touch Options 触摸设置
Touch Highlight Color:触摸到时的高亮颜色。颜色可以被任何全局的设置颜色来覆写(例如InteractTouch 脚本)。
Allowed Touch Controllers:规定哪个手柄可以触摸这个物体。
Ignored Colliders:对象上的碰撞数组在被触摸时被忽略。
Grab Options 抓取设置
Is Grabbable:是否可以被抓起来。
Hold Button To Grab:如果勾选,则抓起对象时需要一直按着设置为抓取的按钮保持抓握状态,如果没勾选则按一下抓握按钮就抓起来,再按一下释放。
Stay Grabbed On Teleport:传送时抓握的物体是否也跟着传送。
Valid Drop:确定控制器抓取按钮可以在什么情况下丢下对象。NoDrop:什么情况下控制器都无法丢下对象,DropAnywhere:控制器可以在任何情况下丢下对象,DropValidSnapDropZone:只有当对象悬停在有效的快速下拉区域上时,该对象才能被放下。
Grab Override Button:覆盖手柄的抓取按键设置。
Allow Grab Controllers:规定哪个手柄可以抓握这个物体。
Grab Attach Mechanic Script:这可以确定被抓取的物品在被抓取时如何连接到控制器。 如果没有提供,则将使用GameObject上的第一个Grab Attach脚本,如果没有找到并且该对象可以被抓住,那么将在运行时创建一个Fixed Joint Grab Attach脚本。
Secondary Grab Action Script:在辅助抓取时处理辅助控制器动作时使用的脚本。 如果没有提供,则将使用GameObject上的第一个辅助控制器抓取操作脚本,如果没有找到,则不会在辅助抓取上执行任何操作。
Use Options 使用设置
Is Usable:决定对象是否可以被使用。
Hold Button To Use:如果勾选,控制器上的使用(use)按钮需要被持续按下来保持使用。如果没有勾选,use按钮用一次按钮按下来启动使用和再一次按下来停止使用。
Use Only If Grabbed:只有该物体被抓取时才能使用。
Pointer Activates Use Action:如果勾选,当一个世界光束指针(由控制器投射)碰到了交互对象,如果对象的Hold Button To Use没有勾选的话,当指针在对象上的时候就会运行对象的StartUsing方法,指针移开失效就会运行StopUsing方法。这个选项在被勾选以后,就需要按住使用按钮,为了防止在用一个指针来使用对象时发生不必要的传送,就不会抛出目标点设置( Destination Set )事件。
Use Override Button:自定义操作按钮。
Allowed Use Controllers:允许使用的控制器。

上面提到了两个新脚本VRTK_FixedJointGrabAttach和VRTK_SwapControllerGrabAction,正如上面所说对抓取有一定作用,请自行深入了解。

Demo 6 开门

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

推荐阅读更多精彩内容