多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity3D手部模拟器开发:XR交互测试与编辑器内手势追踪实现

Unity3D手部模拟器开发:XR交互测试与编辑器内手势追踪实现 1. 项目概述为什么我们需要一个手部模拟器在XR扩展现实包括VR/AR开发领域尤其是使用Unity3d进行项目构建时一个核心的痛点长期困扰着开发者“没有头显我怎么测试手部交互”这个问题在过去几年里几乎成了所有涉足手势追踪、裸手交互项目的“拦路虎”。想象一下你设计了一个精妙的抓取、投掷或者手势识别的玩法每一次微调参数、测试效果都必须戴上沉重的头戴式显示器在有限的物理空间里手舞足蹈。这不仅效率低下严重拖慢迭代速度更让团队协作和设计评审变得异常困难。这正是“基于Unity3d的手部模拟器的设计与实现”这个项目标题背后最核心的驱动力。它不是一个简单的玩具或演示而是一个旨在解放生产力、提升开发体验的工程实践。其目标非常明确在Unity编辑器内构建一个功能完备、响应实时、可高度定制的手部模型及其交互逻辑的模拟系统让开发者能够脱离物理硬件仅凭鼠标和键盘或简单的快捷键就能流畅地进行所有手部交互功能的开发、调试与验证。从技术栈来看这个项目天然地锚定在Unity3d生态中深度依赖其XR交互工具链特别是XR Interaction Toolkit (XRI)和XR Hands这两个核心包。同时它也触及了3D内容生产流程比如如何将来自SolidWorks等专业CAD软件的高精度手部模型通过优化和重拓扑导入Unity并适配骨骼绑定系统。项目的影响范围远不止于个人学习它直接服务于XR应用开发、虚拟培训、医疗仿真、工业设计评审等多个严肃领域是连接创意构思与最终可交付产品之间的关键桥梁。2. 核心需求与设计思路拆解2.1 核心需求解析一个合格的手部模拟器绝不仅仅是屏幕上显示一个会动的手模型。我们需要从开发者的实际工作流出发拆解出多层递进的需求基础模拟能力这是基石。模拟器必须能实时、流畅地驱动一个具有正确解剖结构21个或更多关节的手部模型完成基本的手指弯曲、伸展、并拢、张开等动作。这要求我们深入理解手部骨骼层级与正向动力学FK原理。交互事件映射模拟的手部需要能触发与真实XR设备一致的交互事件。例如当模拟的食指和拇指指尖距离小于某个阈值时应能触发“Pinch捏合”选择事件当整个手部模型与虚拟物体发生碰撞时应能触发“Grab抓取”事件。这需要将模拟的骨骼数据桥接到XRI的交互管理器XR Interaction Manager和交互器XR Direct Interactor, XR Ray Interactor中。编辑器内操作友好性开发者使用鼠标和键盘进行控制操作逻辑必须直观、高效。例如用鼠标拖拽来移动和旋转整只手用键盘按键如ASDFG对应五指弯曲来驱动手指动作或者用滑块UI来精细控制每个关节的角度。状态可视化与调试在复杂的交互逻辑中能实时看到手部的碰撞体、射线、交互状态如悬停、选择、激活是至关重要的。模拟器应提供丰富的Gizmos和调试信息覆盖帮助开发者一眼看清交互的边界和触发条件。与硬件数据的无缝切换模拟器的终极目标不是取代真机测试而是作为高效的前期开发工具。因此它必须设计成能够与真实的XR手部追踪数据如通过OpenXR或特定SDK获取平滑切换。理想情况下只需在Unity编辑器中切换一个“输入源”就能在模拟输入和真实设备输入之间无缝过渡。性能与可扩展性模拟器本身不应成为性能负担。同时它应该设计良好的接口允许开发者自定义手势库、导入自己的手部模型、甚至扩展新的交互类型如双手协同手势。2.2 整体架构设计基于以上需求我设计的手部模拟器核心架构分为三层输入层、逻辑层、表现层。输入层负责接收各种编辑器输入鼠标、键盘、甚至游戏手柄并将其转化为统一的手部姿态命令。这里我设计了一个EditorHandInput单例管理器它持续监听输入事件并更新一个HandPose数据结构该结构包含了每根手指每个关节的目标旋转、手部整体的位置和旋转。逻辑层这是核心。它包含两个主要部分姿态解算与混合接收来自输入层的HandPose并根据当前模拟模式如直接控制、播放录制的手势、插值过渡计算出最终应用于骨骼的每一帧姿态。这里涉及到姿态的平滑插值Lerp/Slerp以防止动作突变以及不同手指动作的权重混合。交互逻辑桥接将计算出的最终手部骨骼世界坐标和关节状态传递给一个自定义的SimulatedHand组件。该组件继承或适配自XRI的XRBaseController或XRHandController负责将模拟的“捏合强度”、“抓取状态”等转换成XRI能够识别的InputFeature从而驱动XR Direct Interactor等标准交互器工作。表现层即最终在场景中看到的3D手部模型及其视觉反馈。它严格受逻辑层驱动骨骼动画系统使用Unity的Animator或直接通过代码操控Transform来驱动手部模型的骨骼层级。视觉反馈根据交互状态如悬停、选中改变手部材质高亮、显示射线、或播放抓取动画。调试视图在Scene视图中绘制手部碰撞体如胶囊体碰撞器组成的简化手形、交互射线的Gizmos并实时显示关键数据。这个分层架构确保了职责清晰输入可以灵活替换今天用键盘明天可以接入Leap Motion的桌面模拟数据交互逻辑与Unity官方XR生态兼容视觉表现也可以独立优化。3. 关键技术实现细节与实操要点3.1 手部骨骼系统的构建与驱动手部模拟的逼真度首先取决于骨骼系统的准确性。一个标准的人手通常采用21关节模型腕部1个每根手指4个掌骨、近端指骨、中间指骨、远端指骨拇指特殊为3个指骨。实操步骤模型准备你可以使用Meta的OVRHand模型、Unity XR Hands示例中的手模或者从SolidWorks等CAD软件导出高精度网格。对于CAD模型关键一步是重拓扑和骨骼绑定。在Blender或Maya中你需要为模型创建符合上述21关节标准的骨骼链并完成蒙皮权重绘制。确保权重分配平滑特别是手掌区域避免手指弯曲时手掌网格不自然扭曲。导入Unity与设置将绑定好的FBX模型导入Unity。在导入设置中确保**“骨骼定义”正确并启用“优化游戏对象”**选项这会让骨骼以清晰的层级结构如Hand_WristRoot/Hand_Middle1/Hand_Middle2...呈现便于代码访问。代码驱动骨骼不建议为如此动态的系统使用复杂的Animator状态机。更高效的方式是直接通过代码控制。我会创建一个HandSkeleton类在Start()方法中使用Transform.Find()或通过名称模式缓存所有关节的Transform引用到一个字典或数组中。public class HandSkeleton : MonoBehaviour { public enum FingerType { Thumb, Index, Middle, Ring, Pinky } public enum JointType { Metacarpal, Proximal, Intermediate, Distal } private DictionaryFingerType, Transform[] _fingerBones new DictionaryFingerType, Transform[](); void Awake() { // 示例初始化食指骨骼链 string[] indexBoneNames new string[] { Hand_Index1, Hand_Index2, Hand_Index3, Hand_Index4 }; Transform[] bones new Transform[indexBoneNames.Length]; for (int i 0; i indexBoneNames.Length; i) { bones[i] transform.FindDeepChild(indexBoneNames[i]); // 需要一个递归查找工具方法 } _fingerBones[FingerType.Index] bones; } public void SetFingerRotation(FingerType finger, JointType joint, Quaternion rotation) { int jointIndex (int)joint; if (_fingerBones.ContainsKey(finger) jointIndex _fingerBones[finger].Length) { _fingerBones[finger][jointIndex].localRotation rotation; } } }注意直接设置localRotation虽然直接但可能会与模型原有的动画或IK冲突。更健壮的做法是使用一个独立的PoseProvider类来计算目标旋转然后在Update()或LateUpdate()中通过插值应用实现平滑过渡。3.2 编辑器输入到姿态的映射策略如何用有限的键盘按键和鼠标操作表达出手部丰富的姿态是设计的关键。我采用了一种“分层混合”策略全局手部控制鼠标移动按住鼠标中键或特定修饰键鼠标左键拖拽根据摄像机视角平面移动手部根节点位置。旋转按住鼠标右键拖拽绕手部自身局部轴或世界轴旋转。这里需要处理欧拉角万向锁问题通常使用四元数Quaternion.Euler进行增量旋转更安全。手指姿态控制键盘姿态预设定义几个关键手势如Fist握拳、Open张开、Point指点、Pinch捏合并绑定到数字键1-4。按下时通过插值将手指驱动到预设姿态。单指精细控制将ASDFG键分别映射到拇指、食指、中指、无名指、小指的弯曲/伸展。例如按住‘S’键食指逐渐弯曲松开则回弹。这需要为每个手指维护一个float curlAmount弯曲量0到1在Update()中根据按键状态进行增减。public class EditorHandInput : MonoBehaviour { public HandPose currentPose; // 包含所有关节目标旋转的数据结构 public float rotationSpeed 100f; public float fingerCurlSpeed 2f; void Update() { HandleHandTransform(); // 处理鼠标移动旋转 HandleFingerCurl(); } void HandleFingerCurl() { // 以食指为例 if (Input.GetKey(KeyCode.S)) { currentPose.indexCurlAmount Mathf.Clamp01(currentPose.indexCurlAmount Time.deltaTime * fingerCurlSpeed); } else if (currentPose.indexCurlAmount 0) { // 缓慢回弹 currentPose.indexCurlAmount Mathf.Clamp01(currentPose.indexCurlAmount - Time.deltaTime * fingerCurlSpeed); } // 将curlAmount转换为具体关节的旋转角需要根据模型骨骼轴向预先定义弯曲轴 UpdateFingerRotationFromCurl(FingerType.Index, currentPose.indexCurlAmount); } }实操心得直接映射按键到关节旋转非常生硬。更好的做法是定义一个“手势状态机”。例如按下‘P’键进入“捏合模式”此时鼠标移动控制拇指和食指指尖的距离松开‘P’键退出。这样交互更符合直觉也更容易与XRI的“Select”动作关联。3.3 与XR Interaction Toolkit的集成这是让模拟手“活”起来能真正与场景物体交互的核心。XRI的核心是交互器Interactor和交互对象Interactable。我们需要让模拟手成为一个合格的交互器。实现方案创建模拟手控制器新建一个SimulatedHandController类继承自XRBaseController。重写其UpdateInput()方法在这里不是从硬件读取输入而是从我们自己的EditorHandInput或HandPose中获取数据。模拟输入特征XRBaseController需要提供InputFeature如trigger、grip、primaryButton等。对于手部模拟我们主要关心Select选择通常对应捏合动作。当模拟的食指与拇指指尖距离小于阈值如0.02米时将selectInteractionState.active设为true并计算一个selectInteractionState.value捏合强度0到1。Activate激活可能对应整个手的抓取动作。当手部与物体碰撞且某个条件满足如中指弯曲时激活。关联直接交互器在模拟手模型上添加XR Direct Interactor组件并将其Controller属性指向我们的SimulatedHandController。这样当我们的控制器报告“Select”激活时直接交互器就会尝试去抓取其碰撞范围内的XR Grab Interactable物体。模拟射线交互对于UI交互或远距离选择需要XR Ray Interactor。同样将其链接到我们的模拟控制器。我们可以用鼠标在屏幕上的位置来模拟射线原点从手部或摄像机发射。public class SimulatedHandController : XRBaseController { public EditorHandInput handInput; public float pinchThreshold 0.02f; protected override void UpdateInput() { base.UpdateInput(); // 1. 更新选择状态捏合 float pinchDistance Vector3.Distance(handInput.thumbTipPosition, handInput.indexTipPosition); bool isPinching pinchDistance pinchThreshold; float pinchStrength Mathf.InverseLerp(pinchThreshold * 2, pinchThreshold, pinchDistance); // 距离越小强度越大 selectInteractionState.active isPinching; selectInteractionState.value isPinching ? pinchStrength : 0f; // 2. 更新激活状态抓取- 这里简化为与选择同步 activateInteractionState.active isPinching; activateInteractionState.value pinchStrength; // 3. 更新手部姿态可选用于驱动视觉模型 // 可以将handInput.currentPose应用到关联的手部模型骨骼上 } }关键点确保UpdateInput在XRI的更新循环中被调用。通常需要将SimulatedHandController添加到一个由XR Interaction Manager管理的GameObject上。模拟的输入特征必须与你在XRI项目设置中定义的“动作映射”一致否则交互器可能无法正确响应。4. 模拟器功能模块的完整实现流程4.1 模块一可配置的手部姿态系统一个强大的模拟器必须支持姿态的录制、保存和回放方便测试特定手势下的交互。实现步骤定义姿态数据结构创建一个[System.Serializable]的HandPose类它应该能序列化存储手部根节点的位置、旋转以及所有手指关节的本地旋转Quaternion数组或列表。也可以存储每根手指的简化弯曲值float数组。姿态录制在编辑模式下创建一个PoseRecorder组件。它提供一个“开始录制”按钮在按下后每一帧或在固定时间间隔将当前HandSkeleton中所有关节的旋转保存到一个临时的HandPose实例中并记录时间戳。姿态保存录制结束后提供“保存姿态”功能将临时的HandPose实例序列化为JSON或Unity的ScriptableObject资源。ScriptableObject是更优选择因为它可以作为资产管理方便在Inspector中编辑和跨场景引用。姿态回放与混合创建一个PosePlayer组件。它可以加载一个HandPose资产并在运行时通过插值Quaternion.Lerp或Slerp将当前手部姿态平滑过渡到目标姿态。支持设置过渡时间和循环模式。[CreateAssetMenu(fileName NewHandPose, menuName XR/Hand Pose)] public class HandPoseAsset : ScriptableObject { public Vector3 wristPosition; public Quaternion wristRotation; public Quaternion[] jointRotations; // 按预定顺序存储所有21个关节的本地旋转 } public class PoseRecorder : MonoBehaviour { public HandSkeleton skeleton; private ListHandPoseFrame _recordedFrames new ListHandPoseFrame(); public void StartRecording() { _recordedFrames.Clear(); isRecording true; } void Update() { if (isRecording) { var frame new HandPoseFrame(); frame.time Time.time; frame.pose skeleton.CaptureCurrentPose(); // 捕获当前所有关节旋转 _recordedFrames.Add(frame); } } public HandPoseAsset SaveRecordingToAsset(string assetName) { // 简化只保存最后一帧的姿态 var poseAsset ScriptableObject.CreateInstanceHandPoseAsset(); poseAsset.jointRotations _recordedFrames.Last().pose.jointRotations; // ... 保存到Assets目录 return poseAsset; } }4.2 模块二交互事件的模拟与反馈模拟器不仅要驱动模型还要提供清晰的视觉和逻辑反馈让开发者知道“交互是否按预期发生”。视觉反馈实现高亮悬停物体为XR Direct Interactor添加事件监听。在其OnHoverEntered和OnHoverExited事件中获取到交互物体Interactable并改变其材质如增加自发光或显示一个外轮廓效果使用Outline组件或Command Buffer绘制。显示抓取状态当OnSelectEntered触发时除了改变物体状态还可以让模拟手模型播放一个轻微的抓握动画通过混合形状或骨骼微调或者在手心显示一个吸附点。射线可视化如果使用了XR Ray Interactor确保其Line Renderer或Visuals模块已启用。在模拟模式下可以根据鼠标位置实时更新射线的方向和终点。逻辑反馈与调试信息屏幕GUI在Game视图的角落使用Unity的GUI或UI Toolkit绘制一个简单的调试面板。实时显示当前手部位置、旋转。各手指弯曲值。当前激活的交互器类型及悬停/抓取的物体名称。捏合距离、抓取强度等关键参数。Scene视图Gizmos编写一个OnDrawGizmos或OnDrawGizmosSelected方法在Scene视图中绘制手部骨骼连线。指尖碰撞体的位置和范围用Gizmos.DrawWireSphere。交互射线的路径和命中点。手部与物体的碰撞边界。void OnDrawGizmosSelected() { if (!Application.isPlaying) return; // 绘制指尖位置 Gizmos.color Color.red; foreach (var fingertip in fingertipTransforms) { Gizmos.DrawSphere(fingertip.position, 0.005f); } // 绘制捏合距离线 Gizmos.color Color.yellow; Gizmos.DrawLine(thumbTip.position, indexTip.position); }4.3 模块三性能优化与扩展性设计在编辑器内运行性能通常不是首要瓶颈但良好的设计能提升体验并支持更复杂的用例。性能优化点骨骼更新频率不是每一帧都需要更新所有骨骼。如果手指没有动画可以跳过。或者可以将骨骼更新放在LateUpdate中确保在摄像机渲染前完成。碰撞检测优化手部通常由多个胶囊碰撞体组成。确保使用合适的物理层Layer和碰撞矩阵避免不必要的碰撞计算。对于模拟器甚至可以考虑在非运行模式下禁用物理模拟改用更轻量的触发器检测。调试信息开关丰富的Gizmos和GUI在调试时是宝但在不需要时会浪费性能。务必为所有调试绘制提供开关并默认关闭。扩展性设计输入源抽象不要将EditorHandInput硬编码到核心逻辑中。定义一个IHandInputSource接口包含获取手部位置、旋转、手指弯曲度等方法。然后分别实现EditorKeyboardMouseInput、RecordedPoseInput、ExternalDeviceInput未来可接入Leap Motion等。核心的HandSimulator只依赖这个接口。姿态识别插件化将手势识别逻辑模块化。可以创建一个基础类GestureRecognizer然后派生出PinchGestureRecognizer、FistGestureRecognizer、CustomGestureRecognizer等。模拟器通过配置加载这些识别器使其能够识别更复杂的手势。配置化将所有关键参数——如捏合阈值、骨骼映射名称、按键配置、调试选项——暴露在Inspector中或制作一个独立的模拟器配置资产SimulatorConfigScriptableObject。这样不同的项目或开发者可以快速调整模拟器行为而无需修改代码。5. 开发中的典型问题与实战排查技巧在实际构建过程中我遇到了不少“坑”。这里记录下最典型的几个问题及其解决方案希望能帮你节省大量时间。5.1 问题一导入的SolidWorks模型骨骼无法驱动现象从SolidWorks导出的FBX手部模型在Unity中显示正常但通过代码修改骨骼Transform的localRotation时模型网格没有任何变化。排查与解决检查导入设置首先确认FBX的导入设置中“动画类型”是否被错误地设置为“无”或“通用”。对于需要代码驱动骨骼的模型应选择“人形”或“泛型”。如果模型没有预制动画选择“泛型”即可。验证骨骼层级和名称在Unity的Project窗口选中FBX模型切换到“模型”选项卡下的“骨骼”子选项卡。检查骨骼层级是否正确以及骨骼名称是否与代码中查找的名称完全一致包括大小写。SolidWorks导出的骨骼名称可能带有特殊前缀或后缀需要调整。蒙皮权重问题这是最常见的原因。在3D软件中绑定时可能某些顶点没有正确分配到骨骼权重或者权重分配错误。在Unity中你可以使用“Skinned Mesh Renderer”组件的“烘焙网格”功能将蒙皮网格转换为静态网格看看原始模型是否正常。如果正常问题就在蒙皮。你需要回到Blender/Maya中仔细检查并修正权重。Transform与Animation的冲突如果模型本身带有动画组件Animator或旧版动画Animation并且正在播放某个状态那么代码直接修改Transform可能会被动画系统覆盖。确保在运行时你的代码驱动逻辑优先级更高或者禁用不需要的动画状态。实操心得对于外部导入的复杂模型建立一个标准的预处理流程。我的流程是在Blender中完成重拓扑和骨骼绑定 - 导出时只保留必要的骨骼和网格 - 在Unity中创建一个空的Prefab将模型作为子物体放入并添加自己的控制脚本。这样可以将模型资产与逻辑Prefab解耦。5.2 问题二模拟的交互无法触发XRI的抓取事件现象手部模型在视觉上可以移动和弯曲也能与物体碰撞但就是无法抓取带有XR Grab Interactable的物体。排查步骤自底向上检查物理碰撞首先确保手部模型上的碰撞体如CapsuleCollider和物体上的碰撞体都启用了且它们的“Is Trigger”设置符合预期对于直接交互通常不需要是Trigger。在Scene视图中开启“物理”调试视图查看碰撞体是否真的在接触。验证交互器状态选中手部模型上的XR Direct Interactor组件在运行时观察Inspector面板。查看“Select Interaction State”和“Hover Interaction State”是否在捏合时变为“Active”。如果没有问题出在输入模拟层。追溯输入源检查你的SimulatedHandController的UpdateInput方法是否被正确调用。在其中打印selectInteractionState.active和value的日志看它们是否随捏合动作变化。检查交互管理器确认场景中存在XR Interaction Manager游戏对象。它是所有交互事件的中枢。检查交互对象设置确保待抓取物体的XR Grab Interactable组件配置正确特别是“Interaction Layer Mask”是否与交互器的层掩码有交集。冲突的交互器如果手部模型上不小心附加了多个交互器比如既有XR Direct Interactor又有XR Ray Interactor它们可能会相互干扰。确保只有一个活跃的交互器用于抓取。一个常见陷阱在模拟selectInteractionState时只设置了active true但没有设置value或设置为0。某些交互逻辑可能依赖于value的强度。确保在激活时提供一个大于0的值如0.8或根据捏合距离计算的值。5.3 问题三姿态插值不平滑动作生硬卡顿现象在切换手势或从录制姿态回放时手指运动不是平滑过渡而是直接“跳”到目标姿态或者移动过程中有卡顿感。解决方案使用四元数球面线性插值对于旋转永远使用Quaternion.Slerp而不是Lerp它能保证在球面上进行最平滑的插值。Lerp在旋转角度大时会产生非均匀角速度。currentBoneRotation Quaternion.Slerp(currentBoneRotation, targetBoneRotation, Time.deltaTime * rotationSpeed);引入动画曲线不要使用线性的Mathf.Lerp。为插值过程引入一个AnimationCurve可以创造出“缓入缓出”的效果让动作更自然。例如在0到1的时间内使用一个Ease-In-Out曲线来控制插值系数t。分关节独立插值不要对所有关节使用相同的插值速度。手指根部的关节可以稍慢指尖可以稍快模拟出更真实的生物运动滞后感。在LateUpdate中更新确保所有骨骼的最终变换在LateUpdate中应用。这样可以避免在同一帧内由于其他逻辑如物理更新、摄像机跟随对Transform的修改导致视觉上的抖动。检查帧率在Unity编辑器中运行游戏时如果场景复杂帧率可能不稳定。这会导致基于Time.deltaTime的插值出现波动。可以尝试使用Time.unscaledDeltaTime或者确保你的模拟逻辑不受帧率影响使用固定时间步长进行累积计算。5.4 问题速查表问题现象可能原因排查方向手部模型不动骨骼引用丢失、驱动代码未执行、模型缩放为01. 检查HandSkeleton中骨骼字典是否成功初始化。2. 在Update中打印日志确认驱动函数被调用。3. 检查手部根节点及其父物体的缩放。能碰撞但无法交互交互器未激活、交互状态未更新、层掩码不匹配、无XR交互管理器1. 检查XR Direct Interactor的Enable状态。2. 检查SimulatedHandController的输入模拟日志。3. 对比交互器与交互物体的Layer Mask。4. 确认场景中存在XR Interaction Manager。手势识别不准关节数据噪声大、阈值设置不合理、未考虑手部朝向1. 对关节位置数据进行低通滤波如Vector3.Lerp平滑。2. 动态调整捏合/抓取阈值或提供校准功能。3. 计算手势时将关节位置转换到手掌局部空间消除整体移动影响。编辑器运行卡顿每帧更新所有骨骼和碰撞体、调试绘制过多、物理计算复杂1. 为骨骼更新添加距离或变化量判断无变化则跳过。2. 关闭不必要的Gizmos和GUI调试信息。3. 简化手部碰撞体或在不需精确物理时使用触发器。切换真机后失效模拟输入源与真机输入源冲突、骨骼映射不一致1. 设计一个运行时输入源切换开关禁用模拟输入启用XR设备输入。2. 确保真机SDK输出的骨骼名称或索引与模拟器使用的保持一致。构建一个成熟可用的手部模拟器是一个涉及3D图形、输入系统、物理交互和软件架构的综合性项目。它没有唯一的“正确”实现但遵循清晰的分层设计、紧密集成现有成熟框架如XRI、并始终以开发者的调试体验为核心就能打造出一个真正提升XR开发效率的强大工具。从最初只能动动手指的模型到最终能在编辑器里流畅地抓取、投掷、操作UI这个过程本身也是对Unity3d深度理解的一次绝佳历练。
返回列表