如何在Unity中实现RTS相机缩放时的平滑过渡

4

现在我正在尝试制作实时战略摄像机缩放和平移,当接近地面时。我现在遇到的问题是,我使用鼠标滚轮进行缩放,它使缩放感觉像是有延迟的。它看起来像是跳过了一些Y值并瞬间传送到所需位置,而不是平滑地移动到所需位置。此外,我想知道如何使相机停止在最小Y值处,因为现在发生的情况是它在大约22处停止,而不是我的相机移动的最小Y值20。

我尝试使用数字键盘上的+和-进行缩放,它按照我想要的方式工作(平滑地缩放而不跳过),但并非所有玩家都有数字键盘,我觉得使用鼠标滚轮更合适。

{

private const int levelArea = 100;

private const int scrollArea = 25;
private const int scrollSpeed = 25;
private const int dragSpeed = 70;

private const int zoomSpeed = 50;

// Maximum/minimum zoom distance from the ground
public int zoomMin = 20;
public int zoomMax = 120;

private const int panSpeed = 40;

// Minimal/maximal angles for camera
private const int panAngleMin = 30;
private const int panAngleMax = 90;


void Update()
{
    // Init camera translation for this frame.
    var translation = Vector3.zero;

    // Zoom in or out
    var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;
    if (zoomDelta != 0)
    {
        translation -= Vector3.up * zoomSpeed * zoomDelta;
    }

    // Start panning camera if zooming in close to the ground or if just zooming out.
    var pan = transform.eulerAngles.x - zoomDelta * panSpeed;
    pan = Mathf.Clamp(pan, panAngleMin, panAngleMax);
    // When to start panning up the camera
    if (zoomDelta < 0 || transform.position.y < (zoomMax -20))
    {
        transform.eulerAngles = new Vector3(pan, 0, 0);
    }

    // Move camera with arrow keys
    translation += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));

    // Move camera with mouse
    if (Input.GetMouseButton(2)) // MMB
    {
        // Hold button and drag camera around
        translation -= new Vector3(Input.GetAxis("Mouse X") * dragSpeed * Time.deltaTime, 0,
                           Input.GetAxis("Mouse Y") * dragSpeed * Time.deltaTime);
    }
    else
    {
        // Move camera if mouse pointer reaches screen borders
        if (Input.mousePosition.x < scrollArea)
        {
            translation += Vector3.right * -scrollSpeed * Time.deltaTime;
        }

        if (Input.mousePosition.x >= Screen.width - scrollArea)
        {
            translation += Vector3.right * scrollSpeed * Time.deltaTime;
        }

        if (Input.mousePosition.y < scrollArea)
        {
            translation += Vector3.forward * -scrollSpeed * Time.deltaTime;
        }

        if (Input.mousePosition.y > Screen.height - scrollArea)
        {
            translation += Vector3.forward * scrollSpeed * Time.deltaTime;
        }
    }

    // Keep camera within level and zoom area
    var desiredPosition = transform.position + translation;
    if (desiredPosition.x < -levelArea || levelArea < desiredPosition.x)
    {
        translation.x = 0;
    }
    if (desiredPosition.y < zoomMin || zoomMax < desiredPosition.y)
    {
        translation.y = 0;
    }
    if (desiredPosition.z < -levelArea || levelArea < desiredPosition.z)
    {
        translation.z = 0;
    }

    // Move camera parallel to world axis
    transform.position += translation;
}

}

我希望在缩放前后,相机能够平稳地过渡到当前位置和期望位置。同时,我也想知道如何让相机停留在最小/最大缩放距离处,而不是靠近它就停下来。谢谢您的帮助。以下是相机运动效果的视频链接:https://youtu.be/Lt3atJEaOjA


尝试使用Lerp插值来进行位置插值,而不是使用translate函数。 - Everts
2个回答

3

好的,我将在这里做三件事情。首先,我建议如果你正在处理高级摄像机行为,那么你可能至少考虑使用Cinemachine。我本人没有太多经验,所以我尝试给出建议可能会对你不利,但有很多优秀的教程可以参考。Youtube和谷歌应该可以提供。

第二件事是以我能想到的最直接方式解决你的问题,然后我们再看看是否能找到更好的方法来解决它。

所以,关键在于Unity的滚轮输入相当二元化。当你检查滚轮轴时,结果基于自上一帧更新以来你的鼠标滚过了多少“次数”,但你真正想要的是有点弹性的东西。实际上,默认情况下,Unity可以通过大部分的轴输入来实现这一点:你可能会注意到,在默认的Unity项目中使用WASD时,有一种“延迟”的感觉,即使你把手指从按键上拿开,Input.GetAxis()仍然会连续几帧返回正值。这与你输入设置中的Gravity值有关,而Input.GetAxisRaw()实际上是用来避免这种情况的。由于某种原因,滚轮轴似乎不受轴重力的影响,因此我们实际上必须自行实现类似的东西。

// Add this property to your class definition (so it persists between updates):
private float wheelAxis = 0;

// Delete this line:
var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;

// And put these three new lines in its place:
wheelAxis += Input.GetAxis("Mouse ScrollWheel");
wheelAxis = Mathf.MoveTowards(wheelTotal, 0f, Time.deltaTime);
var zoomDelta = Mathf.Clamp(wheelAxis, -0.05f, 0.05f) * zoomSpeed * Time.deltaTime;

好的,所以我们在这里做了一些事情。每次更新时,我们将当前的滚轮值添加到我们的wheelAxis中。接下来,我们通过Mathf.MoveTowards()函数应用当前的Time.deltatime作为“重力”。最后,我们调用的基本上只是您旧的zoomDelta代码,有一个简单的修改:我们使用Mathf.Clamp约束wheelAxis来尝试调节缩放的速度。

你可以以几种方式修改此代码。如果你乘以Time.deltaTime参数,你可以影响你的输入会“持续”多长时间。如果你改变Mathf.Clamp()的值,你会得到更快或更慢的缩放。总的来说,如果你只想要一个平稳的缩放,并且对你的代码做出最小的改动,那么这可能是你最好的选择。

好的!

现在我们已经完成了这些,让我们谈谈你的代码,以及你如何处理问题,并看看是否可以找到一个比较清晰的解决方案。

让相机正常工作其实很不容易。您的代码看起来像我见过的许多解决复杂问题的代码:看起来像您添加了一些功能,然后测试它,发现一些边缘情况导致代码崩溃,然后修补这些情况,然后尝试在旧代码的基础上实现新功能,但它在各种其他方面都有点问题,等等。我们现在得到的是有点混乱的。

你的代码最大的问题是相机的位置和相机的旋转紧密地联系在一起。当与角色一起工作时,这通常是可以接受的,但当与相机一起工作时,你需要把它们分开。考虑相机在哪里以及相机正在观察什么,这些是非常不同的事情需要跟踪。

因此,这是一个可供使用并直接运行的工作相机脚本:

using UnityEngine;

public class RTSCamera : MonoBehaviour
{
    public float zoomSpeed = 100f;
    public float zoomTime = 0.1f;

    public float maxHeight = 100f;
    public float minHeight = 20f;

    public float focusHeight = 10f;
    public float focusDistance = 20f;

    public int panBorder = 25;
    public float dragPanSpeed = 25f;
    public float edgePanSpeed = 25f;
    public float keyPanSpeed = 25f;

    private float zoomVelocity = 0f;
    private float targetHeight;

    void Start()
    {
        // Start zoomed out
        targetHeight = maxHeight;
    }

    void Update()
    {
        var newPosition = transform.position;

        // First, calculate the height we want the camera to be at
        targetHeight += Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * -1f;
        targetHeight = Mathf.Clamp(targetHeight, minHeight, maxHeight);

        // Then, interpolate smoothly towards that height
        newPosition.y = Mathf.SmoothDamp(transform.position.y, targetHeight, ref zoomVelocity, zoomTime);

        // Always pan the camera using the keys
        var pan = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * keyPanSpeed * Time.deltaTime;

        // Optionally pan the camera by either dragging with middle mouse or when the cursor touches the screen border
        if (Input.GetMouseButton(2)) {
            pan -= new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y")) * dragPanSpeed * Time.deltaTime;
        } else {
            var border = Vector2.zero;
            if (Input.mousePosition.x < panBorder) border.x -= 1f;
            if (Input.mousePosition.x >= Screen.width - panBorder) border.x += 1f;
            if (Input.mousePosition.y < panBorder) border.y -= 1f;
            if (Input.mousePosition.y > Screen.height - panBorder) border.y += 1f;
            pan += border * edgePanSpeed * Time.deltaTime;
        }

        newPosition.x += pan.x;
        newPosition.z += pan.y;

        var focusPosition = new Vector3(newPosition.x, focusHeight, newPosition.z + focusDistance);

        transform.position = newPosition;
        transform.LookAt(focusPosition);
    }
}

虽然我鼓励你用自己的时间去阅读,但我不想让你逐字逐句地读完。我只会讲解其中的关键点。
这里的关键思想是,我们不直接控制相机的高度和方向,而是让滚轮决定相机的高度,并使用 Mathf.SmoothDamp() 函数在几帧内平滑地将相机移动到该位置。(Unity有许多类似的有用函数,例如 Mathf.MoveTowards() 可以作为一种替代的插值方法)。最后,我们选择在靠近地面前方的某个点上,直接将相机对准那个点,而不是试图直接调整相机的旋转值。
通过完全独立地保持相机的位置和方向,并将相机高度的 “动画” 分离出来,我们避免了很多麻烦,消除了很多潜在的混杂错误的可能性。
希望有所帮助。

非常感谢,它运行得很好,看起来也很棒。 你的代码中唯一需要更改的是平移和边框移动被反转了,所以如果你想向右移动,相机会朝另一个方向移动。 最后我想问的是,是否有更简单的方法来改变相机何时开始旋转其X轴。我尝试更改焦点高度和焦点距离,但我不确定我是否理解正确,因为我不知道如何简单地告诉相机何时开始旋转以及从哪个最大X旋转值到其最小X旋转值。 谢谢。 - Strongo
1
糟糕,倒置平移的问题被你发现了,我以为我已经修复了。 - VPellen
1
关于旋转的“时间” - 从摄像机在任何特定时间点看向哪里的角度来考虑。焦点位置是摄像机所看的位置。高度是绝对的,距离是相对于摄像机的。尝试随着摄像机的Y位置变化改变焦点距离,甚至用特定的目标对象替换焦点。 - VPellen

-2

Cinemachine非常好用,但我建议人们在使用Cinemachine之前自己多学习一些,这样你就能更好地理解背后发生的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接