You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity视差下MoveTowards实现Cinemachine滑动相机异常问题

Unity Cinemachine 滑动相机方向识别异常修复

问题背景

在Unity中结合视差(parallax)效果与MoveTowards方法开发时,出现Cinemachine相机移动方向识别异常问题。开发需求为通过新Input System实现Cinemachine相机跟随玩家左右滑动的交互效果,当前Cinemachine相机上挂载的组件如下:

  • 名为Camera Swipe的滑动控制脚本
  • Player Input组件,映射至Screen Controls资源中名为Swipe Map的默认输入映射表
  • 名为Screen Input Manager的输入管理脚本

原有代码实现

Screen Input Manager 脚本

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

[DefaultExecutionOrder(-1)]
public class ScreenInputManager : MonoBehaviourSingleton<ScreenInputManager>
{
    public GameObject player;

    #region
    public delegate void StartTouch(Vector2 position, float time);
    public event StartTouch OnStartTouch;
    public delegate void EndTouch(Vector2 position, float time);
    public event StartTouch OnEndTouch;
    #endregion

    private ScreenControls screenControls;
    private Camera mainCamera;

    private void Awake()
    {
        screenControls = new ScreenControls(); // new instance of input manager
        screenControls.SwipeMap.Enable();
        screenControls.Enable();
        mainCamera = Camera.main;
    }

    private void OnEnable()
    {
        screenControls.Enable();
    }

    private void OnDisable()
    {
        screenControls.Disable();
    }

    private void Start()
    {
        screenControls.SwipeMap.PrimaryContact.started += context => StartTouchPrimary(context);
        screenControls.SwipeMap.PrimaryContact.canceled += context => EndTouchPrimary(context);
    }

    private void StartTouchPrimary(InputAction.CallbackContext context)
    {
        if (OnStartTouch != null) OnStartTouch(Utils.ScreenToWorld(mainCamera, screenControls.SwipeMap.PrimaryPosition.ReadValue<Vector2>()), (float)context.startTime);
    }

    private void EndTouchPrimary(InputAction.CallbackContext context)
    {
        if (OnEndTouch != null) OnEndTouch(Utils.ScreenToWorld(mainCamera, screenControls.SwipeMap.PrimaryPosition.ReadValue<Vector2>()), (float)context.time);
    }

    public Vector2 PrimaryPosition()
    {
        return screenControls.SwipeMap.PrimaryPosition.ReadValue<Vector2>();
    }
}

Camera Swipe 脚本

using System.Collections.Generic;
using UnityEngine;
using Cinemachine;

[DefaultExecutionOrder(-1)]
public class CameraSwipe : MonoBehaviour
{
    [SerializeField] private float minimumDistance = 1f;
    [SerializeField] private float maximumTime = 1f;
    [SerializeField, Range(0f, 1f)] private float directionThreshhold = 0.9f;

    private Vector2 startPosition;
    private float startTime;
    private Vector2 endPosition;
    private float endTime;
    private Vector3 targetPosition;

    private ScreenInputManager inputProvider;
    private CinemachineVirtualCamera virtualCamera;
    private Rigidbody2D cameraRigid;
    private Transform cameraTransform;

    private void Awake()
    {
        inputProvider = GetComponent<ScreenInputManager>();
        virtualCamera = GetComponent<CinemachineVirtualCamera>();
        cameraRigid = GetComponent<Rigidbody2D>();
        cameraTransform = virtualCamera.VirtualCameraGameObject.transform;
    }

    private void OnEnable()
    {
        inputProvider.OnStartTouch += SwipeStart;
        inputProvider.OnEndTouch += SwipeEnd;
    }

    private void OnDisable()
    {
        inputProvider.OnStartTouch -= SwipeStart;
        inputProvider.OnEndTouch -= SwipeEnd;
    }

    private void SwipeStart(Vector2 position, float time)
    {
        startPosition = position;
        startTime = time;
    }


    private void SwipeEnd(Vector2 position, float time)
    {
        endPosition = position;
        endTime = time;
    }

    private void Update()
    {
        if (Vector3.Distance(startPosition, endPosition) >= minimumDistance && (endTime - startTime) <= maximumTime)
        {
            Debug.DrawLine(startPosition, endPosition, Color.red, 5f);
            Vector3 direction = endPosition - startPosition;

            targetPosition = cameraTransform.position; 
            targetPosition.x =  -direction.x;
            Vector2 direction2D = new Vector2(direction.x, direction.y).normalized;
            SwipeDirection(targetPosition, direction2D);
        }
    }

    private void SwipeDirection(Vector3 direction, Vector2 direction2D)
    {
        if (Vector2.Dot(Vector2.left, direction) > directionThreshhold)
        {
            
            Debug.Log("left " + " the direction 2d is: " + direction2D);
            cameraTransform.position = Vector3.MoveTowards(cameraTransform.position, direction, Time.deltaTime * (float) 3);

        }

        else if (Vector2.Dot(Vector2.right, direction) > directionThreshhold)
        {
            Debug.Log("right " + " the direction 2d is: " + direction2D);
            cameraTransform.position = Vector3.MoveTowards(cameraTransform.position, direction, Time.deltaTime * (float) 3);
        }
    }
}

故障表现

  • 向右滑动时偶发被误识别为左滑
  • 相机移动时偶现卡顿、无法越过屏幕特定区域
  • 左右滑动方向的识别准确率较低

已尝试排查操作

  • 怀疑故障源于在Update()中调用SwipeDirection方法,尝试将逻辑移至SwipeEnd()执行末尾调用,修改后相机仅能小幅移动,完全无法实现流畅移动效果
  • 添加多组Debug.Log日志排查后发现,当CameraSwipe脚本中direction2D变量值为0时相机会出现卡顿

根因分析与修复方案

核心问题点

  1. 委托定义错误:ScreenInputManager中OnEndTouch事件错误使用了StartTouch委托类型,虽然参数结构一致不会抛出编译错误,但会导致事件传参逻辑混乱。
  2. 滑动状态无重置:每次滑动结束后没有清空起止位置、时间的缓存值,导致Update每帧都会重复触发滑动判定,新旧滑动参数混算直接引发方向误判、反复重设目标位置导致卡顿。
  3. 目标位置计算逻辑错误:原代码直接将滑动偏移量取反作为相机目标X坐标,没有基于相机当前位置叠加滑动换算的移动距离,导致目标点始终离相机极近,出现移动幅度过小、无法越过固定区域的问题。
  4. 方向判定传参错误:SwipeDirection方法中做方向点积计算时,传入的第一个参数是相机世界坐标位置,不是归一化的滑动方向向量,点积结果完全不具备方向参考性,直接导致方向识别准确率极低。
  5. 坐标转换时机错误:在触摸开始/结束时就将屏幕坐标转为世界坐标,滑动过程中相机会移动,坐标参考帧变化会导致记录的起止位置出现偏移误差。
  6. 移动速度设置过低:原代码设置的移动速度为3单位/秒,对于滑动交互来说速度过慢,主观感受为相机移动卡顿、挪不动。

修复后代码

修复后的ScreenInputManager

using UnityEngine;
using UnityEngine.InputSystem;

[DefaultExecutionOrder(-1)]
public class ScreenInputManager : MonoBehaviourSingleton<ScreenInputManager>
{
    public GameObject player;

    public delegate void StartTouch(Vector2 screenPosition, float time);
    public event StartTouch OnStartTouch;
    public delegate void EndTouch(Vector2 screenPosition, float time);
    public event EndTouch OnEndTouch; // 修正委托类型

    private ScreenControls screenControls;
    private Camera mainCamera;

    private void Awake()
    {
        screenControls = new ScreenControls();
        mainCamera = Camera.main;
    }

    private void OnEnable()
    {
        screenControls.Enable();
        screenControls.SwipeMap.PrimaryContact.started += StartTouchPrimary;
        screenControls.SwipeMap.PrimaryContact.canceled += EndTouchPrimary;
    }

    private void OnDisable()
    {
        screenControls.Disable();
        screenControls.SwipeMap.PrimaryContact.started -= StartTouchPrimary;
        screenControls.SwipeMap.PrimaryContact.canceled -= EndTouchPrimary;
    }

    private void StartTouchPrimary(InputAction.CallbackContext context)
    {
        // 直接存屏幕坐标,不提前转世界坐标
        Vector2 screenPos = screenControls.SwipeMap.PrimaryPosition.ReadValue<Vector2>();
        OnStartTouch?.Invoke(screenPos, (float)context.startTime);
    }

    private void EndTouchPrimary(InputAction.CallbackContext context)
    {
        Vector2 screenPos = screenControls.SwipeMap.PrimaryPosition.ReadValue<Vector2>();
        OnEndTouch?.Invoke(screenPos, (float)context.time);
    }
}

修复后的CameraSwipe

using UnityEngine;
using Cinemachine;

public class CameraSwipe : MonoBehaviour
{
    [SerializeField] private float minimumSwipeDistance = 50f; // 屏幕像素阈值,更符合触摸操作逻辑
    [SerializeField] private float maximumSwipeTime = 1f;
    [SerializeField, Range(0f, 1f)] private float directionThreshold = 0.9f;
    [SerializeField] private float cameraMoveSpeed = 10f; // 调整到合理移动速度
    [SerializeField] private float swipeSensitivity = 0.01f; // 屏幕滑动距离到世界单位的换算系数

    private Vector2 startScreenPos;
    private float startTime;
    private bool isSwiping = false;
    private bool isMoving = false;
    private Vector3 targetPosition;

    private ScreenInputManager inputProvider;
    private CinemachineVirtualCamera virtualCamera;
    private Transform cameraTransform;
    private Camera mainCamera;

    private void Awake()
    {
        inputProvider = GetComponent<ScreenInputManager>();
        virtualCamera = GetComponent<CinemachineVirtualCamera>();
        cameraTransform = virtualCamera.VirtualCameraGameObject.transform;
        mainCamera = Camera.main;
    }

    private void OnEnable()
    {
        inputProvider.OnStartTouch += SwipeStart;
        inputProvider.OnEndTouch += SwipeEnd;
    }

    private void OnDisable()
    {
        inputProvider.OnStartTouch -= SwipeStart;
        inputProvider.OnEndTouch -= SwipeEnd;
    }

    private void SwipeStart(Vector2 screenPosition, float time)
    {
        // 正在移动时不响应新的滑动,避免状态冲突
        if (isMoving) return;
        
        startScreenPos = screenPosition;
        startTime = time;
        isSwiping = true;
    }


    private void SwipeEnd(Vector2 screenPosition, float time)
    {
        if (!isSwiping) return;
        
        float swipeDistance = Vector2.Distance(startScreenPos, screenPosition);
        float swipeTime = time - startTime;
        
        // 滑动有效才计算目标位置
        if (swipeDistance >= minimumSwipeDistance && swipeTime <= maximumSwipeTime)
        {
            Vector2 swipeDelta = screenPosition - startScreenPos;
            Vector2 swipeDir = swipeDelta.normalized;
            
            // 点积判定方向,传入归一化的滑动方向
            if (Vector2.Dot(Vector2.left, swipeDir) > directionThreshold)
            {
                // 左滑相机向右移动,根据滑动距离算目标位置
                float moveOffset = swipeDelta.magnitude * swipeSensitivity;
                targetPosition = cameraTransform.position + new Vector3(moveOffset, 0, 0);
                isMoving = true;
            }
            else if (Vector2.Dot(Vector2.right, swipeDir) > directionThreshold)
            {
                // 右滑相机向左移动
                float moveOffset = swipeDelta.magnitude * swipeSensitivity;
                targetPosition = cameraTransform.position + new Vector3(-moveOffset, 0, 0);
                isMoving = true;
            }
        }

        // 重置滑动状态
        isSwiping = false;
        startScreenPos = Vector2.zero;
        startTime = 0;
    }

    private void Update()
    {
        // 只有在需要移动时才执行MoveTowards
        if (isMoving)
        {
            cameraTransform.position = Vector3.MoveTowards(
                cameraTransform.position, 
                targetPosition, 
                cameraMoveSpeed * Time.deltaTime
            );

            // 到达目标位置后停止移动,重置状态
            if (Vector3.Distance(cameraTransform.position, targetPosition) < 0.01f)
            {
                isMoving = false;
            }
        }
    }
}

调整说明

  • 滑动距离阈值改用屏幕像素为单位,原代码用世界单位1f对于高分辨率屏幕来说阈值过小,容易把误触识别为滑动
  • 新增滑动状态、移动状态标记,避免逻辑冲突和重复判定
  • 滑动结束后一次性计算目标位置,移动过程中只执行位置插值,到达目标点后自动重置状态,从根源解决卡顿、方向误判问题
  • 新增滑动灵敏度参数,可以根据自己的场景单位和需求调整滑动距离和相机移动距离的比例
  • 移除了不必要的Rigidbody2D获取逻辑,如果相机不需要物理碰撞可以删掉该组件,避免物理更新和Transform更新冲突导致的卡顿

内容的提问来源于stack exchange,提问作者cinnamonrollapplecider

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 15:09:21