使用Cinemachine设置3D格斗游戏的摄像机
本文由独立游戏开发者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
推荐阅读
促销活动
促销时间:10月16日前(最后3天!!!)
购买地址:
https://www.humblebundle.com/software/unity-2019-bundle
喜欢本文,请点“在看”