Unity武器可视化窗口实现:Render Texture方案与移动端PC端输入统一
1. 武器可视化窗口到底在解决什么问题做过角色扮演或者射击类项目的朋友大概率都遇到过这个需求玩家在背包里点一把枪界面上要能转着看、能缩放、能换配件最好还能在手机上用手指头搓着转。这个功能看起来简单实际上手之后你会发现坑一个接一个——模型渲染层级不对被UI挡住、相机裁剪面设置不合理导致模型被切、移动端触摸旋转和PC端鼠标拖拽逻辑打架、不同分辨率下模型位置偏移……这些问题单独拎出来都不难但凑在一起就够喝一壶的。武器可视化窗口说白了就是在UI层里开一个“小房间”把3D武器模型放进去单独渲染让玩家能360度无死角地查看。它和游戏主场景的渲染是隔离的不会受到场景光照、相机位置的影响同时又要保证模型材质、光照效果看起来自然。这个功能在背包系统、皮肤预览、武器改装界面、商城展示等场景中都是刚需。这篇文章面向的是有一定Unity基础、正在做或者准备做这类功能的开发者。我会从整体设计思路讲到具体实现包括Render Texture方案和独立相机方案的取舍、移动端和PC端的输入统一处理、模型旋转缩放的边界控制、光照和背景的处理技巧最后还会分享几个我在实际项目中踩过的坑和排查方法。整个方案不依赖任何第三方插件Unity原生API就能搞定URP和内置管线都适用。2. 整体方案设计与技术选型拆解2.1 两种主流方案对比Render Texture vs 独立相机直出做武器可视化窗口核心要解决的问题是如何把3D模型渲染到UI层面上。目前主流有两种做法各有优劣。第一种是Render Texture方案。创建一个独立的相机让它只渲染武器所在的Layer把渲染结果输出到一张Render Texture上然后在UI中用RawImage来显示这张贴图。这种方案的好处是渲染完全隔离武器模型不会和场景中的任何物体产生交互你可以单独控制这个相机的光照、背景、后处理。缺点是Render Texture会占用额外的显存如果分辨率设置过高在低端移动设备上会有明显的性能开销。第二种是独立相机直接叠加。把UI相机和武器相机分开通过Camera的Depth值控制渲染顺序让武器相机渲染在UI之上。这种方案省去了Render Texture的开销但问题是武器模型会直接暴露在屏幕空间中你很难控制它只显示在某个UI面板区域内。如果UI布局复杂比如武器窗口只占屏幕中间一块区域这种方案就很难处理裁剪问题。我实测下来Render Texture方案在灵活性和可控性上完胜尤其是需要把武器窗口嵌入复杂UI布局的时候。性能方面只要把Render Texture的分辨率控制在合理范围内比如512x512到1024x1024之间中低端手机也完全扛得住。下面我就以这个方案为主线来展开。2.2 层级规划与相机设置的核心逻辑方案定了之后第一件事是规划Layer。Unity默认的Layer有Default、UI、Water等等我们需要新建一个专门的Layer比如叫“WeaponPreview”。把武器模型和它可能挂载的配件都放到这个Layer上。为什么要单独建Layer因为武器预览相机只需要看到武器不需要看到场景里的地形、角色、特效。通过cullingMask只勾选WeaponPreview层相机就只会渲染这一层的东西。同时主相机和UI相机的cullingMask要排除掉WeaponPreview层否则武器模型会在主场景里穿帮。相机的参数设置有几个关键点。Projection必须设为Perspective虽然Orthographic也能看但透视相机才能体现出武器的立体感和纵深感。Field of View一般设在30到45之间太小了武器看起来扁平太大了边缘会有明显的透视畸变。Clipping Planes的Near值可以设到0.01Far值设个10就够了因为武器模型离相机很近不需要渲染远处的东西这样还能提高深度缓冲的精度。注意Near Clip Plane不要设成0否则会出现深度精度问题导致模型闪烁。0.01是个比较安全的经验值。2.3 渲染顺序与UI层级的关系Render Texture方案中武器预览相机渲染到一张贴图上这张贴图再通过RawImage显示在UI上。所以从渲染顺序来说武器相机是在UI之前渲染的不存在UI遮挡武器的问题。但这里有个容易忽略的点RawImage所在的Canvas的Render Mode。如果你的Canvas是Screen Space - Overlay模式那么UI会直接渲染在屏幕最上层RawImage显示的内容自然也在最上层没问题。但如果Canvas是Screen Space - Camera或者World Space模式就需要确保RawImage所在的层级和渲染顺序正确。我一般建议武器预览窗口用一个独立的CanvasRender Mode设为Screen Space - OverlaySort Order设一个比较高的值这样能保证它始终显示在最前面。另外如果UI中有弹窗、提示框之类的元素需要盖在武器窗口上面那武器窗口的Canvas Sort Order就要低于弹窗的Sort Order。这个层级关系在项目初期就要规划好不然后面UI越加越多很容易出现显示错乱。3. 核心细节解析与实操要点3.1 Render Texture的创建与参数选择Render Texture的创建有两种方式一种是在Project面板右键手动创建资源另一种是在代码里动态new一个。手动创建的好处是可以在Inspector里直观地调整参数适合固定分辨率的场景。动态创建则更灵活可以根据屏幕分辨率或者UI面板的实际大小来适配。我一般推荐动态创建因为不同设备的屏幕比例差异很大固定分辨率很容易在某些设备上出现拉伸或者模糊。具体做法是在初始化的时候根据RawImage的RectTransform尺寸乘以一个质量系数来计算Render Texture的分辨率。// 根据RawImage实际显示尺寸动态创建Render Texture RectTransform rt rawImage.rectTransform; int width Mathf.NextPowerOfTwo((int)(rt.rect.width * qualityFactor)); int height Mathf.NextPowerOfTwo((int)(rt.rect.height * qualityFactor)); // 限制最大分辨率防止低端设备性能问题 width Mathf.Min(width, 1024); height Mathf.Min(height, 1024); RenderTexture rt new RenderTexture(width, height, 16, RenderTextureFormat.ARGB32); rt.antiAliasing 2; // 开启2倍抗锯齿这里有几个参数值得展开说。Depth Buffer设为16位就够了武器预览不需要高精度的深度信息。Anti Aliasing开2倍或者4倍能明显改善模型边缘的锯齿但对性能有一定影响移动端建议2倍就够了。RenderTextureFormat用ARGB32如果需要更好的色彩表现可以用ARGBHalf但显存占用会翻倍。实操心得Mathf.NextPowerOfTwo能把分辨率对齐到2的幂次虽然现代GPU对非2的幂次纹理支持已经很好但2的幂次在某些移动GPU上确实有更好的兼容性和性能表现。3.2 武器模型的加载与实例化策略武器模型怎么放到预览场景里有两种思路。一种是在场景中预先摆好一个位置把武器模型实例化到那个位置相机对准它。另一种是在代码中动态创建每次打开预览窗口时实例化模型关闭时销毁。我倾向于第二种但加一个对象池的优化。因为频繁地Instantiate和Destroy会产生GC压力尤其是在移动端玩家反复打开背包查看不同武器的时候卡顿感会很明显。用一个简单的对象池来管理武器预览实例打开时从池里取关闭时还回去能有效减少内存分配。模型实例化之后位置和旋转要重置。位置一般放在相机正前方2到3个单位距离的位置旋转根据武器的默认朝向调整。这里有个细节不同武器的默认朝向可能不一样有的枪口朝前有的朝上有的朝侧面。如果直接统一设置旋转有些武器看起来就是歪的。我的做法是在武器配置数据里加一个previewRotationOffset字段每把武器单独配置一个旋转偏移量加载时应用上去。// 武器预览实例的初始化 GameObject weaponInstance objectPool.Get(); weaponInstance.transform.SetParent(previewRoot, false); weaponInstance.transform.localPosition Vector3.zero; weaponInstance.transform.localRotation Quaternion.Euler(weaponData.previewRotationOffset); weaponInstance.transform.localScale Vector3.one * weaponData.previewScale;3.3 光照方案让武器看起来有质感武器预览的光照和主场景是独立的因为相机只渲染WeaponPreview层主场景的光源如果不在这个层上是不会影响武器的。所以我们需要在预览场景里单独布置光源。最简单的做法是一个主方向光加一个补光。主方向光从斜上方打下来强度1.0左右颜色偏暖白。补光从另一侧打过来强度0.3到0.5颜色偏冷用来填充阴影区域。这样武器看起来有明暗对比立体感强同时暗部也不会死黑。如果项目用的是URP管线还可以加一个Volume来做后处理比如轻微的Bloom和Color Grading能让武器的金属质感更突出。但要注意后处理的性能开销移动端建议只开Bloom强度控制在0.5以下。注意如果武器模型使用了PBR材质环境光的影响很大。没有环境光的话金属部分会看起来黑乎乎的。解决办法是在预览场景里放一个Skybox或者用RenderSettings.ambientMode设置一个纯色环境光。我一般用AmbientMode.Flat颜色设一个中灰色简单有效。3.4 背景处理纯色、渐变还是透明武器预览窗口的背景怎么处理这个看似是小问题实际上对视觉效果影响很大。常见的做法有三种纯色背景、渐变背景、透明背景。纯色背景最简单把相机的clearFlags设为SolidColor选一个和UI风格搭配的颜色就行。渐变背景需要用一张渐变贴图作为相机的背景或者用一个全屏的Quad来渲染渐变。透明背景则是把clearFlags设为SolidColor颜色alpha设为0然后Render Texture的格式要支持alpha通道。我个人的选择是根据UI风格来定。如果UI是深色主题武器预览背景用深灰到黑色的渐变武器会显得更有质感。如果UI是浅色主题用浅灰到白色的渐变。透明背景虽然灵活但武器边缘的抗锯齿会和背景混合产生一些问题需要额外处理。4. 移动端与PC端输入统一处理4.1 输入系统的选择旧版Input还是新版Input SystemUnity目前有两套输入系统旧的Input类和新的Input System包。如果你的项目还在用旧版Input那处理鼠标拖拽很简单Input.GetMouseButton(0)加Input.GetAxis(Mouse X)就能搞定。但移动端的触摸需要单独处理Input.touches数组来获取触摸点。新版Input System则统一了鼠标、触摸、手柄的输入通过Action来抽象输入行为。但它的学习成本更高而且和旧版Input不能混用。我的建议是如果项目已经用了新版Input System那就统一用它的Pointer相关Action来处理拖拽和缩放。如果项目用的是旧版Input也没必要为了这个功能去迁移直接用旧版API加条件编译就行。下面我以旧版Input为例给出一个统一处理的方案。4.2 拖拽旋转的通用实现拖拽旋转的核心逻辑是获取输入的Delta值映射到武器的旋转角度上。PC端用鼠标Delta移动端用触摸Delta。关键是要把两套输入统一成同样的数据格式。public class WeaponPreviewController : MonoBehaviour { [SerializeField] private float rotateSpeed 0.5f; [SerializeField] private float minVerticalAngle -60f; [SerializeField] private float maxVerticalAngle 60f; private float currentRotX 0f; private float currentRotY 0f; private Vector2 lastInputPos; private bool isDragging false; void Update() { Vector2 inputDelta Vector2.zero; bool pressed false; #if UNITY_EDITOR || UNITY_STANDALONE // PC端鼠标输入 if (Input.GetMouseButtonDown(0)) { isDragging true; lastInputPos Input.mousePosition; } if (Input.GetMouseButtonUp(0)) { isDragging false; } if (isDragging Input.GetMouseButton(0)) { Vector2 currentPos Input.mousePosition; inputDelta currentPos - lastInputPos; lastInputPos currentPos; pressed true; } #else // 移动端触摸输入 if (Input.touchCount 1) { Touch touch Input.GetTouch(0); if (touch.phase TouchPhase.Began) { lastInputPos touch.position; isDragging true; } else if (touch.phase TouchPhase.Moved isDragging) { inputDelta touch.position - lastInputPos; lastInputPos touch.position; pressed true; } else if (touch.phase TouchPhase.Ended || touch.phase TouchPhase.Canceled) { isDragging false; } } #endif if (pressed) { currentRotY inputDelta.x * rotateSpeed; currentRotX - inputDelta.y * rotateSpeed; currentRotX Mathf.Clamp(currentRotX, minVerticalAngle, maxVerticalAngle); // 应用旋转到武器模型上 previewModel.transform.localRotation Quaternion.Euler(currentRotX, currentRotY, 0f); } } }这段代码有几个关键点。**旋转速度rotateSpeed**在PC端和移动端可能需要不同的值因为鼠标Delta和触摸Delta的量级不一样。鼠标移动一像素和手指滑动一像素在物理距离上差异很大。我的做法是在Inspector里暴露两个速度值根据平台自动切换或者用一个系数来调整。垂直角度的Clamp很重要。如果不限制玩家可以把武器翻转到背面看到模型的内部结构或者空洞体验很差。一般限制在正负60度左右比较合适既能看清武器的各个角度又不会翻过头。4.3 双指缩放与滚轮缩放的统一缩放的处理逻辑类似。PC端用鼠标滚轮移动端用双指捏合。滚轮的值是离散的捏合的值是连续的需要做归一化处理。// PC端滚轮 float scroll Input.GetAxis(Mouse ScrollWheel); if (Mathf.Abs(scroll) 0.01f) { targetZoom - scroll * zoomSpeed; targetZoom Mathf.Clamp(targetZoom, minZoom, maxZoom); } // 移动端双指捏合 if (Input.touchCount 2) { Touch touch0 Input.GetTouch(0); Touch touch1 Input.GetTouch(1); Vector2 prevTouch0 touch0.position - touch0.deltaPosition; Vector2 prevTouch1 touch1.position - touch1.deltaPosition; float prevMagnitude (prevTouch0 - prevTouch1).magnitude; float currentMagnitude (touch0.position - touch1.position).magnitude; float difference currentMagnitude - prevMagnitude; targetZoom - difference * pinchSpeed; targetZoom Mathf.Clamp(targetZoom, minZoom, maxZoom); }缩放的目标值targetZoom不要直接应用到相机位置或者模型缩放上而是用Mathf.Lerp做平滑过渡。这样操作手感会好很多不会感觉生硬。实操心得移动端双指捏合的时候如果同时有单指拖拽的逻辑在跑会出现旋转和缩放同时触发的混乱情况。解决办法是在检测到双指触摸时立即取消单指拖拽状态。这个细节不处理的话移动端体验会非常糟糕。4.4 惯性效果的实现与参数调优为了让旋转手感更顺滑可以加一个惯性效果。手指离开屏幕后武器继续旋转一段距离然后慢慢停下。实现方式是记录最后一帧的旋转速度在松开后继续应用这个速度并逐渐衰减。// 在拖拽结束时记录速度 if (wasDragging !isDragging) { inertiaVelocity lastFrameDelta * rotateSpeed; } // 在非拖拽状态下应用惯性 if (!isDragging inertiaVelocity.magnitude 0.01f) { currentRotY inertiaVelocity.x; currentRotX - inertiaVelocity.y; currentRotX Mathf.Clamp(currentRotX, minVerticalAngle, maxVerticalAngle); inertiaVelocity Vector2.Lerp(inertiaVelocity, Vector2.zero, Time.deltaTime * inertiaDamping); previewModel.transform.localRotation Quaternion.Euler(currentRotX, currentRotY, 0f); }inertiaDamping这个参数控制衰减速度值越大停得越快。我一般设在5到10之间具体看项目的手感需求。太小了会转个不停太大了又感觉不到惯性。5. 完整实操流程与核心环节实现5.1 场景搭建与UI布局先来搭建基础场景。在Hierarchy中创建一个新的Canvas命名为“WeaponPreviewCanvas”Render Mode设为Screen Space - OverlaySort Order设为10。在这个Canvas下面创建一个RawImage命名为“PreviewImage”调整它的RectTransform到合适的大小和位置这就是武器预览窗口的显示区域。接下来创建武器预览相机。在场景中新建一个Camera命名为“WeaponPreviewCamera”。把它的Culling Mask设置为只包含“WeaponPreview”层Clear Flags设为SolidColor背景色根据UI风格来定。Projection设为PerspectiveField of View设为35Clipping Planes的Near设为0.01Far设为10。然后创建一个空物体作为武器模型的父节点命名为“PreviewRoot”放在相机正前方大约2.5个单位的位置。这个距离需要根据武器的实际大小来调整大武器远一点小武器近一点。最后把相机的Target Texture设置为之前创建的Render Texture把RawImage的Texture也设置为同一张Render Texture。这样相机渲染的内容就会显示在RawImage上了。5.2 脚本编写与组件挂载创建一个名为WeaponPreviewWindow的脚本挂载在WeaponPreviewCanvas上。这个脚本负责管理整个预览窗口的生命周期包括打开、关闭、加载武器、处理输入等。public class WeaponPreviewWindow : MonoBehaviour { [Header(UI引用)] [SerializeField] private RawImage previewImage; [SerializeField] private Button closeButton; [Header(相机引用)] [SerializeField] private Camera previewCamera; [SerializeField] private Transform previewRoot; [Header(光照引用)] [SerializeField] private Light mainLight; [SerializeField] private Light fillLight; [Header(参数配置)] [SerializeField] private float rotateSpeed 0.5f; [SerializeField] private float zoomSpeed 2f; [SerializeField] private float minZoom 1.5f; [SerializeField] private float maxZoom 5f; [SerializeField] private float inertiaDamping 8f; private RenderTexture renderTexture; private GameObject currentWeaponInstance; private WeaponPreviewController controller; void Awake() { // 初始化Render Texture int size 512; renderTexture new RenderTexture(size, size, 16, RenderTextureFormat.ARGB32); renderTexture.antiAliasing 2; previewCamera.targetTexture renderTexture; previewImage.texture renderTexture; // 初始化控制器 controller gameObject.AddComponentWeaponPreviewController(); controller.Init(previewCamera, previewRoot, rotateSpeed, zoomSpeed, minZoom, maxZoom, inertiaDamping); // 默认关闭 gameObject.SetActive(false); } public void Open(WeaponData weaponData) { gameObject.SetActive(true); LoadWeapon(weaponData); controller.ResetRotation(); } public void Close() { UnloadWeapon(); gameObject.SetActive(false); } private void LoadWeapon(WeaponData weaponData) { UnloadWeapon(); // 从对象池或Resources加载武器模型 GameObject prefab Resources.LoadGameObject(weaponData.prefabPath); currentWeaponInstance Instantiate(prefab, previewRoot); currentWeaponInstance.transform.localPosition Vector3.zero; currentWeaponInstance.transform.localRotation Quaternion.Euler(weaponData.previewRotation); currentWeaponInstance.transform.localScale Vector3.one * weaponData.previewScale; SetLayerRecursively(currentWeaponInstance, LayerMask.NameToLayer(WeaponPreview)); } private void UnloadWeapon() { if (currentWeaponInstance ! null) { Destroy(currentWeaponInstance); currentWeaponInstance null; } } private void SetLayerRecursively(GameObject obj, int layer) { obj.layer layer; foreach (Transform child in obj.transform) { SetLayerRecursively(child.gameObject, layer); } } void OnDestroy() { if (renderTexture ! null) { renderTexture.Release(); Destroy(renderTexture); } } }这个脚本把窗口的生命周期管理、武器加载、Render Texture创建都封装在一起了。SetLayerRecursively这个方法很重要因为武器模型可能有很多子物体如果不递归设置Layer子物体可能会被其他相机渲染到。5.3 光照布置与材质调整在预览场景中创建两个方向光。主光命名为“PreviewMainLight”旋转角度设为(50, -30, 0)强度1.2颜色稍微偏暖。补光命名为“PreviewFillLight”旋转角度设为(-30, 150, 0)强度0.4颜色稍微偏冷。如果用的是URP管线还需要在预览相机上添加一个UniversalAdditionalCameraData组件确保它参与URP的渲染流程。同时可以在场景中放一个Global Volume添加Bloom和Color Grading效果但记得把Volume的Layer也设置成WeaponPreview或者调整相机的Volume Mask。材质方面如果武器模型用的是Standard Shader在URP下需要升级为Universal Render Pipeline/Lit。这个转换可以在Edit - Rendering - Materials - Convert Selected Built-in Materials to URP里批量完成。转换之后检查一下金属度和光滑度参数有时候转换会丢失一些数值。5.4 移动端适配的额外处理移动端有几个额外要注意的点。安全区域适配如果武器预览窗口是全屏的要确保UI元素不会被刘海屏或者圆角遮挡。可以用Screen.safeArea来获取安全区域调整Canvas的RectTransform。性能优化移动端上Render Texture的分辨率不要超过1024x1024抗锯齿最多开2倍。如果设备性能较差可以在代码里根据SystemInfo.graphicsMemorySize来判断低于一定阈值时降低Render Texture分辨率。触摸事件的穿透问题如果武器预览窗口上面还有其他的UI元素比如关闭按钮、切换按钮要确保这些按钮能正常接收点击事件而拖拽旋转不会和按钮点击冲突。解决办法是在按钮上添加EventTrigger或者用IPointerClickHandler来处理同时在拖拽逻辑里判断当前触摸点是否在按钮区域内。6. 常见问题与排查技巧实录6.1 模型显示为粉红色或材质丢失这是最常见的问题尤其是在URP项目里导入了内置管线的模型。粉红色说明Shader不兼容Unity找不到合适的Shader来渲染这个材质。解决办法分两步。第一步选中武器模型的所有材质在Inspector里把Shader从Standard改成Universal Render Pipeline/Lit。如果模型用的是自定义Shader需要找到对应的URP版本或者手动改写。第二步检查材质的贴图是否还在有时候转换Shader之后贴图槽位会丢失需要重新指定。实操心得批量转换材质可以用Unity的Material Upgrader工具在Window - Rendering - Render Pipeline Converter里。但转换之前一定要备份项目因为转换是不可逆的。6.2 模型被UI遮挡或者显示不全这个问题通常和相机的Clipping Planes有关。如果Near值设得太大靠近相机的部分模型会被裁掉。如果Far值设得太小远离相机的部分会被裁掉。武器预览的场景里模型离相机很近Near设0.01Far设10就足够了。还有一种情况是模型被UI遮挡。检查一下RawImage所在的Canvas的Sort Order以及武器预览相机的Depth值。如果Canvas是Screen Space - Overlay模式UI永远在最上层不存在被3D物体遮挡的问题。如果是Screen Space - Camera模式需要确保Canvas的Plane Distance小于武器模型到相机的距离。6.3 移动端触摸旋转不灵敏或者过于灵敏这个问题的根源是鼠标Delta和触摸Delta的量级差异。鼠标移动一像素对应屏幕上的一个像素点而手指滑动一像素在物理屏幕上对应的距离要小得多。所以同样的rotateSpeed在移动端会感觉旋转太快。解决办法是在代码里根据平台调整速度值。可以这样写#if UNITY_EDITOR || UNITY_STANDALONE float platformSpeedFactor 1.0f; #else float platformSpeedFactor 0.3f; #endif currentRotY inputDelta.x * rotateSpeed * platformSpeedFactor;这个系数需要在实际设备上调试不同分辨率的手机可能还需要微调。我的经验是先在目标设备上跑一遍感受一下旋转速度然后反推系数。6.4 Render Texture内存泄漏Render Texture是显存大户如果不手动释放反复打开关闭预览窗口会导致显存持续增长最终闪退。必须在窗口关闭或者销毁的时候调用Release()和Destroy()。void OnDisable() { if (renderTexture ! null renderTexture.IsCreated()) { renderTexture.Release(); } } void OnDestroy() { if (renderTexture ! null) { Destroy(renderTexture); renderTexture null; } }另外如果每次打开窗口都new一个Render Texture建议改成复用一个全局的Render Texture只在分辨率变化时才重新创建。这样能进一步减少内存分配。6.5 武器旋转时边缘锯齿严重锯齿问题在移动端尤其明显因为屏幕PPI高但GPU的抗锯齿能力有限。解决办法有三个提高Render Texture分辨率、开启MSAA、使用后处理抗锯齿。提高分辨率是最直接的办法但性能开销也最大。MSAA在Render Texture上可以通过antiAliasing属性设置2倍或4倍。后处理抗锯齿比如FXAA或SMAA可以在URP的Volume里添加但对武器这种边缘清晰的物体效果一般。我一般组合使用Render Texture分辨率设为屏幕宽度的1.5倍上限1024MSAA开2倍基本能接受。如果还觉得锯齿明显可以在材质上做文章比如给武器边缘加一圈轻微的发光转移视觉注意力。6.6 常见问题速查表问题现象可能原因排查方向解决方案模型粉红色Shader不兼容检查材质Shader转换为URP Lit或对应管线Shader模型显示不全相机裁剪面设置不当检查Near/Far值Near设0.01Far设10模型被UI遮挡Canvas渲染顺序问题检查Sort Order和Render Mode调整Canvas层级或改用Overlay模式触摸旋转太快平台Delta量级差异对比PC和移动端手感加平台速度系数显存持续增长Render Texture未释放Profiler查看显存OnDisable时ReleaseOnDestroy时Destroy边缘锯齿严重分辨率或抗锯齿不足检查RT分辨率和MSAA提高分辨率开MSAA加后处理双指缩放和单指旋转冲突输入逻辑未互斥检查触摸计数判断双指触摸时取消单指拖拽状态武器朝向不对模型默认旋转差异检查模型导入设置配置每把武器的旋转偏移量7. 一些实战中攒下来的经验武器可视化窗口这个功能技术难度不算高但细节特别多。我做过好几个项目每次都会遇到新的小问题。这里分享几个我觉得比较有价值的经验。第一武器配置数据一定要可扩展。不要把所有武器的预览参数硬编码在脚本里而是用一个ScriptableObject或者JSON来配置。每把武器需要配置模型路径、预览旋转偏移、预览缩放、默认缩放距离、旋转速度系数。这样美术和策划可以自己调整不需要程序每次改代码。第二预览窗口的打开和关闭加个过渡动画。直接SetActive(true/false)会很生硬加一个简单的缩放或者淡入淡出动画体验会好很多。可以用DOTween或者自己写一个协程来做成本很低但效果提升明显。第三考虑多武器对比的需求。有些游戏允许玩家同时预览两把武器做对比这时候就需要两个预览窗口或者一个窗口里放两个模型。如果提前知道有这个需求在架构设计的时候就要把相机和Render Texture做成可复用的而不是写死一个。第四测试的时候一定要在真机上跑。编辑器里的表现和真机差异很大尤其是触摸手感、性能表现、分辨率适配这几个方面。我踩过好几次坑编辑器里看着好好的一到手机上就各种问题。建议至少在一台低端机和一台高端机上各测一遍。第五Render Texture的格式选择要慎重。ARGB32是通用性最好的但如果你的项目需要HDR效果比如武器上有发光材质那就需要用ARGBHalf。但ARGBHalf的显存占用是ARGB32的两倍移动端要权衡。这个功能后续还可以扩展的方向挺多的比如加一个自动旋转的展示模式、加一个背景切换功能、支持武器配件的实时装卸预览。核心的渲染和输入框架搭好了这些扩展都是在这个基础上做加法不会动到根基。