VRTK使用心得(七):將The Lab中的LongBow移植到VRTK中

近日在我們的“VR共同成長(zhǎng)群”內(nèi)有朋友要求將Interaction System中的LongBow模塊轉(zhuǎn)制為VRTK版本泥栖,本文將對(duì)此過(guò)程實(shí)現(xiàn)的主要步驟進(jìn)行演示佃声。鑒于要求是將LongBow的交互完全移植到VRTK寨典,所以我們從原Interaction System模塊出發(fā)進(jìn)行修改颓帝。在VRTK自帶的第23個(gè)實(shí)例中亦有相關(guān)射箭功能的演示锻煌,但是交互方式和機(jī)制會(huì)有些細(xì)微區(qū)別翩隧,有興趣的讀者可根據(jù)實(shí)例場(chǎng)景進(jìn)行相關(guān)的修改樊展。本文旨在了解射箭交互的實(shí)現(xiàn)原理,比較Interaction System與VRTK兩個(gè)交互系統(tǒng)的區(qū)別,達(dá)到更加深入理解其運(yùn)作機(jī)制的目的专缠,實(shí)際開(kāi)發(fā)中建議不要重復(fù)造輪子雷酪。

修改思路:

通過(guò)對(duì)于模塊的觀察和分析,LongBow基于ItemPackage機(jī)制——關(guān)于ItemPackage的實(shí)現(xiàn)機(jī)制涝婉,可以參見(jiàn)SDK自帶的文檔或我的視頻教程第11課時(shí)——參與交互的物體分別為L(zhǎng)ongBow和ArrowHand哥力。通過(guò)之前將PC端的Survival Shooter移植到VR端的經(jīng)驗(yàn)可知,我們移植的大部分工作是修改其交互墩弯,又吩跋,Interaction System中引入了一個(gè)Player這個(gè)封裝好的預(yù)制體,等同于CameraRig渔工,其中兩個(gè)Hand類便是我們重點(diǎn)修改的對(duì)象锌钮。

模塊中涉及到三個(gè)比較重要的腳本,分別是LongBow.cs,ArrowHand.cs,和Arrow.cs引矩,通過(guò)觀察三個(gè)腳本的變量聲明梁丘,我們看到Arrow.cs的變量多是Unity的基本變量聲明,很少涉及到Player的引用旺韭,即很少有涉及到VR交互的變量氛谜,所以這個(gè)腳本我們基本上不會(huì)做修改。


Arrow.cs

反觀ArrowHand.cs與LongBow.cs茂翔,如下圖混蔼,其中大量引用了Player的Hand對(duì)象履腋,同時(shí)Hand對(duì)象又有otherHand這個(gè)變量的引用珊燎,所以我們要進(jìn)行代碼修改的對(duì)象只有這兩個(gè)腳本。


ArrowHand.cs

LongBow.cs

為了不影響Interaction System中的腳本遵湖,我們新建兩個(gè)腳本VRTKLongBow.cs和VRTKArrowHand.cs悔政,對(duì)應(yīng)原LongBow.cs和ArrowHand.cs,分別掛載到LongBow和ArrowHand預(yù)制體上延旧,命名空間與腳本內(nèi)容保持不變谋国,只修改類名,對(duì)于新腳本在檢視面板上的引用迁沫,保持照抄芦瘾。

原系統(tǒng)中LongBow的實(shí)現(xiàn)過(guò)程:

原系統(tǒng)使用ItemPackage機(jī)制實(shí)現(xiàn)配套道具的抓取。場(chǎng)景中擺放的弓箭為預(yù)覽集畅,包含其中的SphereCollider上掛載了ItemPackageSpawner腳本近弟,與ItemPackage配合實(shí)現(xiàn)兩個(gè)手柄的物體抓取。真正實(shí)現(xiàn)拉弓射箭交互的是掛載了腳本的LongBow和ArrowHand兩個(gè)Prefab挺智,可在相應(yīng)的文件夾下找到祷愉。當(dāng)手柄懸停并按下Trigger鍵進(jìn)行抓取時(shí),根據(jù)ItemPackage的配置,找到并生成(基于Instantiate)需要抓取的物體于指定的手柄二鳄,同時(shí)在指定的位置生成輪廓赴涵,當(dāng)LongBow放回原位置時(shí),銷毀生成的兩個(gè)對(duì)象订讼。
預(yù)制體LongBow自帶一個(gè)動(dòng)畫(huà)效果髓窜,播放弓被拉伸的過(guò)程,掛載的腳本會(huì)實(shí)時(shí)判斷箭與弓的位置關(guān)系來(lái)判斷是否給到瞄準(zhǔn)的姿態(tài)躯嫉,當(dāng)抓取箭的手柄松開(kāi)Trigger鍵的時(shí)候纱烘,給生成的箭一個(gè)方向上的力,實(shí)現(xiàn)箭的射出祈餐,同時(shí)播放弓回彈的動(dòng)畫(huà)擂啥。


image.png

image.png
VRTK替換過(guò)程:

我們先來(lái)看一下LongBow模塊用到的自有組件及可以使用VRTK組件代替的地方,如下圖:

LongBow預(yù)制體

LongBow預(yù)制體中:

  1. Interactble 組件是原系統(tǒng)設(shè)置物體為可交互必須的腳本帆阳,用來(lái)標(biāo)記此物體為可交互哺壶,移植前可直接去掉,轉(zhuǎn)而掛載VRTK_InteractableObject組件蜒谤。
  2. LongBow 組件是實(shí)現(xiàn)交互的核心山宾,也是我們需要改動(dòng)代碼的組件,使用新建的VRTK_LongBow組件代替鳍徽。
  3. Item Package Reference 屬于實(shí)現(xiàn)ItemPackage機(jī)制的組成部分资锰,可直接去掉。
  4. Destroy On Detached From Hand 組件在原系統(tǒng)中用于物體在被釋放以后進(jìn)行銷毀阶祭,這里我們可以在VRTK中物體被Ungrab事件中進(jìn)行處理绷杜,并且移植以后我們只是將其放下,而不對(duì)其進(jìn)行銷毀處理濒募。
  5. Hide On Hand Focus Lost 組件在原系統(tǒng)中用于物體在失去焦點(diǎn)以后隱藏鞭盟,這里我們也直接去掉。
  6. Animator 組件是關(guān)于弓及弓弦張合的過(guò)程動(dòng)畫(huà)瑰剃,所以重點(diǎn)來(lái)了:拉弓的過(guò)程齿诉,就是通過(guò)計(jì)算一個(gè)距離,去動(dòng)態(tài)改變這段動(dòng)畫(huà)播放進(jìn)度的過(guò)程晌姚。
  7. Linear Animator與Linear Mapping 是InteractionSystem中比較重要的機(jī)制粤剧,用來(lái)通過(guò)數(shù)值的映射而改變動(dòng)畫(huà)的播放進(jìn)度,此模塊中通過(guò)手柄與瞄準(zhǔn)點(diǎn)的距離作為數(shù)據(jù)映射挥唠,改變弓弦的張合程度抵恋,移植以后的腳本中,我們可以直接通過(guò)計(jì)算出來(lái)的數(shù)值來(lái)控制動(dòng)畫(huà)的播放猛遍,代碼如下:
//public LinearMapping bowDrawLinearMapping;
public Animator longBowAni;

注釋掉LinearMapping的聲明馋记,新建一個(gè)動(dòng)畫(huà)的引用号坡。

longBowAni = GetComponent<Animator>();
longBowAni.speed = 0;

在Awake函數(shù)中引用Animator組件列肢,并將其速度置0选泻。

 //this.bowDrawLinearMapping.value = drawTension;
 longBowAni.Play(0, 0, drawTension);

在代碼中將所有原bowDrawLinearMapping的引用岭佳,皆替換為如上代碼重虑,drawTension為一個(gè)0到1的浮點(diǎn)數(shù)據(jù)類型糙臼,即控制動(dòng)畫(huà)播放的百分比群叶。

下面再來(lái)看一下ArrowHand預(yù)制體掛載的組件蒋譬。

ArrowHand預(yù)制體掛載的組件

ArrowHand預(yù)制體中:

  1. ArrowHand 組件實(shí)現(xiàn)對(duì)于Arrow預(yù)制體的控制属愤,包括生成号胚、射擊等籽慢,這里我們使用新建的VRTKArrowHand腳本替換。
  2. Item Package Reference 組件同上猫胁,去掉箱亿。
  3. Destroy On Detached From Hand與Hide On Hand Focus Lost 組件同上,去掉弃秆。
  4. Allow Teleport Whild Attached To Hand 組件用來(lái)實(shí)現(xiàn)當(dāng)抓取時(shí)可同時(shí)實(shí)現(xiàn)瞬移届惋。在VRTK中,將物體設(shè)置為可交互的VRTK_Interactable Object組件中有"Stay Grabbed On Teleport"屬性即可實(shí)現(xiàn)此功能菠赚,見(jiàn)下圖脑豹,故去掉。

image.png

新建一個(gè)場(chǎng)景衡查,按照之前文章中的VRTK配置過(guò)程進(jìn)行VRTK的配置瘩欺。
1. 設(shè)置物體為VRTK可交互
選中場(chǎng)景中的LongBow實(shí)例,為其添加可感應(yīng)區(qū)域拌牲,添加一個(gè)位置及大小合適的SphereCollider俱饿,或使用原場(chǎng)景中Pickup攜帶的SphereCollider。然后選擇Window->VRTK->SetUp Interactable Object们拙,如下圖:
image.png

同樣的過(guò)程稍途,配置ArrowHand預(yù)制體阁吝。需要注意的是砚婆,為ArrowHand添加感應(yīng)區(qū)域時(shí),添加的Collider不宜過(guò)大突勇,這樣容易在箭被發(fā)射的瞬間與其發(fā)生碰撞装盯,從而導(dǎo)致不正常的出射軌跡〖撞觯可以將預(yù)制體拖入場(chǎng)景進(jìn)行配置埂奈,完畢以后點(diǎn)擊屬性面板中的Apply,然后將其刪除定躏,因?yàn)锳rrowHand通過(guò)Instantiate prefab生成账磺。
為了達(dá)到與原系統(tǒng)一樣的交互過(guò)程芹敌,實(shí)現(xiàn)抓取的鍵為T(mén)rigger,故修改各Controller上掛載的VRTK_Interact Grab組件的Grab Button為"Trigger Press"垮抗。
設(shè)置抓取鍵

同時(shí)要實(shí)現(xiàn)抓取以后手柄不可見(jiàn)氏捞,在LongBow實(shí)例與ArrowHand預(yù)制體上掛載VRTK_InteractControllerAppearance腳本,勾選Hide Controller On Grab屬性冒版,即抓取時(shí)隱藏手柄液茎,如下圖。
VRTK_Interact Controller Appearance

2. 替換原系統(tǒng)Hand的交互
由于腳本中大量引用了Interaction System中Player預(yù)制體的Hand辞嗡,所以要使用VRTK替換掉原系統(tǒng)中關(guān)于hand的引用捆等,我們將原系統(tǒng)中的hand.otherHand拆分,分別命名為hand续室,otherHand栋烤,并在LongBow被抓取是實(shí)現(xiàn)左右手的判斷和指定,代碼如下:

        //private Hand hand;
        //抓取當(dāng)前物體的控制器
        private GameObject hand;
        //另一只手
        private GameObject otherHand;
        private VRTKArrowHand arrowHand;

3. 替換ItemPackage機(jī)制:
ItemPackage的機(jī)制挺狰,本質(zhì)上是根據(jù)其提供的配置列表班缎,告訴交互系統(tǒng),當(dāng)觸發(fā)該機(jī)制的時(shí)候她渴,當(dāng)前手柄抓取指定的物體达址,同時(shí)另一只手抓取指定的物體(如果指定)。作為替換趁耗,我們將LongBow預(yù)制體放到場(chǎng)景當(dāng)中沉唠,替代預(yù)覽用的弓,不再自動(dòng)生成苛败,而是直接抓取满葛,至于同時(shí)抓取生成箭的功能,我們使用VRTK提供的抓取事件實(shí)現(xiàn)罢屈, 在Awake函數(shù)中注冊(cè)抓取及釋放事件處理函數(shù):

        GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKLongBow_InteractableObjectGrabbed;
        GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKLongBow_InteractableObjectUngrabbed;

在抓取事件中嘀韧,生成ArrowHand預(yù)制體的實(shí)例,代碼如下:

        private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
        {
            bowCollider.enabled = false;
            isGrab = true;
            ///當(dāng)抓取LongBow時(shí)生成ArrowHand
            GameObject arrowHandClone = Instantiate(arrowHandPrefab);

            handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
            ///分別指定相應(yīng)控制器缠捌,即左右手
            if (handType == SDK_BaseController.ControllerHand.Left)
            {
                hand = VRTK_DeviceFinder.GetControllerLeftHand();
                otherHand = VRTK_DeviceFinder.GetControllerRightHand();
            }
            else if (handType == SDK_BaseController.ControllerHand.Right)
            {
                hand = VRTK_DeviceFinder.GetControllerRightHand();
                otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
            }
            //另一只手抓取ArrowHand
            otherHand.GetComponent<VRTK_InteractTouch>().ForceTouch(arrowHandClone);
            otherHand.GetComponent<VRTK_InteractGrab>().AttemptGrab();
        }

4.手柄震動(dòng)的替換锄贷。 VRTK中使用VRTK_SDK_Bridge.HapticPulse()方法替換進(jìn)行震動(dòng)的調(diào)用,代碼如下

ushort hapticStrength = (ushort)Util.RemapNumber(nockDistanceTravelled, 0, maxPull, bowPullPulseStrengthLow, bowPullPulseStrengthHigh);
///手柄震動(dòng)
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), hapticStrength);
VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), hapticStrength);
//hand.controller.TriggerHapticPulse(hapticStrength);
//hand.otherHand.controller.TriggerHapticPulse(hapticStrength);

5. 修改hand發(fā)送的update事件曼月。 雖然我們?cè)诖a中修改了hand的相關(guān)引用谊却,但是代碼中的動(dòng)態(tài)刷新瞄準(zhǔn)、旋轉(zhuǎn)功能即核心功能還是依賴hand從外部SendMessage來(lái)實(shí)現(xiàn)哑芹,可見(jiàn)Hand.cs中的相關(guān)代碼:

        void Update()
        {
            UpdateNoSteamVRFallback();

            GameObject attached = currentAttachedObject;
            if ( attached )
            {
                attached.SendMessage( "HandAttachedUpdate", this, SendMessageOptions.DontRequireReceiver );
            }

            if ( hoveringInteractable )
            {
                hoveringInteractable.SendMessage( "HandHoverUpdate", this, SendMessageOptions.DontRequireReceiver );
            }
        }

所以我們將VRTKLongBow和VRTKArrowHand中的HandAttachedUpdate改為Update函數(shù)炎辨。但此時(shí)如果運(yùn)行程序,Update會(huì)自動(dòng)運(yùn)行函數(shù)內(nèi)容聪姿,由于此時(shí)控制器尚未指定碴萧,會(huì)出現(xiàn)運(yùn)行異常乙嘀,所以需要置一個(gè)標(biāo)志位isGrab,來(lái)控制邏輯的運(yùn)行破喻,即只有在物體被抓取以后才執(zhí)行Update下的程序邏輯乒躺。

說(shuō)明:

本文主要敘述移植過(guò)程思路及主要步驟,代碼修改的過(guò)程也僅是引用片段低缩,因?yàn)榇诉^(guò)程是一個(gè)逐漸調(diào)錯(cuò)的過(guò)程嘉冒,即根據(jù)調(diào)試過(guò)程中的報(bào)錯(cuò)來(lái)對(duì)需要修改的地方進(jìn)行修改,屬于良性調(diào)錯(cuò)咆繁,其過(guò)程瑣碎且丑陋讳推,故省略。至于射箭擊中靶子以及箭與火把的交互玩般,不涉及到VR范疇银觅,亦無(wú)可修改之處,故省略坏为,讀者可自行完善究驴。需要說(shuō)明的是,箭擊中靶子且吸附其上的原理是判斷有無(wú)指定的Physics Material匀伏,如有則認(rèn)為是射中了靶子洒忧,繼而吸附在上面。在此過(guò)程中一些必要且明顯的邏輯修改够颠,亦不再贅述熙侍。整個(gè)過(guò)程詳盡的實(shí)現(xiàn)可參考文末所附的代碼,歡迎提出修改意見(jiàn):

VRTKLongBow.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VRTK;

namespace Valve.VR.InteractionSystem
{
    public class VRTKLongBow : MonoBehaviour
    {

        public enum Handedness { Left, Right };

        public Handedness currentHandGuess = Handedness.Left;
        private float timeOfPossibleHandSwitch = 0f;
        private float timeBeforeConfirmingHandSwitch = 1.5f;
        private bool possibleHandSwitch = false;

        public Transform pivotTransform;
        public Transform handleTransform;

        //private Hand hand;
        //抓取當(dāng)前物體的控制器
        private GameObject hand;
        //另一只手
        private GameObject otherHand;
        private VRTKArrowHand arrowHand;

        public Transform nockTransform;
        public Transform nockRestTransform;

        public bool autoSpawnArrowHand = true;
        public ItemPackage arrowHandItemPackage;
        public GameObject arrowHandPrefab;

        public bool nocked;
        public bool pulled;

        private const float minPull = 0.05f;
        private const float maxPull = 0.5f;
        private float nockDistanceTravelled = 0f;
        private float hapticDistanceThreshold = 0.01f;
        private float lastTickDistance;
        private const float bowPullPulseStrengthLow = 100;
        private const float bowPullPulseStrengthHigh = 500;
        private Vector3 bowLeftVector;

        public float arrowMinVelocity = 3f;
        public float arrowMaxVelocity = 30f;
        private float arrowVelocity = 30f;

        private float minStrainTickTime = 0.1f;
        private float maxStrainTickTime = 0.5f;
        private float nextStrainTick = 0;

        private bool lerpBackToZeroRotation;
        private float lerpStartTime;
        private float lerpDuration = 0.15f;
        private Quaternion lerpStartRotation;

        private float nockLerpStartTime;

        private Quaternion nockLerpStartRotation;

        public float drawOffset = 0.06f;

        //public LinearMapping bowDrawLinearMapping;
        public Animator longBowAni;

        private bool deferNewPoses = false;
        private Vector3 lateUpdatePos;
        private Quaternion lateUpdateRot;

        public SoundBowClick drawSound;
        private float drawTension;
        public SoundPlayOneshot arrowSlideSound;
        public SoundPlayOneshot releaseSound;
        public SoundPlayOneshot nockSound;

        SteamVR_Events.Action newPosesAppliedAction;

        SDK_BaseController.ControllerHand handType;
        private bool isGrab = false;
        /// <summary>
        /// 防止與弓發(fā)生碰撞
        /// </summary>
        private SphereCollider bowCollider;

        //-------------------------------------------------
        //被VRTK抓取事件處理函數(shù)替代
        //private void OnAttachedToHand(Hand attachedHand)
        //{
        //    hand = attachedHand;
        //}

        //-------------------------------------------------
        void Awake()
        {
            longBowAni = GetComponent<Animator>();
            longBowAni.speed = 0;
            bowCollider = GetComponent<SphereCollider>();
            newPosesAppliedAction = SteamVR_Events.NewPosesAppliedAction(OnNewPosesApplied);
            GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKLongBow_InteractableObjectGrabbed;
            GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKLongBow_InteractableObjectUngrabbed;
        }

        private void VRTKLongBow_InteractableObjectUngrabbed(object sender, InteractableObjectEventArgs e)
        {
            isGrab = false;
            bowCollider.enabled = true;

            if (handType == SDK_BaseController.ControllerHand.Left)
            {
                VRTK_DeviceFinder.GetControllerRightHand().GetComponent<VRTK_InteractGrab>().ForceRelease();
            }
            else
            {
                VRTK_DeviceFinder.GetControllerLeftHand().GetComponent<VRTK_InteractGrab>().ForceRelease();
            }
        }

        /// <summary>
        /// 弓被抓取事件處理函數(shù)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VRTKLongBow_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
        {
            bowCollider.enabled = false;
            isGrab = true;
            ///當(dāng)抓取LongBow時(shí)生成ArrowHand
            GameObject arrowHandClone = Instantiate(arrowHandPrefab);

            handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
            ///分別指定相應(yīng)控制器履磨,即左右手
            if (handType == SDK_BaseController.ControllerHand.Left)
            {
                hand = VRTK_DeviceFinder.GetControllerLeftHand();
                otherHand = VRTK_DeviceFinder.GetControllerRightHand();
            }
            else if (handType == SDK_BaseController.ControllerHand.Right)
            {
                hand = VRTK_DeviceFinder.GetControllerRightHand();
                otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
            }
            //另一只手抓取ArrowHand
            otherHand.GetComponent<VRTK_InteractTouch>().ForceTouch(arrowHandClone);
            otherHand.GetComponent<VRTK_InteractGrab>().AttemptGrab();
        }

        //-------------------------------------------------
        void OnEnable()
        {
            newPosesAppliedAction.enabled = true;
        }


        //-------------------------------------------------
        void OnDisable()
        {
            newPosesAppliedAction.enabled = false;
        }


        //-------------------------------------------------
        void LateUpdate()
        {
            if (deferNewPoses)
            {
                lateUpdatePos = transform.position;
                lateUpdateRot = transform.rotation;
            }
        }


        //-------------------------------------------------
        private void OnNewPosesApplied()
        {
            if (deferNewPoses)
            {
                // Set longbow object back to previous pose position to avoid jitter
                transform.position = lateUpdatePos;
                transform.rotation = lateUpdateRot;

                deferNewPoses = false;
            }
        }


        //-------------------------------------------------
        //private void HandAttachedUpdate(Hand hand)
        private void Update()
        {
            if (!isGrab)
                return;
            // Reset transform since we cheated it right after getting poses on previous frame
            transform.localPosition = Vector3.zero;
            transform.localRotation = Quaternion.identity;

            // Update handedness guess
            //EvaluateHandedness();

            if (nocked)
            {
                deferNewPoses = true;

                Vector3 nockToarrowHand = (arrowHand.arrowNockTransform.parent.position - nockRestTransform.position); // Vector from bow nock transform to arrowhand nock transform - used to align bow when drawing

                // Align bow
                // Time lerp value used for ramping into drawn bow orientation
                float lerp = Util.RemapNumberClamped(Time.time, nockLerpStartTime, (nockLerpStartTime + lerpDuration), 0f, 1f);

                float pullLerp = Util.RemapNumberClamped(nockToarrowHand.magnitude, minPull, maxPull, 0f, 1f); // Normalized current state of bow draw 0 - 1

                //Vector3 arrowNockTransformToHeadset = ((Player.instance.hmdTransform.position + (Vector3.down * 0.05f)) - arrowHand.arrowNockTransform.parent.position).normalized;

                Vector3 arrowNockTransformToHeadset = ((VRTK_SDK_Bridge.GetHeadset().position + (Vector3.down * 0.05f)) - arrowHand.arrowNockTransform.parent.position).normalized;
                Vector3 arrowHandPosition = (arrowHand.arrowNockTransform.parent.position + ((arrowNockTransformToHeadset * drawOffset) * pullLerp)); // Use this line to lerp arrowHand nock position
                                                                                                                                                      //Vector3 arrowHandPosition = arrowHand.arrowNockTransform.position; // Use this line if we don't want to lerp arrowHand nock position

                Vector3 pivotToString = (arrowHandPosition - pivotTransform.position).normalized;
                Vector3 pivotToLowerHandle = (handleTransform.position - pivotTransform.position).normalized;
                bowLeftVector = -Vector3.Cross(pivotToLowerHandle, pivotToString);
                pivotTransform.rotation = Quaternion.Lerp(nockLerpStartRotation, Quaternion.LookRotation(pivotToString, bowLeftVector), lerp);

                // Move nock position
                if (Vector3.Dot(nockToarrowHand, -nockTransform.forward) > 0)
                {
                    float distanceToarrowHand = nockToarrowHand.magnitude * lerp;

                    nockTransform.localPosition = new Vector3(0f, 0f, Mathf.Clamp(-distanceToarrowHand, -maxPull, 0f));

                    nockDistanceTravelled = -nockTransform.localPosition.z;

                    arrowVelocity = Util.RemapNumber(nockDistanceTravelled, minPull, maxPull, arrowMinVelocity, arrowMaxVelocity);

                    drawTension = Util.RemapNumberClamped(nockDistanceTravelled, 0, maxPull, 0f, 1f);

                    //this.bowDrawLinearMapping.value = drawTension; // Send drawTension value to LinearMapping script, which drives the bow draw animation
                    longBowAni.Play(0, 0, drawTension);

                    if (nockDistanceTravelled > minPull)
                    {
                        pulled = true;
                    }
                    else
                    {
                        pulled = false;
                    }

                    if ((nockDistanceTravelled > (lastTickDistance + hapticDistanceThreshold)) || nockDistanceTravelled < (lastTickDistance - hapticDistanceThreshold))
                    {
                        ushort hapticStrength = (ushort)Util.RemapNumber(nockDistanceTravelled, 0, maxPull, bowPullPulseStrengthLow, bowPullPulseStrengthHigh);
                        ///手柄震動(dòng)
                        VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), hapticStrength);
                        VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), hapticStrength);
                        //hand.controller.TriggerHapticPulse(hapticStrength);
                        //hand.otherHand.controller.TriggerHapticPulse(hapticStrength);

                        drawSound.PlayBowTensionClicks(drawTension);

                        lastTickDistance = nockDistanceTravelled;
                    }

                    if (nockDistanceTravelled >= maxPull)
                    {
                        if (Time.time > nextStrainTick)
                        {
                            //hand.controller.TriggerHapticPulse(400);
                            //hand.otherHand.controller.TriggerHapticPulse(400);
                            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), 400);
                            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 400);

                            drawSound.PlayBowTensionClicks(drawTension);

                            nextStrainTick = Time.time + Random.Range(minStrainTickTime, maxStrainTickTime);
                        }
                    }
                }
                else
                {
                    nockTransform.localPosition = new Vector3(0f, 0f, 0f);

                    //this.bowDrawLinearMapping.value = 0f;
                    longBowAni.Play(0, 0, 0);
                }
            }
            else
            {
                if (lerpBackToZeroRotation)
                {
                    float lerp = Util.RemapNumber(Time.time, lerpStartTime, lerpStartTime + lerpDuration, 0, 1);

                    pivotTransform.localRotation = Quaternion.Lerp(lerpStartRotation, Quaternion.identity, lerp);

                    if (lerp >= 1)
                    {
                        lerpBackToZeroRotation = false;
                    }
                }
            }
        }


        //-------------------------------------------------
        public void ArrowReleased()
        {
            nocked = false;
            //hand.HoverUnlock(GetComponent<Interactable>());
            //hand.otherHand.HoverUnlock(arrowHand.GetComponent<Interactable>());

            if (releaseSound != null)
            {
                releaseSound.Play();
            }

            this.StartCoroutine(this.ResetDrawAnim());
        }


        //-------------------------------------------------
        private IEnumerator ResetDrawAnim()
        {
            float startTime = Time.time;
            float startLerp = drawTension;

            while (Time.time < (startTime + 0.02f))
            {
                float lerp = Util.RemapNumberClamped(Time.time, startTime, startTime + 0.02f, startLerp, 0f);
                //this.bowDrawLinearMapping.value = lerp;
                longBowAni.Play(0, 0, lerp);
                yield return null;
            }

            //this.bowDrawLinearMapping.value = 0;
            longBowAni.Play(0, 0, 0);

            yield break;
        }


        //-------------------------------------------------
        public float GetArrowVelocity()
        {
            return arrowVelocity;
        }


        //-------------------------------------------------
        public void StartRotationLerp()
        {
            lerpStartTime = Time.time;
            lerpBackToZeroRotation = true;
            lerpStartRotation = pivotTransform.localRotation;

            Util.ResetTransform(nockTransform);
        }


        //-------------------------------------------------
        public void StartNock(VRTKArrowHand currentArrowHand)
        {
            arrowHand = currentArrowHand;
            //hand.HoverLock(GetComponent<Interactable>());
            nocked = true;
            nockLerpStartTime = Time.time;
            nockLerpStartRotation = pivotTransform.rotation;

            // Sound of arrow sliding on nock as it's being pulled back
            arrowSlideSound.Play();

            // Decide which hand we're drawing with and lerp to the correct side
            DoHandednessCheck();
        }


        //-------------------------------------------------
        //private void EvaluateHandedness()
        //{
        //    Hand.HandType handType = hand.GuessCurrentHandType();

        //    if (handType == Hand.HandType.Left)// Bow hand is further left than arrow hand.
        //    {
        //        // We were considering a switch, but the current controller orientation matches our currently assigned handedness, so no longer consider a switch
        //        if (possibleHandSwitch && currentHandGuess == Handedness.Left)
        //        {
        //            possibleHandSwitch = false;
        //        }

        //        // If we previously thought the bow was right-handed, and were not already considering switching, start considering a switch
        //        if (!possibleHandSwitch && currentHandGuess == Handedness.Right)
        //        {
        //            possibleHandSwitch = true;
        //            timeOfPossibleHandSwitch = Time.time;
        //        }

        //        // If we are considering a handedness switch, and it's been this way long enough, switch
        //        if (possibleHandSwitch && Time.time > (timeOfPossibleHandSwitch + timeBeforeConfirmingHandSwitch))
        //        {
        //            currentHandGuess = Handedness.Left;
        //            possibleHandSwitch = false;
        //        }
        //    }
        //    else // Bow hand is further right than arrow hand
        //    {
        //        // We were considering a switch, but the current controller orientation matches our currently assigned handedness, so no longer consider a switch
        //        if (possibleHandSwitch && currentHandGuess == Handedness.Right)
        //        {
        //            possibleHandSwitch = false;
        //        }

        //        // If we previously thought the bow was right-handed, and were not already considering switching, start considering a switch
        //        if (!possibleHandSwitch && currentHandGuess == Handedness.Left)
        //        {
        //            possibleHandSwitch = true;
        //            timeOfPossibleHandSwitch = Time.time;
        //        }

        //        // If we are considering a handedness switch, and it's been this way long enough, switch
        //        if (possibleHandSwitch && Time.time > (timeOfPossibleHandSwitch + timeBeforeConfirmingHandSwitch))
        //        {
        //            currentHandGuess = Handedness.Right;
        //            possibleHandSwitch = false;
        //        }
        //    }
        //}


        //-------------------------------------------------
        private void DoHandednessCheck()
        {
            // Based on our current best guess about hand, switch bow orientation and arrow lerp direction
            if (currentHandGuess == Handedness.Left)
            {
                pivotTransform.localScale = new Vector3(1f, 1f, 1f);
            }
            else
            {
                pivotTransform.localScale = new Vector3(1f, -1f, 1f);
            }
        }


        //-------------------------------------------------
        public void ArrowInPosition()
        {
            DoHandednessCheck();

            if (nockSound != null)
            {
                nockSound.Play();
            }
        }


        //-------------------------------------------------
        public void ReleaseNock()
        {
            // ArrowHand tells us to do this when we release the buttons when bow is nocked but not drawn far enough
            nocked = false;
            //hand.HoverUnlock(GetComponent<Interactable>());
            this.StartCoroutine(this.ResetDrawAnim());
        }


        //改寫(xiě)Shutdown內(nèi)容
        private void ShutDown()
        {
            //if (hand != null && hand.otherHand.currentAttachedObject != null)
            //{
            //    if (hand.otherHand.currentAttachedObject.GetComponent<ItemPackageReference>() != null)
            //    {
            //        if (hand.otherHand.currentAttachedObject.GetComponent<ItemPackageReference>().itemPackage == arrowHandItemPackage)
            //        {
            //            hand.otherHand.DetachObject(hand.otherHand.currentAttachedObject);
            //        }
            //    }
            //}

        }


        //-------------------------------------------------
        private void OnHandFocusLost(Hand hand)
        {
            gameObject.SetActive(false);
        }


        //-------------------------------------------------
        private void OnHandFocusAcquired(Hand hand)
        {
            gameObject.SetActive(true);
            //調(diào)用抓取事件處理函數(shù)蛉抓,不再使用
            ///OnAttachedToHand(hand);
        }


        //不再使用,不再銷毀
        private void OnDetachedFromHand(Hand hand)
        {
            Destroy(gameObject);
        }


        //-------------------------------------------------
        void OnDestroy()
        {
            ShutDown();
        }
    }
}

VRTKArrowHand.cs:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using VRTK;

namespace Valve.VR.InteractionSystem
{
    public class VRTKArrowHand : MonoBehaviour
    {

        //private Hand hand;
        private GameObject hand;
        private GameObject otherHand;
        private VRTKLongBow bow;

        private GameObject currentArrow;
        public GameObject arrowPrefab;

        public Transform arrowNockTransform;

        public float nockDistance = 0.1f;
        public float lerpCompleteDistance = 0.08f;
        public float rotationLerpThreshold = 0.15f;
        public float positionLerpThreshold = 0.15f;

        private bool allowArrowSpawn = true;
        private bool nocked;

        private bool inNockRange = false;
        private bool arrowLerpComplete = false;

        public SoundPlayOneshot arrowSpawnSound;

        //private AllowTeleportWhileAttachedToHand allowTeleport = null;

        public int maxArrowCount = 10;
        private List<GameObject> arrowList;

        private bool triggerPressed = false;
        /// <summary>
        /// 物體是否被抓取
        /// </summary>
        private bool isGrab = false;
        SDK_BaseController.ControllerHand handType;
        //-------------------------------------------------
        void Awake()
        {
            //allowTeleport = GetComponent<AllowTeleportWhileAttachedToHand>();
            //allowTeleport.teleportAllowed = true;
            //allowTeleport.overrideHoverLock = false;

            arrowList = new List<GameObject>();
            GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += VRTKArrowHand_InteractableObjectGrabbed;
            GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += VRTKArrowHand_InteractableObjectUngrabbed;
        }

        /// <summary>
        /// ArrowHand被釋放處理函數(shù)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VRTKArrowHand_InteractableObjectUngrabbed(object sender, InteractableObjectEventArgs e)
        {
            isGrab = false;
            hand = e.interactingObject;
            hand.GetComponent<VRTK_ControllerEvents>().TriggerReleased -= VRTKArrowHand_TriggerReleased;
            hand.GetComponent<VRTK_ControllerEvents>().TriggerPressed -= VRTKArrowHand_TriggerPressed;
            //Destroy(gameObject);
        }

        /// <summary>
        /// ArrowHand被抓取處理函數(shù)剃诅,替代OnAttachedToHand函數(shù)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VRTKArrowHand_InteractableObjectGrabbed(object sender, InteractableObjectEventArgs e)
        {
            isGrab = true;
            hand = e.interactingObject;
            handType = VRTK_DeviceFinder.GetControllerHand(e.interactingObject);
            ///分別指定相應(yīng)控制器巷送,即左右手
            if (handType == SDK_BaseController.ControllerHand.Left)
            {
                hand = VRTK_DeviceFinder.GetControllerLeftHand();
                otherHand = VRTK_DeviceFinder.GetControllerRightHand();
            }
            else if (handType == SDK_BaseController.ControllerHand.Right)
            {
                hand = VRTK_DeviceFinder.GetControllerRightHand();
                otherHand = VRTK_DeviceFinder.GetControllerLeftHand();
            }
            hand.GetComponent<VRTK_ControllerEvents>().TriggerReleased += VRTKArrowHand_TriggerReleased;
            hand.GetComponent<VRTK_ControllerEvents>().TriggerPressed += VRTKArrowHand_TriggerPressed;
            FindBow();
        }

        /// <summary>
        /// Trigger鍵按下
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VRTKArrowHand_TriggerPressed(object sender, ControllerInteractionEventArgs e)
        {
            triggerPressed = true;
        }

        /// <summary>
        /// Trigger鍵松開(kāi)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void VRTKArrowHand_TriggerReleased(object sender, ControllerInteractionEventArgs e)
        {
            triggerPressed = false;
        }


        //等同于onGrab
        //private void OnAttachedToHand(Hand attachedHand)
        //{
        //    hand = attachedHand;
        //    FindBow();
        //}

        /// <summary>
        /// 生成Arrow
        /// </summary>
        /// <returns></returns>
        private GameObject InstantiateArrow()
        {
            GameObject arrow = Instantiate(arrowPrefab, arrowNockTransform.position, arrowNockTransform.rotation) as GameObject;
            arrow.name = "Bow Arrow";
            arrow.transform.parent = arrowNockTransform;
            Util.ResetTransform(arrow.transform);

            arrowList.Add(arrow);

            while (arrowList.Count > maxArrowCount)
            {
                GameObject oldArrow = arrowList[0];
                arrowList.RemoveAt(0);
                if (oldArrow)
                {
                    Destroy(oldArrow);
                }
            }

            return arrow;
        }


        //注釋掉HandAttachedUpdate函數(shù),函數(shù)內(nèi)容保持不變矛辕,函數(shù)名改為Update
        //private void HandAttachedUpdate(Hand hand)
        private void Update()
        {
            if (!isGrab)
                return;
            if (bow == null)
            {
                FindBow();
            }

            if (bow == null)
            {
                return;
            }

            if (allowArrowSpawn && (currentArrow == null)) // If we're allowed to have an active arrow in hand but don't yet, spawn one
            {
                currentArrow = InstantiateArrow();
                arrowSpawnSound.Play();
            }

            float distanceToNockPosition = Vector3.Distance(transform.parent.position, bow.nockTransform.position);

            // If there's an arrow spawned in the hand and it's not nocked yet
            if (!nocked)
            {
                // If we're close enough to nock position that we want to start arrow rotation lerp, do so
                if (distanceToNockPosition < rotationLerpThreshold)
                {
                    float lerp = Util.RemapNumber(distanceToNockPosition, rotationLerpThreshold, lerpCompleteDistance, 0, 1);

                    arrowNockTransform.rotation = Quaternion.Lerp(arrowNockTransform.parent.rotation, bow.nockRestTransform.rotation, lerp);
                }
                else // Not close enough for rotation lerp, reset rotation
                {
                    arrowNockTransform.localRotation = Quaternion.identity;
                }

                // If we're close enough to the nock position that we want to start arrow position lerp, do so
                if (distanceToNockPosition < positionLerpThreshold)
                {
                    float posLerp = Util.RemapNumber(distanceToNockPosition, positionLerpThreshold, lerpCompleteDistance, 0, 1);

                    posLerp = Mathf.Clamp(posLerp, 0f, 1f);

                    arrowNockTransform.position = Vector3.Lerp(arrowNockTransform.parent.position, bow.nockRestTransform.position, posLerp);
                }
                else // Not close enough for position lerp, reset position
                {
                    arrowNockTransform.position = arrowNockTransform.parent.position;
                }


                // Give a haptic tick when lerp is visually complete
                if (distanceToNockPosition < lerpCompleteDistance)
                {
                    if (!arrowLerpComplete)
                    {
                        arrowLerpComplete = true;
                        //hand.controller.TriggerHapticPulse(500);
                        VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(hand), 500);
                    }
                }
                else
                {
                    if (arrowLerpComplete)
                    {
                        arrowLerpComplete = false;
                    }
                }

                // Allow nocking the arrow when controller is close enough
                if (distanceToNockPosition < nockDistance)
                {
                    if (!inNockRange)
                    {
                        inNockRange = true;
                        bow.ArrowInPosition();
                    }
                }
                else
                {
                    if (inNockRange)
                    {
                        inNockRange = false;
                    }
                }

                // If arrow is close enough to the nock position and we're pressing the trigger, and we're not nocked yet, Nock
                //拉弓瞄準(zhǔn)
                if ((distanceToNockPosition < nockDistance) && triggerPressed && !nocked)
                {
                    if (currentArrow == null)
                    {
                        currentArrow = InstantiateArrow();
                    }

                    nocked = true;
                    bow.StartNock(this);
                    //hand.HoverLock(GetComponent<Interactable>());
                    //allowTeleport.teleportAllowed = false;
                    currentArrow.transform.parent = bow.nockTransform;
                    Util.ResetTransform(currentArrow.transform);
                    Util.ResetTransform(arrowNockTransform);
                }
            }


            // If arrow is nocked, and we release the trigger
            if (nocked && !triggerPressed)
            {
                if (bow.pulled) // If bow is pulled back far enough, fire arrow, otherwise reset arrow in arrowhand
                {
                    FireArrow();
                }
                else
                {
                    arrowNockTransform.rotation = currentArrow.transform.rotation;
                    currentArrow.transform.parent = arrowNockTransform;
                    Util.ResetTransform(currentArrow.transform);
                    nocked = false;
                    bow.ReleaseNock();
                    //hand.HoverUnlock(GetComponent<Interactable>());
                    //allowTeleport.teleportAllowed = true;
                }

                bow.StartRotationLerp(); // Arrow is releasing from the bow, tell the bow to lerp back to controller rotation
            }
        }


        //改寫(xiě)為grab unity事件
        private void OnDetachedFromHand(Hand hand)
        {
            Destroy(gameObject);
        }


        //-------------------------------------------------
        private void FireArrow()
        {
            currentArrow.transform.parent = null;

            Arrow arrow = currentArrow.GetComponent<Arrow>();
            arrow.shaftRB.isKinematic = false;
            arrow.shaftRB.useGravity = true;
            arrow.shaftRB.transform.GetComponent<BoxCollider>().enabled = true;

            arrow.arrowHeadRB.isKinematic = false;
            arrow.arrowHeadRB.useGravity = true;
            arrow.arrowHeadRB.transform.GetComponent<BoxCollider>().enabled = true;

            arrow.arrowHeadRB.AddForce(currentArrow.transform.forward * bow.GetArrowVelocity(), ForceMode.VelocityChange);
            arrow.arrowHeadRB.AddTorque(currentArrow.transform.forward * 10);

            nocked = false;

            currentArrow.GetComponent<Arrow>().ArrowReleased(bow.GetArrowVelocity());
            bow.ArrowReleased();

            allowArrowSpawn = false;
            Invoke("EnableArrowSpawn", 0.5f);
            StartCoroutine(ArrowReleaseHaptics());

            currentArrow = null;
            //allowTeleport.teleportAllowed = true;
        }


        //-------------------------------------------------
        private void EnableArrowSpawn()
        {
            allowArrowSpawn = true;
        }


        //-------------------------------------------------
        private IEnumerator ArrowReleaseHaptics()
        {
            yield return new WaitForSeconds(0.05f);

            //hand.otherHand.controller.TriggerHapticPulse(1500);
            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 1500);
            yield return new WaitForSeconds(0.05f);

            //hand.otherHand.controller.TriggerHapticPulse(800);
            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 800);
            yield return new WaitForSeconds(0.05f);

            //hand.otherHand.controller.TriggerHapticPulse(500);
            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 500);
            yield return new WaitForSeconds(0.05f);

            //hand.otherHand.controller.TriggerHapticPulse(300);
            VRTK_SDK_Bridge.HapticPulse(VRTK_ControllerReference.GetControllerReference(otherHand), 300);
        }


        //-------------------------------------------------
        private void OnHandFocusLost(Hand hand)
        {
            gameObject.SetActive(false);
        }


        //-------------------------------------------------
        private void OnHandFocusAcquired(Hand hand)
        {
            gameObject.SetActive(true);
        }


        //-------------------------------------------------
        private void FindBow()
        {
            SDK_BaseController.ControllerHand handType = VRTK_DeviceFinder.GetControllerHand(hand);
            GameObject bowGo;
            bowGo = otherHand.GetComponent<VRTK_InteractGrab>().GetGrabbedObject();
            bow = bowGo.GetComponent<VRTKLongBow>();
            //bow = hand.otherHand.GetComponentInChildren<VRTKLongBow>();
        }
    }
}

本文是我的視頻教程《HTC VIVE交互開(kāi)發(fā)實(shí)例教程》的節(jié)選文字版笑跛,更多VRTK實(shí)例教程可參見(jiàn)蠻牛教育首頁(yè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市如筛,隨后出現(xiàn)的幾起案子堡牡,更是在濱河造成了極大的恐慌抒抬,老刑警劉巖杨刨,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異擦剑,居然都是意外死亡妖胀,警方通過(guò)查閱死者的電腦和手機(jī)芥颈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赚抡,“玉大人爬坑,你說(shuō)我怎么就攤上這事⊥砍迹” “怎么了盾计?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赁遗。 經(jīng)常有香客問(wèn)我署辉,道長(zhǎng),這世上最難降的妖魔是什么岩四? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任哭尝,我火速辦了婚禮,結(jié)果婚禮上剖煌,老公的妹妹穿的比我還像新娘材鹦。我一直安慰自己,他們只是感情好耕姊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布桶唐。 她就那樣靜靜地躺著,像睡著了一般茉兰。 火紅的嫁衣襯著肌膚如雪莽红。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天邦邦,我揣著相機(jī)與錄音安吁,去河邊找鬼。 笑死燃辖,一個(gè)胖子當(dāng)著我的面吹牛鬼店,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播黔龟,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妇智,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了氏身?” 一聲冷哼從身側(cè)響起巍棱,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛋欣,沒(méi)想到半個(gè)月后航徙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陷虎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年到踏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了杠袱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窝稿,死狀恐怖楣富,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伴榔,我是刑警寧澤纹蝴,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站踪少,受9級(jí)特大地震影響骗灶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秉馏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一耙旦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧萝究,春花似錦免都、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至栽连,卻和暖如春险领,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秒紧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工绢陌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人熔恢。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓脐湾,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親叙淌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子秤掌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容