ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity场景搭建与角色移动实现全流程详解

2026/9/24 18:59:04 拓冰建站 浏览量
Unity场景搭建与角色移动实现全流程详解 最近帮朋友搭了一套Unity小Demo从空白工程一路折腾到角色能在场景里流畅跑跳过程中踩了不少坑也整理出一套比较顺手的搭建流程。这篇文章就把整个“Unity场景搭建”和“角色移动”的实现思路、关键参数、脚本写法以及调试心得完整记录下来适合刚接触Unity不久、想自己动手做一个小关卡的朋友参考。先说清楚这篇博文能解决什么问题很多人一上来就急着拖模型、写移动代码结果场景又暗又乱角色不是穿墙就是镜头乱晃。其实场景搭建和角色移动并不是两个独立的环节场景结构会影响角色移动的写法角色参数又反过来决定场景边界、碰撞体怎么摆。所以我会把从项目规划、场景地形、灯光氛围、角色控制器到相机跟随的全链路拆开讲每一步都给出实际可复现的参数和代码顺便把那些调试时特别容易踩的坑提前指出来。文章的阅读对象主要是有一定Unity基础、但缺少完整项目经验的开发者。哪怕你只是刚装好Unity 2022照着这篇文章一步步操作也能在半天内做出一个能跑能跳、镜头跟随正常的小关卡。如果你已经写过角色移动代码那重点看第三章的参数调优和第四章的问题排查应该能帮你解决不少旧毛病。1. 场景搭建的第一步项目规划比动手更重要1.1 先想清楚你要做什么样的场景很多人打开Unity就拖一个Plane出来然后开始摆Cube最后角色怎么走都别扭。原因很简单没有想清楚场景的核心体验。动手写代码前先回答三个问题场景是室内还是室外室内场景对光照烘焙要求高室外场景要处理地形、远处遮挡和天空盒。角色移动是第三人称还是第一人称第三人称需要相机跟随逻辑第一人称要考虑头部视角和碰撞体高度。场景大小是多少是10x10的小房间还是1000x1000的开放世界决定了移动速度、碰撞体精度和性能优化策略。以我常用的小型第三人称Demo为例我会把场景规模控制在100x100米以内地面用Terrain或一个Mesh Plane角色移动速度设定在4米/秒左右这样玩家从场景一端跑到另一端大约25秒节奏刚好不会觉得场景太空旷也不会因为太小而显得局促。确定这些后再动手后面每一步都有依据。比如角色胶囊体高度设为1.8米移动速度4米/秒跳跃高度1.2米这些参数都不是随手填的而是按照场景尺度和真实感算出来的。1.2 渲染管线选错后面全是返工Unity从2019版本开始渲染管线分成内置管线Built-in Render Pipeline、URPUniversal Render Pipeline和HDRPHigh Definition Render Pipeline。不少人建工程的时候随手选了默认的3D模板等到想加后处理效果才发现内置管线配置起来很别扭。我的建议新项目直接用URP模板。原因很直接URP在移动端跑得动桌面端效果也不差后期加后处理比如泛光、环境光遮蔽、色调映射都是开箱即用。HDRP效果最好但对硬件要求高适合做影视级画面内置管线虽然万能但很多新功能不再更新属于付出同样工作量但收益偏低的选择。需要说明的是如果你的工程已经建成了内置管线也不是非得重建工程可以到Window Package Manager里安装URP包并创建渲染管线资产然后到Project Settings Graphics里指定引用即可。不过这种方式在项目后期切换可能会引起材质偏色建议在项目早期就定好管线。管线选好后默认的线性空间、HDR高动态范围等参数也建议保持开启。线性空间能让光照计算更真实HDR配合后处理能让亮部不过曝这是提升画面质感性价比最高的两个开关。1.3 规划层级结构和命名规范场景搭建中很多人忽略的一点是Hierarchy窗口里的层级组织。看似不影响功能但场景物件一多找某个对象能找半天Inspector面板也全是资源路径提示效率低到怀疑人生。我习惯的层级结构是这样SceneRoot ├── Environment地形、建筑、植被 │ ├── Terrain │ ├── Buildings │ └── Vegetation ├── Props道具、装饰物 ├── Lighting灯光、反射探针 ├── Navigation导航网格相关后续做AI用 └── Player角色及相机命名规则也很重要。所有对象名禁止叫Cube (1)、Sphere (2)这种Unity默认生成的名字至少改成“Ground_Main”、“Wall_Left”、“Rock_Small_01”这样的格式。这样做的好处不仅是方便自己找资源更重要的是后期写代码查找物件、做预制体替换或者队友接手项目时能省下大量沟通时间。这算是一个全是免费但很多人都没做到的习惯。2. 从空白场景到可视化关卡地形与美术资源落位2.1 用Terrain工具刷出基础地形我习惯先创建TerrainGameObject 3D Object Terrain把Terrain参数设置为常见的中型地图配置参数推荐值说明Terrain Width / Length500场景边长单位米Terrain Height50最高处海拔Resolution X / Y256分辨率越高细节越多但性能开销也大Pixel Error5数值越小地形越精细但运行时顶点开销越大分辨率不是越高越好。我用256作为默认值因为大多数小Demo不需要专业地形那么细的顶点密度。如果后面发现山体棱角太明显再把分辨率提高到512同时注意检查场景帧率。地形高度调整我一般用Raise/Lower Terrain工具画笔大小设为30~50Opacity设为30~40慢慢堆出山脉轮廓再用Set Height工具把大片区域压平用来放建筑或NPC出生点。刷地形有一个独门技巧不要一次性把高度拉到目标值而是多次、低透明度、大面积地刷出来的地形会自然很多不会出现悬崖一样的方块痕迹。地形材质方面选中Terrain后在Terrain Layers里创建Terrain Layer把草地、岩石、泥土等贴图分别添加进去然后用Paint Texture工具在对应区域涂抹。按照官方推荐贴图的Mask Map、Normal Map记得在导入设置里把Texture Type改成Terrain否则刷出来的地表会很奇怪。2.2 光照设置决定场景氛围的第一要素场景灰蒙蒙往往不是美术资源的问题而是光照没调好。在场景中创建一个Directional Light当作太阳设置Rotation大约在X轴20度、Y轴30度颜色偏暖一点比如RGB(255, 244, 214)Intensity设在1.0~1.2之间开启Shadows的Soft Shadows模式。这样能得到一个比较自然的光照基线。同时建议打开Window Rendering Lighting面板把Environment Lighting的Source设为SkyboxAmbient Intensity保持在1.0Environment Reflections可以留默认。这样场景中阴影区域不会死黑整体会有环境光来补亮。如果你是第一次接触光照记住这条经验光照不是越亮越好有对比才有层次。主光源负责模拟太阳给一个明显的方向暗部区域靠环境光补一点亮度如果还需要烘托氛围就加一个暖色的Point Light或Spot Light放在篝火、路灯这类焦点上。这里还要说一个容易遗漏的点实时阴影很吃性能。小型场景里可以全部用实时光源但场景一大建议把光源的Shadow Distance控制在50米左右在Project Settings Quality里设置超出这个距离的物件就不生成阴影能明显提高帧率。2.3 场景物件摆放与Prefab复用手动摆放物体也有讲究。很多人习惯用鼠标一点点拖动效率低且容易叠在一起。推荐三个快捷键V键顶点吸附可以精确把物件贴到地面或墙面上。CtrlD快速复制适合连续摆放相同物品。CtrlShiftF将视图对齐到选中的物体方便从特定视角观察。摆放时注意物体底部与地面之间不要有缝隙。比如一棵树直接拖到Terrain上很可能是悬浮的要用V键把树根部的顶点吸附到地形表面。墙角、石头这类物件也一样吸附之后如果还有微小缝隙就把Transform的Y轴往下调0.01~0.05米实测这样能避免阴影缝隙和角色脚下穿模。对于重复使用的物件比如路灯、树木、石块一定要做成Prefab。做法很简单把摆放好的对象拖到Project窗口的Assets目录下它会生成一个预制体文件后面需要修改颜色、缩放、碰撞体时直接修改Prefab本体场景中所有引用它的实例都会同步更新。还可以创建Prefab Variant比如基础的路灯Prefab派生出“路灯_暖色光”和“路灯_冷色光”两个变体灵活程度比复制粘贴高得多。2.4 碰撞体场景搭建最容易忽略的隐形墙很多新手做完场景角色一走就掉出地图或者走到墙角直接穿透过去问题基本都出在碰撞体上。在Unity中一个空Mesh是不会参与物理碰撞的。你从Asset Store下载的房子模型如果不额外添加碰撞体角色可以穿墙而过。两种解决方案给单个物体添加Collider。比如Cube加Box Collider球体加Sphere Collider角色能正常阻挡。使用Mesh Collider。直接选模型自带的网格做碰撞体适合复杂但不运动的场景物件。Mesh Collider虽然省事但性能开销比基本几何碰撞体高很多Unity官方也建议优先用基本几何碰撞体组合出近似的形状。比如一堵墙用一个“瘦长”的Box Collider套进去角色碰撞效果和Mesh Collider几乎没差别但物理计算快一个量级。地形Terrain默认自带碰撞但你手动添加的Plane不会。这也是为什么很多教程里建议用Terrain而不是Plane做地面。如果你确实想用Plane记得给它加一个Mesh Collider。场景边界也要处理。最简单的方式是在地图四周摆放几面透明的墙或者用四个Box Collider围一圈确保角色不会从边缘掉出去。这个细节看着低级但忘了加就会让玩家产生“这个游戏很糙”的感觉。3. 角色移动系统的搭建与参数调优3.1 角色控制器的两种主流方案Unity中实现角色移动最常用的是CharacterController和Rigidbody两种方案。很多人纠结选哪个其实判断标准很简单对比项CharacterControllerRigidbody是否受物理引擎影响不受重力以外的力影响受力和碰撞影响适用场景传统角色控制、动作游戏需要真实物理交互的场景实现难度较低控制感强较高参数多典型问题无法处理复杂物理交互容易抖动、漂移我的经验如果做的是传统RPG、射击游戏、平台跳跃这类对操作手感要求高的项目用CharacterController。它自带碰撞检测、自动处理斜坡和台阶写起来省心。如果做的是物理解谜、载具、布娃娃这种需要力和碰撞模拟的项目用Rigidbody。还有一套方案是两者结合外层用Rigidbody控制移动和碰撞内部用CharacterController做额外限制。这种做法适合对移动精度要求极高的硬核动作游戏不适合新手阶段尝试容易把物理系统搞乱。3.2 用CharacterController实现第一人称角色移动先创建一个简易第一人称控制器。在Hierarchy中新建一个空物体命名为Player给它添加CharacterController组件然后把Camera拖到它下面作为子物体位置调整到(0, 1.7, 0)模拟人眼高度。CharacterController的参数建议参数推荐值说明Height1.8角色高度和相机高度匹配Radius0.3胶囊体半径控制角色宽度Center(0, 0.9, 0)底部对齐到地面Slope Limit45超过该角度的坡会阻挡Step Offset0.3能自动跨上的台阶高度移动脚本我一般不用Update里直接改transform.position的方式而是用CharacterController的Move方法因为它会自动处理碰撞using UnityEngine; [RequireComponent(typeof(CharacterController))] public class PlayerMovement : MonoBehaviour { private CharacterController controller; public float moveSpeed 4f; public float jumpHeight 1.2f; public float gravity -9.81f; private Vector3 velocity; private bool isGrounded; void Start() { controller GetComponentCharacterController(); } void Update() { isGrounded controller.isGrounded; if (isGrounded velocity.y 0) { velocity.y -2f; } float x Input.GetAxis(Horizontal); float z Input.GetAxis(Vertical); Vector3 move transform.right * x transform.forward * z; controller.Move(move * moveSpeed * Time.deltaTime); if (Input.GetButtonDown(Jump) isGrounded) { velocity.y Mathf.Sqrt(jumpHeight * -2f * gravity); } velocity.y gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }在垂直运动方面为什么手动给一个初始速度因为跳跃高度1.2米时根据物理公式 v² -2gh速度为Mathf.Sqrt(1.2 * -2 * -9.81) ≈ 4.85米/秒。在重力为-9.81米/秒²的情况下角色会上升到约1.2米的高度再下落和真实世界的跳跃手感接近。如果直接用固定速度往上挪就跳不出这种抛物线效果。第一人称还需要视角旋转。把下面代码挂到Camera上或者在PlayerMovement里扩展鼠标控制public float mouseSensitivity 100f; public Transform playerBody; private float xRotation 0f; void Update() { float mouseX Input.GetAxis(Mouse X) * mouseSensitivity * Time.deltaTime; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity * Time.deltaTime; xRotation - mouseY; xRotation Mathf.Clamp(xRotation, -90f, 90f); transform.localRotation Quaternion.Euler(xRotation, 0f, 0f); playerBody.Rotate(Vector3.up * mouseX); }把playerBody指向Player根物体这样鼠标横向转动带动全身纵向转动只影响相机。xRotation用Clamp限制在-90到90度之间防止镜头翻转到人物背后。3.3 第三人称相机的平滑跟随第三人称和第一人称最大的区别在相机。直接用Camera的父物体跟随角色会出现镜头抖动、穿墙、和角色重叠等问题所以需要通过脚本来控制。最简单的平滑跟随方案public class CameraFollow : MonoBehaviour { public Transform target; public float distance 5f; public float height 2f; public float smoothSpeed 10f; void LateUpdate() { Vector3 desiredPosition target.position - target.forward * distance Vector3.up * height; Vector3 smoothedPosition Vector3.Lerp(transform.position, desiredPosition, smoothSpeed * Time.deltaTime); transform.position smoothedPosition; transform.LookAt(target); } }这里必须放在LateUpdate而不是Update里。原因是Update阶段角色刚移动完LateUpdate阶段相机再去跟随可以避免角色移动和相机跟随在同一帧里产生位置滞后。这个顺序问题不是玄学Unity官方也是这样建议的。距离、高度、平滑速度三个参数需要根据场景尺度调整。默认5米、2米、10在大多数场景里还算舒适但如果你把角色移动速度调到了6米/秒smoothSpeed最好提高到12以上否则镜头会像“拖影”一样跟不上。再进一步相机穿墙是一个高频问题。最简单的应对方案是加透明墙检测从角色向相机方向打一条射线如果碰到墙就把相机拉到墙前。public class CameraCollision : MonoBehaviour { public Transform target; public float minDistance 1f; void LateUpdate() { RaycastHit hit; if (Physics.Linecast(target.position, transform.position, out hit)) { float newDistance Mathf.Clamp(hit.distance - 0.2f, minDistance, 100f); transform.position target.position - target.forward * newDistance Vector3.up * 2f; } } }这里用Linecast判断相机与角色之间是否有障碍物有的话就把相机往前拉近避免墙面挡住视线。Owning让相机始终保持在合理的观察距离内这是第三人称项目的标配。3.4 用Rigidbody实现物理移动适合物理交互场景如果你的角色需要推箱子、被击飞、或者踩在移动平台上那就得用Rigidbody方案。给Player添加Rigidbody组件把Use Gravity勾上Interpolate设置为InterpolateCollision Detection设置为Continuous Dynamic。Continuous Dynamic模式对高速移动的物体更准确能减少“高速穿透”问题。移动脚本不再是直接改transform而是通过速度控制[RequireComponent(typeof(Rigidbody))] public class PlayerMoveRigidbody : MonoBehaviour { private Rigidbody rb; public float moveSpeed 5f; public float jumpForce 6f; void Start() { rb GetComponentRigidbody(); } void Update() { float x Input.GetAxis(Horizontal); float z Input.GetAxis(Vertical); Vector3 move transform.right * x transform.forward * z; move.Normalize(); Vector3 velocity move * moveSpeed; velocity.y rb.velocity.y; rb.velocity velocity; if (Input.GetButtonDown(Jump)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } }这里最关键的一行是velocity.y rb.velocity.y如果直接把move乘以moveSpeed赋给velocity会覆盖掉重力产生的纵向速度角色就会变成“悬浮”状态永远不落地。记住处理Rigidbody移动时一定要保留物体的原有Y轴速度。物理方案也有它的坑。最常见的是Rigidbody在斜坡上会打滑因为物理引擎默认没有“斜坡锁定”的逻辑。解决方式有几种我常用的做法是把角色刚体的Constraints里的Freeze Rotation全部勾上这样角色不会因为物理碰撞而翻倒对于斜坡再接一个向斜坡面方向的射线检测来修正速度方向。3.5 新输入系统和老输入系统的兼容问题如果你用的是Unity 2021可能会碰到新输入系统Input System Package和老的UnityEngine.Input API同时存在的问题。新输入系统在Project Settings Player Active Input Handling里可以切换可以选择Input Manager (Old)、Input System Package (New)、或者Both。我的建议是新项目直接用新输入系统因为它支持手柄、触屏、键鼠统一管理后面做跨平台不用重写输入逻辑。但是要注意新输入系统默认事件机制是异步回调的如果你在Update里直接写Input.GetAxis(Horizontal)会拿不到数据必须从InputAction中读取。简洁的做法是在Inspector里创建.InputActions资产然后在脚本中引用using UnityEngine.InputSystem; public class PlayerInput : MonoBehaviour { [SerializeField] private InputActionAsset actions; private InputAction moveAction; void OnEnable() { moveAction actions.FindActionMap(Gameplay).FindAction(Move); moveAction.Enable(); } void Update() { Vector2 input moveAction.ReadValueVector2(); // 用 input.x / input.y 驱动移动 } }如果你只是想快速做原型验证切回“Both”模式然后用老Input API也能跑通。但正式项目迟早要迁移到新输入系统越早学越好。4. 调试中高频踩坑与排查实录4.1 角色抖动、穿模、下坡打滑角色移动抖动是最常见的反馈之一。排查顺序我一般按这个来现象可能原因解决方案角色整体抖动相机跟随放在Update里改成LateUpdate高频微抖CharacterController和Rigidbody混用同一物体只用一种控制器穿墙碰撞体Mesh Collider缺失添加Box Collider组合下坡打滑速度方向没有按地面修正加地面检测用地面法线修正移动方向快速移动穿透物体碰撞检测模式不够碰撞检测设为Continuous/Continuous Dynamic其中“Rigidbody CharacterController混用”是新手最常踩的重灾区。CharacterController本身处理碰撞Rigidbody又要求物理引擎控制物体位置两者叠加就会导致内部计算冲突表现就是抖动、卡顿、甚至无法移动。要么只挂CharacterController要么只挂Rigidbody二选一。4.2 相机穿墙的几种处理策略前面提到了Linecast透明墙检测那是对付墙体遮挡的基础手段。但如果场景里有玻璃、透明物体Linecast会把玻璃也当成遮挡物相机就会莫名其妙拉近。解决办法是把透明物体的Layer设置成“Transparent”然后在射线检测时使用LayerMask过滤public LayerMask obstacleMask; Physics.Linecast(target.position, transform.position, out hit, obstacleMask);然后在Inspector里取消勾选透明层只让不透明的墙体参与遮挡检测。4.3 角色悬空或陷入地面CharacterController的高度的计算结果要额外关注。如果角色从预制体拖进来时Transform的位置是(0, 0, 0)而Center也设了(0, 0.9, 0)那胶囊体的底部其实是-0.9米肉眼看到的就是角色半截埋在地里。解决办法是把Player物体的Transform Y设为0.9米或者把Center改为(0, -0.9, 0)。更稳妥的做法是写代码时生成角色后先执行一次对齐地面的操作void SnapToGround() { Ray ray new Ray(transform.position Vector3.up * 10f, Vector3.down); if (Physics.Raycast(ray, out RaycastHit hit, 20f)) { transform.position hit.point Vector3.up * controller.height * 0.5f; } }4.4 场景大了帧率骤降小型场景看不出性能问题但当你把地形扩大到500x500摆上几百棵树的时候帧率就会开始掉。性能优化的优先级我按实际收益排遮挡剔除Occlusion Culling在Window Rendering Occlusion Culling里烘焙让相机看不到的物体不参与渲染。LOD Group给高模树、建筑添加LOD远处显示低面数版本。合并静态物体把不动的建筑物勾选Static启用Static Batching。阴影距离调小把Quality里的Shadow Distance从默认的100降到40~50。灯光数量控制场景中实时灯光最好不超过2个其余用Lightmap烘焙。按照这个顺序处理大多数中小型场景的帧率能从30FPS提升到60FPS以上。我曾经把一个测试场景的阴影距离从100调到40再合并了几个静态建筑帧率直接从45跳到90优化效果非常立竿见影。4.5 常见问题速查表现象可能原因快速解法角色无法跳跃Jump输入没绑定检查Project Settings Input Manager里的Jump角色持续下坠没加地面碰撞体给地面加Collider或改用Terrain按WASD没反应新输入系统未启用切到Both模式或改用InputAction镜头晃得厉害smoothSpeed太低提到10~15角色穿透墙体Mesh Collider没加用Box Collider组合相机乱转鼠标灵敏度太高mouseSensitivity调到80~120之间角色被卡在角落CharacterController半径太大半径从0.3降到0.2角色原地抖动数据库脚本里直接改transform改用CharacterController.Move相机穿高墙Linecast未过滤透明物体用LayerMask过滤透明层阴影闪烁Shadow Distance过大或Shadow Bias异常调整Shadow Distance和Bias5. 项目扩展建议与个人经验5.1 从单角色到多角色的扩展思路当角色移动稳定后你大概率会发现玩家体验还缺很多东西动画、音效、交互、敌人。不要急着把所有东西一次性塞进去我的建议是按模块迭代。第一优先级是动画。把角色控制器跑通后接入Animator用移动速度驱动Blend Tree里的“Speed”参数可以让角色从静止到奔跑过渡自然。此时之前的moveSpeed就派上用场了直接在脚本里引用animator.SetFloat(Speed, velocity.magnitude)即可。第二优先级是交互。添加一个Interaction脚本用Raycast或Trigger Collider检测玩家前方物体按E键触发事件比如开门、拾取道具。这个模块和角色移动解耦独立开发不会破坏已有系统。第三优先级才是敌人AI。最简单的方式是用Unity自带的Navigation也就是NavMesh烘焙把场景地面设置成Walkable敌人生成后让NavMeshAgent自动寻路到玩家位置。注意在角色控制器里加上transform.position agent.nextPosition来同步物理位置避免角色模型和NavMeshAgent不同步。5.2 我踩过最深的坑场景与角色独立开发的代价最后分享一个项目教训。我早期做项目时很喜欢先把场景细节全部打磨完美再去做角色移动。结果场景做了两个星期开始写移动脚本时才发现地形高度变化太大角色的跳跃高度完全不够又花了一个星期调整地形和碰撞体分布等于白白返工。现在我的个人流程是先搭建一个场景“毛坯房”一块平整地形、几堵墙、几个台阶、一把椅子然后第一时间接入角色移动。等移动手感调顺、跳跃和碰撞都稳定了再回去补场景细节。这个过程虽然看起来绕了一圈但实际上是最省时间的做法。场景的可玩性依赖于角色能否顺畅地和场景互动移动系统就是那个“测试探针”先用它测试地形边界和碰撞体比后面再返工划算得多。做场景搭建和角色移动这两件事本质上是同一个问题让玩家在虚拟空间中的行动符合直觉。场景提供的是空间逻辑角色移动是空间逻辑的验证者。两个系统从一开始就要放在一起调而不是各做各的再合并。这也是我给你最核心的操作建议。