资讯中心

Unity场景搭建与角色移动:从基础到实战的完整指南

📅 2026/9/24 18:58:56
Unity场景搭建与角色移动:从基础到实战的完整指南
做Unity开发这些年经常被刚入行的朋友问一个问题场景搭建和角色移动这么基础的东西还需要单独拿出来研究吗我的回答向来都是——越基础的东西越能拉开项目差距。你去看网上那些半成品的Demo十个里有八个是角色滑着走、镜头乱晃、人物卡进地板里。问题通常不是出在复杂系统上就是“基础功”没练扎实。Unity的场景搭建和角色移动看似是两个独立的模块实际上一旦角色真正跑起来你会发现它们耦合得比你想象中深得多地面材质影响移动物理反馈碰撞体尺寸决定角色能不能顺畅通过摄像机跟随手感直接决定玩家对这个“移动”评价是好是坏。这篇文章我不打算讲那种“点击GameObject然后拖一个Cube进来”的幼儿园教程。我以一套完整可落地的第三人称角色移动Demo为主线把场景搭建的核心要点、角色移动的三种常见实现方案、摄像机跟随的选型逻辑、以及我自己踩过的坑全部串起来讲一遍。无论你是刚装好Unity准备做第一个小项目的新手还是做了几个Demo但总觉得手感不对的老朋友这篇文章都值得你花二十分钟看完。1. 场景搭建先把“地面”和“光”弄对再谈后续1.1 动手前先定三件事单位、轴向、坐标原点很多新手搭建第一个场景时上去就拉一个Plane再拖几个Cube然后突然发现角色模型放到场景里不是飘在天上就是陷进地里半截身子。这背后的根源往往不在模型而在Unity的坐标系和单位制上。Unity默认的度量衡是1个Unit等于现实中的1米Y轴朝上。这意味着你在建模软件里做一棵10米高的树导入进来就应该是10个Unit高一个1.8米的人类角色导入后高度也差不多是1.8。但问题在于美术同学从不同的软件导出模型时单位的换算标准不一定统一——有的软件用厘米有的用英寸甚至有的DCC工具导出FBX时把轴向也改了。所以我每次拿到新模型的第一件事不是往场景里拖而是先检查模型导入面板里的Scale Factor是否合理以及模型本身是否“站在”Y轴的原点上。坐标原点的重要性则体现在出生点和初始朝向。我习惯把场景里的Player出生点放在世界坐标(0, 0, 0)附近而不是随手摆在一个“看起来差不多”的位置。原因很简单后续做NavMesh寻路烘焙、做多人联机同步、做存档读档所有逻辑都依赖于一个稳定的坐标基准。如果出生点乱七八糟调试的时候光是找坐标就能浪费大把时间。另外还有一个特别容易被忽略的细节场景里所有可交互物体的碰撞体统一采用“角色能通过的最小尺寸”为标准。举个例子两个障碍物之间过道宽度是1.2个Unit而你的角色碰撞体半径是0.6那刚好能过但游戏手柄摇杆稍微推偏一点就会卡住。所以在搭建场景时过道宽度至少留出角色碰撞体直径的1.3倍以上这个经验数值能帮你免掉大量后期的“卡墙”Bug报告。1.2 用Plane还是Terrain取决于你的地图类型Unity里搭建地表主要有两种方式一是直接创建一个3D Plane或者Cube拉平作为地面二是用内置的Terrain地形系统来刷。很多教程推荐新手用Plane因为简单、干净、没有多余的性能开销。但对于实际项目来说Plane只在两种场景下合适一是在做纯逻辑测试根本不需要好看二是做的是俯视角或者横版游戏地面本来就是平的。如果你的项目是第三人称3D游戏哪怕是个人Demo我也建议直接上Terrain。原因很直接Terrain自带高度图编辑、纹理刷层、树木和草地绘制而且Unity引擎对Terrain有原生的GPU实例化支持刷几千棵树也不会把Draw Call压爆。相比之下如果你用美术模型拼地形很容易出现贴图接缝、碰撞体过大或漏缝等问题。不过用Terrain有一个坑默认创建的Terrain尺寸是1000x1000个Unit对一个小Demo来说大得离谱而且大量空白地形区域的碰撞计算完全没必要。我习惯把Terrain的Width和Length改成100或者50然后再把Heightmap Resolution调整到匹配的精度。地形的高度图精度并不是越高越好精度越高意味着运行时的碰撞网格顶点越多移动端的性能压力会直线上升。做PC或者主机项目你可以用512以上移动端建议256就够用了。不管用哪种方式所有可站立的地面必须挂Collider。Plane自带Mesh ColliderTerrain自带Terrain Collider但如果你自己建模导入的地面一定要确认FBX导入设置里的Generate Colliders是否勾选或者手动补一个Mesh Collider。否则角色会直接穿过地面掉进“深渊”这种Bug看起来特别业余排查起来又特别隐蔽。1.3 光照和氛围让场景不灰的根本操作Unity新建场景默认自带一个平行光很多新手就直接开工了结果搭出来的场景灰蒙蒙一片。这不怪你而是Unity默认的灯光参数和环境光照设置偏中性它要兼顾所有用户不会替你预设任何风格。我在搭建场景时有几个固定动作首先是打开Window - Rendering - Lighting面板把Environment Lighting的Source从Skybox改成Color然后手动指定一个偏冷色或暖色的环境光。环境光决定了所有物体在阴影部分的颜色倾向默认的Skybox光照会让室外场景偏蓝室内场景偏暗。对于新手来说先手动控制环境光就能避免后面调材质时颜色的不确定性。其次是平行光的Rotation和Shadow Type。平行光就是太阳光它的角度直接决定了物体阴影的方向和长度。我通常会把平行光的Rotation.X设为30到50度之间这样阴影不会太长也不会太短物体的立体感比较自然。Shadow Type默认是Soft Shadows这个保持就行但要注意Shadow Distance这个参数。PC项目可以开到100移动端我建议40到60超过这个距离之外物体不再投射阴影能省不少GPU开销。再有就是后处理Post Processing。如果你的Unity版本是2021以上可以直接用URPUniversal Render Pipeline自带的Volume框架和后处理栈加点Bloom和Color Adjustments整个场景的质感立刻提升一个档次。但不要在一开始就沉迷调后处理参数——先把场景结构和角色移动调好画面风格是后面锦上添花的事。2. 角色移动三条路线各有各的适用场景2.1 Rigidbody刚体移动物理反馈最真实但别乱改Transform角色移动最“物理正确”的方式是给角色挂上Rigidbody组件通过施加力或者直接修改速度来控制运动。Rigidbody方案的优势在于角色能自动响应碰撞、重力、摩擦力还能被其他刚体推动如果以后要加入爆炸效果、力场、子弹击退等交互刚体方案几乎不需要额外改结构。但刚体方案有一个新手最容易踩的大坑直接在Update里用transform.position new Vector3(...)去改位置。Rigidbody是物理引擎的一部分物理引擎的模拟发生在FixedUpdate的固定时间步长里。如果你在Update里直接改Transform位置会跟物理引擎的碰撞检测打架结果就是你发现角色一会儿被卡在墙里一会儿又弹飞到天上运气好点也会产生剧烈的抖动。用刚体做移动的正规姿势应该是在FixedUpdate里获取输入然后给rigidbody.velocity赋值或者在受力时调用AddForce。其中velocity赋值适合做“街机感”比较强的操作手感例如平台跳跃、动作游戏AddForce则适合做车辆、弹球这类需要表现加速度的对象。对大多数角色移动场景来说直接控制velocity是最容易上手、手感最可控的方案。另一个刚体相关的重要参数是Interpolate插值。物理引擎默认在FixedUpdate和帧渲染之间不做插值如果你把角色速度拉得比较高会看到角色运动有肉眼可见的“一顿一顿”感。把Rigidbody的Interpolate设为Interpolate或者Extrapolate能明显改善这个问题。我个人的习惯是设为Interpolate因为它的预测方向更保守不容易出现超前位置回弹。2.2 CharacterController人形角色的亲妈组件如果你做的是标准的人形角色——走路、跑步、跳跃、上下坡——我强烈推荐CharacterController组件。它的工作方式和刚体完全不同刚体是让物理引擎帮你处理外力CharacterController则是一套“运动学”系统你给它一个移动向量它自己负责跟场景中的碰撞体做检测并自动处理斜坡、台阶、碰头这些麻烦事。最直观的好处是角色不会被卡在1厘米高的门槛上走斜坡不会被甩出去碰撞手感非常稳定。用CharacterController写移动脚本的核心API就两个Move()和SimpleMove()。两者都需要传入一个速度向量区别在于SimpleMove会自动应用重力并且Y轴会被忽略Move则完全由你控制包括重力在内的所有速度分量都必须自己算。大多数动作游戏需要精确控制跳跃和下落所以Move更常用。下面的代码是一个最基础的鼠标/键盘控制第三人称移动脚本的核心部分using UnityEngine; [RequireComponent(typeof(CharacterController))] public class PlayerMovement : MonoBehaviour { public float moveSpeed 6f; public float gravity -9.81f; public float jumpHeight 1.2f; private CharacterController controller; private Vector3 velocity; private bool isGrounded; private void Awake() { controller GetComponentCharacterController(); } private void Update() { isGrounded controller.isGrounded; if (isGrounded velocity.y 0f) { 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); } }这段代码的核心逻辑是把水平移动和垂直运动拆成两个独立部分。水平移动由玩家的输入向量控制垂直方向则单独计算重力和跳跃速度。如果你把两者混在一起算很容易出现斜坡上加速、空中转向不自然的问题。CharacterController的Center、Radius、Height三个参数特别关键Radius决定了角色穿越缝隙的能力Height决定了头部碰撞点Center则控制碰撞体的锚定位置。我一般把Radius设为0.4到0.5Height设为1.8左右Center的Y设在0.9附近让碰撞体底部刚好贴地。2.3 输入系统与移动脚本把“按键盘”变成“动起来”很多人写移动脚本时把输入获取直接写在移动逻辑里项目小的时候没问题项目一复杂就乱成一锅粥。所以从第一天起我就建议你把输入获取和角色运动解耦开。Unity 2020以前大家用的都是老的Input Manager就是上面代码里的Input.GetAxis(Horizontal)这种写法。它简单易用但问题是轴名称是硬编码的字符串改键位得跑到Project Settings里折腾而且它默认只能响应键盘、鼠标、旧手柄如果你要接入触屏虚拟摇杆或者新式手柄支持起来很别扭。Unity后来主推的新Input System包就是为了解决这些问题的。它的核心思路是把“输入设备”和“输入行为”剥离开先在.asset文件里定义InputAction比如名为“Move”的Action绑定到WASD和左摇杆然后在代码里通过回调或读取操作值来驱动角色。对新手来说新Input System的上手成本比老的Input Manager高不少但它带来的灵活性和跨平台能力是完全值得学的。我的建议是个人学习项目就用老输入系统快速出效果如果是准备长期维护或者要上多平台的产品直接用新输入系统少走弯路。不管用哪种输入系统最终移动脚本都遵循一个相同模式读取输入值 - 转换到世界坐标 - 驱动角色组件。唯一要提醒的是Input.GetAxis的返回值本身带了轻微平滑直接用即可但如果用新Input System的ReadValue 它是原始值你会明显感觉转向更“硬”这时候需要自己做平滑插值。3. 摄像机跟随与第三人称视角3.1 最简单的跟随方案把摄像机塞进角色子物体很多新手做完角色移动顺手把Main Camera拖到角色模型底下当子物体发现角色转视角的时候摄像机也跟着转感觉“能用就行”。这个方案在编程上确实零成本但实际体验很差角色左右旋转时摄像机的朝向会被强制带偏玩家看到的画面会持续晃动角色上下楼梯时摄像机也会跟着上下抖动而且这种方案完全没法做镜头碰撞和遮挡处理。更致命的问题在于当角色碰撞体在旋转时子物体摄像机的位置受到父级Transform的缩放、旋转影响极易出现奇怪的相机震荡。尤其是你在角色身上挂了Animator动画播放时模型根节点位置会有微小偏移这时候摄像机子物体方案会让玩家画面肉眼可见地“点头”晕3D的玩家几分钟就会感到不适。所以我做第三人称项目时始终坚持摄像机作为场景里的独立物体不挂到角色下面。让摄像机在LateUpdate里根据角色的世界坐标计算自己的位置和朝向这就引出了下面更平滑的方案。3.2 LateUpdate 平滑阻尼让镜头像粘在角色身上摄像机的更新时机有讲究。Unity的脚本执行顺序是Update - LateUpdate物理计算发生在FixedUpdate里渲染在最末。如果摄像机在Update里去跟随角色而角色的位置可能在FixedUpdate或者Update里被更新顺序稍有错位就会导致画面抖动。LateUpdate能保证在所有Update逻辑执行完毕之后再去计算摄像机位置这样才能拿到角色本帧的最终位置画面才会稳。平滑跟随的代码思路非常直接先计算一个期望位置比如角色后方某个偏移量再用插值函数把摄像机当前位置往期望位置过渡。Vector3.Lerp或Mathf.Lerp是最常用的但要注意不要每次直接用固定插值系数0.1这种写法因为插值系数乘以帧率会带来帧率依赖问题——60帧和30帧下的跟随手感会不一样。Unity提供了更合适的工具Vector3.SmoothDamp。这个函数内置了速度估算能根据时间差自动调整插值而且需要传入一个ref类型的当前速度参数表现效果相当于一个阻尼弹簧系统。下面是我常用的第三人称摄像机跟随脚本核心using UnityEngine; public class ThirdPersonCamera : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0f, 2f, -4f); public float smoothTime 0.15f; private Vector3 velocity Vector3.zero; private void LateUpdate() { if (target null) return; Vector3 desiredPosition target.position target.TransformDirection(offset); transform.position Vector3.SmoothDamp( transform.position, desiredPosition, ref velocity, smoothTime ); transform.LookAt(target.position Vector3.up * 1.5f); } }offset里的Y值2代表镜头在角色头部上方一点Z值-4代表镜头在角色身后4个单位。用target.TransformDirection把本地偏移转换成世界偏移这样角色转身后镜头会跟着绕到背后。LookAt的注视点我故意加了Vector3.up * 1.5f避免镜头盯着角色脚底看。smoothTime的0.15秒是我个人用起来比较舒服的阻尼值数值越小跟随越快但过小会丧失阻尼感过大则镜头像“拖着一根橡皮筋”。这套方案还可以扩展出鼠标控制视角环绕在Update里读取鼠标X轴移动绕角色的Y轴旋转offset向量。如果再加上射线检测摄像机靠近墙壁时自动前移就构成了一个完整的第三人称摄像机系统。4. 实操从空项目到完成一个可跑动的小场景4.1 新项目怎么建版本、模板与初始设置Unity Hub安装版本的时候我建议新手直接装最新的LTSLong Term Support长期支持版本系列比如Unity 2022 LTS或Unity 6 LTS。LTS版本的特点是稳定性优先网上能找到的教程和插件兼容性也更好。不要追最新的Tech Stream除非你明确知道自己需要那些前瞻性特性否则就是给工作流找不痛快。创建项目时选择3D模板如果你打算用内置渲染管线直接选3D Core如果以后想做更高级的画面效果建议一开始就选Universal 3D模板URP因为URP的后处理、光照、Shader支持都更现代化。项目创建成功后先别急着写代码我习惯先把Project Settings里的几个项设置好在Player设置里改公司名和产品名保证打包时不出乱码路径在Quality设置里把默认的Pixel Light Count和Shadow Resolution调成比中档略高的数值避免编辑器和手机上看差太远。4.2 0到1搭建一个测试场景的全过程我这里带你亲手搭一个简单的测试场地就是很多独立游戏最典型的“绿色平原加几个障碍物”。第一步创建Terrain。层级面板右键 - 3D Object - Terrain然后把Terrain的Width和Length改到100Position归零。用Terrain工具里的Raise/Lower Height在地图边缘拉起一圈低矮的山丘中间留出一块开阔平地。接着用Paint Texture给地面刷两层材质底层刷草地纹理边缘刷泥土纹理。注意刷纹理时不用追求精细大色块过渡即可测试场景重点是功能验证。第二步放置障碍物。我习惯从Asset Store里找免费的低多边形模型资源包或者直接用Unity内置的基本几何体。用Cube拉长变成墙体用Cylinder当柱子用Sphere当可推动的装饰物。摆放时开启Snap Settings菜单Edit - Snap Settings把Move增量设为0.5旋转增量设为15度这样摆放的物体会自动对齐看起来整洁得多。第三步补光。保留默认平行光把它Rotation设为(35, 30, 0)颜色稍微偏暖一点点(255, 244, 214)Intensity设1.0。如果场景太暗再加一个区域光或者补一个点光源但不要用太多不然光照烘焙的时候会非常痛苦。第四步放入角色。最简单的角色就是一个Capsule胶囊体加一个Camera子物体这里注意按我上一节讲的摄像机不该挂载为子物体。所以我们需要从Asset Store或网上找一个人形角色模型或者直接先用Capsule加一个头部的Cube来顶替。无论用哪种确保角色物体上挂了CharacterController或者Rigidbody然后再把移动脚本拖上去。4.3 角色移动脚本的挂载与联调场景搭好之后就是最关键的联调环节。新建一个C#脚本名字就叫PlayerMovement把上面CharacterController的代码粘进去。然后选中角色在Inspector里点击Add Component搜索并添加CharacterController把脚本也加上。注意顺序不影响功能但Add Component搜索框里输入“Character Controller”时别选成“Character Joint”。脚本加好后检查几个参数CharacterController的Center是否为(0, 0, 0)Height是否和你角色高度匹配。如果你用的是Capsule加Cube当角色模型Height改成2.0Center的Y设为1.0就好。脚本里的moveSpeed默认6这个速度对于测试场景来说适中。挂好脚本后点Play用WASD测试移动、空格测试跳跃。如果角色没反应先别看代码去看Console窗口有没有红色报错。最常见的报错是“NullReferenceException: Object reference not set to an instance of an object”这种九成是Inspector里某个引用没拖进去导致的。如果角色移动了但很卡检查一下是不是同时挂了Rigidbody和CharacterController——这两个组件同时存在时物理作用会互相干扰导致角色行为异常。CharacterController本身就自带碰撞处理只需要挂它一个就行没必要额外加Rigidbody。5. 踩坑实录新手最容易栽的10个跟头5.1 角色动不了、抖动、穿模优先排查哪些地方排查问题如果没有一套系统方法很容易在一堆可能原因里打转。我根据自己的经验整理了一个速查表照着这个顺序查绝大多数移动和场景问题都能在五分钟内定位问题现象优先排查项解决思路角色完全不动是否挂了Controller脚本且Inspector引用正确确认CharacterController或Rigidbody组件存在确认脚本没有报错角色移动方向错乱模型面朝方向和实际运动方向是否一致调整角色模型或者统一Forward方向用transform.forward而不是Vector3.forward角色抖动Rigidbody.Interpolate是否开启Update里是否直接改Transform开启插值改用FixedUpdate和velocity角色穿墙碰撞体尺寸过小或移动速度过快导致隧穿增大碰撞体或开启Continuous碰撞检测模式角色卡在墙角碰撞体半径太大调小Radius或把CharacterController的Skin Width适当加大角色掉出地图地面是否有Collider给地面补Mesh Collider或Terrain Collider跳跃失控没检测isGrounded使用CharacterController.isGrounded注意它只在接触地面时为true镜头乱晃或穿透摄像机更新时机和位置计算方式不对使用LateUpdate SmoothDamp 射线碰撞检测其中特别要提的是“隧穿效应”也就是角色移动速度太快一帧内穿透了整面墙体。CharacterController组件内部使用了连续的碰撞检测基本可以避免大部分隧穿问题但如果你用的是Rigidbody方案就得注意Collision Detection模式默认的Discrete离散检测在高速运动下极易穿透改成Continuous或者Continuous Speculative能缓解但代价是CPU开销上升。对大多数游戏来说把角色速度限制在合理范围、加上碰撞体最小厚度是性价比最高的方案。5.2 场景搭建里的性能隐患新手期就该避开场景搭建不只是“看起来好看”性能才是C端用户真正能感知的东西。我之前接过一个小项目场景里摆了300多个独立的小石块每块都有单独的Mesh Renderer和Collider结果在手机上只有20帧。这就是典型的Draw Call爆炸和碰撞体过多问题。在搭建场景时有几个性能原则值得从第一天就遵守。第一能用合批合并的物体尽量合并。Unity的Static Batching和GPU Instancing都能大幅降低Draw Call对于重复度高的模型树木、石头、路灯务必勾选Static标志或者使用预制体变体配合GPU Instancing。第二Collider能少则少。很多纯装饰物体根本不需要Collider你可以在它旁边放一个不可见的简化Cube作为碰撞区域而不是让每根柱子都挂Mesh Collider。Mesh Collider是最昂贵的碰撞体类型能用Box Collider或Capsule Collider替代就尽量替代。第三光照烘焙尽早做。默认的实时灯光会让每个受光物体都参与实时计算几百个物体的场景在手机上立刻卡成幻灯片。用Baked GI烘焙全局光照把静态物体的光影烘焙到Lightmap纹理里运行时只需要做一次纹理采样性能开销可以忽略不计。前提是场景里所有不动的物体都标记为Static灯光模式选Baked或者Mixed。烘焙完成后运行时的画面效果看起来还会比实时灯光柔和不少一举两得。5.3 关于场景搭建和角色移动的扩展思考写到这里我想延伸一个经常被忽视的话题场景搭建和角色移动之间的“手感匹配”。很多人把这两块当成独立模块来做最后拼起来却发现角色走路“像在冰面上滑”——这种感觉问题常常不是移动脚本的错而是场景地面的摩擦系数、材质类型和移动脚本的加速度参数不匹配导致的。比如你在Inspector里把Physic Material的Friction设为0那角色停下来的时间会明显变长再用物理引擎驱动移动时“滑”的感觉就会被无限放大。另外如果你打算做包含AI敌人、NPC或队友的项目场景搭建时就应该提前为NavMesh寻路留好余地。导航网格的烘焙结果是基于场景静态碰撞体生成的所以地形高度、障碍物摆放位置、可行走区域标记都需要在场景搭建阶段就规划好。等你角色移动做完了再回头补NavMesh往往要改动大量场景结构返工成本极高。移动手感这块我最后再分享一个自己反复验证过的经验给角色移动加一点“起步加速”和“停止减速”哪怕只是极其微小的加速度都能大幅提升手感。直接给固定速度会让角色看起来像一个被推着走的模型而加入平滑过渡后玩家会感觉这个角色“有重量”“听指挥”。如果你用CharacterController最简单的实现就是维护一个当前水平速度向量每帧向目标速度做Lerp阻尼系数按你的目标手感在5到15之间调整。这一个小改动往往比换一套高大上的动画效果更能提升游戏的整体质感。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案