简介:这是一份面向Unity开发者、AR/VR学习者及产品展示设计师的3D交互实践工程。资源完整涵盖模型导入与优化、3D标注面板搭建、绕物相机控制、步骤列表UI及拆装动画制作等核心模块,帮助读者掌握从静态展示到动态拆解的完整交互设计思路。包内共4300个文件,以735个cs脚本为主体,包含280个png界面素材、43个mat材质资源、26个asset配置及4个prefab预制体,并附有场景说明与开发笔记,注释清晰、目录规整,压缩包约65.71MB。目前已有3752人学习下载。通过研读工程中的Animator状态机设置、相机旋转脚本与UI事件绑定,可快速复用到产品选型、教学演示或虚拟仿真项目中,是进阶Unity人机交互与空间展示能力的实用参考资料。
1. 把模型展示、标注和拆装放进同一个Unity场景:这条技术路线到底值不值得走
做工业产品展示或数字孪生项目时,需求方最常提的一句话就是“能不能在模型上做标记、拆开给大家看”。单独做模型旋转展示,用Unity自带的轨道相机脚本就能应付;单独做拆装动画,用Animator录制关键帧也行。但把3D模型展示、3D标注、拆装动画塞进同一个场景,让用户在“转着看—点零件—拆开看结构”之间无缝切换,就成了另一回事:相机手感、射线检测、拆装状态管理、标注UI的显隐逻辑会互相干扰。本文章要拆的,就是这条整合路线上最扎实的从业方案——不依赖付费插件,纯C#脚本把三个模块串起来,从交互设计到脚本落地再到避坑,一次讲透。适合做产品展示、工业培训、售前演示系统的开发者,也适合被需求方一句话“加个拆装就行”折磨过的Unity业务开发。
2. 展示、标注、拆装三种交互的本质:为什么不能用三个独立脚本硬凑
2.1 模型展示的本质是相机控制,不是模型旋转
很多新手拿到模型,第一个动作是“让模型自己转起来”。这其实是个误区——展示类需求里,用户要的是“我来控制怎么看”,而不是“它给我表演旋转”。我一般会把展示层拆成两套相机方案:轨道相机(Orbit)和正交六视图相机。轨道相机适合外观展示,正交相机适合拆装过程中的结构观察。
轨道相机的核心参数有三个:旋转灵敏度、缩放范围、相机距离。旋转灵敏度决定了用户拖拽鼠标时模型转动的速度,工业展示场景我通常设在0.3到0.5之间——太高容易晕,太低显得笨重。缩放范围要结合模型尺寸设置,一个常见错误是把缩放最小值设成0.1,导致相机穿模进模型内部,看到一片黑。正确的做法是:在Start里计算模型包围盒(MeshRenderer.bounds)的半径,把缩放最小值绑定到半径的0.5倍,最大值绑定到半径的5倍。
public class OrbitCamera : MonoBehaviour { public Transform target; // 目标模型 public float sensitivity = 0.4f; // 鼠标拖拽灵敏度 public float zoomSpeed = 2f; // 滚轮缩放速度 private float currentDistance; // 当前相机距离 private float minDistance; // 最小距离 private float maxDistance; // 最大距离 private Vector2 orbitAngles = new Vector2(30f, 45f); // 初始俯仰角/方位角 void Start() { // 根据模型包围盒动态计算距离范围,避免穿模和过远 Bounds bounds = CalculateBounds(target.gameObject); currentDistance = bounds.size.magnitude * 1.5f; minDistance = bounds.size.magnitude * 0.5f; maxDistance = bounds.size.magnitude * 5f; } void Update() { // 鼠标左键拖拽控制旋转角度 if (Input.GetMouseButton(0)) { orbitAngles.x += Input.GetAxis("Mouse Y") * sensitivity; orbitAngles.y += Input.GetAxis("Mouse X") * sensitivity; orbitAngles.x = Mathf.Clamp(orbitAngles.x, -89f, 89f); // 防止翻转 } // 滚轮控制缩放距离 float scroll = Input.GetAxis("Mouse ScrollWheel"); currentDistance -= scroll * zoomSpeed; currentDistance = Mathf.Clamp(currentDistance, minDistance, maxDistance); // 应用位置和旋转 Quaternion rotation = Quaternion.Euler(orbitAngles.x, orbitAngles.y, 0); Vector3 offset = rotation * Vector3.back * currentDistance; transform.position = target.position + offset; transform.rotation = rotation; } Bounds CalculateBounds(GameObject root) { // 合并所有子物体的渲染包围盒,避免模型中心偏移 Renderer[] renderers = root.GetComponentsInChildren<Renderer>(); Bounds bounds = new Bounds(root.transform.position, Vector3.zero); foreach (Renderer r in renderers) bounds.Encapsulate(r.bounds); return bounds; } }这段脚本里最关键的是CalculateBounds方法。一个模型往往由几十个零件组成,每个零件有自己的Renderer,如果不合并计算包围盒,相机距离就会以某个零件为中心,整体看起来就偏了。合并后以包围盒对角线长度作为距离基准,无论模型是单一齿轮还是整台设备,缩放比例都成立。另一个值得注意的点是俯仰角限制在正负89度,如果不限制,相机绕到模型正上方或正下方时,控制会出现万向锁现象,鼠标拖拽方向会突然反转,这在展示场景里非常影响体验。
2.2 3D标注的交互根基:射线检测与UI锚定
标注功能听起来简单——点一个零件,弹出信息框。但落地时有三个问题必须处理:怎么知道用户点了哪个零件、标注信息框跟不跟模型移动、多零件重叠时怎么选。
第一个问题的标准方案是Camera.main.ScreenPointToRay配合Physics.Raycast。这里有一个容易忽略的细节:零件模型如果用了多个材质,或者模型由多个子物体拼成,射线命中的可能是子物体而不是根物体。所以我在每个零件根物体上挂一个标识组件,射线命中后通过GetComponentInParent查找标识,保证用户无论点到哪里,拿到的都是逻辑上的“零件”。
public class PartIdentifier : MonoBehaviour { [Tooltip("零件ID,用于映射标注数据")] public string partId; [Tooltip("显示名称")] public string displayName; [Tooltip("标注描述")] [TextArea] public string description; } public class AnnotationManager : MonoBehaviour { public GameObject annotationPanel; // 标注信息面板 public Text titleText; // 标题文本 public Text contentText; // 内容文本 private PartIdentifier currentSelected; // 当前选中的零件 void Update() { // 左键点击优先处理标注选择,与旋转拖拽手势区分 if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100f)) { PartIdentifier part = hit.collider.GetComponentInParent<PartIdentifier>(); if (part != null) { SelectPart(part); } } } } void SelectPart(PartIdentifier part) { // 取消上一个零件的选中状态 if (currentSelected != null) { // 恢复默认材质颜色 currentSelected.transform.GetComponentInChildren<Renderer>().material.color = Color.white; } currentSelected = part; // 高亮选中零件 part.transform.GetComponentInChildren<Renderer>().material.color = Color.yellow; // 更新UI内容 titleText.text = part.displayName; contentText.text = part.description; annotationPanel.SetActive(true); } }这里的PartIdentifier就是标注系统的“数据锚点”。我见过有些项目把零件名称和描述直接写在脚本字段里,然后硬编码到场景中——这在小项目里能用,但一旦模型改版或零件数量超过20个,维护成本会指数上升。更好的做法是把零件描述存在一个Excel或ScriptableObject里,运行时用partId去查表。对于中小型项目,ScriptableObject是最稳妥的方案,它享受Unity的序列化和引用管理能力,而且不依赖外部文件读取。
标注UI是否跟随模型移动取决于产品需求。如果只是“点击查看信息”,面板固定在屏幕左下角就够了;但如果需要标注文字悬浮在零件上方,就得用WorldSpace Canvas或者Camera.WorldToScreenPoint把3D坐标转换到屏幕坐标。我通常的做法是:官方拆装演示用固定面板,培训模式用悬停标签,两个模式切换不复杂,只是换一套UI父节点。
2.3 拆装动画的原理解析:动画曲线驱动的核心是“变换分离”
拆装动画表面上看是“零件朝某个方向移动”,但实现方式有高下之分。最业余的做法是直接给每个零件写协程,让它在2秒内从初始位置平移到目标位置——这能跑,但没法控制拆装节奏,也没法做装配动画(倒放),一旦零件数量到了30个以上,代码规模会失控。
我在项目中用的方案是“变换分离 + 动画曲线驱动”。所谓变换分离,是指零件的“安装位置”(装配态坐标)统一存在一个脚本里,拆装过程中只修改“显示位置”——即用一个中间节点包裹模型,把模型的Transform绑到中间节点下,动画只驱动中间节点。这样做的好处是,模型原始Transform不会被动过,装配数据永远可靠。
public class PartExploder : MonoBehaviour { [Tooltip("零件集合:每个零件需要一个中间节点作为拆装驱动")] public Transform[] partRoots; [Tooltip("默认装配位置")] private Vector3[] originalPositions; [Tooltip("拆解偏移方向")] public Vector3 explodeDirection = Vector3.up; [Tooltip("拆解距离")] public float explodeDistance = 2f; [Tooltip("动画时长(秒)")] public float animDuration = 1.5f; private float animProgress = 0f; // 当前动画进度 0~1 private bool isAnimating = false; private bool isExploded = false; // 当前是否处于拆解状态 void Start() { // 记录装配位置 originalPositions = new Vector3[partRoots.Length]; for (int i = 0; i < partRoots.Length; i++) { PartWrapper wrapper = partRoots[i].GetComponent<PartWrapper>(); if (wrapper == null) { // 动态创建中间节点 GameObject child = new GameObject("PartWrapper_" + partRoots[i].name); child.transform.SetParent(partRoots[i].parent, false); child.transform.position = partRoots[i].position; child.transform.rotation = partRoots[i].rotation; partRoots[i].SetParent(child.transform, true); wrapper = child.AddComponent<PartWrapper>(); } originalPositions[i] = wrapper.transform.position; } } void Update() { if (isAnimating) { // 用插值驱动进度,支持正放和倒放 float step = Time.deltaTime / animDuration; animProgress = Mathf.MoveTowards(animProgress, isExploded ? 1f : 0f, step); for (int i = 0; i < partRoots.Length; i++) { PartWrapper wrapper = partRoots[i].GetComponent<PartWrapper>(); // 每个零件可以有独立的偏移方向,默认向外 Vector3 offset = wrapper.explodeDirection.normalized * wrapper.explodeDistance; wrapper.transform.position = originalPositions[i] + offset * animProgress; } if (animProgress <= 0f || animProgress >= 1f) isAnimating = false; } } public void ToggleExplode() { isExploded = !isExploded; isAnimating = true; } } /// <summary> /// 零件包装器:挂在中间节点上,可以单独设置拆装方向和距离 /// </summary> public class PartWrapper : MonoBehaviour { [Tooltip("拆装方向")] public Vector3 explodeDirection = Vector3.up; [Tooltip("拆装距离")] public float explodeDistance = 1f; }这段代码有几个关键设计。PartWrapper挂在中间节点上而不是模型本身上,这意味着模型原始Transform完全不受影响,装配态的位置数据可以直接用于后续的计算。每个零件独立的explodeDirection让拆装动画看起来更自然——大型设备往往是“上半部分向上、侧门向左、底座不动”,而不是所有零件朝同一个方向平移。animProgress用Mathf.MoveTowards来驱动,配合isExploded状态判断方向,这样装配动画就是拆装动画的精确倒放,不需要单独再写一套逻辑。
拆装距离怎么确定?我有个经验值:按零件包围盒对角线长度乘以1.2到1.5倍。距离太短零件会挤在一起看不出结构,太远又会让整个场景显得松散。如果你的零件层级比较复杂,可以在PartWrapper上挂一个运行时调试脚本,显示当前零件的包围盒大小,直接在场景视图里看到合理的偏移距离是多少。
3. 三个模块如何整合:主控制器与事件驱动的架构设计
3.1 状态机:展示、标注、拆装三种模式的切换时机
三种交互不能同时生效——比如用户正在拆装动画播放途中,不应该能点选零件做标注;正在拖拽旋转模型时,也不应该触发拆装。所以需要一个主控制器来管理全局状态。我把状态分成三种:浏览模式(Browse)、标注模式(Annotate)、拆装模式(Explode)。
状态切换的触发条件是:模式按钮点击、拆装动画播放结束。标注模式的射线检测只响应鼠标左键单点,浏览模式的拖拽也需要左键——这两者天然冲突。我的解法是加一个“模式开关”UI,浏览模式下左键拖拽旋转,标注模式下点击选择零件。如果需求方非要“一键切换而不加按钮”,也可以用鼠标在UI面板上的悬停判断——鼠标悬停在标注面板上时,拖拽不生效,点击生效;悬停在3D场景区域时,拖拽生效,点击不生效。
public class SceneController : MonoBehaviour { public enum ViewMode { Browse, Annotate, Explode } public ViewMode currentMode = ViewMode.Browse; public OrbitCamera orbitCamera; // 轨道相机 public AnnotationManager annotation; // 标注管理器 public PartExploder exploder; // 拆装管理器 public GameObject annotationModeButton; // 模式按钮 void Start() { // 初始状态:浏览模式 SetMode(ViewMode.Browse); } public void SetMode(ViewMode mode) { currentMode = mode; // 状态切换对外设的影响:相机是否可控、点击是否响应 orbitCamera.enabled = (mode == ViewMode.Browse); annotation.enabled = (mode == ViewMode.Annotate); exploder.enabled = (mode == ViewMode.Explode); // 拆装模式下禁用标注射线,避免误选 if (mode == ViewMode.Explode) annotation.ClearSelection(); } public void OnBrowseButtonClicked() { SetMode(ViewMode.Browse); orbitCamera.ResetView(); // 复位相机到初始视角 } public void OnAnnotateButtonClicked() { SetMode(ViewMode.Annotate); } public void OnExplodeButtonClicked() { SetMode(ViewMode.Explode); exploder.ToggleExplode(); } }这个控制器的核心思路是“用开关控制脚本的enabled”,而不是在Update里加一堆if判断。enabled控制的优势是:一旦拆装模式进入,Update函数直接不执行,性能开销归零;逻辑也干净——每个脚本只管自己的事情,不需要知道别的模块的状态。
3.2 数据流设计:零件ID如何串联模型、标注与拆装
一个工业模型可能有几十个零件,每个零件同时参与标注和拆装。如果零件信息散落在不同脚本的字段里,会出现数据不一致:标注显示名称是“电机”,拆装动画里却引用的是“马达”。数据流的正解是统一零件ID作为主键。
我在项目中用的结构是:PartWrapper上挂partId字段,PartIdentifier上同样挂partId,两者引用的字符串相同,但职责不同——PartWrapper只管拆装数据和中间节点,PartIdentifier只管标注数据和UI。这样每个零件有两个组件,但共享同一个ID,用代码保证关联:
public class PartBinder : MonoBehaviour { [Tooltip("零件ID,全局唯一")] public string partId; [Tooltip("标注组件引用")] private PartIdentifier identifier; [Tooltip("拆装组件引用")] private PartWrapper wrapper; void Awake() { // 自动获取同一物体上的组件,简化场景配置 identifier = GetComponent<PartIdentifier>(); wrapper = GetComponent<PartWrapper>(); // 校验:必须有标注或拆装之一 if (identifier == null && wrapper == null) { Debug.LogError($"PartBinder on {gameObject.name} has no functional component."); } } void Reset() { // 编辑器状态下自动生成唯一ID partId = System.Guid.NewGuid().ToString("N").Substring(0, 8); UnityEditor.EditModeUtility.MarkSceneDirty(gameObject.scene); } }这里使用GUID的短形式作为零件ID,好处是全局唯一,不用手工起名。如果项目里需要人读的ID,比如“part_01”这种,可以做成Excel或ScriptableObject配置表——但要保证唯一性和可读性之间的平衡。对于20个零件以下的小项目,用编辑器自动生成短GUID就够了;超过20个零件时,建议把标注信息集中到一个配置表里,代码中只存ID。
3.3 一键整合的示例脚本:把展示、标注、拆装串成完整Demo
为了让整个架构看得见摸得着,我给一个最小可跑通的整合脚本。这个脚本假设场景中已有:一个主相机、一个模型根节点(包含所有零件)、一个UI面板(包含模式按钮和标注信息面板)。搭建顺序是:把PartWrapper和PartIdentifier挂到每个零件上,然后挂PartBinder保证ID统一,最后把OrbitCamera、AnnotationManager、PartExploder的引用拖到SceneController里。
public class DemoBootstrap : MonoBehaviour { public GameObject modelRoot; // 模型根节点 public GameObject uiPanel; // UI根节点 public Camera mainCamera; // 主相机 void Start() { // 预检场景:自动定位缺失的引用,避免运行时才发现 if (modelRoot == null || uiPanel == null || mainCamera == null) { Debug.LogError("请检查场景引用配置"); return; } // 动态生成核心管理器(如果场景中不存在) EnsureManager<OrbitCamera>(); EnsureManager<AnnotationManager>(); EnsureManager<PartExploder>(); // 初始化标注模式按钮事件 Button browseBtn = uiPanel.transform.Find("BrowseButton").GetComponent<Button>(); Button annotateBtn = uiPanel.transform.Find("AnnotateButton").GetComponent<Button>(); Button explodeBtn = uiPanel.transform.Find("ExplodeButton").GetComponent<Button>(); SceneController sceneCtrl = GetComponent<SceneController>(); browseBtn.onClick.AddListener(sceneCtrl.OnBrowseButtonClicked); annotateBtn.onClick.AddListener(sceneCtrl.OnAnnotateButtonClicked); explodeBtn.onClick.AddListener(sceneCtrl.OnExplodeButtonClicked); } private void EnsureManager<T>() where T : MonoBehaviour { if (FindObjectOfType<T>() == null) { // 创建空物体并挂组件 GameObject mgrObj = new GameObject(typeof(T).Name); mgrObj.transform.SetParent(transform); mgrObj.AddComponent<T>(); } } }这段脚本的试错成本很低。如果场景中某个管理器没挂,它会自动创建空物体并添加——这在快速原型验证时非常方便。但正式项目里我反而会删掉动态创建逻辑,全部在编辑器里显式配置引用,因为显式配置能让项目里在拆包或者协作者接手时一眼看懂结构,动态创建会让场景依赖关系变得隐形。
4. 三个避坑要点:设计、动画与标注的系统性坑
4.1 拆装顺序与相机冲突:动画运行时相机千万别锁定
现象:拆装动画播放时,用户拖拽相机,导致视角恰好卡在某个零件内部,画面一片黑;或者相机切到正交模式后,拆装方向看起来完全不对。
原因:拆装动画本质上是改变零件位置,零件移动过程中如果相机角度不变,某些零件会从画面外滑入,或者穿过相机近裁剪面。相机如果跟随某个零件一起动,又会让用户失去全局视角。
解决:两个措施同时做——动画播放期间把OrbitCamera的拖拽输入禁用掉,但保留缩放(因为缩放不改变视角方向,只是调节观察距离);拆装动画结束时自动把相机切回透视模式并复位到包围盒中心。这样既保证了画面稳定,又不会让用户觉得相机被锁死了。
void Update() { // 在拆装动画播放期间,禁用拖拽旋转,但允许缩放 if (exploder != null && exploder.isAnimating) { dirty = true; if (Input.GetMouseButton(0)) return; // 不回应用鼠标拖拽 } if (dirty && !exploder.isAnimating) { dirty = false; // 动画结束:复位视角 ResetView(); } }4.2 标注身份错乱:渲染顺序与碰撞体层级
现象:点击零件A,弹出的信息却是零件B的描述;或者明明看到零件C在眼前,点击却选到了背后的零件D。
原因:场景里存在重叠的碰撞体,射线命中了距离相机更近但逻辑上不相关的零件(比如某些空隙填充块);模型使用了LOD或多个网格碰撞体嵌套,射线穿透了一个还没隐藏的次级碰撞体。
解决:统一在可交互零件上只挂Collider,并设置固定的Layer,比如"Interactive"。射线检测用LayerMask过滤,只检测这一层的碰撞体;每个零件的碰撞体数量只保留一个(Box或MeshCollider),其他全删掉。路径选择方面,用LayerMask的GetMask方法,而不是硬编码整数数值。
public LayerMask interactiveMask; // 在Inspector里设置为 "Interactive" 层 void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100f, interactiveMask)) { // 命中后获取PartIdentifier组件 PartIdentifier part = hit.collider.GetComponentInParent<PartIdentifier>(); if (part != null) SelectPart(part); } }4.3 拆装动画的“弹性回弹”:打断重置的状态丢失
现象:拆装动画播到一半,用户突然点了装配按钮,零件动画没有继续播放,而是瞬间全部回到原始位置,中间过程丢了。
原因:拆装协程或Update动画被打断时,没有正确重置animProgress值。比如点击TogleExplode时,isExploded状态翻转,但animProgress停在0.6,下一个Update直接用0.6作为起点继续插值,看起来就像零件“跳”了一下。
解决:在ToggleExplode时如果想要“反转动画”,需要先判断当前状态;如果想要“立即完成切换”,就先把animProgress设置到目标值再开始。我一般会在UI上区分“播放动画”和“直接复位”两个按钮,前者用插值,后者直接把位置插到初始值。
public void JumpToAssembled() { // 立即回到装配态,动画进度清零 isAnimating = false; animProgress = 0f; for (int i = 0; i < partRoots.Length; i++) { partRoots[i].GetComponent<PartWrapper>().transform.position = originalPositions[i]; } isExploded = false; }4.4 UI遮挡空间:Canvas模式与交互穿透
现象:标注信息面板显示后,鼠标在3D模型上点击,却点到的是UI上的透明区域,标注功能失效。
原因:Canvas默认会拦截射线,即使用GraphicRaycaster的透明度为0,也照样拦截。导致3D射线无法穿透UI面板。
解决:两种情况分别处理——如果面板需要鼠标滑过(比如悬浮样式),就设置BlockingObjects为All并接受UI拦截;如果面板只是“展示文字”,不需要交互,就把Canvas Group里的Blocks Raycasts取消勾选,或者干脆在需要时动态关闭GraphicRaycaster组件。我一般给标注面板加一个背景按钮,点击背景时同时关闭面板和取消两次射线,这样交互层次最清晰。
4.5 模型尺寸不一致导致的距离跳动
现象:场景里有多个模型,切换模型后相机位置不对,不是离得太近就是太远。
原因:不同模型包围盒差异巨大,用一个固定距离作为相机初始距离,小模型会显得很远,大模型会穿模。设计时没把Scale纳入角度和距离计算。
解决:在切换模型时,重新获取模型的Bounds并计算出合适的相机距离,同时把相机位置重置到该距离下。相机距离的计算公式:distance = bounds.size.magnitude * 1.5f,这个系数可以根据模型复杂度调整,比如细长零件用1.2,宽体设备用1.8。
5. 性能与体验的取舍:控制节点层级深度与GPU实例化
当模型零件数量达到50个以上,场景中会有50个中间节点(PartWrapper),每个节点带Transform组件,再加上模型本身的子节点,总共可能超过150个Transform。Transform的同步更新在Unity里会触发层级遍历,性能影响虽然不致命,但在低端设备上(比如手机端)会明显卡顿。
我习惯的做法是主动压平层级。进入拆装模式时,把所有PartWrapper下面的模型根节点放到场景根目录,同时为它们设置worldPositionStays = true,保证世界坐标不变化;进入浏览模式时再把它们重新挂回PartWrapper下。这样拆装模式下没有深层级,渲染和物理检测都更快。代价是切换时有极短暂的内存重排,但物理上可以忽略。
GPU实例化是另一个方向。如果场景中大量使用同一类零件(比如螺丝、垫片),拆装动画里它们一起移动时,用GPU Instancing可以显著降低Draw Call。实现方式是在Shader里启用Instancing属性,然后把零件批量移动到同一个层级下,并用MaterialPropertyBlock为每个实例设置不同的位置。但要注意:实例化的前提是模型共享材质,如果你的零件各自有独立的材质颜色,就得先合并材质。
6. 验证与调试:让展示、标注和拆装真正可用的验收清单
写完整套交互逻辑后,我会在编辑器里做一轮系统性验证。流程分四步:静态检查、交互检查、动画检查、回归检查。静态检查包括场景里是否有重复的Collider、所有PartIdentifier是否都有唯一的partId、所有PartWrapper的方向向量是否非零;交互检查是逐个点选零件验证标注信息是否正确显示,同时确认拖拽旋转时不会误触发标注。
有一个技巧是使用Debug.DrawRay把射线可视化,在Scene视图里看到射线方向的走向,能快速发现碰撞体层级问题;另一个是在拆装动画播放时用Time.timeScale = 0.1f放帧观察插值路径是否平滑、是否出现零件交穿插。
验收清单我贴在下面,读者可以直接照着逐条勾选:模型展示——包围盒计算正确、远近缩放不穿模、鼠标拖拽方向不反转;标注——点零件弹面板、描述无错乱、选中高亮颜色正常;拆装——装配态与初始态一致、动画正放倒放顺滑、每个零件偏移方向合理;整合——切换模式不会被误触发、相机在拆装动画后自动复位、性能在目标设备保持30 FPS以上。
最后说一个我的习惯:关于拆装距离设置,如果零件是细长形状,比如一个长轴,偏移方向最好与轴向垂直而不是平行——否则它会横穿其他零件,看起来像穿模。我曾经在这个问题上翻过车,还是现场演示的时候才发现的。当时一个管状零件拆解方向设置成沿轴向向外,动画一播放直接戳进了相邻设备里。这个坑验证过多次了,希望帮到你——先把方向向量在场景视图里用Gizmos画出来再跑动画,能避免八成不自然的拆装路径。
本文还有配套的精品资源,点击获取