This document is about: QUANTUM 3
SWITCH TO

Quantum XR

Level 4

概述

Quantum XR科技示例演示了如何將Quantum引擎用於XR遊戲或應用程式。
因此,它為管理XR設備的空間位置同步提供了解決方案,並具有平滑的顯示效果,而Quantum旨在同步輸入(而不是空間位置)。
此外,還提供了一些基於物理的迷你遊戲(籃球、拳擊、網球等),以展示由於Quantum引擎,它們在多用戶體驗中的實現是多麼容易和快速。

Quantum XR scene overview

開始之前

  • 該專案是用Unity 2022.3和Quantum 3.0開發的
  • 要運行示例,首先在PhotoneEngine儀錶板中創建Quantum AppId和Voice AppId,並將其粘貼到Photon伺服器設定中的App Id QuantumApp Id Voice欄位中(可從“工具/Quantum”選單存取)。然後載入場景並按Play

下載

版本 發佈日期 下載

處理輸入

Meta Quest

  • 傳送:按A、B、X、Y或任何搖杆以顯示指標。你將在釋放時傳送任何被接受的目標
  • 抓取:首先將手放在物體上,然後使用控制器抓取按鈕抓取
  • 牽引機光束:瞄準要吸引的物體,按下操縱杆上的觸發器。觸發按鈕上的壓力將決定吸引力。定義了三個級別:緩慢吸引、快速吸引或待機(相當於遠程抓取)。

全域架構

設備位置同步

在沉浸式應用程式中,裝備描述了代表用戶所需的所有移動部件,通常是雙手、頭部和遊戲區域(例如,當用戶傳送時,可以移動的個人空間)。

在這個示例中,“硬體裝備”收集設備資料,以了解所有裝備部件相對於裝備(事實上,相對於遊戲區域)的位置。

然後,這些資料透過輸入端發送到Quantum引擎。
在那裡,Quantum系統移動具有代表相同裝備零件的組件的實體:

  • 裝備本身會根據用戶的輸入進行相應的移動(這裡是傳送,但也可以使用基於搖桿的運動)。
  • 根據透過輸入的局部位置,頭部和手相對於裝備部件移動

然後Unity中的視圖顯示這些實體。

最後,使用裝備視圖位置來向後移動硬體裝備:這是必要的,因為我們希望相機根據Quantum系統中確定的裝備位置變化而相應地移動,並且透過在XR中更改裝備(遊玩區域)位置來在場景中移動相機位置。

Quantum XR Overall flow

手部平滑

在通常的Quantum應用程式中,輸入包含對預測幀有用的資料:你可以假設如果玩家向前移動,它可能會繼續這樣做。輸入描述的是移動,而不是最終位置,因此這個移動可以在預測中重複使用。

在VR中,我們需要在輸入中共享頭部和手的局部位置。這些是最終位置,而不是運動,不能直接用於預測在預測幀期間的運動。
有幾種方法可以解決這個問題(這裡主要描述了手的情況,因為它們是最有問題的,但必須對頭部採取類似的方法):

  • 我們可以要求視圖在經過驗證的幀之間進行插值。這將包括延遲。對於本機用戶來說,可以透過推斷手的位置來補償響應性的不足(用實際的當前硬體狀態覆蓋插值的手的位置),但在許多情況下,與Quantum狀態的感應差異是不合適的(涉及手的反應物理,…)
  • 我們可以存儲輸入,並稍微延遲其使用,根據需要在存儲的位置之間進行延遲插值。這種基於時間的Quantum插值本身工作良好,同時仍然保留了物理特性,但它也引入了在某些情況下可能存在問題的延遲。

在這裡,採取的預設方法是:

  1. 嘗試根據驗證幀中收到的最新資訊實際預測手部動作。然後,預測幀實際上會嘗試預測手的位置(例如,基於最新的手速)
  2. 當然,這些預測往往會失敗,因此引入了基於最新位置的平滑處理,以避免做出過於明顯的修正。
Quantum XR Hand position prediction

這1)和2)的方法是對立的,從某種意義上說,1)試圖猜測未來,而2)將其推回過去一點。
當前的實現提供了一種特定的平衡,但這必須針對實際的遊戲玩法進行定制。

例如,所選的預設方法對於非常快的變化(如球拍遊戲)來說反應稍顯不足,因此一隻手在抓球拍時使用了不同的配置。

物理抓取

在此示例中,大多數物件的目標是:

  • 可被手抓住
  • 被碰撞的表面和物體阻擋。

要做到這一點,抓取邏輯必須依賴於力,這樣碰撞的表面就可以推回抓取邏輯:在現實生活中,我們的手可以穿過這些虛擬的阻擋表面,但我們不希望被抓取的物體這樣做。

為了實現這一目標:

  • 手實體可以自由移動,不受面阻礙
  • 被抓取的物體試圖透過施加力來跟隨手的位置
  • 為了確定這些力並具有平滑的軌跡,抓取力是透過一個專用的簡單PID控制器計算的(詳見此處,或非常清晰的解釋影片在此處
  • 當抓取的物體碰撞到表面時,PID控制器的整合因子被禁用(預設),以避免新增它無法到達目的地的“記憶”

物理手

對於某些物體,我們想演示直接推動或擊打它們,而不需要抓住物體。

由於我們選擇讓手實體自由移動並遵循實際生活中的手位置,因此Quantum中不適合作為物理體。
因此,我們新增了物理手實體。

這些物理手與抓取的物體非常相似:

  • 他們跟隨手的位置,但持續
  • 此跟隨基於相同的基於PID控制器的力

為了限制這些物理手可以碰撞的物體,我們設定了一個特殊的碰撞矩陣,以控制可以撞哪個物體。物理手位於PhysicsHands層,只能與PhysicsHandsPushable層中的物體互動。

Quantum XR Collision matrix

這是必需的,因為物理手碰撞器在這裡沒有動畫,它們有一個簡單的盒碰撞器,所以很難抓住可以撞的東西。
如果需要,可以使用複合碰撞器,並根據手部狀態改變其部件位置。請參閱運行時更改形狀檔案。

手視圖:位置覆蓋和偽觸覺回饋

為了更自然地顯示用戶的手,在某些情況下,在HandView中會覆寫手的位置:

  • (1) 在一般情況下,對於本機用戶,顯示硬體資料中最新和最靈敏的手部位置
  • (2) 當發生物理抓取時,確保手在最初抓取的位置“粘”在被抓取的物體上(我們不想讓人看到可抓取的物體正在以力跟隨手,涉及輕微的偏移)
  • (3) 當物理手碰撞表面並被表面阻擋時
  • (4) 當物理抓取的物體碰撞表面並被表面阻擋時

其中一些情況((1)和(2))將被默默地覆寫,而對於其他情况,我們希望用戶“感受”到現實生活中手部位置和顯示位置之間的差異。
如果我們在現實生活中的位置顯示一隻幽靈手,並應用與差異成比例的觸覺回饋,用戶可以“感覺到”這個問題,他們的大腦將其解釋為實際的阻力。

這種“偽觸覺迴響”(見此處這裡對於該領域的研究論文)解決了視覺手與現實生活中的手位置不匹配的問題,同時提供了部分物理阻力模擬。

關於用手位置:

  • (1) 一般情況下本機用戶的推斷:使用硬體手部位置
  • (2) 和 (4) 物理抓取:抓取的物體現在是參照,手放在物體上,以保持初始抓取的偏移量
  • (3) 物理手碰撞:物理手現在成為手表示的參照位置
Quantum XR Hand view override

傳送

為了提供良好的用戶體驗,當用戶傳送時,我們不能簡單地改變Quantum中的裝備位置。
事實上,在移動玩家之前,用漸變到黑色來隱藏場景是一種很好的做法,以避免因位置變化而引起的運動。
這就是為什麼傳送玩家需要幾個步驟:

  • 首先我們從控制器讀取資訊以檢查傳送按鈕是否已被啟動,
  • 然後,輸入被發送到Quantum,因此運動系統可以檢查每個手的輸入,以更新每個手實體的LocomotionRayState狀態機。如果發生傳送請求,則會向Unity發送一個事件以開始淡入淡出。
  • 當淡入完成時向Quantum發送命令以移動裝備的元件,
  • 傳送完成後,會向Unity發送一個事件以進行淡出。

在此過程中,光線的視覺效果在Unity側基於LocomotionRayState狀態機進行管理。

更多詳細資訊請參閱下面的RayAndLocomotionSystem部分。

牽引機光束

為了能夠在遠處操縱物體,傳送光束也可以用作牽引光束,透過在按下控制器上的觸發按鈕的同時指向可抓取的物體。

該吸引力基於與物理抓取和物理手相同的基於PID控制器的力。

根據觸發按鈕的壓力水準,有3個吸引力級別:

  • 快速吸引:當觸發器幾乎完全按下時,物體會被吸引到一個快速收斂到用戶手上的點
  • 距離鎖定:當輕輕按下觸發器時,物體會被吸引到一個保持恒定距離的點。如果需要,它允許橫向發射物體,就像鞭子一樣
  • 緩慢吸引:在這些壓力水準之間,物體會被吸引到一個緩慢向手移動的點上

射線設定檔包括一個選項,用於在光束處於活動狀態時禁用重力(預設啟用)。

非物理抓取

除了最適合Quantum高級物理功能的物理抓取外,示例中還包括一個非物理抓取選項。

對於配置為使用此抓取模式的實體,當抓取時,它們將與手的位置完全匹配,即使這意味著要穿過面。因此,此模式應僅用於沒有物理體的實體。

目標是有一個抓取位置和一個非抓取位置,與用戶手的真實位置完全匹配(不包括物理抓取中物理引起的微小差異,也不包括手平滑邏輯)。

為此,此抓取模式依賴於以下附加功能:

  • 當物件被抓取時,對於本機用戶來說,它的位置被外插補點,就像對於手一樣:抓取的對象相對於現實生活中的手位置(硬體手位置)顯示,無論其Quantum視圖內插補點如何。當抓取發生時,現實生活中的抓取位置(抓取物件與硬體手部變換位置之間的偏移)透過命令發送到Quantum內核,以便抓取系統可以稍微調整抓取偏移以匹配現實生活位置。
  • 同樣,當一個物件未被抓取時,其精確的未抓取位置會透過命令發送給Quantum,以確保未被抓取的物件移動到這個精確的位置

預測剔除

在自主XR耳機等有限設備上節省CPU資源非常重要。
因此,在這個樣本中,預測剔除被啟動:這意味著對於不在玩家視野內的物件,不會計算Quantum預測和復原。
為此,在硬體裝備的相機上添加了PredictionCulling組件。

Quantum XR Prediction Culling

它包括在用戶面前設定一個預測區域。該區域的半徑必須略大於偏移量,以包括玩家的手和近側的物體。

C#

        if (enablePredictionCulling)
        {
            centerPosition = transform.position + transform.forward * offset;
            QuantumRunner.Default.Game.SetPredictionArea(centerPosition.ToFPVector3(), radius.ToFP());
            wasCulled = true;  

連接管理器

由於耳機渲染和互動,此示例不使用通常的Quantum選單。

因此,該示例提供了一個非常簡單的ConnectionManager,用於從QuantumXRDemoScene場景啟動連接。

如果沒有創建房間,它將創建一個房間,否則將隨機加入一個現有的房間。

此連接管理器始終啟動線上連接。因此,為了能單獨進行開發測試,它提供了向AppVersion添加“開發應用程式版本首碼”的選項,以確保只看到具有相同首碼的人

它執行以下操作:

  • 它以MatchmakingExtensions.ConnectToRoomAsync啟動底層RealTime遊戲階段。
  • 然後,它用SessionRunner.StartAsync啟動Quantumrunner,傳遞存儲在RuntimeConfig runtimeConfig中的配置(包括系統配置、模擬配置等)
  • 最後,當遊戲開始時,它會用game.AddPlayer添加本機玩家,傳遞存儲在RuntimePlayer runtimePlayer中的玩家資料

核心系統詳細資訊

HandMoveSystem及HeadMoveSystem

這些系統處理裝備零件(頭部和手部)的運動。

兩者具有相同的 邏輯:

  • 他們讀取輸入以找到裝備零件的本機位置
  • 他們決定是否應該調整這些值(平滑、預測等)
  • 它們將本機位置作為裝備位置的偏移

他們使用位置平滑設定檔,PositionSmoothingConfig(手的HandPositionSmoothingConfig,它提供了有關視圖位置覆寫的其他選項),來確定如何調整輸入位置。

Quantum XR Hand view override

有兩種模式可供選擇:

  • 直接輸入:輸入本地位置按原樣使用,無需修改
  • 預測(預設):系統試圖根據裝備零件的最新位置和速度猜測其可能移動的位置

預測

預測的邏輯是在驗證幀期間將本機裝備零件位置存儲在緩衝區中

C#

if (f.IsVerified)
{
    filter.Head->PositionsBuffer.Insert(headLocalization, f.Number);
}

然後,計算平均速度:

C#

FPVector3 accumulatedDeltaPosition = FPVector3.Zero;
for (int i = 0; i < (UsedEntries -1); i++)
{
    accumulatedDeltaPosition = accumulatedDeltaPosition + LastPositions[i + 1] - LastPositions[i];
}
RigPartPosition predictedLocation = default;
var ratio = (FP)1 / (UsedEntries - 1);
var lastInstantSpeed = accumulatedDeltaPosition * ratio;

然後,我們對自上一個驗證幀以來的每一幀應用此速度,但考慮到我們離驗證幀越遠,我們就越不信任計算的速度,speedConversionOverTime(從0到100)減少了此驗證幀速度的使用。

C#

FPVector3 cumulatedMove = FPVector3.Zero;
var distancePerFrame = lastInstantSpeed;

while (remainingFrameToPredict > 0)
{
    cumulatedMove = cumulatedMove + distancePerFrame;
    distancePerFrame = distancePerFrame * config.speedConversionOverTime / 100;
    remainingFrameToPredict--;
}

Lerp平滑

選擇預測後,可以添加額外的lerp平滑(預設是這樣)。

此平滑lerp將顯示2個 值:

  • 速度: 重要的是要有一個平穩的手速,以避免出現視覺偽影
  • 位置: 我們當然希望確保從當前位置移動到預測位置

所有lerp(速度、旋轉和位置)都可以透過其各自的PositionSmoothingConfig設定進行調整。

C#

if (previousSpeed != FPVector3.Zero)
{
    lastInstantSpeed = FPVector3.Lerp(previousSpeed, lastInstantSpeed, config.speedSmoothingRatio);
}
var continuationPosition = currentPosition + lastInstantSpeed;

smoothedPredictedLocation.Position = FPVector3.Lerp(continuationPosition, predictedLocation.Position, config.positionSmoothingRatio);
smoothedPredictedLocation.Rotation = FPQuaternion.Slerp(currentRotation, predictedLocation.Rotation, config.rotationSmoothingRatio);

previousSpeed = lastInstantSpeed;

GrabberSystem及GrabbableSystem

這些系統處理具有Grabbable組件的實體的抓取。

GrabberSystem中,具有Grabber組件的實體(手實體)被篩選。
抓取狀態從輸入中檢查,可以觸發對懸停可抓取物的抓取,以及對之前抓取的可抓取物的取消抓取。

懸停在可抓取物上是透過Physics3D系統訊號OnTriggerEnter3DOnTriggerExit3D檢測到的。

C#

#region Hovering detection
public void OnTriggerEnter3D(Frame f, TriggerInfo3D info)
{
    var grabberEntity = info.Entity;
    var grabbableEntity = info.Other;
    if (f.Unsafe.TryGetPointer<Grabbable>(grabbableEntity, out var grabbable) && f.Unsafe.TryGetPointer<Grabber>(grabberEntity, out var grabber))
    {
        // Grabber hovering a grabbbable
        grabber->HoveredEntity = grabbableEntity;
    }
}

public void OnTriggerExit3D(Frame f, ExitInfo3D info)
{
    var grabberEntity = info.Entity;
    var grabbableEntity = info.Other;
    if (f.Unsafe.TryGetPointer<Grabbable>(grabbableEntity, out var grabbable) && f.Unsafe.TryGetPointer<Grabber>(grabberEntity, out var grabber))
    {
        // Grabber stop hovering a grabbbable
        if (grabber->HoveredEntity == grabbableEntity)
        {
            grabber->HoveredEntity = EntityRef.None;
        }
    }
}
#endregion

在抓取和取消抓取過程中,GrabberGrabbable組件都引用了它們的對應實體

然後,在GrabbableSystem中,Grabbable實體“跟隨”抓取器實體。無論是位置跟隨(物體將跟隨手的位置,穿過物體),還是基於力的跟隨,都會迫使物體向抓握的手移動。

可以透過ForceFollowConfig設定檔配置強制抓取邏輯

Quantum XR Hand view override

該配置檔顯著改變了PID控制器確定施加的以下力的邏輯。

C#

var error = targetPosition - followingTransform->Position;

if (error.Magnitude > config.teleportDistance)
{
    // Teleport due to distance
    positionPid.Reset();
    followingBody->Velocity = FPVector3.Zero;
    followingTransform->Position = targetPosition;
}
else
{
    var command = positionPid.UpdateCommand(error, f.DeltaTime, config.pidSettings, ignoreIntegration: config.ignorePidIntegrationWhileColliding && isColliding);
    var impulse = FPVector3.ClampMagnitude(commandScaleRatio * config.commandScale * command, config.maxCommandMagnitude) * followingBody->Mass;
    followingBody->AddLinearImpulse(impulse);
}

配置檔還允許說明我們是否要人為地新增取消抓取速度(以簡化啟動物件):

C#

// Release velocity kick
if (config.applyReleaseVelocityKick)
{
    var velocityMagnitude = grabbableBody->Velocity.Magnitude;
    if (config.minVelocityTriggeringKick <= velocityMagnitude && velocityMagnitude <= config.maxVelocityTriggeringKick)
    {
        grabbableBody->Velocity = config.velocityKickFactor * grabbableBody->Velocity;
    }
}

PhysicsHandsSystem

PhysicsHandSystem應用了與Grabbablesystem相同的邏輯,用於力追蹤可抓取物,但用於始終跟隨手的2個不可見碰撞器。

唯一的特點是:

  • 物理手碰撞器在抓取物體時被禁用,以停止任何觸覺回饋
  • 如果可抓取物在抓取之前發生了碰撞,我們會取消存儲在可抓取物組件中的碰撞資訊(因為,由於我們禁用了碰撞器,OnCollision3D可能無法在GrabbableSystem中正確觸發)

RayAndLocomotionSystem

以下章節詳細介紹了傳送玩家的過程。
請注意,這裡沒有描述玩家輪換,以簡化閱讀,但它非常相似。

Quantum XR Teleport overview

讀取硬體資訊

input QTN檔案定義了要同步的輸入資料。
為了減少頻寬,FPVector3和FPQuaternion被優化版本所取代,這些版本只序列化原始值的32個LSB,而不是完整的64位元。
只要值在FP可用範圍內(-32K、32K),此操作就是無損的。
對於每個手,輸入包括光線狀態:LeftHandIsRayEnabledRightHandIsRayEnabled

C#

        import struct FPVector3RawInt(12);
        import struct FPQuaternionRawInt(16);

        input
        {
            // Headset
            FPVector3RawInt HeadsetPosition;     // Local position relatively to the rig root
            FPQuaternionRawInt HeadsetRotation;  // Local position relatively to the rig root
            RigDetectionState DetectionState;    // Rig parts state

            // LeftHand
            FPVector3RawInt LeftHandPosition;     // Local position relatively to the rig root
            FPQuaternionRawInt LeftHandRotation;  // Local position relatively to the rig root
            Button LeftHandIsRayEnabled;          // RayCast Status
            Button LeftHandIsGrabbing;
            Byte LeftHandGripLevel;
            Byte LeftHandTriggerLevel;
            button LeftHandIsThumbTouched;
            button LeftHandIsIndexTouched;

            // RightHand
            FPVector3RawInt RightHandPosition;     // Local position relatively to the rig root
            FPQuaternionRawInt RightHandRotation;  // Local position relatively to the rig root
            Button RightHandIsRayEnabled;          // RayCast Status
            Button RightHandIsGrabbing;
            Byte RightHandGripLevel;
            Byte RightHandTriggerLevel;
            button RightHandIsThumbTouched;
            button RightHandIsIndexTouched;
        }

QuantumXRInput指令碼負責收集Unity輸入並將其傳遞給Quantum引擎

C#


        private void OnEnable()
        {
            QuantumCallback.Subscribe(this, (CallbackPollInput callback) => PollInput(callback));
        }

        public void PollInput(CallbackPollInput callback)
        {
            Quantum.Input input = new Quantum.Input();
            if (hardwareRig == null)
            {
                hardwareRig = FindObjectOfType<HardwareRig>();
            }
            if (hardwareRig)
            {
                var headsetPosition = hardwareRig.transform.InverseTransformPoint(hardwareRig.headset.transform.position).ToFPVector3();
                var headsetRotation = (Quaternion.Inverse(hardwareRig.transform.rotation) * hardwareRig.headset.transform.rotation).ToFPQuaternion();
                input.HeadsetPosition = (FPVector3RawInt)headsetPosition;
                input.HeadsetRotation = (FPQuaternionRawInt)headsetRotation;

                var leftHandInfo = FillHandInfo(hardwareRig.leftHand);
                input.LeftHandPosition = (FPVector3RawInt)leftHandInfo.Position;
                input.LeftHandRotation = (FPQuaternionRawInt)leftHandInfo.Rotation;
                input.LeftHandIsRayEnabled = leftHandInfo.IsRayEnabled;
                input.LeftHandIsGrabbing = leftHandInfo.IsGrabbing;
                input.LeftHandGripLevel = leftHandInfo.Buttons.GripLevel;
                input.LeftHandTriggerLevel = leftHandInfo.Buttons.TriggerLevel;
                input.LeftHandIsThumbTouched = leftHandInfo.Buttons.IsThumbTouched;
                input.LeftHandIsIndexTouched = leftHandInfo.Buttons.IsIndexTouched;

                var rightHandInfo = FillHandInfo(hardwareRig.rightHand);
                input.RightHandPosition = (FPVector3RawInt)rightHandInfo.Position;
                input.RightHandRotation = (FPQuaternionRawInt)rightHandInfo.Rotation;
                input.RightHandIsRayEnabled = rightHandInfo.IsRayEnabled;
                input.RightHandIsGrabbing = rightHandInfo.IsGrabbing;
                input.RightHandGripLevel = rightHandInfo.Buttons.GripLevel;
                input.RightHandTriggerLevel = rightHandInfo.Buttons.TriggerLevel;
                input.RightHandIsThumbTouched = rightHandInfo.Buttons.IsThumbTouched;
                input.RightHandIsIndexTouched = rightHandInfo.Buttons.IsIndexTouched;

                input.DetectionState = RigDetectionState.Detected;
            }
            else
            {
                Debug.LogError("Input polled while hardware rig is not found");
                input.DetectionState = RigDetectionState.NotDetected;
            }

            callback.SetInput(input, DeterministicInputFlags.Repeatable);
        }

Quantum傳送請求處理

運動QTN檔案定義了LocomotionRay組件。

C#

component LocomotionRay{
    FPVector3 PositionOffset;
    FPVector3 RotationOffset;
    [ExcludeFromPrototype]
    LocomotionRayState State;
    [ExcludeFromPrototype]
    FPVector3 Target;
    AssetRef<LocomotionConfig> Config;
    [...]
}

enum LocomotionRayState{
    NotPointing,
    PointingValidTarget,
    PointingInvalidTarget,
    MoveToTargetRequested,
    AttractableGrabbableTargeted,
    AttractingGrabbable
}

必須在每個手部實體上添加此組件。

Quantum XR hand prototype

因此,在Quantum方面,在每次Update()時,RayAndLocomotionSystem都可以讀取Unity提供的輸入並更新運動射線狀態。

C#

    public override void Update(Frame f, ref RayAndLocomotionSystem.Filter filter)
        {
            var input = f.GetPlayerInput(filter.PlayerLink->Player);
            [...]

            if (Rig.TryGetComponents<LocomotionRay>(f, filter.Rig->LeftHandEntity, out var leftHandLocomotionRay, out var leftHandTransform) == false)
                return;
            if (Rig.TryGetComponents<LocomotionRay>(f, filter.Rig->RightHandEntity, out var rightHandLocomotionRay, out var rightHandTransform) == false)
                return;

            var leftHandInfo = HandInfo.HandInfoFromInput(HandSide.Left, input);
            var rightHandInfo = HandInfo.HandInfoFromInput(HandSide.Right, input);

            UpdateRayState(f, ref filter, input->LeftHand, leftHandTransform, filter.Rig->LeftHandEntity, leftHandLocomotionRay, out var leftHitEntity);
            UpdateRayState(f, ref filter, input->RightHand, rightHandTransform, filter.Rig->RightHandEntity, rightHandLocomotionRay, out var rightHitEntity);
            [...]
        }

如果玩家請求傳送,並且目標是有效目標,則會引發OnMoveToTargetRequested事件。

C#


 public unsafe void UpdateRayState(Frame f, ref Filter filter, HandInfo handInfo, Transform3D* handTransform, EntityRef handEntity, LocomotionRay* locomotionRay, out EntityRef hitEntity)
        {
         [...]
            else if (locomotionRay->State == LocomotionRayState.PointingValidTarget)
            {
                // Planning teleport
                locomotionRay->State = LocomotionRayState.MoveToTargetRequested;
                f.Events.OnMoveToTargetRequested(filter.PlayerLink->Player, locomotionRay->Target);
            }
            [...]
        }

請注意,可以在LocomotionConfig資產檔中定義運動和阻擋層遮罩,以指定哪些物件阻擋光線投射或可用作傳送目標。

Unity準備傳送

在Unity方面,事件接收由RigView處理。
如果本機玩家發起了傳送請求事件,則會開始淡入黑色以隱藏場景並避免因位置變化而引起的運動。
當淡入淡出完成時,Unity會透過CommandReadyForTeleport命令通知Quantum引擎。

C#

 public class RigView : QuantumEntityViewComponent<XRViewContext>
    {

        private void Start()
        {
            QuantumEvent.Subscribe<EventOnMoveToTargetRequested>(listener: this, handler: MoveToTargetRequested);
            QuantumEvent.Subscribe<EventOnMoveToTargetDone>(listener: this, handler: MoveToTargetDone);
            [...]
        }

        private void MoveToTargetRequested(EventOnMoveToTargetRequested callback)
        {
            if (isLocalUserRig == false) return;
            var movingPlayer = callback.Player;
            var rigPlayer = VerifiedFrame.Get<PlayerLink>(EntityRef).Player;
            if (rigPlayer != movingPlayer) return;
            // fadein
            StartCoroutine(TeleportPreparation());
        }

        IEnumerator TeleportPreparation()
        {
            if (ViewContext.hardwareRig.headset.fader)
            {
                yield return ViewContext.hardwareRig.headset.fader.FadeIn();
            }
            Game.SendCommand(new CommandReadyForTeleport());
        }

Quantum傳送

在每次Update()時,Quantum RayAndLocomotionSystem都會檢查是否發生了CommandReadyForTeleport命令。
如果是這樣,它會將裝備零件移動到目標位置,並引發OnMoveToTargetDone事件。

C#

    public override void Update(Frame f, ref RayAndLocomotionSystem.Filter filter)
        {
            [...]
            CheckTeleportAuthorization(f, ref filter, leftHandLocomotionRay, rightHandLocomotionRay, leftHandTransform, rightHandTransform);
            [...]
        }

        void CheckTeleportAuthorization(Frame f, ref Filter filter, LocomotionRay* leftHandLocomotionRay, LocomotionRay* rightHandLocomotionRay, Transform3D* leftHandTransform, Transform3D* rightHandTransform)
        {
            // check command
            var command = f.GetPlayerCommand(filter.PlayerLink->Player) as CommandReadyForTeleport;
            if (command != null)
            {
                LocomotionRay* sourceRay = null;
                if (leftHandLocomotionRay->State == LocomotionRayState.MoveToTargetRequested)
                    sourceRay = leftHandLocomotionRay;
                else if (rightHandLocomotionRay->State == LocomotionRayState.MoveToTargetRequested)
                    sourceRay = rightHandLocomotionRay;

                if (sourceRay != null)
                {
                    Teleport(f, ref filter, sourceRay->Target, leftHandTransform, rightHandTransform);
                    f.Events.OnMoveToTargetDone(filter.PlayerLink->Player, sourceRay->Target);
                    sourceRay->State = LocomotionRayState.NotPointing;
                }
            }
        }

傳送的Unity結束

OnMoveToTargetRequested事件一樣,在Unity端,RigView負責接收OnMoveToTargetDone事件,以處理淡出並將硬體裝備位置同步到Quantum引擎設定的網路裝備位置。

C#


private void MoveToTargetDone(EventOnMoveToTargetDone callback)
        {
            // Check that we are the local user, and that this rig is the one related to the local user, before applying fade and camera moves based on the rig move
            var movingPlayer = callback.Player;
            if (IsLocalRigPLayer(movingPlayer) == false) return;

            // Synchronize hardware rig position to network rig position (ensure that the camera, located in the hardware rig, will follow the actual network head position)
            var rigTransform3D = PredictedFrame.Get<Transform3D>(EntityRef);
            ViewContext.hardwareRig.transform.position = rigTransform3D.Position.ToUnityVector3();
            // fadeout
            StartCoroutine(TeleportEnd());
        }

射線束視覺

每個手部實體都有LocomotionHandler組件。
它負責根據LocomotionRay實體的LocomotionRayState狀態機顯示射線束。

此外,此類別實現了IHandViewListener介面,以便在手部位置發生變化時更新光線位置。

遊戲特定元素

計分門系統

Quantum XR Scoring Gates prototype

一些遊戲需要軌跡分析來確定玩家是否成功射門,即球是否進入了定義的球門或大門。
為此,ScoringGates QTN檔案定義了:

  • ScoringGate組件:用於定義門屬性(大小和方向以標記點)。要在場景中添加評分門,必須添加具有ScoringGate實體的Quantum實體。
  • Scorable組件:必須添加要扔進門的物體(球)。
  • 當一個Scorable實體通過大門時,會引發OnScore事件。

請注意,可以定義球是否必須以特定方向進入大門。

球軌跡分析由ScoringGateSystem管理。
篩檢程式為所有實體提供了一個ScoringGate,當球穿過頂部和底部時,OnTrigger3D()方法會生成OnScore事件。
ConfigurePhysicsCallbacks()方法驗證物理回調標誌是否在門上正確設定。

在Unity方面,ScoringGateView負責在玩家成功射擊時更改門材質並播放音訊檔案。
為此,它訂閱Quantum的OnScore事件,並調用Score()方法來生成視覺和音訊回饋。

C#

        private void Start()
        {
            QuantumEvent.Subscribe<EventOnScore>(listener: this, handler: Score);
        }


        private void Score(EventOnScore callback)
        {
            if (callback.scoringGateEntity != EntityRef) return;

            restoreMaterialTime = Time.time + scoredEffectDuration;
            scoringRenderer.material = scoredMatarial;
            if(audioSource)
                audioSource.Play();
        }

邊界系統

場景中的一些物體(球、球拍)可能會被扔到玩家夠不著的地方。
為了在這些物件離得太遠時將其返回到初始位置,BoundarySystem會檢查實體是否在邊界內,並相應地傳送它們。

為此,Boundary QTN檔案包含此功能所需的資料。

C#

component Bounded
{
    FPVector3 InitialPosition;
    FPQuaternion InitialRotation;
    bool Initialized;
    FPVector3 Extends;    
    FPVector3 LimitCenter;
}

BoundarySystem中,篩檢程式檢索所有具有已邊界組件的實體

C#

        public struct Filter
        {
            public EntityRef Entity;
            public Bounded* Bounded;
            public Transform3D* Transform;
        }

Update()中,如果實體的起始位置尚未保存(在啟動時),則初始位置和旋轉將被保存。

C#

 if(filter.Bounded->Initialized == false)
            {
                filter.Bounded->Initialized = true;
                filter.Bounded->InitialPosition = filter.Transform->Position;
                filter.Bounded->InitialRotation = filter.Transform->Rotation;
            }

然後,對於每一幀,系統檢查實體是否超出定義的邊界。
在這種情況下,除了重新初始化物件的位置和旋轉外,還需要重置物理體的速度。

C#

   if (IsOutOfBounds(filter.Transform->Position, filter.Bounded->LimitCenter, extends))
            {   
                if(f.Unsafe.TryGetPointer<PhysicsBody3D>(filter.Entity,out var physicsBody3D))
                {
                    physicsBody3D->Velocity = FPVector3.Zero;
                    physicsBody3D->AngularVelocity = FPVector3.Zero;
                }
                filter.Transform->Teleport(f, filter.Bounded->InitialPosition, filter.Bounded->InitialRotation);
            }

當然,要在物件上啟動此功能,必須將組件添加到實體的“實體組件”清單中。
如果組件中未定義延伸,則設定預設延伸12公尺。
此外,儘管場景中沒有顯示,但由於LimitCenter變數,可以為邊界定義一個特定的中心。
例如,對於籃球,可以在投籃位置定義界外球的中心,並在距離投籃位置幾公尺遠時重置球的位置,以避免每次投籃後都取球。

擊球

Quantum XR Punching ball

使用Quantum開發擊球遊戲非常容易。
這包括在可以用手擊的物件上添加一個彈簧接頭。
因此,正如“物理手”一章所解釋的那樣,球必須配置PhysicsHandsPushable層。

必須將Spring類型的PhysicsJoint3D添加到球Quantum實體中。連接的實體是位於遊戲支架上的實體。

繩索視覺由非常簡單的RopeAnchor組件管理:它在球的頂部和繩索的另一側(位於遊戲支架上的實體)之間繪製一條線渲染器。

Plinko

Quantum XR Plinko

對於這款遊戲,唯一開發的事情是只有當球擊中其中一個障礙物時才能獲得音訊回饋。
為了實現這一點,在qrabbing QTN檔案中定義了一個OnCollisionDetectedWithStaticCollider事件。

C#

event OnCollisionDetectedWithStaticCollider {
    EntityRef GrabbableEntity;
    EntityRef CollidedEntity;
    String CollidedEntityName;
    LayerMask CollidedEntityLayer;
    String CollidedEntityTag;
    nothashed FP Velocity;
}

此事件包括碰撞的實體標記,如果可抓取物件與靜態實體碰撞,則由OnCollisionEnter3D回調中的GrabbableSystem調用。

C#

public void OnCollisionEnter3D(Frame f, CollisionInfo3D info)
        {
            var grabbableEntity = info.Entity;
            var infoStatic = info.IsStatic;

            if (f.Unsafe.TryGetPointer<Grabbable>(grabbableEntity, out var grabbable))
            {
                if (f.Unsafe.TryGetPointer<PhysicsBody3D>(grabbableEntity, out var grabbablePhysicsBody3D))
                {
                    if (infoStatic)
                    {
                        var collidedEntity = info.Other;
                        var collisionStaticData = info.StaticData;
                        var collidedEntityName = collisionStaticData.Name;
                        var collidedEntityLayer = collisionStaticData.Layer;
                        var collidedEntityTag = collisionStaticData.Tag;
                        f.Events.OnCollisionDetectedWithStaticCollider(grabbableEntity, collidedEntity, collidedEntityName, collidedEntityLayer, collidedEntityTag, grabbablePhysicsBody3D->Velocity.Magnitude);
                    }
                    else
                    {
                        f.Events.OnCollisionDetectedWithDynamicCollider(grabbableEntity, grabbablePhysicsBody3D->Velocity.Magnitude);
                    }
                }
            }
        }

在Unity端,GrabbableView訂閱此事件,只有當碰撞的實體標籤與配置的標籤篩檢程式匹配時,才會播放音訊片段。

C#

        private void Awake()
        {
            [...]
            if (enableAudioFeedback)
            {
                if (audioSource == null)
                    audioSource = gameObject.GetComponent<AudioSource>();

                QuantumEvent.Subscribe<EventOnCollisionDetectedWithStaticCollider>(listener: this, handler: CollisionDetectedWithStaticCollider);
                QuantumEvent.Subscribe<EventOnCollisionDetectedWithDynamicCollider>(listener: this, handler: CollisionDetectedWithDynamicCollider);
            }
        }


        private void CollisionDetectedWithStaticCollider(EventOnCollisionDetectedWithStaticCollider callback)
        {
            if (callback.GrabbableEntity == EntityRef)
            {
                if(useStaticColliderTagFilter && staticColliderTagFilter != callback.CollidedEntityTag)
                    return;

                if(callback.Velocity.AsFloat > minVelocityForAudioFeedback)
                    PlayAudioFeeback(Mathf.Clamp01(callback.Velocity.AsFloat / 10f));
            }
        }

Punch Them遊戲

Quantum XR No Gravity

在這個遊戲中,我們使用了上面解釋的得分門,並為球配置了PhysicsHandsPushable層,這樣玩家就可以用手擊打它們。
為了避免球上的重力,PhysicsBody3D的參數Gravity Scale設定為0。

Racket遊戲

Quantum XR Racket

一些遊戲需要更大的回應能力,即使這意味著犧牲視覺流暢性。
為了實現這一點,球拍實體的可抓取組件配置了一個特定的力跟踪設定檔:啟用了“在力抓取時使用直接輸入模式”選項,並為“旋轉處理”選擇“旋轉捕捉”。

Quantum XR Racket Config

籃球遊戲

Quantum XR Basket Ball

球實體的可抓取組件配置了特定的力跟踪設定檔:啟用“應用釋放速度踢”選項,以便在投擲物體時提高速度。

Quantum XR Basket Ball Velocity config

第三方資產

此示例包括第三方免費和CC0資產。您可以在各自的網站上為自己的專案獲取完整的套裝軟體:

  • 聲音
    • https://freesound.org/
    • Nathan Gibson (https://nathangibson.myportfolio.com)
Back to top