查看原文
其他

使用Cinemachine设置3D格斗游戏的摄像机

Matt DeLucas Unity官方平台 2022-05-07

本文由独立游戏开发者Matt DeLucas分享在Unity中使用Cinemachine实现《VR战士》、《铁拳》等3D格斗游戏的摄像机效果。


或许有人会有疑问:为什么使用Cinemachine摄像机Cinemachine摄像机的一个优点是:可以快速融合其它摄像机视图


如果我们有一个特别的摄像机动画,用于突出展现格斗游戏的技巧或超必杀,我们可以使用Cinemachine快速融合动画,在动画结束后返回到主摄像机,整个过程只要切换虚拟摄像机的优先级即可。


因此,我们使用Cinemachine摄像机要实现三个目标:

  • 在环境中跟踪两个游戏角色。

  • 摄像机要随着两个角色在场景中的移动方向而旋转。

  • 摄像机要随着角色互相靠近和远离而进行移动。


下图是场景的最初画面,有一个红色战士和一个绿色战士两个游戏角色,它们会被Cinemachine摄像机跟踪。


跟踪角色

为了跟踪角色,我们点击Cinemachine > Create Target Group Camera,创建Cinemachine Target Group摄像机,这样会同时创建Cinemachine Virtual Camera和Cinemachine Target Group 。


Cinemachine Target Group可以跟踪多个Transform,下面是Cinemachine Target Group的设置。



我们将Position Mode设为Group Center,Rotation Mode设为Manual,Update Method设为Late Update。此后,我们将使用脚本来控制旋转。


我们在Target列表中将两个战士对象的Transform设置了相同的Weight权重值,Radius值设为1.7。最初,由于我们尝试把虚拟摄像机的Body设为Framing Transposer,因此Radius值非常重要。而现在,重要的是确保两个Transform的Radius值相同。


Cinemachine Virtual Camera组件设置如下图所示。



通过这样设置,我们让摄像机跟随(Follow)并观察(Look At)目标对象组。如果Body设为Framing Transposer,那么在摄像机旋转时会发生抖动现象,Framing Transposer更适合用于2D摄像机,所以我们需要将虚拟摄像机的Body选项设为Transposer


我们设置的数值类似默认数值,只不过在每个轴把Damping值减小为0.5,并把Follow Offset设为(0, 2, -4.3333333)。这样可以使摄像机在运行时比目标对象的中心高2个单位,并远离-4.33333333个单位。


Aim设置为Same as Follow Target,这表示该组件会使用和目标对象Transform相同的Rotation设置。


完成设置后,摄像机设置只会跟随目标的中心位置。效果如下图所示。



现在,摄像机可以很好地跟踪两个角色的中心位置,但在红色战士远离绿色战士特定距离后,摄像机不会后退来让视图包含两个战士,而且在红色战士绕着对手移动时,摄像机也不会相应地旋转。


下面,我们将介绍如何设置摄像机,使其可以通过旋转和移动,更好地跟踪战士。


旋转并对齐摄像机

为了实现这个效果,我们没有使用Cinemachine内置工具,而是编写脚本。我们将MonoBehaviour脚本命名为Align3DCam,并将其附加给Target Group游戏对象,设置如下图所示。


 

上图中的TA和TB是被跟踪的两个Transform,也就是示例中的两个战士。


然后,在Virtual Camera中引用虚拟摄像机本身,它的Transposer组件也会被引用,但该引用会在Awake阶段设置。

 

Framing Normal设为法向量,它会根据虚拟摄像机上Transposer值的Follow Offset属性,在Awake阶段设置。


Distance表示两个被跟踪Transform之间的距离,该值会在检视窗口以调试为目的进行序列化。

 

Transposer Linear Slope和Transposer Linear Offset两个值表示简单的线性等式,即y = mx + b,x表示两个被跟踪Transform之间的距离,y表示沿着Framing Normal的虚拟摄像机偏移距离。

 

Framing helpers用于帮助确定Slope斜率值和Offset偏移值,并设置最小允许距离,使摄像机不会在两个战士贴着站时距离过近。


Align3DCam脚本的代码如下。

using Cinemachine;

using UnityEngine;

 

public class Align3DCam : MonoBehaviour

{

    [Tooltip("The transforms the camera attempts to align to.")]

    public Transform tA, tB;

 

    [Tooltip("The cinemachine camera that will be updated.")]

    public Cinemachine.CinemachineVirtualCamera virtualCamera;

 

    /// <summary>

    /// Cinemachine摄像机的Transposer组件

    /// </summary>

    private Cinemachine.CinemachineTransposer tranposer;

 

    /// <summary>

    /// 布尔值会根据是否提供虚拟摄像机而设置

    /// </summary>

    private bool hasVirtualCamera;

 

    [SerializeField(), Tooltip("The starting normal of the cinemachine transposer.")]

    private Vector3 framingNormal;

 

    [SerializeField(), Tooltip("The current distance between the two tracked transforms.")]

    float distance;

 

    [Tooltip("Slope Value (m) of the linear equation used to determine how far the camera should be based on the distance of the tracked transforms.")]

    public float transposerLinearSlope;

 

    [Tooltip("Offset Value (b) of the linear equation used to determine how far the camera should be based on the distance of the tracked transforms.")]

    public float transposerLinearOffset;

 

    [Header("Framing helpers")]

    [Tooltip("The minimum distance allowed between the two transforms before the camera stops moving in and out.")]

    public float minDistance;

 

    [Tooltip("The minimum distance the camera will be from the tracked transforms.")]

    public float minCamDist;

 

    [Tooltip("A secondary distance between the two transforms used for reference.")]

    public float secondaryDistance;

 

    [Tooltip("A secondary distance the camera should be at when the tracked transforms are at the secondary distance.")]

    public float secondaryCamDistance;

 

    /// <summary>

    /// 用于确定Slope斜率值的函数

    /// </summary>

    [ContextMenu("Calculate Slope")]

    void CalculateSlopes()

    {

        if (virtualCamera == null)

            return;

        tranposer = virtualCamera.GetCinemachineComponent<CinemachineTransposer>();

        if (transposer == null)

            return;

 

        // 如果应用正在运行,不会更新最小值

        if (!Application.isPlaying)

        {

            // 获取此时Transform之间的距离

            minDistance = Vector3.Distance(tA.position, tB.position);

            distance = minDistance;

 

            // 获取跟随偏移向量的大小

            minCamDist = tranposer.m_FollowOffset.magnitude;

        }

 

        // 计算斜率值,计算公式为((y2-y1)/(x2-x1))

        transposerLinearSlope = (secondaryCamDistance - minCamDist) / (secondaryDistance - minDistance);

 

        // 通过b = y - mx计算偏移值

        transposerLinearOffset = minCamDist - (transposerLinearSlope * minDistance);

    }

 

    private void Awake()

    {

        // 确定虚拟摄像机是否存在并启用

        hasVirtualCamera = virtualCamera != null;

        if (hasVirtualCamera)

        {

            transposer = virtualCamera.GetCinemachineComponent<CinemachineTransposer>();

 

            if (transposer == null)

            {

                hasVirtualCamera = false;

            }

            else

            {

                // 通过Transposer的初始偏移值,设置framingNormal值。

                framingNormal = tranposer.m_FollowOffset;

                framingNormal.Normalize();

            }

        }

    }

 

    // Update方法会在每帧调用一次

    void LateUpdate()

    {

        // 获取两个跟踪Transform之间的距离

        Vector3 diff = tA.position - tB.position;

        distance = diff.magnitude;

 

        // Y值被移除,然后归一化处理向量。

        diff.y = 0f;

        diff.Normalize();

 

        // 根据跟踪Transform之间的距离和最小值,调整Transposer的跟随偏移值。

        if (hasVirtualCamera)

        {

            tranposer.m_FollowOffset = framingNormal * (Mathf.Max(minDistance, distance) *

                transposerLinearSlope + transposerLinearOffset);

        }

 

        // 如果两个Transform处于相同位置,不进行更新

        if (Mathf.Approximately(0f, diff.sqrMagnitude))

            return;

 

        // 创建一个Quaternion值,它会向着初始方向,然后旋转90度

        Quaternion q = Quaternion.LookRotation(diff, Vector3.up) * Quaternion.Euler(0, 90, 0);

 

        // 创建另一个Quaternion值,它会旋转180度

        Quaternion qA = q * Quaternion.Euler(0, 180, 0);

 

        // 确定当前方向和之前创建的两个方向之间的角度

        float angle = Quaternion.Angle(q, transform.rotation);

        float angleA = Quaternion.Angle(qA, transform.rotation);

 

        // Transform的Rotation值设为更靠近当前方向的值。

        if (angle < angleA)

            transform.rotation = q;

        else

            transform.rotation = qA;

    }

}


Align3DCam脚本主要完成两件事:

  • 根据线性方程的结果偏移摄像机。

  • 根据两个跟踪Transform之间的向量旋转摄像机。


斜率值在CalculateSlope方法中计算,该方法有ContextMenu属性,这表示它可以通过右键单击Align3DCam组件菜单来访问。



Calculate Slope会获取两个战士之间的当前距离和Cinemachine摄像机跟随偏移位置的大小,来设置最小距离和最小摄像机距离。两个Secondary的值会用于计算Transposer Linear Slope值和Transposer Linear Offset值。

 

为了得到合适的Secondary值,我们需要手动调整数值,直到获得理想的结果。如果在游戏运行时使用Calculate Slope,最小值不会被调整,因此我们可以测试不同的数值,在结束运行前复制组件信息,在结束后粘贴这些数值。

 

关于旋转角度,该方法会获取两个Transform之间的向量,该向量通过让TB位置和TA位置相减得到。然后,把该向量的y值设为0,并归一化处理该向量。

 

我们使用Quaternion.LookAt创建了一个Quaternion值,它会使用归一化的diff向量和Vector3.up,创建朝着该向量方向的旋转值。然后把该Quaternion值和90度旋转角相乘,得到观察两个角色的旋转值。


然而,这种设置设定的情况是:TA的对象总在屏幕左边,TB的对象总在屏幕右边。


如果两个对象换边,例如:其中一个战士跳过另一个战士,摄像机会进行旋转,并突然转换镜头,过程如下图所示。



为了解决这个问题,我们创建了另一个Quaternion值,它基于第一个Quaternion值,在Y轴旋转180度,也就是反方向的相同旋转角度。


然后,我们会获取两个Quaternion值和摄像机的当前旋转值之间的角度。我们会使用角度较小的旋转值。


现在,当红色战士跳过对手时,摄像机不会突然切换来保持红色战士一直在左边。



在使用合适的数值后,Cinemachine虚拟摄像机和目标对象组会得到下图的效果。



上图中,红色战士绕着绿色战士移动时,摄像机也会随之旋转。红色战士远离绿色战士时,摄像机会向后移动。战士跳过对手时,摄像机不会突然转换镜头。


小结

我们使用非常简单的设置实现了使用Cinemachine为格斗游戏设置3D摄像机。


本文的设置特别适用于早期原型,如果场景比较复杂的话,我们可能还要添加和物品之间的碰撞功能,或使用边界框来更好地捕捉战士。


下载Unity Connect APP,请点击此处 观看更多Unity官方精彩视频,请关注“Unity官方”B站账户。


你可以访问Unity答疑专区留下你的问题,Unity社区和官方团队帮你解答:

Connect.unity.com/g/discussion


推荐阅读

利用Cinemachine快速创建游戏中的相机系统

Cinemachine在2D游戏中的开发小技巧

Unity影视动画示例项目发布

Unity实时渲染:影视动画制作的未来

Unity与《狮子王》,再创奇幻世界

Marza动画星球新作《The Peak》


促销活动

Humble Unity Bundle 2019限时优惠

促销时间:10月16日前(最后3天!

购买地址:

https://www.humblebundle.com/software/unity-2019-bundle



喜欢本文,请点“在看”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存