ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity简易三维场景完整开发指南:从建场景到WebGL发布

Unity简易三维场景完整开发指南:从建场景到WebGL发布 做了这么多年 Unity我越来越觉得“简易三维场景”其实是最好练手也最能暴露基本功的项目。很多朋友一上来就想着搭大型开放世界追着高精度模型和 PBR 材质跑结果工程文件乱成一锅粥运行帧率也惨不忍睹。我自己的经验是先老老实实做一个能走、能看、能交互的迷你场景把地形、灯光、阴影、角色控制、UI 交互和发布流程全部跑通后面再去碰数字孪生、大场景优化、XR 开发这些进阶方向才有底气。这篇就基于我最近用 Unity 做的一个简易三维场景项目把从建工程到发布 WebGL 的完整思路、核心参数和踩坑记录都整理出来适合刚学 Unity 不久、想完成第一个完整作品的朋友也适合做毕设或者项目演示底子的人参考。1. 项目定位与整体设计思路1.1 这个“简易三维场景”到底在做什么先明确一下项目边界。我要做的不是游戏 Demo也不是高精度的建筑可视化而是一个“麻雀虽小五脏俱全”的三维演示场景核心目标是验证 Unity 场景搭建的完整工作流。整个场景包含一块可自由行走的地形地面上有树木、石块、房子这类占位物件头顶有方向光负责照明和阴影角色用第一人称视角在场景里漫游同时界面上有一个滑动条可以控制时间变化场景里还放了几个可以拾取的小物品互动后会播放一个淡出效果。这种定位有几个好处。第一覆盖了 Unity 开发中最常用的模块地形系统、灯光渲染、角色控制、UGUI 交互、脚本生命周期、协程、Build 发布几乎每个初学者都需要过一遍第二所有资源都可以用 Unity 内置的基础几何体和自带资源生成不需要外部建模也不用买素材适合零美术基础的人第三后续扩展性很强不管是往数字孪生方向做成一个沙盘演示还是接入真实传感器数据做可视化底子都是这一套。1.2 Unity 版本与渲染管线选择关于版本我强烈建议用 LTS长期支持版。我这次用的是 Unity 2022.3 LTS国际版直接从官网下载即可。这里多说一句Unity Hub 里可以同时装多个版本不同项目用不同版本是很正常的不要追求最新的 6000.x稳定性永远比新功能重要。渲染管线的选择是一个容易纠结的点。Unity 目前主流有内置渲染管线Built-in、URP 和 HDRP 三套。对于简易三维场景我的建议是直接用内置渲染管线。虽然 URP 的效果上限更高但新手最容易碰到的“阴影问题”“光照发灰”“后期效果不生效”有一大半是渲染管线切换引起的配置差异。内置管线默认参数就能跑得很好文档和教程存量也最大等玩熟了再迁移到 URP 不迟。还有一个很容易被忽略的点项目创建时的模板选择。Unity Hub 新建项目时有 3D Core、3D URP、3D HDRP 等多个模板很多人会随手选一个标准 3D 模板后来装的资源包却对应 HDRP 渲染结果模型渲染出来变成洋红色。我这次选的是 3D Core 模板后期也没有换管线省了一堆麻烦。1.3 场景漫游的核心模块拆解在动手前我习惯先把一个“可漫游三维场景”拆成几个独立模块每个模块只干一件事。这个思维习惯很重要因为 Unity 工程一旦变大脚本之间相互引用会变得非常混乱反过来如果一开始就划分清楚后面加功能会非常顺畅。我的拆法是环境模块地形、天空盒、雾效、树木和建筑占位体光照模块方向光、环境光、阴影参数角色模块第一人称视角、CharacterController 移动、鼠标旋转视角交互模块可拾取物品、UI 提示、淡出效果界面模块滑动条控制时间、按钮事件发布模块WebGL 测试与打包每个模块独立开发、独立测试最后再串起来。比如角色控制不影响 UI 逻辑UI 逻辑不影响光照参数这样某个模块出问题时可以快速定位。我在实际操作中见过不少朋友把移动、交互、动画全部写在一个脚本里出 bug 的时候根本不知道是哪段代码的锅这种架构问题一定要从一开始就避免。2. 环境搭建地形、天空与基础摆件2.1 用 Unity 地形系统刷出一个能走的小场景Unity 自带的地形系统在 Window General Terrain 面板也可以直接在 Hierarchy 里右键 3D Object Terrain 创建。我这次建的地形是 200x200 单位分辨率 512这个配置在性能和细节之间比较平衡太小的地形没有漫游感太大的地形会引入烘焙耗时和帧率压力对简易项目来说没必要。地形创建之后第一步是“抬地面”。选中地形在 Terrain 工具栏切到 Raise/Lower Terrain 模式用鼠标左键在场景视图里涂抹可以按住 Shift 降低。这里有一个技巧笔刷大小Brush Size和强度Opacity要配合使用。做大面积山脉时用大笔刷低强度做局部坡道细节时用小笔刷这样地面起伏才自然。我见过有人一上来就用满强度的笔刷狠刷结果地形变成一坨锯齿形的土堆想修回来还得花更多时间。第二步是给地形刷纹理。在 Terrain 工具栏切到 Paint Texture先添加一张草地纹理作为基础层再加一张泥土纹理用于道路和坡地。这里要注意一个细节Unity 的“目标强度”Target Strength决定纹理的透明度刷道路时可以先把强度降到 0.3 左右多次涂抹比一次性刷满要自然得多。如果没有现成纹理用自带的 TerrainTextures 包就可以只要 Window Package Manager Terrain 里勾选导入示例资源。第三步是添加树木和植被。切到 Paint Trees选中一棵树预制体在场景里点几下就能种出树。但我这里要提醒一下简易场景不要种太多树也不要用高模树。树的数量一多Draw Call 会爆炸第一人称漫游时会明显掉帧。我这次整个场景只种了二十多棵树模型用的是 Unity 自带资源里的 Low Poly Tree远处再用雾效掩盖边界观感不差性能也完全扛得住。2.2 天空盒与全局光照参数细节天空盒决定整个场景的色调和氛围但很多新手会忽略。默认天空盒是蓝色的渐变天空有点假我习惯换成 Procedural 模式的 Skybox 材质参数可以自己调。在 Window Rendering Lighting 的 Environment 选项卡里把 Skybox Material 指到一个新建的 Procedural Skybox 材质然后把 Atmosphere Thickness 调小一点太阳亮度稍微提高地面会被环境光轻微照亮整体会舒服很多。环境光这一项特别关键。因为场景里没有补光角色背光面会非常黑看起来像硬砍出来的阴影。我通常会把 Environment Lighting 里的 Ambient Mode 设为 TrilightAmbient Sky、Ambient Equator、Ambient Ground 分别给一个偏蓝的亮色、中灰色和深灰色。这三个值的含义很好理解天顶光、水平环境和地面反射光调得好能让建筑的阴面也有体积感。这个参数在“阴影太黑”的排查中也经常用到先别急着加补光灯试试把环境光调高一点可能问题就解决了。最后是雾效。我这次在 Lighting 窗口勾选了 FogFog Mode 用 LinearStart 和 End 分别设为 100 和 300。雾效的作用不只是让远处变模糊它还能把地形和天空的交界线柔和掉让场景边缘不生硬。简易场景通常没有远处的高模细节用一层淡淡的线性雾是最省钱也最出效果的规避方案。2.3 用基础几何体搭出房子和道路场景里如果全是地形会显得空但没必要去下载建筑模型。Unity 内置的 Cube、Cylinder、Capsule 这些基础几何体组合一下就能做出简单的房屋和道路。我这次搭了一个“方盒子房子”四个 Cube 拉长做成墙面再用一个 Cube 压扁做屋顶屋顶材质给个偏红的纯色整体看上去像一个抽象风格的乡村小屋。柱子用 Cylinder围栏用 Cube 横着摆一排这种极简风格在演示场景里很常见比贴图拉满的写实模型还耐看。材质这一块我建议新建材质后把 Surface 类型设为 Standard 而非 URP/Lit然后主要调 Albedo 颜色、Metallic 和 Smoothness 三个参数。比如地面道路用深灰色把 Smoothness 调到 0.1 左右避免强烈的镜面反射水面可以用浅蓝色加 Smoothness 0.8看起来有反光效果。这里有个原理要理解Smoothness 越高越像光滑的塑料或水面越低越像粗糙的布或岩石这是 PBR 材质最基础的部分做任何场景都会遇到。3. 灯光、阴影与渲染细节处理3.1 方向光角色与阴影参数的设置方向光负责模拟太阳是整个场景最主要的可见光源。我在 Hierarchy 里创建一个 Directional Light旋转角度设置为 (50, -30, 0) 左右刚创建时默认朝下需要手动调整。旋转角度决定了太阳高度和阴影投射方向这直接决定画面层次感调出来效果最顺的角度通常让物体影子拉得比较长。然后是阴影设置。选中方向光在 Inspector 面板里把 Shadow Type 设为 Soft Shadows这个选项会开启柔和阴影边缘比 Hard Shadows 的锯齿边缘看起来真实很多。但要注意性能代价Soft Shadows 采样成本更高在简易场景里没有问题但如果是移动端项目就要小心。接着把 Shadows 下的 Strength 保持默认 1Normal Bias 和 Shadow Bias 一般是 0.2这是 Unity 的常见默认值也可以不调。阴影距离是一个大坑。在 Quality Settings 的 Shadows 区域有个 Shadow Distance 参数默认一般是 150 或 100。如果这个值太小比如默认在某些模板里只有 40那么角色走到离摄像机稍远的位置时地面上的阴影会突然消失非常违和。我这次直接设为 300覆盖整个地形范围且没有明显的性能下降。这个参数就是热词里“unity阴影问题”最常见的来源之一很多所谓阴影丢失的问题一查 Shadow Distance 就明白了。3.2 阴影边缘锯齿与阴影闪烁的排坑思路做三维场景时最常遇到的阴影问题有三个过硬、过暗、闪烁。过硬的阴影解决方式是在方向光上把 Shadow Type 改成 Soft Shadows同时到 Quality Settings 里把 Shadow Resolution 提高。这里我给一个参考简易场景用 Medium 和 High 之间足够改到 Ultra 之后边缘确实更细腻但 Draw Call 显存开销会涨而且对肉眼来说提升有限。阴影过暗常见原因是环境光太低导致阴影区域死黑一片。解决办法就是上一步里我说的 Trilight 环境光调整或者直接在方向光上把 Shadow Strength 从 1 降到 0.7 左右阴影不会那么“闷”。阴影闪烁通常叫 Shadow Acne是指在阴影边缘出现密密麻麻的噪点或网格状条纹。这个主要是因为 Bias 值太小。选中方向光把 Shadow Bias 从默认的 0.2 调高到 0.5 左右闪烁会明显减少。但 Bias 不能太大太大会造成 Peter Panning也就是阴影脱离物体本身的悬浮现象。这个值需要在具体场景里来回试没有万能参数我的经验是室外大场景从 0.5 起步室内小场景从 0.2 起步。3.3 利用雾效和后期调整整体氛围氛围处理上雾效是最省事的工具。我前面提到用 Linear 模式的雾但如果你希望雾效更自然可以把 Fog Mode 改成 Exponential Squared密度 Density 设为 0.005 左右这种模式的颜色变化更柔和适合清晨或黄昏的感觉。雾的颜色最好和天空盒地平线颜色一致否则远处会有一条明显的“雾墙”很出戏。我这次把雾色调成偏灰的蓝紫色和天空盒的暮色呼应整个场景看起来多了一层氛围感。如果还想再进一步可以在场景里加一个 Directional Light 的 Color 渐变模拟一天中不同时段的色调。比如中午的白光、傍晚的橙红、夜晚的深蓝。这个可以直接在灯光上改 Color 属性配合后面要做的滑动条时间控制效果会非常直观。但要注意的是夜景下如果灯光 Color 太暗场景会什么都看不见需要额外补一个低强度点光源作为补光或者调高环境光亮度。4. 角色控制与摄像机跟随4.1 第一人称控制器的完整脚本角色控制是最影响“能看能走”体验的模块。Unity 有自带的 Character Controller 组件用它来实现第一人称移动比较稳因为它自带碰撞和重力处理比直接用 Rigidbody 加 Transform 控制要简单且稳定。我创建一个空物体命名为 Player给它挂上 CharacterController 组件再挂上一个 Camera 作为子物体摄像机放在 Player 内部大约 1.7 身高的位置。下面是控制脚本可以直接抄走用。脚本要挂在 Player 空物体上Camera 作为子物体由玩家鼠标旋转控制。using UnityEngine; public class FirstPersonController : MonoBehaviour { [Header(移动参数)] public float walkSpeed 5f; public float runSpeed 10f; public float jumpHeight 1.2f; public float gravity -9.81f; [Header(视角参数)] public float mouseSensitivity 2f; public float pitchLimit 80f; private CharacterController controller; private Camera playerCamera; private Vector3 velocity; private float pitch 0f; private void Awake() { controller GetComponentCharacterController(); playerCamera GetComponentInChildrenCamera(); Cursor.lockState CursorLockMode.Locked; } private void Update() { // 鼠标控制视角 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; pitch - mouseY; pitch Mathf.Clamp(pitch, -pitchLimit, pitchLimit); playerCamera.transform.localRotation Quaternion.Euler(pitch, 0f, 0f); transform.Rotate(Vector3.up * mouseX); // 键盘控制移动 float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 move transform.right * horizontal transform.forward * vertical; bool isRunning Input.GetKey(KeyCode.LeftShift); float currentSpeed isRunning ? runSpeed : walkSpeed; if (controller.isGrounded velocity.y 0f) { velocity.y -2f; } controller.Move(move * currentSpeed * Time.deltaTime); // 跳跃与重力 if (Input.GetButtonDown(Jump) controller.isGrounded) { velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }这里有一个很多人会忽略的细节重力加速度如果是 -9.81然后直接乘 Time.deltaTime在地表附近速度其实很小但落到地面附近时会被 controller.isGrounded 拦截所以要在 grounded 时给 velocity.y 一个小的负值比如 -2f防止角色在地面时因为速度不为 0 而不断抖动。4.2 摄像机平滑跟随的深度原理解析很多项目需要第三人称视角这里我拆开讲一下摄像机跟随的原理。我见过不少新手把摄像机直接设为角色的子物体这样省事但会有两个问题一是角色快速旋转时摄像机被强制带动画面会很晕二是后续相机做插值或碰撞检测时父子关系会带来额外复杂度。正确做法是让摄像机通过脚本在 LateUpdate 里每帧跟着目标物体的 Position而不是把它挂成子物体。下面是常用的平滑跟随脚本挂在主摄像机上即可。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0f, 3f, -6f); public float smoothSpeed 5f; private void LateUpdate() { Vector3 desiredPosition target.position target.rotation * offset; transform.position Vector3.Lerp(transform.position, desiredPosition, smoothSpeed * Time.deltaTime); transform.LookAt(target.position Vector3.up * 1.5f); } }为什么必须用 LateUpdate 而不是 Update因为在 Update 中角色的移动、物理、动画都在同一帧进行如果你在 Update 里就读取了目标位置那么摄像机可能要比角色早知道一帧的位置导致画面的抖动或者轻微滞后。LateUpdate 在场景内所有 Update 执行完之后才运行摄像机这时候读取的目标位置一定是这一帧的最终位置所以跟随效果才平滑。这是做三维场景最容易忽略的细节之一新手往往遇到“画面抽搐”问题查了一圈发现就是 Update 和 LateUpdate 的时序问题。4.3 新输入系统Input System的兼容处理Unity 近期的模板默认启用新输入系统Input System Package这时候如果直接用 Input.GetAxis控制台会报错“InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package”。处理方案有三种一是 Player Settings 的 Active Input Handling 改成 Both也就是新旧输入都支持二是项目只用新输入系统在 Project Settings Player 里勾选然后改用 InputSystem 的 API三是像我们这次一样把 Active Input Handling 设为 Input Manager (Old) 或 Both直接沿用旧 API。考虑到简易场景快速上手的诉求我在 Player Settings 里把 Active Input Handling 设成了 Both。这样 Input.GetAxis 和 Input.GetButtonDown 这类经典 API 可以正常工作后续想引入新 Input System 的 PlayerInput 组件也不会报错。但我必须说明Both 模式会有额外的性能开销做正式项目时还是应该尽早选定一套输入方案不要两边混着用。5. UI 交互与动态效果扩展5.1 用滑动条控制场景时间变化做完角色漫游后场景还缺最后一个能“玩”的点。我加了一条 UI 滑动条用来控制太阳角度从而模拟一天中的时间变化。这个功能用 UGUI 的 Slider 做非常简单它也是热词里“unity做一个滑动条”这个需求的典型场景。操作步骤是在 Hierarchy 右键 UI Slider自动生成 Canvas 和 EventSystem然后设置 Slider 的 Min Value 为 0Max Value 为 360。我给方向光挂了一个脚本接收一个 float 参数实时改变灯光的旋转角度和颜色脚本如下using UnityEngine; using UnityEngine.UI; public class TimeController : MonoBehaviour { public Light sunLight; public Slider timeSlider; public float minSunAngle 0f; public float maxSunAngle 360f; private void Start() { timeSlider.minValue minSunAngle; timeSlider.maxValue maxSunAngle; timeSlider.value 40f; timeSlider.onValueChanged.AddListener(OnTimeChanged); OnTimeChanged(timeSlider.value); } private void OnTimeChanged(float value) { sunLight.transform.rotation Quaternion.Euler(value, -30f, 0f); float normalized Mathf.InverseLerp(minSunAngle, maxSunAngle, value); if (normalized 0.25f || normalized 0.75f) { sunLight.color new Color(1f, 0.7f, 0.6f); RenderSettings.fogColor new Color(0.6f, 0.5f, 0.5f); } else { sunLight.color Color.white; RenderSettings.fogColor new Color(0.8f, 0.8f, 0.8f); } } }这里有一个技巧把 Slider 的事件通过 OnValueChanged 绑定而不是在 Update 里每帧检测 Slider.value这样只有在用户拖动时才会触发逻辑避免每帧轮询带来的无谓开销。当时我实测下来动态修改方向光旋转角度会有极轻微的阴影延迟感但这个对演示场景来说完全可以接受。5.2 可拾取物品与淡出动效实现交互部分我放了三颗小金块玩家在场景中走动时可以按 E 拾取拾取后会播放一个 1.5 秒的淡出效果然后物品销毁同时在屏幕左上角显示一个“已拾取 X/3”的提示文本。首先是拾取检测。我给每个金块挂上一个 BoxCollider 并勾选 Is Trigger然后在 Player 的摄像机上写一个射线检测脚本朝屏幕中心发射一条射线检测命中物体的标签是否为 Pickup。下面是简化逻辑using UnityEngine; public class PickupController : MonoBehaviour { public Camera playerCamera; public float pickupRange 3f; public string pickupTag Pickup; public GameObject pickupHint; public Text pickupStatus; private int totalPickups 3; private int pickedCount 0; private void Update() { Ray ray new Ray(playerCamera.transform.position, playerCamera.transform.forward); RaycastHit hit; if (Physics.Raycast(ray, out hit, pickupRange) hit.collider.CompareTag(pickupTag)) { pickupHint.SetActive(true); if (Input.GetKeyDown(KeyCode.E)) { StartCoroutine(FadeOutAndDestroy(hit.collider.gameObject)); pickedCount; pickupStatus.text 已拾取 pickedCount / totalPickups; if (pickedCount totalPickups) { pickupStatus.text 全部拾取完毕; } } } else { pickupHint.SetActive(false); } } private System.Collections.IEnumerator FadeOutAndDestroy(GameObject target) { Renderer targetRenderer target.GetComponentRenderer(); Color originalColor targetRenderer.material.color; float duration 1.5f; float elapsed 0f; while (elapsed duration) { elapsed Time.deltaTime; float alpha Mathf.Lerp(1f, 0f, elapsed / duration); targetRenderer.material.color new Color(originalColor.r, originalColor.g, originalColor.b, alpha); // 注意标准材质需要设置 Transparency 模式才能正确显示半透明 targetRenderer.material.SetFloat(_Mode, 3f); targetRenderer.material.SetInt(_SrcBlend, (int)UnityEngine.Rendering.BlendMode.SrcAlpha); targetRenderer.material.SetInt(_DstBlend, (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); targetRenderer.material.SetInt(_ZWrite, 0f); targetRenderer.material.DisableKeyword(_ALPHATEST_ON); targetRenderer.material.EnableKeyword(_ALPHABLEND_ON); targetRenderer.material.renderQueue 3000; yield return null; } Destroy(target); } }这里有一个重要提醒在 Unity 的标准 Shader 中材质的 Rendering Mode 如果保持 Opaque你直接修改材质的 Alpha 值不会生效模型不会变透明。上面代码里手动设置了 _Mode、_SrcBlend 等内部属性把这个材质切到了 Transparent 模式才能看到真正的半透明淡出效果。这是热词里“unity脚本控制逐渐消失”最常见的坑很多人在这卡好久。如果你不想写这些内部参数也有更省事的替代方案把材质直接设为 Legacy Shaders/Transparent/Diffuse这是在 Inspector 里手动改脚本只管 Alpha。但那样材质的光照效果会打折扣因为 Transparent 不支持阴影相当于做了一次降级处理。我建议还是保留材质原有属性在脚本里用上述方法动态切模式效果更完整。5.3 扩大 UI 按钮点击范围与 Raycast 穿透问题在做 UI 时很多人会遇到“按钮太小点半天点不中”的问题也就是热词里“unity 如何扩大按钮的点击范围”。新手最容易想到的做法是把 Image 组件拉大但这样图片会被拉伸变形。正确做法是把一个空物体挂在按钮下给它挂上 Image 组件并把 Image 的 Source Image 设为空Color 里 Alpha 设为 0这样一个不可见的透明层就可以自由拉伸用来扩大点击区域。但这里有个坑透明 Image 的 Raycast Target 属性默认是勾选的它会拦截射线导致它后面或者它底下的其他 UI 元素点不到。如果你在按钮上层放了一个透明层用来做遮罩那么按钮很可能就点不了了或者按钮和透明层同时相应事件出现奇怪的行为。需要把不需要响应的透明层 Image 的 Raycast Target 取消勾选。按钮点击区域扩大后我还会在 Graphic Raycaster 的 Blocking Objects 设置里调整一下但简易项目里不需要深入这一块。记住一个原则UI 的射线检测是基于 Graphic 的透明只是影响视觉只要 Raycast Target 勾选了它照样能拦截点击。6. WebGL 发布与性能优化实战6.1 从编辑器到 WebGL 的发布流程场景做好之后我选择发布成 WebGL这样可以直接在浏览器里跑也方便给朋友看和嵌入到网页。流程是File Build Settings选中 WebGL点 Switch Platform然后点 Build。这一步要注意Unity 第一次切到 WebGL 平台会花不少时间做转换别以为卡死了。WebGL 的 Player Settings 有几个参数很关键。在 Resolution and Presentation 里Fullscreen Mode 可以选 Windowed 或 FullscreenCanvas 分辨率建议设为 1280x720 或者自适应具体看你的演示目标。Publishing Settings 里的 Compression Format 建议选 Brotli这是现在浏览器支持率最高的压缩格式能显著减小包体积生成速度也快。Memory Size 我设成了 256MB这个值如果太小复杂场景加载时容易崩溃热词里“unity 发布 webgl 使用 idbfs 写入失败”就和这个还有浏览器存储有关。打包部署时最简单的方式是本地建一个 HTTP 服务来托管生成的文件夹因为直接双击 index.html 用 file:// 协议打开浏览器会禁止加载很多资源画面通常是灰的或有跨域报错。部署到正式服务器要保证 HTTPSWebGL 的很多浏览器 API包括 IndexedDB 和全屏在非安全上下文里会受到限制。6.2 IndexedDB 写入失败问题排查我在 WebGL 本地服务器上测试时遇到了“idbfs 写入失败”的报错日志里会提示 IndexedDB 存储失败。这个问题本质上是浏览器在尝试把 Unity 的持久化数据写入 IndexedDB一种浏览器内置的数据库但写入操作没有成功。常见原因有三个。第一是浏览器处于隐私模式或无痕模式很多浏览器在隐私模式下禁止 IndexedDB 写入这个只能换普通模式测。第二是存储空间不足Unity WebGL 默认会把存档数据存进 IndexedDB如果之前的浏览器历史数据占满了配额写入就会失败。处理办法是清理浏览器缓存和站点数据或者在浏览器设置里查看该站点占用的存储空间。第三是跨域 iframe 的限制。如果你把 WebGL 嵌入到别人的网页的 iframe 里而父页面没有设置恰当的权限策略IndexedDB 可能被拒绝。在实际发布场景里这种问题需要后端设置跨域响应头但简易演示的话直接在新标签页打开就行。还有一个排查思路是看 Player Settings 里的 WebGL 内存大小。我遇到过 Memory Size 只能设到 128MB但场景稍微复杂就局部崩溃的情况调到 256MB 之后明显稳定很多。但内存也不是越大越好因为浏览器会为页面预留这么多内存设置值太高反而会让移动端浏览器加载过慢。6.3 简易场景必备的优化清单做 WebGL 发布时性能优化不能省。这里分享几个我实测下来性价比最高的优化手段。第一是合批。同一种材质、同一个图集的 UI 和几何体可以被 Unity 自动合批Draw Call 数量会大幅下降。在做场景时尽量让同样材质的物件共享同一个材质实例不要每个物件都新建一个材质。我场景里几十棵树共用一棵树的低模和一个材质渲染开销非常低。第二是光照烘焙。方向光的阴影如果仍然用实时计算室外场景树多的区域会比较吃性能。简易场景里可以不用立即上烘焙因为场景规模小实时阴影影响不大。但如果后面扩展到数字孪生这类大场景一定要学会烘焙光照贴图把实时光照关掉效果和性能都会更好。这属于进阶话题我这里只提出方向不作为当前项目的必需步骤。第三是纹理大小和压缩。如果用了外部贴图记得在导入设置里把 Max Size 调低比如 1024 或 512Android 或 WebGL 平台要选择合适的压缩格式。我这次场景基本没使用贴图全部用纯色或程序化生成的材质省了很多空间加载速度也很快。WebGL 打包出来的文件在本地开发服务器上首屏加载大约 5 秒左右配合 Brotli 压缩大小控制在 30MB 以内这个体量对简易场景来说可以接受。第四是使用 Profiler 面板查看瓶颈。在 Window Analysis Profiler 里可以实时看 CPU、GPU 和内存占用定位到底是哪段代码或者哪个渲染项拖了后腿。我遇到过一个小细节地形树在远处绘制了大量三角形通过检查 Profiler 发现 Draw Call 偏高后来调整了树的距离策略就把数据降了下来。这个习惯要在做项目的初期就养成不要等发布到浏览器才发现顿卡。我再补充一个关于热词“unity游戏优化”的心得对于这种小场景最主要的优化手段是“少放东西”和“共享材质”而不是疯狂调整代码。场景里只有 20 棵树、3 个拾取金块、1 座小房子、1 块地形根本没有多少可渲染对象帧率自然全程稳定 60。很多游戏卡顿不是引擎不行是美术资源堆得太满这在简易场景阶段一定要克制。7. 实操心得与后续扩展建议做完整个简易三维场景我最深的一个体会是不要一上来就追求大而全先把完整链路跑通比什么都强。之前我自己也走过弯路最开始想做一个包含河流、山谷、植被、天气系统的复杂场景弄了三天连地形都还没刷完后来果断砍需求一个 200x200 的小地形加几棵树就完成了第一版。当第一版能跑、能走、能发布的时候那个成就感是无可替代的。在使用 Unity 的过程中我还发现一个容易被忽视的细节项目文件夹的组织。创建文件夹时就要分成 Art、Scripts、Scenes、Prefabs 等几个目录后续资源多了会好找很多。这个习惯看似简单但真的遇到数字孪生、PLC 通信、XR 开发这些进阶需求时好的工程结构比任何优化技巧都重要。最后再分享一个实际开发小技巧在做场景时尽量把同一个功能的脚本和资源放在一起并频繁使用 Prefab。比如树做一个 Prefab、金块做一个 Prefab、房子做一个 Prefab改动一处就能统一更新场景中的所有实例。这次项目里房子最开始是用多个 Cube 手工摆的后来发现门的位置不对得一个个调整浪费了不少时间改成 Prefab 之后就只需要改一个预制体所有场景实例自动同步。如果你打算继续深入这个简易场景可以往几个方向发展接上串口或 PLC 数据把滑块改成实时传感器数值展示就能变成一个简单的数字孪生演示台把第一人称换成第三人称再接上 Mapbox 的大范围地图数据就能开发城市漫游类项目如果想跑在 VR 设备上把摄像机换成 XR Origin结合 Pico 这类头显又是一个完全不同的体验方向。这些扩展都基于同样的场景搭建基本功把这篇文章里这套流程吃透后面换任一方向都只是换层皮底层逻辑不会变。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进