简介:这份资源是面向Unity初学者与在校学生的期末大作业级项目,基于Unity2021版本实现第三人称漫游玩法,帮助读者理解角色控制、场景搭建与UI交互的完整开发流程。压缩包共约2000个文件,以cs脚本、png贴图、fbx模型、prefab预制体、unity场景与mat材质为主,另含anim动画、shader着色器及dll依赖库,整体约113MB,目录结构完整可直接导入编辑器运行。项目包含山谷、房屋、桌椅等精细场景模型,通过鼠标控制小狐狸移动,并配有血条、菜单与按钮等UGUI界面,覆盖CharacterController角色控制、光照与PBR材质、Animator动画及Input Manager输入管理等知识点。目前已有2994人学习下载,适合需要参考完整赛题方案、拆解场景组织方式与排错思路的开发者对照练习。
1. 期末作业里的第三人称漫游:为什么你的场景总像“纸片人逛空城”
做 Unity 期末作业的同学,十个里有八个会选第三人称漫游。原因很直接:第一人称太晕,固定视角太呆,第三人称既能展示角色模型,又能让镜头绕着场景转一圈,把“精美场景”四个字直接怼到老师脸上。但真正动手之后,翻车现场往往高度一致——角色像纸片一样飘着走,镜头穿墙穿到地心,场景光照要么惨白要么死黑,最后交上去的是一个“能跑但不想看第二眼”的 Demo。
这个标题拆开来看,核心是三件事:Unity 2021 版本下的第三人称控制器、一个能拿得出手的场景模型、以及“漫游”这个交互目标。它适合正在赶期末作业的学生,也适合刚接触 Unity 角色控制、想用最短路径跑通一套可演示方案的开发者。我一般会把这类作业当成一个最小可交付系统来做:角色能走能跑能跳,镜头跟得住不穿墙,场景有光有影有层次,最后打包成一个能双击运行的 exe。下面按这个顺序,把每一步的参数和坑讲清楚。
2. 第三人称控制器:从 CharacterController 到镜头臂的完整搭建
2.1 为什么优先用 CharacterController 而不是 Rigidbody
在 Unity 2021 里做第三人称角色,常见做法有两种:Rigidbody + CapsuleCollider,或者 CharacterController。期末作业场景里通常有台阶、斜坡、窄桥,Rigidbody 的物理模拟很容易让角色在斜坡上往下滑,或者被台阶卡住弹飞。CharacterController 是 Unity 专门为角色移动提供的组件,自带斜坡检测、台阶偏移和碰撞滑动,不需要你写额外的物理约束。
我一般会这样配:角色根节点挂 CharacterController,参数里 Slope Limit 设 45,Step Offset 设 0.3,Skin Width 设 0.08,Min Move Distance 设 0。Slope Limit 决定角色能爬多陡的坡,超过这个角度就会滑下来;Step Offset 是能自动迈上去的台阶高度,0.3 大约对应 30 厘米,适合大多数室内外场景;Skin Width 太小会导致角色卡进碰撞体,太大会让角色悬浮,0.08 是经验值;Min Move Distance 设 0 是为了避免微小移动被忽略。
角色模型不要直接放在根节点下,而是作为子物体,并且把模型的局部坐标 Y 轴对齐到脚底。很多人把模型拖进来之后发现角色半截身子陷进地面,就是因为模型原点在腰部而不是脚底。
2.2 移动与转向的代码实现
下面这段脚本挂在角色根节点上,负责读取输入、计算移动方向、处理重力和跳跃。代码里用到了 CharacterController 的 Move 方法,并且把镜头朝向作为移动参考系。
using UnityEngine; public class ThirdPersonMovement : MonoBehaviour { public CharacterController controller; public Transform cam; public float speed = 6f; public float turnSmoothTime = 0.1f; public float gravity = -9.81f; public float jumpHeight = 1.2f; private float turnSmoothVelocity; private Vector3 velocity; private bool isGrounded; void Update() { // 检测是否落地,用于重置重力和允许跳跃 isGrounded = controller.isGrounded; if (isGrounded && velocity.y < 0) { velocity.y = -2f; // 给一个向下的微小速度,保证贴地 } // 读取水平输入 float horizontal = Input.GetAxisRaw("Horizontal"); float vertical = Input.GetAxisRaw("Vertical"); Vector3 direction = new Vector3(horizontal, 0f, vertical).normalized; // 只有输入方向不为零时才旋转和移动 if (direction.magnitude >= 0.1f) { // 根据镜头朝向计算目标角度 float targetAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg + cam.eulerAngles.y; float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref turnSmoothVelocity, turnSmoothTime); transform.rotation = Quaternion.Euler(0f, angle, 0f); Vector3 moveDir = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward; controller.Move(moveDir.normalized * speed * Time.deltaTime); } // 跳跃:落地时按空格给一个向上的速度 if (Input.GetButtonDown("Jump") && isGrounded) { velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); } // 重力累积并应用 velocity.y += gravity * Time.deltaTime; controller.Move(velocity * Time.deltaTime); } }逻辑说明:targetAngle把输入方向从世界坐标转到镜头坐标系,这样按 W 永远是往镜头前方走,而不是往世界 Z 轴正方向走。SmoothDampAngle让角色转向有缓冲,不会瞬间扭头。velocity.y在落地时被设成 -2,是为了让 CharacterController 的 isGrounded 稳定返回 true,否则在斜坡上会反复在“落地”和“空中”之间跳变。
参数说明:speed设 6 适合大多数场景,跑图不累;turnSmoothTime设 0.1 手感比较跟手,设 0.3 会显得笨重;jumpHeight设 1.2 大约能跳过一个 1 米高的箱子;gravity用 -9.81 是地球重力,如果觉得跳跃太飘可以改成 -15 到 -20。
2.3 镜头臂与碰撞回避
第三人称镜头不能直接挂在角色头上,否则角色会挡住半个屏幕。常见做法是用一个空物体作为镜头臂的支点,放在角色肩膀高度,然后让主摄像机沿着支点后方偏移。镜头碰撞回避用 SphereCast 从支点向摄像机位置发射球体检测,如果打到墙就把摄像机拉近。
using UnityEngine; public class ThirdPersonCamera : MonoBehaviour { public Transform target; // 角色根节点 public Transform pivot; // 镜头支点,放在角色肩膀高度 public float distance = 4f; public float mouseSensitivity = 2f; public float minPitch = -30f; public float maxPitch = 70f; public LayerMask collisionMask; private float yaw; private float pitch = 20f; void Start() { Cursor.lockState = CursorLockMode.Locked; } void LateUpdate() { // 鼠标控制 yaw 和 pitch yaw += Input.GetAxis("Mouse X") * mouseSensitivity; pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity; pitch = Mathf.Clamp(pitch, minPitch, maxPitch); // 支点跟随角色位置 pivot.position = target.position + Vector3.up * 1.5f; pivot.rotation = Quaternion.Euler(pitch, yaw, 0f); // 计算理想摄像机位置 Vector3 desiredPos = pivot.position - pivot.forward * distance; // 球体检测,防止穿墙 RaycastHit hit; if (Physics.SphereCast(pivot.position, 0.3f, -pivot.forward, out hit, distance, collisionMask)) { desiredPos = pivot.position - pivot.forward * (hit.distance - 0.1f); } transform.position = desiredPos; transform.LookAt(pivot.position + pivot.forward * 2f); } }逻辑说明:LateUpdate保证镜头在角色移动之后更新,避免画面抖动。pivot的位置每帧跟随角色,但旋转由鼠标控制。SphereCast的半径 0.3 大约对应摄像机近裁剪面附近的安全距离,检测到碰撞后把摄像机拉到碰撞点前方 0.1 的位置,避免穿模。
参数说明:distance设 4 是第三人称常见距离,设 2.5 会变成越肩视角;mouseSensitivity设 2 在 1080p 下比较舒服,设 5 会晕;minPitch和maxPitch限制上下视角,防止镜头翻到角色头顶或脚底。
3. 场景模型与光照:让“精美”不只是一句口号
3.1 模型导入的缩放与材质检查
从外部拖进 Unity 2021 的模型,第一件事是看 Inspector 里的 Scale Factor。很多模型默认单位是厘米,导入后 Scale Factor 是 0.01,但有些资源包已经预处理过,Scale Factor 是 1。如果角色和场景比例不对,先检查这里。我一般会放一个 Unity 自带的 Cube 作为参照,Cube 默认边长 1 米,角色身高应该在这个 Cube 的 1.7 到 1.8 倍左右。
材质方面,Unity 2021 默认渲染管线是 Built-in,如果你用的是 URP 模板,导入 Standard 材质的模型会显示成粉色。解决办法是在 Package Manager 里确认 URP 已安装,然后用 Edit > Rendering > Materials > Convert Selected Built-in Materials to URP 批量转换。这个操作不可逆,转换前建议备份。
3.2 用混合光照做出层次感
期末作业场景最怕两种极端:全是实时光,帧率掉到 20;全是烘焙光,角色走过去没有影子。我一般用混合光照:场景里的静态物体标记为 Static,用 Baked 模式烘焙;角色和可移动物体用 Realtime 模式,只保留一盏主方向光实时计算。
具体操作:Window > Rendering > Lighting,把 Scene 标签页里的 Environment Lighting 的 Source 改成 Gradient,Sky 用默认天空盒,Equator 调成偏灰蓝,Ground 调成偏暖灰。然后切到 Mixed Lighting,勾选 Baked Global Illumination,Lighting Mode 选 Shadowmask。Shadowmask 的好处是远处物体用烘焙阴影,近处角色用实时阴影,兼顾效果和性能。
方向光的参数:Intensity 设 1.2,Shadow Type 选 Soft Shadows,Strength 设 0.8。如果场景偏暗,不要直接拉高 Intensity,而是调 Environment Lighting 里的 Intensity Multiplier,设 1.5 左右。直接拉高方向光会让受光面过曝,暗部依然死黑。
3.3 反射探针与后处理的最小配置
场景里如果有金属或光滑地面,没有反射探针会显得很假。在场景中央放一个 Reflection Probe,Type 选 Baked,Box Size 覆盖主要活动区域,Resolution 设 128 就够。烘焙之后金属材质会反射周围环境,质感提升明显。
后处理用 Post Processing Stack v2,在摄像机上加 Post-process Layer,然后建一个 Global Volume。Profile 里加三个效果:Bloom 强度设 0.3,Threshold 设 1.1;Color Grading 的 Tonemapping 选 ACES,曝光设 0.1;Vignette 强度设 0.2。这三个效果加起来对性能影响很小,但画面会从“学生作业”变成“能看”。
4. 避坑与排查:期末作业里最容易翻车的五个点
4.1 角色移动时抖动或卡顿
现象:角色在平坦地面上移动时画面轻微抖动,或者走到某些位置突然卡住。
原因:CharacterController 的 Skin Width 太小,角色和地面碰撞体之间产生穿透;或者移动逻辑写在 Update 里但镜头更新也在 Update 里,两者执行顺序不确定。
解决:把 Skin Width 调到 0.08 到 0.1,Min Move Distance 设为 0。镜头脚本改用 LateUpdate,保证在角色移动之后执行。如果还抖,检查角色模型子物体的局部坐标是否对齐脚底。
4.2 镜头穿墙后看到场景背面
现象:角色靠近墙壁时,镜头穿进墙里,画面出现破面或直接看到场景外部。
原因:没有做镜头碰撞检测,或者碰撞检测的 LayerMask 把墙壁排除了。
解决:给场景静态物体统一设一个 Layer,比如 Environment,然后在镜头脚本的 collisionMask 里只勾选这个 Layer。SphereCast 的半径不要小于 0.2,否则会漏检薄墙。
4.3 烘焙光照后角色没有影子
现象:场景烘焙完,静态物体有影子,但角色走过去地面上没有影子。
原因:方向光的 Lighting Mode 设成了 Baked,实时阴影被关闭。
解决:把方向光的 Lighting Mode 改成 Mixed,Lighting 面板里 Mixed Lighting 的 Lighting Mode 选 Shadowmask。这样静态物体用烘焙阴影,动态角色用实时阴影。
4.4 导入模型后材质全部变粉
现象:从资源包导入的模型在场景里显示为亮粉色。
原因:资源包用的是 Built-in 管线的 Standard 材质,当前项目是 URP 管线。
解决:确认当前渲染管线。如果是 URP,用 Edit > Rendering > Materials > Convert Selected Built-in Materials to URP 转换。转换前备份项目,转换后检查材质球是否还有贴图丢失。
4.5 打包后运行报错或黑屏
现象:在编辑器里一切正常,Build 成 exe 后双击黑屏或报错。
原因:场景没有加入 Build Settings 的 Scenes In Build 列表;或者脚本里用了 UnityEditor 命名空间下的 API,打包后不存在。
解决:File > Build Settings,把当前场景拖进 Scenes In Build。检查所有脚本,确保没有using UnityEditor;。如果有,用#if UNITY_EDITOR预处理指令包起来。
5. 进阶技巧:用 Cinemachine 替换手写镜头并做性能验证
手写镜头脚本适合理解原理,但期末作业如果想让镜头更“专业”,可以用 Cinemachine。Unity 2021 在 Package Manager 里搜 Cinemachine 安装,然后创建一个 FreeLook Camera,把 Follow 和 LookAt 都设成角色根节点。FreeLook 自带三层轨道和阻尼,鼠标控制比手写脚本顺滑得多。碰撞回避在 Cinemachine Collider 扩展里勾选,设置 Camera Radius 0.3,Minimum Distance 1.5。
性能验证方面,Window > Analysis > Profiler,在编辑器里跑一遍漫游,看 CPU 的 Rendering 和 Scripts 占比。如果 Rendering 超过 8ms,检查场景里有没有过多的实时光源,或者把远处小物件改成 Baked 光照。如果 Scripts 超过 4ms,检查有没有在 Update 里做 GetComponent 或 Find 操作,这些应该放在 Start 里缓存。
最后打包前,把 Quality Settings 里的 Shadow Distance 设成 50,Shadow Resolution 设成 Medium,VSync Count 设成 Every VBlank。这样在大多数笔记本上能稳定 60 帧。我自己的习惯是打包后一定在另一台没装 Unity 的电脑上跑一遍,确认没有缺 DLL 或路径写死的问题。希望帮到你。
本文还有配套的精品资源,点击获取