喵的Unity遊戲開發之路 - 軌道攝像機

MarsZhou發表於2020-08-21

前言
        很多童鞋沒有系統的Unity3D遊戲開發基礎,也不知道從何開始學。為此我們精選了一套國外優秀的Unity3D遊戲開發教程,翻譯整理後放送給大家,教您從零開始一步一步掌握Unity3D遊戲開發。 本文不是廣告,不是推廣,是免費的純乾貨!本文全名:喵的Unity遊戲開發之路 - 移動 - 軌道攝像機 - 相對控制

 

  • 建立一個軌道攝像機。

  • 支援手動和自動相機旋轉。

  • 相對於相機進行移動。

  • 防止相機相交。

 

這是有關控制角色的運動的教程系列的第四部分。這次,我們將注意力集中在相機上,建立一個從中控制球體的繞行軌道。

本教程使用Unity 2019.2.18f1建立。它還使用ProBuilder軟體包。

效果之一

 
喵的Unity遊戲開發之路 - 軌道攝像機

 

跟隨球體

 

 

只有當球體被限制在完全可見的區域時,固定的視角才起作用。但是通常遊戲中的角色可以在大範圍內漫遊。使之成為可能的典型方法是使用第一人稱視角或在第三人稱視角模式下讓相機跟隨玩家的頭像。還存在其他方法,例如根據化身的位置在多個攝像機之間切換。

 

 

 

有第二人稱視角嗎?

第三人稱存在於遊戲世界之外,代表玩家。遊戲中存在第二個人。可能是任何人或不是玩家頭像的任何東西。這種情況很少見,但有些遊戲將這種視角用作暗機關,例如,它是意識世界中的一種精神力量。

 

 

 

 

軌道攝像機

 

 

我們將建立一個簡單的軌道攝像機,以第三人稱模式跟隨我們的球體。為它定義一個 OrbitCamera 元件型別,為其賦予 RequireComponent 屬性,以強制將其附加到也具有常規 Camera 元件。

 

 

 

using UnityEngine;[RequireComponent(typeof(Camera))]public class OrbitCamera : MonoBehaviour {}

 

 

 

使用單個球體調整場景的主攝像機,使其具有此元件。為此,我使用一個較大的平面製作了一個新場景,將攝影機定位為使其與球體在其視點中心成45°角向下傾斜,大約相距五個單位。

 

 

 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機

 

 

 

保持相對地位

 

為了使相機聚焦在球體上,我們需要告訴它聚焦的物件。這實際上可以是任何東西,因此請為焦點新增一個可配置的 Transform 欄位。還要為軌道距離新增一個選項,預設情況下設定為5個單位。

[SerializeField]  Transform focus = default;
[SerializeField, Range(1f, 20f)] float distance = 5f;

每次更新時,我們都必須調整相機的位置,以使其保持所需的距離。我們將在 LateUpdate 中執行此操作,以防萬一任何東西將焦點移到 Update 中。通過將照相機從焦點位置朝與觀察方向相反的方向移開等於配置的距離的量,可以找到照相機的位置。我們將使用焦點的 position 屬性而不是 localPosition ,以便我們可以正確地關注層次結構中的子物件。

void LateUpdate () {    Vector3 focusPoint = focus.position;    Vector3 lookDirection = transform.forward;    transform.localPosition = focusPoint - lookDirection * distance;  }

相機不會始終保持相同的距離和方向,但是由於PhysX會以固定的時間步長調整球體的位置,因此相機也會如此。如果幀速率與幀速率不匹配,則會導致相機抖動。

抖動的動作;時間步長0.2。

解決此問題的最簡單,最可靠的方法是將球體的 Rigidbody 設定為插值其位置。這消除了球體和照相機的抖動運動。通常只有相機對焦的物件才需要這樣做。

插補運動;時間步長0.2。

 

 

 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機

 

 

焦點半徑

 

始終將球保持精確的聚焦可能會感覺太僵硬。相機甚至會複製球體的最小運動,這會影響整個檢視。我們可以通過使相機僅在其焦點與理想焦點相差太大的情況下移動來放鬆此約束。我們將通過新增聚焦半徑(預設設定為一個單位)來使其可配置。

[SerializeField, Min(0f)]  float focusRadius = 1f;

放鬆焦點需要我們跟蹤當前焦點,因為它可能不再與焦點位置完全匹配。將其初始化為 Awake 中焦點物件的位置,然後將其更新為單獨的 UpdateFocusPoint 方法。

Vector3 focusPoint;
void Awake () { focusPoint = focus.position; }
void LateUpdate () { //Vector3 focusPoint = focus.position; UpdateFocusPoint(); Vector3 lookDirection = transform.forward; transform.localPosition = focusPoint - lookDirection * distance; }
void UpdateFocusPoint () { Vector3 targetPoint = focus.position; focusPoint = targetPoint; }

如果聚焦半徑為正,請檢查目標和當前對焦點之間的距離是否大於半徑。如果是這樣,請將焦點拉向目標,直到距離與半徑匹配。可以使用半徑除以當前距離作為插值器,從目標點到當前點進行插值。

    Vector3 targetPoint = focus.position;    if (focusRadius > 0f) {      float distance = Vector3.Distance(targetPoint, focusPoint);      if (distance > focusRadius) {        focusPoint = Vector3.Lerp(          targetPoint, focusPoint, focusRadius / distance        );      }    }    else {      focusPoint = targetPoint;    }

放鬆的相機運動。

 

集中焦點

 

使用聚焦半徑會使相機僅對更大的聚焦運動做出響應,但是當聚焦停止時,相機也會響應。也可以保持相機移動,直到焦點回到其檢視中心。為了使此動作看起來更加微妙和自然,我們可以在焦點移向中心時向後拉慢一點。

例如,焦點從距中心一定距離處開始。我們將其拉回,以便一秒鐘後該距離減半。我們一直在這樣做,每秒將距離減半。距離永遠不會以這種方式減小到零,但是當距離變得足夠小以至於不明顯時,我們可以停止。

每秒將起始距離減半可以通過將其乘以½到經過的時間來完成:“ d_(n 1)= d_n(1/2)^(t_n)”。我們不需要每秒精確地將距離減半,我們可以在0到1之間使用任意定心因子:“ d_(n 1)= d_nc ^(t_n)”。

 

為焦點居中係數新增一個配置選項,該選項的值必須在0–1範圍內,預設值為0.75。

[SerializeField, Range(0f, 1f)]  float focusCentering = 0.5f;

為了應用預期的居中行為,我們必須在目的碼和當前焦點之間進行插值,並使用(1-c)^ t作為插值器,並藉助 Mathf.Pow 方法。僅當距離足夠大(例如大於0.01)並且對中因子為正時,才需要執行此操作。為了使居中半徑和聚焦半徑都相等,我們將兩個插值器中的最小值用於最終插值。

      float distance = Vector3.Distance(targetPoint, focusPoint);      float t = 1f;      if (distance > 0.01f && focusCentering > 0f) {        t = Mathf.Pow(1f - focusCentering, Time.deltaTime);      }      if (distance > focusRadius) {        //focusPoint = Vector3.Lerp(        //  targetPoint, focusPoint, focusRadius / distance        //);        t = Mathf.Min€(t, focusRadius / distance);      }      focusPoint = Vector3.Lerp(targetPoint, focusPoint, t);

但是依靠正常的時間增量會使攝像機受遊戲時間的限制,因此在慢動作效果下攝像機也會減慢速度,甚至在遊戲暫停時也會凍結在原地。為防止這種情況,請改為依賴 Time.unscaledDeltaTime

        t = Mathf.Pow(1f - focusCentering, Time.unscaledDeltaTime);

集中焦點。

 

 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機
 
喵的Unity遊戲開發之路 - 軌道攝像機

 

 

 

環繞球體

 

 

下一步是可以調整相機的方向,以便可以描述圍繞焦點的軌道。我們將可以手動控制軌道,並使相機自動旋轉以跟隨其焦點。

 

 

 

軌道角度

 

 

攝像機的方向可以用兩個軌道角來描述。X角定義其垂直方向,其中0°直視地平線,90°直視向下。Y角定義水平方向,沿著世界的Z軸看為0°。在 Vector2 欄位中跟蹤這些角度,預設情況下設定為45°和0°。

 

 

  •  

    Vector2 orbitAngles = new Vector2(45f, 0f);
    
    

     

    LateUpdate 中,我們現在必須通過 Quaternion.Euler 方法構造一個四元數來定義相機的外觀旋轉,並將其傳遞給軌道角度。它需要一個 Vector3 ,我們的向量隱式轉換為該向量,Z旋轉設定為零。

     

    然後可以通過用四元數乘以正向向量替換 transform.forward 來找到視線方向。現在,不僅要設定攝像機的位置,我們還要呼叫 transform.SetPositionAndRotation 並具有一次的外觀位置和旋轉。

     

     

  •  

      void LateUpdate () {    UpdateFocusPoint();    Quaternion lookRotation = Quaternion.Euler(orbitAngles);    Vector3 lookDirection =lookRotation * Vector3.forward;    Vector3 lookPosition= focusPoint - lookDirection * distance;    transform.SetPositionAndRotation(lookPosition, lookRotation);  }

     

     

     

     

    控制軌道

     

     

    要手動控制軌道,請新增轉速配置選項,以每秒度數表示。每秒90°是合理的預設設定。

     

     

  •  

    [SerializeField, Range(1f, 360f)]  float rotationSpeed = 90f;
    
    

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    新增 ManualRotation 方法來檢索輸入向量。為此,我定義了垂直攝像機“水平攝像機” 輸入軸,並繫結到第三和第四軸,ijkl和qe鍵,滑鼠的靈敏度提高到0.5。最好在遊戲中配置靈敏度並允許翻轉軸方向,但這是一個好主意,但是在本教程中我們不會為之煩惱。

     

    如果輸入值超過某個小ε值(如0.001),則將輸入值新增到軌道角度,並按旋轉速度和時間增量進行縮放。同樣,我們將其與遊戲時間無關。

     

     

  •  

    void ManualRotation () {    Vector2 input = new Vector2(      Input.GetAxis("Vertical Camera"),      Input.GetAxis("Horizontal Camera")    );    const float e = 0.001f;    if (input.x < -e || input.x > e || input.y < -e || input.y > e) {      orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input;    }  }
    
    

     

    LateUpdate 中的 UpdateFocusPoint 之後呼叫此方法。

     

     

  •  

      void LateUpdate () {    UpdateFocusPoint();    ManualRotation();  }

     

    手動旋轉;聚焦半徑為零。

     

    請注意,無論相機的方向如何,球體仍在世界空間中受到控制。因此,如果將攝像機水平旋轉180°,則球體的控制元件將顯示為翻轉狀態。這樣無論攝像機的視線如何,都可以輕鬆保持相同的航向,但可能會迷失方向。如果您對此有疑問,可以同時開啟遊戲視窗和場景視窗,並依靠後者的固定角度。稍後,我們將使球形控制元件相對於攝影機檢視。

     

     

     

     

    約束角度

     

     

    雖然相機可以描述完整的水平軌道,但垂直旋轉將使世界在任何方向超過90°時都可以將其顛倒。甚至在此之前,在上下左右看時都很難看清要去的地方。因此,讓我們新增配置選項來約束最小和最大垂直角度,極端情況在任一方向上的最大限制為89°。讓我們使用−30°和60°作為預設值。

     

     

  •  

    [SerializeField, Range(-89f, 89f)]  float minVerticalAngle = -30f, maxVerticalAngle = 60f;
    
    

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    最大值永遠不會低於最小值,因此請在 OnValidate 方法中強制實施。由於這僅通過檢查器清理配置,因此我們不需要在構建中呼叫它。

     

     

  •  

    void OnValidate () {    if (maxVerticalAngle < minVerticalAngle) {      maxVerticalAngle = minVerticalAngle;    }  }
    
    

     

    新增 ConstrainAngles 方法,將垂直軌道角度鉗位到配置的範圍。水平軌道沒有限制,但請確保角度保持在0–360範圍內。

     

     

  •  

    void ConstrainAngles () {    orbitAngles.x =      Mathf.Clamp(orbitAngles.x, minVerticalAngle, maxVerticalAngle);
    if (orbitAngles.y < 0f) { orbitAngles.y += 360f; } else if (orbitAngles.y >= 360f) { orbitAngles.y -= 360f; } }
    
    

     

     

     

    我們是否應該迴圈執行直到處於0–360範圍內?

    如果軌道角度是任意的,那麼確實要繼續加減360°直到落入該範圍內才是正確的。但是,我們僅少量地逐步調整角度,所以這不是必需的。

     

     

    當角度改變時,我們只需要約束角度。因此,使 ManualRotation 返回是否進行了更改,並基於 LateUpdate 中的內容呼叫 ConstrainAngles 。如果發生更改,我們也只需要重新計算輪換,否則我們可以檢索現有的輪換。

     

     

  •  

    bool ManualRotation () {    if (input.x < e || input.x > e || input.y < e || input.y > e) {      orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input;      return true;    }    return false;  }      void LateUpdate () {    UpdateFocusPoint();    Quaternion lookRotation;    if (ManualRotation()) {      ConstrainAngles();      lookRotation = Quaternion.Euler(orbitAngles);    }    else {      lookRotation = transform.localRotation;    }    //Quaternion lookRotation = Quaternion.Euler(orbitAngles);  }

     

    我們還必須確保初始旋轉與 Awake 中的軌道角度相匹配。

     

     

  •  

      void Awake () {    focusPoint = focus.position;    transform.localRotation = Quaternion.Euler(orbitAngles);  }

     

     

     

     

    自動對齊

     

     

    軌道攝像頭的一個共同特徵是,它們會對齊以保持在玩家頭像後面。我們將通過自動調整水平軌道角度來做到這一點。但是重要的是,播放器可以始終覆蓋此自動行為,並且自動旋轉不會立即開始。因此,我們將新增可配置的對齊延遲,預設情況下設定為5秒。此延遲沒有上限。如果您根本不希望自動對齊,則只需設定很高的延遲即可。

     

     

  •  

    [SerializeField, Min(0f)]  float alignDelay = 5f;
    
    

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    跟蹤上次手動旋轉發生的時間。再一次,我們依靠的是這裡的非標度時間,而不是遊戲中的時間。

     

     

  •  

    float lastManualRotationTime;

    bool ManualRotation () { if (input.x < -e || input.x > e || input.y < -e || input.y > e) { orbitAngles += rotationSpeed * Time.unscaledDeltaTime * input; lastManualRotationTime = Time.unscaledTime; return true; } return false; }

     

    然後新增 AutomaticRotation 方法,該方法還返回是否更改了軌道。如果當前時間減去上次手動旋轉時間小於對齊延遲,它將中止。

     

     

  •  

    bool AutomaticRotation () {    if (Time.unscaledTime - lastManualRotationTime < alignDelay) {      return false;    }        return true;  }
    
    

     

    現在,在 LateUpdate 中,按照順序嘗試手動或自動旋轉時,限制角度並計算旋轉。

     

     

  •  

        if (ManualRotation()|| AutomaticRotation()) {      ConstrainAngles();      lookRotation = Quaternion.Euler(orbitAngles);    }

     

     

     

     

    聚焦前進方向

     

     

    用於對齊相機的條件各不相同。在我們的案例中,我們將僅基於自上一幀以來焦點的移動。這個想法是,朝著焦點最後前進的方向看是最有意義的。為了使之成為可能,我們需要知道當前和先前的焦點,因此,請 UpdateFocusPoint 設定這兩個焦點。

     

     

  •  

      Vector3 focusPoint, previousFocusPoint;

    void UpdateFocusPoint () { previousFocusPoint = focusPoint; }

     

    然後讓 AutomaticRotation 計算當前幀的運動向量。由於我們僅水平旋轉,因此只需要在XZ平面中進行2D移動。如果此運動向量的平方幅度小於一個較小的閾值(例如0.0001),那麼運動就不多了,我們就不會打擾旋轉。

     

     

  •  

      bool AutomaticRotation () {    if (Time.unscaledTime - lastManualRotationTime < alignDelay) {      return false;    }
    Vector2 movement = new Vector2( focusPoint.x - previousFocusPoint.x, focusPoint.z - previousFocusPoint.z ); float movementDeltaSqr = movement.sqrMagnitude; if (movementDeltaSqr < 0.000001f) { return false; }
    return true; }

     

    否則,我們必須找出與當前方向匹配的水平角度。建立一個靜態 GetAngle 方法以將2D方向轉換為該角度。方向的Y分量是所需角度的餘弦,因此將其放入 Mathf.Acos ,然後從弧度轉換為度。

     

     

  •  

    static float GetAngle (Vector2 direction) {    float angle = Mathf.Acos(direction.y) * Mathf.Rad2Deg;    return angle;  }
    
    

     

    但是該角度可以表示順時針或逆時針旋轉。我們可以看一下方向的X分量來知道它是什麼。如果X為負,則它為逆時針方向,我們必須從360°中減去該角度。

     

     

  •  

        returndirection.x < 0f ? 360f - angle :angle;

     

    回到 AutomaticRotation 中,我們可以使用 GetAngle 來獲取航向角,並向其傳遞標準化的運動向量。由於我們已經有了平方的強度,所以自己進行歸一化會更有效率。結果成為新的水平軌道角。

     

     

  •  

        if (movementDeltaSqr < 0.0001f) {      return false;    }
    float headingAngle = GetAngle(movement / Mathf.Sqrt(movementDeltaSqr)); orbitAngles.y = headingAngle; return true;

     

    立即對齊。

     

     

     

     

    平滑對齊

     

     

    自動對齊有效,但立即對齊以匹配前進方向太突然了。我們也通過將配置的旋轉速度也用於自動旋轉來降低它的速度,因此它模仿手動旋轉。我們可以為此使用 Mathf.MoveTowardsAngle ,它與 Mathf.MoveTowards 一樣,除了它可以處理0-360度的角度範圍。

     

     

  •  

        float headingAngle = GetAngle(movement / Mathf.Sqrt(movementDeltaSqr));    float rotationChange = rotationSpeed * Time.unscaledDeltaTime;    orbitAngles.y =      Mathf.MoveTowardsAngle(orbitAngles.y,headingAngle, rotationChange);


     

    受轉速限制。

     

    這樣比較好,但是即使對於較小的重新排列,也始終使用最大轉速。一種更自然的行為是使旋轉速度與當前角度和所需角度之差成比例。我們將使其線性縮放至全速旋轉的某個角度。通過新增對齊平滑範圍配置選項(0-90範圍,預設值為45°)來使該角度可配置。

     

  •  

    [SerializeField, Range(0f, 90f)]  float alignSmoothRange = 45f;
    
    

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    為了完成這項工作,我們需要知道 AutomaticRotation 中的角度增量,我們可以通過將當前角度和所需角度傳遞給 Mathf.DeltaAngle 並取其絕對值來找到它。如果此增量落在平滑範圍內,則進行相應的旋轉調整。

     

     

  •  

    float deltaAbs = Mathf.Abs(Mathf.DeltaAngle(orbitAngles.y, headingAngle));    float rotationChange = rotationSpeed * Time.unscaledDeltaTime;    if (deltaAbs < alignSmoothRange) {      rotationChange *= deltaAbs / alignSmoothRange;    }    orbitAngles.y =      Mathf.MoveTowardsAngle(orbitAngles.y, headingAngle, rotationChange);

     

    這涵蓋了焦點移離相機的情況,但是當焦點移向相機時,我們也可以這樣做。這樣可以防止攝像機全速旋轉,每次航向越過180°邊界時都會改變方向。除了我們使用180°減去絕對增量之外,它的工作原理相同。

     

     

  •  

        if (deltaAbs < alignSmoothRange) {      rotationChange *= deltaAbs / alignSmoothRange;    }    else if (180f - deltaAbs < alignSmoothRange) {      rotationChange *= (180f - deltaAbs) / alignSmoothRange;    }
    
    

     

    最後,通過將旋轉速度縮放為時間增量和平方運動增量中的最小值,可以進一步減小微小角度的旋轉。

     

     

  •  

        float rotationChange =      rotationSpeed *Mathf.Min(Time.unscaledDeltaTime, movementDeltaSqr);
    
    

     

    平滑對齊。

     

    請注意,通過這種方法,可以將球體朝相機方向直線移動而不會旋轉。方向上的微小偏差也將得到抑制。一旦方向發生重大變化,自動旋轉將順利生效。

     

    180°對齊。

     

     

     

     

    相機相對運動

     

     

    至此,我們有了一個像樣的簡易軌道攝像機。現在,我們要相對於攝像機的視角來輸入玩家的運動輸入。

     

     

     

    輸入空間

     

     

    輸入可以在任何空間中定義,而不僅僅是世界空間或軌道攝像機的空間。它可以是 Transform 元件定義的任何空間。為此,將玩家輸入空間配置欄位新增到 MovingSphere

     

     

  •  

    [SerializeField]  Transform playerInputSpace = default;
    
    

     

    將軌道攝像機分配給該欄位。這是一種特定於場景的配置,因此不是球形預製件的一部分,儘管可以將其設定為自身,這將使其相對於自己的方向運動。

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    如果未設定輸入空間,那麼我們會將玩家輸入保留在世界空間中。否則,我們必須從提供的空間轉換為世界空間。如果設定了玩家輸入空間,我們可以通過在 Update 中呼叫 Transform.TransformDirection 來實現。

     

     

  •  

    if (playerInputSpace) {      desiredVelocity = playerInputSpace.TransformDirection(        playerInput.x, 0f, playerInput.y      ) * maxSpeed;    }    else {      desiredVelocity =        new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;    }
    
    

     

     

    相對運動,只有前進(受公眾號視訊數目限制,請通過原文檢視)。

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

     

     

     

    歸一化方向

     

     

    儘管轉換為世界空間會使球體朝正確的方向移動,但其前進速度會受到垂直軌道角的影響。它與水平面的距離越遠,球的移動速度就越慢。發生這種情況是因為我們期望所需的速度位於XZ平面中。我們可以通過從玩家輸入空間中檢索前向向量和右向向量,丟棄它們的Y分量並對其進行規範化來做到這一點。然後,所需的速度將成為由玩家輸入縮放的向量的總和。

     

    
    

     

  •  

        if (playerInputSpace) {      Vector3 forward = playerInputSpace.forward;      forward.y = 0f;      forward.Normalize();      Vector3 right = playerInputSpace.right;      right.y = 0f;      right.Normalize();      desiredVelocity =        (forward * playerInput.y + right * playerInput.x)* maxSpeed;    }                                  

     

     

    相機遮擋

     

     

    目前,我們的相機僅關心其相對於焦點的位置和方向。它對場景的其餘部分一無所知。因此,它直接穿過其他幾何形狀,這會引起一些問題。首先,這很醜。其次,它可能導致幾何形狀阻塞我們對球體的視線,從而使其難以導航。第三,裁剪幾何可以揭示不可見的區域。我們將僅考慮將相機的焦距設定為零的情況。

     

     

     

     

     

    縮短視線距離

     

     

    有多種策略可用於保持相機的視角有效。我們將應用最簡單的方法,如果攝像機和對焦點之間出現物體,則將攝像機沿其外觀方向向前拉。

     

    檢測問題的最明顯方法是從焦點向我們要放置相機的位置投射光線。確定外觀方向後,即可在 OrbitCamera.LateUpdate 中執行此操作。如果我們命中了某物,那麼我們將使用命中距離而不是配置的距離。

     

     

  •  

        Vector3 lookDirection = lookRotation * Vector3.forward;    Vector3 lookPosition = focusPoint - lookDirection * distance;
    if (Physics.Raycast( focusPoint, -lookDirection, out RaycastHit hit, distance )) { lookPosition = focusPoint - lookDirection * hit.distance; } transform.SetPositionAndRotation(lookPosition, lookRotation);

     

     

     

    將相機拉近對焦點可以使其靠近以使其進入球體。當球體與相機的近平面相交時,它可能會部分被截斷,甚至被完全截斷。您可以強制執行最小距離來避免這種情況,但這將意味著相機仍保留在其他幾何圖形內。對此沒有完美的解決方案,但是可以通過限制垂直軌道角度,不使水準儀幾何形狀過緊以及減小相機的近裁剪平面距離來緩解這種情況。

     

     

     

     

    保持近平面暢通

     

     

    投射單一光線不足以完全解決問題。這是因為,即使在相機的位置和對焦點之間有一條清晰的線,相機的近平面矩形仍可以部分切穿幾何圖形。解決方案是改為執行盒子投射,以匹配攝影機在世界空間中的近平面矩形,該矩形代表攝影機可以看到的最接近的物體。它類似於相機的感測器。

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    首先, OrbitCamera 需要對其 Camera 元件的引用。

     

     

  •  

    Camera regularCamera;
    void Awake () { regularCamera = GetComponent<Camera>(); focusPoint = focus.position; transform.localRotation = Quaternion.Euler(orbitAngles); }

     

    其次,盒子投射需要一個3D向量,其中包含盒子的一半延伸,這意味著它的寬度,高度和深度是一半。

     

    高度的一半可以通過將相機視場角的一半的正切值(以弧度為單位)找到,並由其近乎剪輯平面的距離縮放。寬度的一半是由相機的縱橫比縮放的。盒子的深度為零。讓我們在一個方便的屬性中進行計算。

     

     

  •  

    Vector3 CameraHalfExtends {    get {      Vector3 halfExtends;      halfExtends.y =        regularCamera.nearClipPlane *        Mathf.Tan(0.5f * Mathf.Deg2Rad * regularCamera.fieldOfView);      halfExtends.x = halfExtends.y * regularCamera.aspect;      halfExtends.z = 0f;      return halfExtends;    }  }
    
    

     

     

     

    我們不能快取halfExtends嗎?

    是的,假設相關的相機屬性未更改。計算每個框架可確保它始終有效,但是您也可以僅在必要時顯式重新計算它。

     

     

    現在,用 LateUpdate 中的 Physics.BoxCast 替換 Physics.Raycast 。必須將擴充套件的一半作為第二個自變數新增,並將框的旋轉作為新的第五個自變數新增。

     

     

  •  

        if (Physics.BoxCast(      focusPoint,CameraHalfExtends,-lookDirection, out RaycastHit hit,      lookRotation,distance    )) {      lookPosition = focusPoint - lookDirection * hit.distance;    }

     

    近平面位於相機位置的前面,因此我們只能向上投射到該距離,該距離是配置的距離減去相機的近平面距離。如果我們最終撞到東西,那麼最終距離就是命中距離加上近平面距離。

     

     

  •  

        if (Physics.BoxCast(      focusPoint, CameraHalfExtends, -lookDirection, out RaycastHit hit,      lookRotation, distance- regularCamera.nearClipPlane    )) {      lookPosition = focusPoint -        lookDirection *(hit.distance+ regularCamera.nearClipPlane);    }

     

     

     

    請注意,這意味著相機的位置仍可以在幾何圖形內部結束,但是其近平面矩形將始終保留在外部。當然,如果盒子投射已經在幾何體內部開始,則這可能會失敗。如果焦點物件已經與幾何相交,則相機也可能會相交。

     

     

     

     

    焦點半徑

     

     

    我們當前的方法有效,但前提是聚焦半徑為零。放寬焦點後,即使理想的焦點有效,我們也可以在幾何體內部得到焦點。因此,我們不能指望焦點是盒子投射的有效起點,因此我們必須使用理想的焦點。我們將從那裡投射到近平面框位置,方法是從相機位置移至焦點位置,直到到達近平面。

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

    從理想焦點投射的盒子。 

     

     

  •  

        Vector3 lookDirection = lookRotation * Vector3.forward;    Vector3 lookPosition = focusPoint - lookDirection * distance;
    Vector3 rectOffset = lookDirection * regularCamera.nearClipPlane; Vector3 rectPosition = lookPosition + rectOffset; Vector3 castFrom = focus.position; Vector3 castLine = rectPosition - castFrom; float castDistance = castLine.magnitude; Vector3 castDirection = castLine / castDistance;
    if (Physics.BoxCast( castFrom, CameraHalfExtends,castDirection, out RaycastHit hit, lookRotation,castDistance )) { … }

     

    如果有東西被擊中,則我們將盒子放置在儘可能遠的地方,然後我們偏移以找到相應的相機位置。

     

     

  •  

        if (Physics.BoxCast(      castFrom, CameraHalfExtends, castDirection, out RaycastHit hit,      lookRotation, castDistance    )) {      rectPosition = castFrom + castDirection * hit.distance;      lookPosition =rectPosition - rectOffset;    }

     

     

     

     

     

     

    遮蔽物

     

     

    通過在執行箱式投射時忽略攝像機,可以使攝像機與某些幾何形狀相交。出於效能原因或相機穩定性,這可以忽略小的細微幾何形狀。可選地,這些物體仍然可以被檢測到,但是會淡出而不是影響相機的位置,但是在本教程中我們不會介紹該方法。透明幾何也可以忽略。最重要的是,我們應該忽略領域本身。從球體內部進行投射時,它將始終被忽略,但是響應速度較慢的攝影機最終可能會從球體外部進行投射。如果它隨後撞擊球體,相機將跳到球體的另一側。

     

    我們可以通過圖層蒙版配置欄位來控制此行為,就像球體使用的欄位一樣。

     

     

  •  

    [SerializeField]  LayerMask obstructionMask = -1;      void LateUpdate () {    if (Physics.BoxCast(      focusPoint, CameraHalfExtends, castDirection, out RaycastHit hit,      lookRotation, castDistance, obstructionMask    )) {      rectPosition = castFrom + castDirection * hit.distance;      lookPosition = rectPosition - rectOffset;    }  }

     

     
    喵的Unity遊戲開發之路 - 軌道攝像機

     

    下一個教程是自定義重力(Custom Gravity)

    資源庫(Repository)

    https://bitbucket.org/catlikecodingunitytutorials/movement-05-custom-gravity/


    往期精選

    Unity3D遊戲開發中100+效果的實現和原始碼大全 - 收藏起來肯定用得著

    Shader學習應該如何切入?

    UE4 開發從入門到入土


    宣告:釋出此文是出於傳遞更多知識以供交流學習之目的。若有來源標註錯誤或侵犯了您的合法權益,請作者持權屬證明與我們聯絡,我們將及時更正、刪除,謝謝。

    原作者:Jasper Flick

    原文:

    https://catlikecoding.com/unity/tutorials/movement/orbit-camera/

    翻譯、編輯、整理:MarsZhou


    More:【微信公眾號】 u3dnotes

相關文章