Unity 移动端相机控制系统详解:打造流畅自然的移动、旋转与缩放体验

前言

在移动平台上开发 3D 游戏或可视化应用时,良好的相机交互体验至关重要。相机不仅是用户观察世界的窗口,更是交互流畅性和沉浸感的重要保障。

本文将基于一个实战级 Unity 脚本,从原理到实现,逐步拆解移动端相机控制的关键要素。内容涵盖:

  • 单指拖动实现平滑移动
  • 双指捏合控制缩放并平滑过渡角度
  • 缩放与旋转结合的渐进式插值逻辑
  • 移动速度自适应高度的动态控制
  • 如何优雅实现这些功能并保持代码整洁

文章适用于中高级 Unity 开发者,也适合希望深入理解移动交互与相机控制机制的开发者学习参考。


一、项目背景与目标

在移动端相机控制中,用户的手指就是操控相机的摇杆。我们希望实现以下交互方式:

  1. 单指拖动:相机沿水平面左右移动、前后推进。
  2. 双指缩放:相机高度改变,同时实现从平视到俯视的过渡。
  3. 控制平滑自然:包括移动和旋转都通过插值函数完成,避免突兀跳动。
  4. 缩放高度限制:防止相机飞入地面或升至过高位置。
  5. 逻辑解耦:拖动和缩放互不干扰,操作清晰。

基于此目标,我们来逐步构建完整的系统。


二、系统架构与组件职责

整个控制器封装在一个脚本类 MobileCameraController 中。该类主要负责:

  • 触控识别与输入处理:判断当前是拖动还是缩放操作
  • 相机移动逻辑:根据拖动方向调整目标位置
  • 缩放逻辑与旋转插值:根据捏合手势调整高度和角度
  • 平滑过渡:通过 SmoothDampSlerp 实现平滑移动和旋转
  • 可拓展接口:如设置是否启用缩放、设置目标位置等方法

我们先来看下初始化过程:

代码语言:csharp复制
void Start()
{
    targetPosition = transform.position;
    targetRotation = transform.rotation;
}

这里初始化相机的目标位置和旋转,使得后续每一帧都以此为参照进行插值。


三、输入识别与主逻辑流程

核心逻辑位于 Update() 函数中:

代码语言:csharp复制
void Update()
{
    if (Input.touchCount == 2)
    {
        HandleMobileZoom(); // 双指缩放
        isDragging = false;
    }
    else if (Input.touchCount == 1)
    {
        HandleSingleTouch(); // 单指拖动
    }

    // 平滑过渡到目标位置与角度
    transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime);
    transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime / smoothTime);
}

这里通过 Input.touchCount 判断是一个手指还是两个手指操作,从而决定是移动还是缩放。之后通过内置的 SmoothDampSlerp 实现平滑过渡。


四、拖动操作原理详解

单指拖动逻辑封装在 HandleSingleTouch() 方法中,分三个阶段处理:按下、移动、抬起。

代码语言:csharp复制
case TouchPhase.Moved:
    if (isDragging)
    {
        // 计算标准化后的拖动距离
        Vector2 delta = touch.position - lastTouchPosition;
        Vector2 normalizedDelta = delta / screenDiagonal;

        // 基于当前高度动态调整移动速度
        float heightFactor = Mathf.Log(transform.position.y / minZoomHeight + 1f);
        float dynamicSpeed = moveSpeed * heightFactor;

        // 应用相机朝向,构建移动向量
        float moveX = -normalizedDelta.x * dynamicSpeed;
        float moveZ = normalizedDelta.y * dynamicSpeed;

        Vector3 right = transform.right;
        Vector3 forward = -transform.forward;
        forward.y = 0;

        Vector3 horizontalMovement = right * moveX;
        Vector3 forwardMovement = forward * moveZ;

        targetPosition += horizontalMovement + forwardMovement;
        lastTouchPosition = touch.position;
    }

为什么使用标准化距离?

由于不同设备的屏幕分辨率不同,我们将手指的位移除以屏幕对角线长度进行归一化,保证在不同尺寸设备上的操作一致性。

为什么用高度来调节速度?

在高视角时,用户希望更快地移动画面;在低视角时,移动应更细腻。因此通过当前相机高度对速度进行对数变换,实现非线性自适应调整。

为什么用 -transform.forward

因为 Unity 中相机的 forward 是朝前(屏幕外),而我们通常期望“手指向上滑动,画面向前推进”,因此取负方向。


五、缩放操作与视角插值

我们希望双指缩放不仅改变高度,还能够渐变相机角度。这个特性对于 3D 空间交互尤为重要。

代码语言:csharp复制
float newHeight = targetPosition.y - normalizedDelta * zoomSensitivity * 100f;
newHeight = Mathf.Clamp(newHeight, minZoomHeight, maxZoomHeight);
targetPosition = new Vector3(targetPosition.x, newHeight, targetPosition.z);

这里是通过手指之间距离变化的差值,推算出新的相机高度,并限制在设定范围内。

接着我们插值旋转:

代码语言:csharp复制
float t = Mathf.InverseLerp(minZoomHeight, maxZoomHeight, newHeight);
targetRotation = Quaternion.Slerp(zoomInRotation, zoomOutRotation, t);

使用 InverseLerp 将当前高度转换为 0~1 的比例因子,再用它在两个预设角度之间插值,从而实现“缩小时仰角大,放大时仰角小”的自然视角过渡。


六、平滑过渡机制分析

位置过渡:SmoothDamp

代码语言:csharp复制
transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime);

SmoothDamp 是 Unity 中非常实用的平滑插值函数,适用于实现类似“渐近收敛”的平移效果。相比 Lerp 它不会卡在目标点附近。

旋转过渡:Slerp

代码语言:csharp复制
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime / smoothTime);

Slerp(球形线性插值)则用于实现四元数之间的平滑旋转,适用于相机这种需要自然变换方向的场景。


总结

移动端相机控制不仅关乎操作的实现,更关系到用户体验的细节打磨。从本篇内容我们可以看到,仅凭单指拖动与双指缩放这两种基础手势,通过合理设计参数、动态调整逻辑以及引入插值平滑机制,就可以构建出一个高质量、专业感十足的相机控制系统