当前位置: 首页 > news >正文

Unity相机交互开发:从Scene视图到Game视图的工业级迁移方案

1. 项目概述:为什么我们需要关注相机交互的迁移?

在Unity3D的开发流程中,Scene视图和Game视图是我们最熟悉的两个工作区。Scene视图是开发者的“上帝视角”,我们在这里摆放物体、调整光照、构建世界;而Game视图则是玩家的“第一人称视角”,是最终产品运行时的样子。很多开发者,尤其是新手,常常会遇到一个尴尬的局面:在Scene视图里精心调整好的相机角度、平滑的镜头移动逻辑,一到Game视图里就“变了味”——要么交互失灵,要么视角错乱,要么性能骤降。

这背后的核心问题,就是相机操控逻辑在编辑态(Scene)与运行态(Game)之间的“迁移”没有做好。Scene视图的交互(如鼠标拖拽旋转、滚轮缩放)是编辑器内置的、基于屏幕坐标的便捷操作,而Game视图的交互则需要我们通过代码,将玩家的输入(鼠标、键盘、触屏)转化为相机在三维世界中的精确变换。这不仅仅是写几行transform.Translate那么简单,它涉及到输入系统的抽象、坐标系的转换、运动曲线的平滑处理,以及至关重要的——性能与体验的平衡。

最近,无论是“虚拟相机”还是“深度相机”等热词的火爆,都指向了一个趋势:交互的沉浸感和真实感要求越来越高。一个操控别扭的相机,足以毁掉一个 otherwise 很出色的游戏或应用。因此,掌握从Scene视图那种“所见即所得”的便捷感,无缝迁移到Game视图下稳定、流畅、可预测的相机交互,是每个Unity开发者必须跨过的一道坎。本文将从一个实战者的角度,拆解这其中的核心逻辑、常见陷阱,并提供一个可直接复用的、工业级的解决方案。

2. 核心设计思路:拆解“操控”与“视图”的耦合

要实现优雅的迁移,首先得理解两者本质的不同。Scene视图的相机操控是编辑器功能,它高度集成,但对我们透明;Game视图的相机操控是我们自己写的游戏逻辑的一部分。我们的目标不是复现编辑器,而是汲取其交互设计的精髓,并适配我们自己的游戏需求。

2.1 Scene视图交互的逆向工程

虽然我们不能直接调用UnityEditor.SceneView的API(它们在构建后不存在),但我们可以观察并抽象其行为模式:

  1. 右键拖拽:围绕一个焦点(通常是选择物体的中心或视图中心)旋转相机。
  2. 中键拖拽:平移整个视图(Pan)。
  3. 滚轮:以鼠标位置为中心进行缩放(Zoom)。
  4. F键聚焦:将选中的物体快速置于视图中央。

这些操作的共同点是以视图内某个点或物体为交互锚点,操作感非常符合人类在三维空间中观察物体的直觉。我们的Game视图相机交互,也应该追求这种直觉性。

2.2 Game视图交互的核心挑战

在Game视图下,我们面临几个关键挑战:

  • 输入抽象:需要处理PC(鼠标/键盘)、移动端(触屏)、甚至手柄等多种输入设备。
  • 坐标系转换:鼠标在屏幕上的二维移动(Input.mousePosition),如何转化为相机在三维空间中的旋转、平移和缩放?
  • 焦点与插值:如何让相机的运动平滑(如使用Mathf.SmoothDampVector3.SmoothDamp),而不是生硬地跳跃?如何实现像Scene视图那样围绕一个目标点旋转?
  • 性能与边界:相机移动是否需要物理碰撞检测?移动范围是否要限制?缩放是否有最小/最大值限制?

基于这些分析,一个健壮的相机操控系统应该采用分层设计:

  1. 输入层:抽象并统一处理来自不同设备的原始输入,输出标准的“平移”、“旋转”、“缩放”指令和强度。
  2. 逻辑层:根据指令,计算相机目标位置(TargetPosition)和目标旋转(TargetRotation)。这是核心算法所在,例如计算绕点旋转的新坐标。
  3. 表现层:使用插值方法,让相机的Transform平滑地趋向于目标位置和旋转。同时处理碰撞检测、边界限制等“后处理”效果。

注意:切勿在Update中直接将输入变化加到相机transform上,这会导致运动生硬且帧率依赖。正确的做法是每帧计算一个“目标状态”,然后让相机平滑地跟随这个目标状态。

3. 核心模块实现:构建可复用的相机控制器

下面,我将构建一个名为AdvancedCameraController的组件,它融合了Scene视图的核心交互逻辑,并针对Game视图进行了增强。

3.1 输入抽象模块

首先,我们需要一个模块来屏蔽输入设备的差异。这里以PC端为主,但结构易于扩展。

using UnityEngine; public class CameraInputModule : MonoBehaviour { // 平移 public Vector3 MoveInput { get; private set; } // 旋转(欧拉角变化量) public Vector2 RotateInput { get; private set; } // 缩放 public float ZoomInput { get; private set; } // 当前交互焦点(世界坐标) public Vector3? InteractionFocusPoint { get; private set; } [Header("鼠标设置")] public float mouseRotateSensitivity = 1.0f; public float mousePanSensitivity = 0.5f; public float mouseZoomSensitivity = 5.0f; public int mouseRotateButton = 1; // 右键 public int mousePanButton = 2; // 中键 [Header("键盘设置")] public KeyCode forwardKey = KeyCode.W; public KeyCode backKey = KeyCode.S; public KeyCode leftKey = KeyCode.A; public KeyCode rightKey = KeyCode.D; public KeyCode riseKey = KeyCode.E; public KeyCode fallKey = KeyCode.Q; void Update() { HandleMouseInput(); HandleKeyboardInput(); } void HandleMouseInput() { // 1. 鼠标旋转(右键拖拽) if (Input.GetMouseButton(mouseRotateButton)) { RotateInput = new Vector2( Input.GetAxis("Mouse X") * mouseRotateSensitivity, Input.GetAxis("Mouse Y") * mouseRotateSensitivity ); // 尝试通过射线投射获取焦点 UpdateFocusPoint(); } else { RotateInput = Vector2.zero; } // 2. 鼠标平移(中键拖拽) if (Input.GetMouseButton(mousePanButton)) { MoveInput = new Vector3( -Input.GetAxis("Mouse X") * mousePanSensitivity, -Input.GetAxis("Mouse Y") * mousePanSensitivity, 0 // 屏幕平面移动,Z轴在逻辑层处理 ); } else if (MoveInput.magnitude < 0.01f) // 避免键盘输入被覆盖 { MoveInput = Vector3.zero; } // 3. 鼠标滚轮缩放 ZoomInput = Input.mouseScrollDelta.y * mouseZoomSensitivity; } void HandleKeyboardInput() { Vector3 keyboardMove = Vector3.zero; if (Input.GetKey(forwardKey)) keyboardMove += Vector3.forward; if (Input.GetKey(backKey)) keyboardMove += Vector3.back; if (Input.GetKey(leftKey)) keyboardMove += Vector3.left; if (Input.GetKey(rightKey)) keyboardMove += Vector3.right; if (Input.GetKey(riseKey)) keyboardMove += Vector3.up; if (Input.GetKey(fallKey)) keyboardMove += Vector3.down; if (keyboardMove != Vector3.zero) { // 将世界方向转换为相机本地方向 MoveInput = Camera.main.transform.TransformDirection(keyboardMove.normalized); } else if (!Input.GetMouseButton(mousePanButton)) { // 只有既没有鼠标平移也没有键盘输入时,才清零 MoveInput = Vector3.zero; } } void UpdateFocusPoint() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { InteractionFocusPoint = hit.point; } else { // 如果没有击中任何物体,可以设置为相机前方一定距离的点,或者null InteractionFocusPoint = Camera.main.transform.position + Camera.main.transform.forward * 10f; } } }

这个模块的关键在于,它将原始的InputAPI封装成了逻辑指令MoveInputRotateInputZoomInput和一个可选的InteractionFocusPoint。这样,上层的相机逻辑就与具体的按键设置解耦了。

3.2 相机逻辑与运动模块

这是核心算法模块,负责将输入指令转化为相机的目标变换。

using UnityEngine; public class AdvancedCameraController : MonoBehaviour { public CameraInputModule inputModule; [Header("目标与跟随")] public Transform target; // 可选:相机可以围绕或跟随一个目标 public Vector3 targetOffset = new Vector3(0, 2, -5); // 相对于目标的偏移 public bool followTarget = false; [Header("旋转控制")] public float rotationSmoothTime = 0.1f; public Vector2 rotationLimitY = new Vector2(-80, 80); // 垂直旋转限制 private float currentRotationX; private float currentRotationY; private Vector3 rotationSmoothVelocity; [Header("平移控制")] public float moveSpeed = 10f; public float panSpeedMultiplier = 1f; // 鼠标平移的额外速度系数 [Header("缩放控制")] public float zoomSpeed = 20f; public float minZoomDistance = 2f; public float maxZoomDistance = 50f; private float currentZoomDistance = 10f; [Header("边界与碰撞")] public bool enableCollision = true; public LayerMask collisionMask = -1; public float collisionRadius = 0.3f; private Vector3 currentVelocity; void Start() { if (inputModule == null) inputModule = GetComponent<CameraInputModule>(); if (inputModule == null) Debug.LogError("CameraInputModule is required!"); // 初始化旋转角度为当前相机的欧拉角 Vector3 startRotation = transform.eulerAngles; currentRotationX = startRotation.y; currentRotationY = startRotation.x; // 初始化缩放距离为相机到目标的距离(如果有目标) if (target != null) { currentZoomDistance = Vector3.Distance(transform.position, target.position); } else { currentZoomDistance = Vector3.Distance(transform.position, transform.position + transform.forward * 10f); } } void LateUpdate() // 在Update之后执行,确保目标移动已完成 { if (inputModule == null) return; HandleRotation(); HandleZoom(); HandlePosition(); ApplyCollision(); } void HandleRotation() { if (inputModule.RotateInput != Vector2.zero) { // 累积旋转输入 currentRotationX += inputModule.RotateInput.x; currentRotationY -= inputModule.RotateInput.y; // 注意Y轴取反,符合鼠标操作直觉 currentRotationY = Mathf.Clamp(currentRotationY, rotationLimitY.x, rotationLimitY.y); } // 如果有跟随目标,且没有鼠标旋转输入时,可以平滑看向目标(可选) // 这里我们专注于处理输入驱动的旋转 // 计算目标旋转 Quaternion targetRotation = Quaternion.Euler(currentRotationY, currentRotationX, 0); // 平滑旋转 transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSmoothTime * Time.deltaTime * 10); } void HandleZoom() { if (Mathf.Abs(inputModule.ZoomInput) > 0.01f) { // 基于当前距离进行缩放 currentZoomDistance -= inputModule.ZoomInput * zoomSpeed * Time.deltaTime; currentZoomDistance = Mathf.Clamp(currentZoomDistance, minZoomDistance, maxZoomDistance); } } void HandlePosition() { Vector3 targetPosition; if (followTarget && target != null) { // ***模式一:围绕/跟随目标*** // 计算基于旋转和缩放距离的理想位置 Vector3 desiredPosition = target.position - (transform.rotation * Vector3.forward) * currentZoomDistance; // 加上偏移量(例如,让相机稍微高于目标) desiredPosition += transform.rotation * targetOffset; targetPosition = desiredPosition; } else { // ***模式二:自由漫游(类似Scene视图)*** // 这是迁移的关键:将鼠标平移输入(屏幕空间)转换为世界空间移动 if (inputModule.MoveInput != Vector3.zero) { // 将输入向量从屏幕/相机空间转换到世界空间 // 注意:inputModule.MoveInput的x,y对应屏幕的左右和上下 Vector3 moveDelta = transform.right * inputModule.MoveInput.x * panSpeedMultiplier + transform.up * inputModule.MoveInput.y * panSpeedMultiplier; // 平滑移动到新位置 targetPosition = Vector3.SmoothDamp(transform.position, transform.position + moveDelta, ref currentVelocity, 0.1f); } else { // 如果没有平移输入,则保持原位或根据其他逻辑更新 // 对于自由漫游模式,缩放是通过改变FOV或向前后移动实现的,这里我们结合缩放逻辑 // 缩放时,沿视线方向移动 if (Mathf.Abs(inputModule.ZoomInput) > 0.01f) { Vector3 zoomMove = transform.forward * inputModule.ZoomInput * zoomSpeed * Time.deltaTime; targetPosition = transform.position + zoomMove; } else { targetPosition = transform.position; } } } // 应用位置(碰撞检测前) transform.position = targetPosition; } void ApplyCollision() { if (!enableCollision || target == null) return; Vector3 desiredPosition = target.position - (transform.rotation * Vector3.forward) * currentZoomDistance; desiredPosition += transform.rotation * targetOffset; RaycastHit hit; Vector3 dir = desiredPosition - target.position; if (Physics.SphereCast(target.position, collisionRadius, dir.normalized, out hit, dir.magnitude, collisionMask)) { // 如果发生碰撞,将相机拉到碰撞点前方 transform.position = hit.point - dir.normalized * collisionRadius; // 同时更新当前缩放距离,避免下次计算时又穿回去 currentZoomDistance = Vector3.Distance(transform.position, target.position); } } // 提供一个外部调用方法,模拟Scene视图的“F”聚焦功能 public void FocusOnTarget(Transform newTarget, float duration = 0.5f) { // 这里可以扩展一个协程,平滑地将相机移动到观察新Target的位置 target = newTarget; followTarget = true; // 可以立即重置距离和角度,或启动平滑过渡动画 currentZoomDistance = Mathf.Clamp(Vector3.Distance(transform.position, target.position), minZoomDistance, maxZoomDistance); Vector3 relativePos = transform.position - target.position; currentRotationX = Mathf.Atan2(relativePos.x, relativePos.z) * Mathf.Rad2Deg; currentRotationY = -Mathf.Asin(relativePos.y / relativePos.magnitude) * Mathf.Rad2Deg; } }

这个控制器实现了两种核心模式:

  1. 目标跟随模式:相机始终尝试保持与一个target的相对位置和角度,并可以围绕其旋转、缩放。这是实现“围绕物体观察”的经典模式,类似于在Scene视图中选中一个物体后操作。
  2. 自由漫游模式:当没有指定target或关闭followTarget时,相机进入自由模式。此时,鼠标中键拖拽(Pan)会沿着相机自身的右方和上方移动,完美模拟了Scene视图的平移感觉。滚轮缩放则沿着视线方向前进后退。

关键迁移点解析

  • 鼠标平移的实现HandlePosition方法中,自由漫游模式下的moveDelta计算。它将inputModule.MoveInput(来自鼠标中键的屏幕XY变化)乘以相机的transform.righttransform.up方向,从而将屏幕上的拖拽完美映射为在世界平面上的移动。这是还原Scene视图操作感的核心代码。
  • 旋转与缩放的分离处理:旋转直接修改相机的欧拉角,而缩放通过改变currentZoomDistance这个逻辑变量来实现。在目标跟随模式下,这个距离用于计算相机相对于目标的位置;在自由模式下,它直接转化为沿视线方向的位移。这种分离设计使得逻辑更清晰。
  • 平滑处理:使用了Quaternion.SlerpVector3.SmoothDamp进行平滑插值,避免了运动的生硬感。rotationSmoothTimeSmoothDamp的平滑时间参数是调节手感的关键。

3.3 高级功能:射线聚焦与区域限制

Scene视图的“F”键聚焦是一个非常实用的功能。我们可以扩展我们的控制器来实现它。

// 在AdvancedCameraController中添加 [Header("高级交互")] public float focusSmoothTime = 0.3f; private Coroutine focusCoroutine; public void SmartFocus(Vector3 focusPointWorldPos, float targetDistance = -1) { if (focusCoroutine != null) StopCoroutine(focusCoroutine); focusCoroutine = StartCoroutine(DoSmartFocus(focusPointWorldPos, targetDistance)); } IEnumerator DoSmartFocus(Vector3 focusPointWorldPos, float targetDistance) { Vector3 startPos = transform.position; Quaternion startRot = transform.rotation; // 计算理想的观察位置 if (targetDistance < 0) targetDistance = currentZoomDistance; // 保持当前距离 // 让相机看向焦点 Vector3 directionFromFocus = (startPos - focusPointWorldPos).normalized; Quaternion endRot = Quaternion.LookRotation(-directionFromFocus); // LookRotation需要看向焦点的方向,所以取反 // 计算终点位置:从焦点沿视线反方向退后targetDistance Vector3 endPos = focusPointWorldPos - (endRot * Vector3.forward) * targetDistance; float elapsed = 0; while (elapsed < focusSmoothTime) { elapsed += Time.deltaTime; float t = Mathf.SmoothStep(0, 1, elapsed / focusSmoothTime); transform.position = Vector3.Lerp(startPos, endPos, t); transform.rotation = Quaternion.Slerp(startRot, endRot, t); yield return null; } transform.position = endPos; transform.rotation = endRot; // 更新内部状态 Vector3 euler = endRot.eulerAngles; currentRotationX = euler.y; currentRotationY = euler.x; focusCoroutine = null; }

这个协程提供了平滑的聚焦动画,比瞬间跳转体验好得多。你可以通过点击UI按钮或按特定按键(如F键)来触发它,并传入通过射线检测得到的世界坐标。

区域限制也是实战中常需要的,比如防止相机飞出地图边界。

// 在AdvancedCameraController中添加 [Header("区域限制")] public bool enableBounds = false; public Bounds movementBounds; void ClampToBounds() { if (!enableBounds) return; Vector3 clampedPosition = transform.position; clampedPosition.x = Mathf.Clamp(clampedPosition.x, movementBounds.min.x, movementBounds.max.x); clampedPosition.y = Mathf.Clamp(clampedPosition.y, movementBounds.min.y, movementBounds.max.y); clampedPosition.z = Mathf.Clamp(clampedPosition.z, movementBounds.min.z, movementBounds.max.z); transform.position = clampedPosition; } // 在LateUpdate末尾调用ClampToBounds();

4. 实战调试与性能优化

将脚本挂载到主相机上,并配置好CameraInputModule后,你就可以在Game视图获得类似Scene视图的操控体验了。但在实际项目中,还需要注意以下几点。

4.1 输入冲突与优先级管理

在一个复杂的游戏中,相机控制可能不是唯一的输入消费者。UI点击、角色控制等都会争夺输入事件。Unity的EventSystem会阻止点击UI时的场景操作。但对于键盘和鼠标拖拽,我们需要更精细的管理。

  • 解决方案:在CameraInputModuleUpdate方法开始,可以检查EventSystem.current.IsPointerOverGameObject()。如果鼠标在UI上,则忽略某些相机操作(如旋转、平移),但可能保留缩放(如果游戏逻辑允许)。
using UnityEngine.EventSystems; // 需要引入命名空间 void HandleMouseInput() { // 检查鼠标是否在UI上 if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) { // 如果希望UI阻断所有相机鼠标操作,则直接返回 // return; // 如果只希望阻断拖拽,保留滚轮 RotateInput = Vector2.zero; if (Input.GetMouseButton(mousePanButton)) { MoveInput = Vector3.zero; return; // 不处理中键拖拽 } // 滚轮缩放可以保留 } // ... 原有的输入处理逻辑 }

4.2 不同场景下的参数调优

相机操控的“手感”极度依赖参数:

  • mouseRotateSensitivity/mousePanSensitivity:根据项目类型调整。对于精细编辑类应用,灵敏度要低;对于快速观察的游戏,灵敏度可以高一些。
  • rotationSmoothTime:平滑时间越长,相机运动越“粘滞”,感觉越沉稳;越短则响应越迅速,但也可能更“飘”。对于跟随角色的第三人称相机,平滑时间通常需要设置(如0.1s);对于RTS或编辑器的自由相机,可以设为0以获得即时反馈。
  • zoomSpeedmin/maxZoomDistance:必须根据场景尺度来设定。一个室内场景的缩放范围可能是[0.5, 10],而一个开放世界则可能是[5, 5000]

实操心得:不要迷信默认值。最好的调参方法是创建一个简单的测试场景,放置几个不同尺度的参考物体(如一个胶囊体代表角色,一个立方体代表建筑),然后边操作边调整参数,直到找到最符合项目需求和用户直觉的那组值。可以将调好的参数保存为ScriptableObject资产,方便在不同相机配置间复用。

4.3 性能考量

  • LateUpdatevsUpdate:相机控制务必放在LateUpdate中。这样可以确保相机在所有其他对象(尤其是它要跟随的目标)移动完毕后再更新,避免出现一帧的抖动。
  • 碰撞检测开销Physics.SphereCastRaycast在每帧调用是有开销的。如果相机移动不频繁,可以改为在相机即将移动时检测。或者,对于固定场景,可以预先使用导航网格(NavMesh)或触发器体积来定义可移动区域,而不是每帧进行物理检测。
  • 不必要的计算:在自由漫游模式下,如果targetnull,应跳过所有与目标相关的计算(如碰撞检测中的target.position)。

4.4 常见问题与排查技巧

问题1:相机旋转时发生剧烈抖动或翻转。

  • 排查:检查currentRotationY(上下旋转)的累加逻辑。确保使用了-=而不是+=来处理鼠标Y轴输入,因为屏幕Y轴向上为正,而欧拉角X轴(对应上下看)向上旋转为负。同时,务必用Mathf.ClampcurrentRotationY限制在[-80, 80]度之间,避免万向节死锁导致的翻转。
  • 技巧:在计算旋转时,可以考虑使用Quaternion进行运算,而非直接操作欧拉角,但欧拉角对初学者更直观。关键是确保旋转顺序一致(通常为Y-X-Z)。

问题2:鼠标中键平移时,相机移动方向不符合预期(例如上下拖拽却让相机前后走)。

  • 排查:检查HandlePosition中自由漫游模式下的moveDelta计算。transform.right对应相机本地X轴(左右),transform.up对应本地Y轴(上下)。确保inputModule.MoveInput.x.y与之正确相乘。可以打印出这些向量进行调试。

问题3:相机穿过墙壁或其他物体。

  • 排查
    1. 首先确认enableCollision已勾选。
    2. 检查collisionMask是否正确设置了需要碰撞的层。墙壁等障碍物应该位于这些层中。
    3. 调整collisionRadius。这个值太小,相机边缘可能会穿模;太大,则相机可能在离墙还很远时就停下。通常设置为略小于相机近裁剪面到模型中心的距离。
    4. SphereCast的起点是target.position,如果targetnull或者在墙另一侧,检测会失效。对于自由相机,可以考虑从相机位置向目标方向(或视线反方向)做检测。

问题4:在UI上操作时,相机仍然响应了鼠标拖拽。

  • 排查:按照4.1节的方法,在输入模块中添加对EventSystem.current.IsPointerOverGameObject()的判断。确保UI元素的Raycast Target属性为true。

问题5:缩放时感觉卡顿或不线性。

  • 排查:缩放输入inputModule.ZoomInput通常来自鼠标滚轮,其值是一个整数(如±1)。在HandleZoom中,我们将其乘以zoomSpeedTime.deltaTime。如果zoomSpeed过大,单次滚轮滚动会导致距离变化过大,感觉像“跳变”。可以尝试降低zoomSpeed,或者对滚轮输入进行平滑处理(如使用Mathf.SmoothDamp来平滑currentZoomDistance本身)。

5. 扩展:从PC到移动端的交互适配

要将这套操控逻辑迁移到移动端(触屏),核心是重写CameraInputModule。我们需要处理触摸手势。

public class TouchCameraInputModule : CameraInputModule { [Header("触摸设置")] public float touchRotateSensitivity = 0.2f; public float touchPanSensitivity = 0.01f; public float touchZoomSensitivity = 0.005f; private Vector2? lastPanPosition; private float lastPinchDistance; void Update() { // 先调用基类处理可能的键盘输入(如果外接键盘) base.HandleKeyboardInput(); if (Input.touchCount == 1) { Touch touch = Input.GetTouch(0); // 单指拖拽:平移或旋转?这里通常定义为平移 if (touch.phase == TouchPhase.Began) { lastPanPosition = touch.position; } else if (touch.phase == TouchPhase.Moved && lastPanPosition.HasValue) { Vector2 delta = touch.position - lastPanPosition.Value; MoveInput = new Vector3(-delta.x * touchPanSensitivity, -delta.y * touchPanSensitivity, 0); lastPanPosition = touch.position; RotateInput = Vector2.zero; // 单指通常不作为旋转 ZoomInput = 0; } else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) { lastPanPosition = null; MoveInput = Vector3.zero; } } else if (Input.touchCount == 2) { // 双指手势:缩放和旋转 Touch touch1 = Input.GetTouch(0); Touch touch2 = Input.GetTouch(1); if (touch1.phase == TouchPhase.Began || touch2.phase == TouchPhase.Began) { lastPinchDistance = Vector2.Distance(touch1.position, touch2.position); lastPanPosition = (touch1.position + touch2.position) / 2; } else if (touch1.phase == TouchPhase.Moved || touch2.phase == TouchPhase.Moved) { float currentPinchDistance = Vector2.Distance(touch1.position, touch2.position); // 缩放 ZoomInput = (currentPinchDistance - lastPinchDistance) * touchZoomSensitivity; lastPinchDistance = currentPinchDistance; // 双指中点移动作为平移(可选,或作为旋转) Vector2 currentMidPos = (touch1.position + touch2.position) / 2; if (lastPanPosition.HasValue) { Vector2 delta = currentMidPos - lastPanPosition.Value; // 这里可以将delta作为旋转输入,实现双指旋转 RotateInput = new Vector2(delta.x * touchRotateSensitivity, delta.y * touchRotateSensitivity); // 或者作为平移输入 // MoveInput = new Vector3(-delta.x * touchPanSensitivity, -delta.y * touchPanSensitivity, 0); } lastPanPosition = currentMidPos; } else if (touch1.phase == TouchPhase.Ended || touch2.phase == TouchPhase.Ended) { lastPanPosition = null; MoveInput = Vector3.zero; RotateInput = Vector2.zero; ZoomInput = 0; } } else { // 没有触摸时,确保输入归零(除非有键盘输入) if (MoveInput.magnitude < 0.01f) MoveInput = Vector3.zero; RotateInput = Vector2.zero; ZoomInput = 0; } // 注意:触摸输入不需要调用base.HandleMouseInput() } }

这个触摸模块将单指拖拽映射为平移,双指捏合映射为缩放,双指滑动映射为旋转(或平移,取决于你的设计)。你需要替换原有的CameraInputModule为这个TouchCameraInputModule,并根据实际手感调整灵敏度参数。这样,同一套AdvancedCameraController逻辑就能同时服务于PC和移动端,真正实现了交互逻辑与平台输入的解耦。

整个迁移过程的核心思想,是将Scene视图那种直观、流畅的操控感,通过清晰的输入抽象、数学计算和状态管理,在Game视图中用代码复现出来。它考验的不是高深的图形学知识,而是对三维空间变换、用户交互设计和代码架构的扎实理解。

http://www.jsqmd.com/news/1327424/

相关文章:

  • AI毕业论文工具实测:写论文的AI效果如何
  • 设计-简约而不简单
  • 论文AI检测率高的原因与物理降AI法实操指南
  • 5分钟打造你的专属知识中心:Obsidian个性化首页完整指南
  • 工业地板怎么选最有性价比?别只看价格,先看产品体系、防腐能力和供应链稳定性 - 中国华商产业观察网
  • 终极Windows热键冲突检测指南:Hotkey Detective完整解决方案
  • 5步实现专业级虚拟背景:obs-backgroundremoval完整实战指南
  • 5种强力脚本彻底改变你的Illustrator设计工作流
  • 2026 武汉南华光电职业技术学校招生办电话_招生老师联系方式汇总 - 武汉中职最新信息发布
  • 电商平台系统是什么?从零理解电商数字化运营的核心基础设施
  • 2026昆山卫生间、外墙、楼顶、地下室、阳台阳光房渗漏不用愁!3家正规靠谱防水服务商甄选:选对专业团队,告别反复渗水,长效售后有保障 - 吉林同城获客
  • AI毕业论文平台测评:写论文工具如何选
  • 7T fMRI技术揭示稳态-内感受系统全脑连接图谱
  • Python构建多国专利数据采集系统实战指南
  • FlexSim 与 Python Socket 通信完整指南
  • 零基础AI应用编程开发入门 | 吴恩达Prompt工程极简通关指南:新手从零学会工业级提示词开发(可直接复用代码)
  • 输电线路缺陷数据集 电缆缺陷识别 输电线破损数据集
  • 武汉城铁技工学校电话号码多少? - 升学择校早知道
  • UnityWebRequest SSL证书验证:自定义CertificateHandler实现安全绕过与指纹钉扎
  • Windows系统CertPolEng.dll丢失的解决方案与预防措施
  • 宫老师助力廊坊十八酒坊客户私享会 赋能酒水商户数字化经营升级
  • 5分钟实战:从OFD到PDF的高效转换全解
  • 新手申请香港优才不想费心,深圳一站式服务商从哪些维度筛选? - 滚动商讯
  • 2026年Q3膳食营养分析与配餐实训系统厂家观察:从商科实训到垂直细分,谁在引领院校数智化教学新浪潮——西安青软领衔五大实训厂商盘点,膳食营养配餐系统成新商科建设热门赛道 - 深度智识库
  • 从初稿到终稿一步到位!Gradpaper不愧是论文顶配工具
  • 企业级数据中台开源软件:数智云的数据治理与服务化架构
  • 山西GEO优化哪家服务好?优选山西中航云创科技有限公司 - GrowUME
  • Clockwork for Dynamo:450+节点打造BIM参数化设计的终极工具箱
  • 2026年8月蛋品企业全自动鸡蛋品质哈夫值测定仪实测:四大品牌选购指南 - 云唐专业仪器测评
  • 终极指南:如何用Hotkey Detective快速检测Windows热键冲突