喵的Unity游戏开发之路 - 轨道摄像机

前言
        很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 移动 - 轨道摄像机 - 相对控制

  • 创建一个轨道摄像机。

  • 支持手动和自动相机旋转。

  • 相对于相机进行移动。

  • 防止相机相交。

  • 这是有关控制角色的运动的教程系列的第四部分。这次,我们将注意力集中在相机上,创建一个从中控制球体的绕行轨道。

    本教程使用Unity 2019.2.18f1创建。它还使用ProBuilder软件包。

    效果之一

    跟随球体



    只有当球体被限制在完全可见的区域时,固定的视角才起作用。但是通常游戏中的角色可以在大范围内漫游。使之成为可能的典型方法是使用第一人称视角或在第三人称视角模式下让相机跟随玩家的头像。还存在其他方法,例如根据化身的位置在多个摄像机之间切换。




    有第二人称视角吗?

    第三人称存在于游戏世界之外,代表玩家。游戏中存在第二个人。可能是任何人或不是玩家头像的任何东西。这种情况很少见,但有些游戏将这种视角用作暗机关,例如,它是意识世界中的一种精神力量。




    轨道摄像机



    我们将创建一个简单的轨道摄像机,以第三人称模式跟随我们的球体。为它定义一个 OrbitCamera 组件类型,为其赋予 RequireComponent 属性,以强制将其附加到也具有常规 Camera 组件。


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


    使用单个球体调整场景的主摄像机,使其具有此组件。为此,我使用一个较大的平面制作了一个新场景,将摄影机定位为使其与球体在其视点中心成45°角向下倾斜,大约相距五个单位。


    保持相对地位


    为了使相机聚焦在球体上,我们需要告诉它聚焦的对象。这实际上可以是任何东西,因此请为焦点添加一个可配置的 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。


    焦点半径


    始终将球保持精确的聚焦可能会感觉太僵硬。相机甚至会复制球体的最小运动,这会影响整个视图。我们可以通过使相机仅在其焦点与理想焦点相差太大的情况下移动来放松此约束。我们将通过添加聚焦半径(默认设置为一个单位)来使其可配置。

    [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);

    集中焦点。



    环绕球体



    下一步是可以调整相机的方向,以便可以描述围绕焦点的轨道。我们将可以手动控制轨道,并使相机自动旋转以跟随其焦点。



    轨道角度



    摄像机的方向可以用两个轨道角来描述。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;



    添加 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;



    最大值永远不会低于最小值,因此请在 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;



    跟踪上次手动旋转发生的时间。再一次,我们依靠的是这里的非标度时间,而不是游戏中的时间。


  • 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;



    为了完成这项工作,我们需要知道 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;


    将轨道摄像机分配给该字段。这是一种特定于场景的配置,因此不是球形预制件的一部分,尽管可以将其设置为自身,这将使其相对于自己的方向运动。



    如果未设置输入空间,那么我们会将玩家输入保留在世界空间中。否则,我们必须从提供的空间转换为世界空间。如果设置了玩家输入空间,我们可以通过在 Update 中调用 Transform.TransformDirection 来实现。


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


    相对运动,只有前进(受公众号视频数目限制,请通过原文查看)。




    归一化方向



    尽管转换为世界空间会使球体朝正确的方向移动,但其前进速度会受到垂直轨道角的影响。它与水平面的距离越远,球的移动速度就越慢。发生这种情况是因为我们期望所需的速度位于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);



    将相机拉近对焦点可以使其靠近以使其进入球体。当球体与相机的近平面相交时,它可能会部分被截断,甚至被完全截断。您可以强制执行最小距离来避免这种情况,但这将意味着相机仍保留在其他几何图形内。对此没有完美的解决方案,但是可以通过限制垂直轨道角度,不使水准仪几何形状过紧以及减小相机的近裁剪平面距离来缓解这种情况。




    保持近平面畅通



    投射单一光线不足以完全解决问题。这是因为,即使在相机的位置和对焦点之间有一条清晰的线,相机的近平面矩形仍可以部分切穿几何图形。解决方案是改为执行盒子投射,以匹配摄影机在世界空间中的近平面矩形,该矩形代表摄影机可以看到的最接近的物体。它类似于相机的传感器。



    首先, 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);    }



    请注意,这意味着相机的位置仍可以在几何图形内部结束,但是其近平面矩形将始终保留在外部。当然,如果盒子投射已经在几何体内部开始,则这可能会失败。如果焦点对象已经与几何相交,则相机也可能会相交。




    焦点半径



    我们当前的方法有效,但前提是聚焦半径为零。放宽焦点后,即使理想的焦点有效,我们也可以在几何体内部得到焦点。因此,我们不能指望焦点是盒子投射的有效起点,因此我们必须使用理想的焦点。我们将从那里投射到近平面框位置,方法是从相机位置移至焦点位置,直到到达近平面。


    从理想焦点投射的盒子。 

  •     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;    }  }



    下一个教程是自定义重力(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

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