1. 项目概述:为什么Unity物理系统值得深挖?
如果你在Unity里做过一个让盒子掉下来、让球滚起来,或者让角色被墙挡住的功能,那你已经和Physics类打过交道了。但很多时候,我们只是停留在“加个Rigidbody和Collider就能动”的层面,一旦遇到角色卡墙、物体穿透、性能骤降或者想实现一个“真实”的爆炸效果时,就开始头疼了。这个系列,尤其是这篇“上篇”,就是要带你从“能用”走到“精通”,把Unity物理引擎里那些藏在API文档和默认行为背后的门道,一次讲清楚。
Unity的物理系统,尤其是面向对象的3D物理(基于NVIDIA PhysX),它不是一个黑盒魔法。它是一套精密的、可预测的、可深度定制的模拟系统。理解它,意味着你能做出手感更扎实的移动、更可信的交互反馈,以及更高效的游戏性能。无论是想做一款物理解谜游戏,还是在一个大型开放世界里管理成千上万的动态物体,亦或是仅仅想让你的角色跳跃感觉不那么“飘”,深入Physics类都是必经之路。这篇文章适合已经了解Unity基础操作、写过一些简单物理交互脚本,但渴望掌控全局、解决实际复杂问题的开发者。我们会从最核心的物理模拟循环讲起,深入到刚体运动的每一个细节控制,最后探讨高级查询与性能优化,确保你读完不仅能复现效果,更能理解背后的“为什么”。
2. 物理模拟的核心循环与架构解析
很多开发者对物理的理解始于Rigidbody.AddForce,但这就像只看到了汽车的油门踏板,却不了解发动机和传动系统。要真正驾驭物理,必须从它的“心跳”——模拟循环开始。
2.1 FixedUpdate:物理世界的独立时钟
Unity的主循环有两个核心更新方法:Update和FixedUpdate。Update的调用频率与游戏帧率同步,是不稳定的。而FixedUpdate的调用间隔是固定的(默认0.02秒,即50Hz),物理引擎的所有计算都发生在这里。这是为了保证物理模拟的确定性和稳定性。一个常见的误区是在Update里直接修改Rigidbody的位置(transform.position)或施加力,这会导致不可预测的物理行为,比如抖动或穿透。
注意:所有对物理状态(位置、旋转、速度、力)的修改,都应该在
FixedUpdate中进行。在Update中读取物理状态(如刚体的当前位置用于渲染)是安全的,但写入操作必须留给FixedUpdate。
物理模拟在FixedUpdate中大致遵循以下顺序:
- 积分前阶段:Unity调用所有脚本的
FixedUpdate方法。这是你施加力(AddForce)、设置速度(velocity)或执行其他物理相关逻辑的最佳时机。 - 物理引擎解算:Unity物理引擎(PhysX)接管,执行以下核心工作:
- 碰撞检测:检查所有碰撞体(Collider)之间的接触和重叠。
- 求解约束:计算碰撞响应、关节约束等,生成冲量(Impulse)。
- 积分:根据受到的合力(包括重力、你施加的力、碰撞冲量等)和物体的质量,积分计算新的速度和位置。
- 积分后阶段:物理引擎将计算出的新位置和旋转应用回
Rigidbody和Transform组件。
理解这个循环,你就明白了为什么在FixedUpdate里AddForce是即时的,而在Update里可能会延迟或丢失。
2.2 物理层(Layer)与碰撞矩阵:性能与逻辑的第一道闸
当场景中有成百上千个物体时,让每个物体都互相检测碰撞在计算上是灾难性的。物理层和碰撞矩阵(Collision Matrix)就是用来精细化控制“谁和谁碰撞”的规则系统。
- 物理层(Physics Layer):你可以给任何GameObject分配一个层(如Player, Enemy, Ground, Projectile, Debris)。
- 碰撞矩阵(Edit > Project Settings > Physics):这是一个NxN的表格,定义了任意两个层之间是否进行碰撞检测和物理交互。
实操心得:
- 默认全开是性能杀手:新建项目时,碰撞矩阵默认是所有层之间都碰撞。你的第一个优化动作就应该是去这里,取消所有不必要的交叉勾选。例如,“UI”层通常不需要与任何物理层碰撞;“Debris”(碎片)层内部的小碎片之间可能也不需要互相碰撞,以避免“计算风暴”。
- 逻辑分离:利用层可以轻松实现逻辑过滤。比如,你希望子弹能击中玩家和敌人,但不能击中其他子弹或触发器。通过精心设计碰撞矩阵,可以免去在代码里写一堆
if (other.gameObject.tag == ...)的判断,提升性能和代码清晰度。 - 一个常被忽略的细节:
Physics.IgnoreCollision方法可以在运行时动态忽略两个特定碰撞体之间的碰撞,这比修改层更灵活,但管理成本较高,适用于特定场景下的临时忽略(如角色穿过刚刚生成的爆炸冲击波)。
2.3 物理材质:摩擦与弹性的微观世界
物理材质(Physic Material)是赋予碰撞体表面特性的资产。它主要控制两个属性:
- 动态/静态摩擦力(Dynamic/Static Friction):物体在表面上滑动或开始滑动所需的力的大小。值在0(无摩擦,如冰面)到1(极大摩擦)之间。通常静态摩擦力略大于动态摩擦力,模拟“启动更费力”的效果。
- 弹力(Bounciness):碰撞后速度恢复的系数。0表示完全非弹性碰撞(如泥巴),1表示完全弹性碰撞(理想情况,会有能量损失)。组合弹力取两个碰撞体弹力的最大值。
高级应用与避坑:
- “冰面”与“胶水”:想做一个溜冰关卡?创建一个摩擦力接近0的物理材质赋给地面。想做一个沼泽地让角色移动迟缓?可以尝试增加摩擦力,但更物理的方法是使用
Rigidbody.drag(线性阻尼)来模拟介质阻力。 - 弹力叠加的陷阱:两个弹力为0.8的球相撞,组合弹力是0.8,这可能导致它们弹跳很多次才停下,看起来能量不衰减。这是符合物理的(理想弹性碰撞能量守恒),但可能不符合游戏体验。通常我们会通过代码或在物理材质中设置一个小于1的弹力,并可能结合
Rigidbody.angularDrag(角速度阻尼)来让旋转也停下来。 - 摩擦力方向:PhysX默认使用各向同性的摩擦模型。对于像汽车轮胎这样前后、左右摩擦力不同的情况,需要更复杂的车轮碰撞体(WheelCollider)或自定义实现。
3. 刚体运动控制的进阶技巧
掌握了宏观循环,我们来微观操控每一个刚体。Rigidbody组件是物理对象的核心,它的控制方式决定了物体的“手感”。
3.1 力的艺术:AddForce的多种模式
Rigidbody.AddForce是最常用的方法,但它的ForceMode参数选择至关重要:
ForceMode.Force:添加一个持续的力(单位:牛顿)。这是最符合物理直觉的,力会随着时间持续改变物体的动量(mass * velocity)。适合模拟火箭推进器、风力等持续作用的效果。// 模拟一个持续向前的推进力 void FixedUpdate() { rb.AddForce(transform.forward * enginePower, ForceMode.Force); }ForceMode.Impulse:添加一个瞬时冲量(单位:牛顿·秒或千克·米/秒)。它会立即改变物体的动量。适合模拟爆炸、射击、跳跃等瞬间效果。// 模拟跳跃,瞬间施加一个向上的冲量 if (Input.GetKeyDown(KeyCode.Space)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); }ForceMode.Acceleration:添加一个持续的加速度(单位:米/秒²),忽略质量。这意味着无论物体多重,它获得的加速度都是一样的。在某些需要忽略质量差异的游戏场景中很有用(比如所有玩家受同一重力加速度影响)。ForceMode.VelocityChange:直接改变速度(单位:米/秒),忽略质量。这是最直接、最“不物理”但有时最有效的控制方式,可以瞬间将速度设为一个值。
选择策略:对于需要真实物理反馈的物体(如被炸飞的箱子),使用Force或Impulse。对于需要精确控制移动的角色控制器,有时直接修改velocity或使用VelocityChange反而能获得更佳的手感,但会牺牲一些物理真实性。
3.2 直接速度控制与插值
直接修改Rigidbody.velocity是控制运动最强大的方式之一,尤其适用于角色移动、平台追逐等场景。
// 让物体以恒定速度向目标移动(忽略质量、摩擦) Vector3 direction = (target.position - transform.position).normalized; rb.velocity = direction * moveSpeed;但直接设置velocity会覆盖物理引擎计算出的所有速度,包括重力。所以常见的模式是只修改水平速度,而保留垂直速度(如重力影响下的Y轴速度)。
// 典型的第一人称/第三人称角色水平移动,保留重力影响 Vector3 horizontalVelocity = rb.velocity; horizontalVelocity.y = 0; // 保存原始的垂直速度(重力、跳跃等) horizontalVelocity = transform.forward * verticalInput + transform.right * horizontalInput; horizontalVelocity = horizontalVelocity.normalized * moveSpeed; // 将计算好的水平速度与原始的垂直速度合并 rb.velocity = new Vector3(horizontalVelocity.x, rb.velocity.y, horizontalVelocity.z);运动平滑与插值:直接设置速度或位置可能导致运动在视觉上卡顿(因为FixedUpdate的频率可能低于帧率)。Rigidbody组件上的Interpolation(插值)选项就是为了解决这个问题。
- None:无插值。物体的渲染位置严格等于物理更新后的位置,在帧率高于物理更新频率时会出现抖动。
- Interpolate:插值。物体根据当前帧和上一帧的物理位置进行平滑插值渲染。这是最常用的选项,能让运动看起来非常平滑,即使物理更新频率不高。
- Extrapolate:外推。物体根据当前速度和上一帧的位置预测并渲染。在物体运动稳定时效果很好,但突然转向或停止时可能出现预测错误导致的“鬼影”。
实操心得:对于玩家控制或主要运动的物体,务必开启
Interpolate。对于大量静止或缓慢运动的背景物体,可以设为None以节省性能。Extrapolate使用需谨慎,在网络预测同步中可能有其用途,但单机环境下容易出问题。
3.3 刚体休眠与唤醒机制
为了性能,PhysX引入了刚体休眠(Sleeping)机制。当一个刚体的速度低于某个阈值(Sleep Threshold)并持续一段时间后,它会进入休眠状态。休眠的刚体不再参与物理计算,直到受到外力(碰撞、力等)被唤醒。
常见问题排查:
- “我的物体怎么推不动了?”:检查刚体是否意外进入了休眠状态。你可以通过
Rigidbody.IsSleeping()查询,并用Rigidbody.WakeUp()强制唤醒它。有时一个微小的力不足以唤醒它,需要确保施加的力或碰撞足够强。 - 性能优化:确保非动态物体(如静态地形)使用
Static碰撞体而非带刚体的Dynamic碰撞体。对于需要偶尔移动的物体(如可开关的门),可以设置刚体为Kinematic(运动学),由代码完全控制其运动,它不会休眠但也不会受物理力影响,直到你将其改回Dynamic。 - 手动控制休眠:通过
Rigidbody.Sleep()可以手动让刚体休眠,Rigidbody.WakeUp()手动唤醒。在你知道一堆物体将长时间静止时(如关卡初始化后),手动让它们休眠可以提升初始帧的性能。
4. 碰撞检测与信息获取的深度应用
碰撞不仅仅是“碰到”,更是游戏逻辑交互的触发器。Unity提供了从简单到复杂的多种碰撞信息获取方式。
4.1 碰撞回调函数详解
MonoBehaviour中与碰撞相关的回调主要有三组,区别必须厘清:
OnCollisionEnter/Stay/Exit:
- 触发条件:两个GameObject都有非触发器的Collider,且至少有一个带有非运动学的Rigidbody。
- 信息:通过
Collision参数提供丰富的碰撞点(contacts)、法线(normal)、相对速度(relativeVelocity)和碰撞体(collider)信息。 - 用途:处理真实的物理碰撞效果,如播放撞击音效(根据
relativeVelocity.magnitude决定音量)、产生撞击粒子、计算伤害等。
OnTriggerEnter/Stay/Exit:
- 触发条件:至少有一个Collider是触发器(Is Trigger勾选)。触发器不会产生物理碰撞响应,物体会直接穿过。
- 信息:通过
Collider参数提供另一个碰撞体的引用。 - 用途:处理逻辑区域,如拾取物品、进入危险区域、触发剧情、检测玩家进入房间等。
OnControllerColliderHit:
- 触发条件:与
CharacterController组件相关。当CharacterController与任何非触发器Collider碰撞时调用。 - 信息:提供
ControllerColliderHit参数,包含碰撞点、法线、被撞物体等。 - 用途:专门用于处理角色控制器的碰撞逻辑,比如沿斜坡行走、推开轻量物体等。
- 触发条件:与
避坑指南:
- 性能注意:
OnCollisionStay和OnTriggerStay在接触的每一帧都会被调用,避免在其中执行昂贵的操作(如查找对象、复杂计算)。可以考虑使用计时器或只在特定帧处理。 - 刚体类型:两个都是运动学刚体(Kinematic Rigidbody)之间不会触发
OnCollision系列回调。运动学刚体与静态碰撞体之间也不会。 - 获取对端对象:在回调中,使用
collision.gameObject或other.gameObject来获取碰撞的对方GameObject,进而获取其身上的组件(如PlayerHealth)来交互。
4.2 射线检测:从屏幕到世界的桥梁
射线检测(Raycasting)是物理查询中最常用、最灵活的工具。它不依赖于碰撞回调,而是主动向场景中投射一条无形的射线,检测沿途的碰撞体。
基础用法:
// 从摄像机屏幕中心发射一条射线 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 用于存储碰撞信息 float maxDistance = 100f; int layerMask = LayerMask.GetMask("Ground", "Enemy"); // 只检测特定层 if (Physics.Raycast(ray, out hit, maxDistance, layerMask)) { // 命中!hit.point是世界坐标中的命中点 // hit.normal是命中点的法线方向 // hit.collider是命中的碰撞体 Debug.Log("Hit: " + hit.collider.gameObject.name); }高级应用与参数解析:
- LayerMask:极其重要!永远不要使用默认的
Physics.Raycast(ray, out hit),这会对所有层进行检测,包括UI、忽略层等,浪费性能且可能产生错误结果。始终指定你需要检测的层。 - QueryTriggerInteraction:枚举参数,控制射线是否检测触发器。
QueryTriggerInteraction.Ignore:忽略触发器(默认)。QueryTriggerInteraction.Collide:检测触发器。- 在需要检测触发器区域(如拾取范围)时使用。
- RaycastAll 与 RaycastNonAlloc:
Physics.RaycastAll:返回所有命中的RaycastHit数组。简单但每次调用都会分配新数组,在频繁调用(如每帧)时会产生GC(垃圾回收)压力。Physics.RaycastNonAlloc:将命中结果填充到一个预先分配的RaycastHit[]数组中,无GC分配。这是性能关键优化点。
private RaycastHit[] hits = new RaycastHit[10]; // 预分配数组 void Update() { Ray ray = new Ray(transform.position, transform.forward); int hitCount = Physics.RaycastNonAlloc(ray, hits, 10f, groundLayer); for (int i = 0; i < hitCount; i++) { // 处理 hits[i] } } - SphereCast, CapsuleCast, BoxCast:这些是形状投射(Shape Cast),相当于发射一个有体积的形状(球体、胶囊体、盒子)进行检测。对于角色移动前的障碍物检测(防止卡进墙里)或车辆驾驶检测极其有用。
// 检测角色前方一个胶囊体范围内是否有障碍物 float radius = 0.5f; float height = 2f; Vector3 point1 = transform.position + Vector3.up * radius; Vector3 point2 = transform.position + Vector3.up * (height - radius); if (Physics.CapsuleCast(point1, point2, radius, transform.forward, out hit, 1f)) { // 前方1米处有障碍物,可以提前减速或转向 }
4.3 重叠检测与物理查询
除了射线,有时你需要检测一个区域内的所有物体,比如爆炸范围、技能作用区域。
- Physics.OverlapSphere/Box/Capsule:返回指定形状区域内所有碰撞体的数组。同样,有
OverlapSphereNonAlloc等无GC版本。Collider[] colliders = Physics.OverlapSphere(explosionCenter, explosionRadius, enemyLayer); foreach (var collider in colliders) { // 对范围内的每个敌人造成伤害 Enemy enemy = collider.GetComponent<Enemy>(); if (enemy != null) { enemy.TakeDamage(damage); } } - Physics.SphereCast/BoxCast/CapsuleCast:与
Overlap不同,Cast是检测在移动路径上是否会与某物相交,常用于移动前的可行性检测。
性能警告:所有物理查询(Raycast, Overlap, Cast)都是CPU密集型操作,尤其是在每帧对大量物体或复杂网格进行检测时。务必:
- 使用合适的层掩码(LayerMask)缩小检测范围。
- 在不需要每帧检测时(如技能冷却期间),降低检测频率。
- 优先使用
NonAlloc版本。 - 对于固定区域的持续检测,可以考虑使用触发器(Trigger)配合
OnTriggerStay,但要注意其调用频率也很高。
5. 关节与约束:构建物理机关
Unity提供了多种关节(Joint)组件,用于在刚体之间创建约束,模拟现实世界的连接关系,如铰链、弹簧、固定等。
5.1 常用关节类型与应用场景
- Fixed Joint(固定关节):将两个刚体牢固地连接在一起,像一个整体。可用于将小部件(如剑柄上的宝石)固定在主体上,或者临时将两个物体焊接在一起。
- Hinge Joint(铰链关节):模拟门、杠杆、钟摆的运动。围绕一个轴旋转。你需要配置锚点(Anchor,局部空间中的旋转中心)和轴(Axis,旋转围绕的局部方向)。
- 技巧:使用
HingeJoint.useMotor和HingeJoint.motor可以给铰链添加一个驱动马达,让它自动旋转到目标速度或角度,常用于制作自动门、旋转平台。 - 限制:通过
HingeJoint.limits可以设置旋转的最小和最大角度,制作只能打开一定角度的门。
- 技巧:使用
- Spring Joint(弹簧关节):用弹簧力连接两个刚体。可以设置弹簧的刚度(Stiffness)、阻尼(Damper)和最小/最大距离。非常适合做绳索、蹦极、悬挂系统(汽车悬挂)的简化模拟。
- 避坑:弹簧的刚度和阻尼需要仔细调校,数值过大容易导致系统不稳定(剧烈抖动),数值过小则弹簧过于松软。通常需要反复测试。
- Configurable Joint(可配置关节):这是最强大也是最复杂的关节。它几乎可以模拟任何类型的约束,通过自由配置每个平移和旋转自由度的锁定(Locked)、限制(Limited)或自由(Free)状态。可以用来制作自定义的车辆悬挂、玩偶关节、复杂的机械臂等。
- 警告:配置不当的
Configurable Joint极易导致奇怪的抖动或崩溃。建议从一个简单的约束开始,逐步增加复杂度。
- 警告:配置不当的
5.2 关节的配置要点与常见问题
- 连接体(Connected Body):关节需要绑定两个刚体,一个是在关节组件所在物体上的刚体,另一个是
Connected Body。如果Connected Body为空,则意味着关节连接到世界空间的一个固定点。 - 锚点与轴(Anchor & Axis):这些都是在关节所在物体的局部空间中定义的。锚点是连接点,轴是旋转或移动的方向。错误理解局部空间是关节配置出错的主要原因。在Scene视图中启用关节的Gizmo显示可以直观地看到锚点和轴。
- 断裂力(Break Force)与断裂扭矩(Break Torque):当关节受到的力或扭矩超过这个阈值时,关节会自动断开销毁。这可以用来模拟绳子被拉断、木板被撬开的效果。断开后,可以通过监听关节的
OnJointBreak回调函数来触发后续逻辑(如播放断裂音效)。 - 性能考虑:每个关节都会增加物理解算的复杂度。关节链(如多个铰链连接成的链条)越长,稳定性越差,对求解器迭代次数的要求也越高。在
Project Settings > Physics中,可以调整Default Solver Iterations和Default Solver Velocity Iterations来改善复杂约束的稳定性,但会增加CPU开销。
一个铰链门的快速实现示例:
- 创建一个Cube作为门板,添加
Rigidbody(质量适中,如20)。 - 添加
Hinge Joint组件。 - 设置
Anchor为(0, 0.5, 0)(假设门高1米,铰链在顶部)。 - 设置
Axis为(0, 1, 0)(绕Y轴旋转)。 - 勾选
Use Limits,设置Limits的Min为-90,Max为90(门可以向内或向外打开90度)。 - (可选)勾选
Use Spring,给门添加一个自动关闭的弹簧力。
这样,一个基本的物理门就完成了,玩家或NPC可以通过碰撞推开它,松手后它会因摩擦或弹簧力慢慢停下或关闭。
6. 物理材质进阶与自定义碰撞响应
虽然Unity提供了标准的碰撞响应(基于法线和弹力/摩擦力),但有时我们需要更定制化的效果,比如根据碰撞部位播放不同动画,或者实现“踩头跳跃”这种平台游戏经典机制。
6.1 通过碰撞信息判断碰撞点
Collision对象包含了contacts数组,这是一个ContactPoint结构体数组,存储了本次碰撞的所有接触点信息。即使是一次碰撞,也可能有多个接触点(比如一个盒子以一个角着地)。
void OnCollisionEnter(Collision collision) { ContactPoint contact = collision.contacts[0]; // 通常取第一个接触点 Vector3 hitPoint = contact.point; // 世界空间碰撞点 Vector3 hitNormal = contact.normal; // 碰撞点法线(从自身指向对方) // 判断是否是“头顶”碰撞 float dot = Vector3.Dot(hitNormal, Vector3.down); if (dot > 0.7f) { // 法线大致朝下,说明撞到的是头顶上方的物体 // 处理头顶碰撞逻辑,比如顶碎砖块 } // 判断是否是“脚底”碰撞 if (Vector3.Dot(hitNormal, Vector3.up) > 0.7f) { // 处理落地逻辑,比如重置跳跃次数 canJump = true; } }通过分析接触点法线与角色自身方向(上、下、前、后等)的点积,可以粗略判断碰撞发生在哪个部位。
6.2 修改碰撞响应:PhysicsMaterial与脚本结合
标准物理材质提供的摩擦和弹力是全局的、被动的。我们可以在碰撞回调中主动修改碰撞响应。
- 忽略后续碰撞:在
OnCollisionEnter中,如果你希望这次碰撞后,两个物体暂时不再碰撞(比如子弹击中敌人后消失,不应再产生碰撞),可以调用Physics.IgnoreCollision(collision.collider, thisCollider)。注意管理好忽略的时机和恢复。 - 自定义力施加:你可以不依赖物理引擎的自动计算,而是根据碰撞信息,自己计算并施加力。
这种方式让你完全掌控碰撞后的行为,但需要一定的物理和数学知识来保证效果自然。void OnCollisionEnter(Collision collision) { // 计算一个自定义的反弹力,比如总是以固定速度反弹 Vector3 reflectDirection = Vector3.Reflect(rb.velocity.normalized, collision.contacts[0].normal); rb.velocity = reflectDirection * bounceSpeed; // 或者,根据碰撞的相对速度计算一个冲击力 float impactStrength = collision.relativeVelocity.magnitude; rb.AddForce(-collision.contacts[0].normal * impactStrength * someFactor, ForceMode.Impulse); }
6.3 实现平台游戏角色控制器
一个经典的2D/3D平台游戏角色控制器,其核心物理需求是:
- 可靠的地面检测:使用
Physics.SphereCast或Physics.CapsuleCast从角色底部向下投射,检测一定距离内是否有“地面”层物体。比单纯用OnCollisionStay更可靠,因为可以控制检测距离和形状。 - 可控的跳跃:跳跃不是简单施加一个向上的力。为了手感更好,通常采用“可变高度跳跃”——按跳跃键时间越长,跳得越高。这可以通过在起跳时给一个初始冲量,然后在空中如果松开按键就减小垂直速度来实现。
void HandleJump() { if (isGrounded && Input.GetButtonDown("Jump")) { rb.velocity = new Vector3(rb.velocity.x, jumpInitialSpeed, rb.velocity.z); isJumping = true; } // 如果正在跳跃且松开了按键,减少上升速度(实现小跳) if (isJumping && !Input.GetButton("Jump") && rb.velocity.y > 0) { rb.velocity = new Vector3(rb.velocity.x, rb.velocity.y * jumpCutMultiplier, rb.velocity.z); } } - 斜坡处理:当角色在斜坡上时,重力会将其拉向坡底。为了能在斜坡上站立和行走,需要将移动方向投影到斜坡的切平面上。这可以通过射线检测获取地面法线,然后使用
Vector3.ProjectOnPlane来实现。Vector3 groundNormal = ... // 从地面检测中获得 Vector3 moveDirection = ... // 玩家输入的方向 Vector3 slopeMoveDirection = Vector3.ProjectOnPlane(moveDirection, groundNormal).normalized; rb.velocity = new Vector3(slopeMoveDirection.x * speed, rb.velocity.y, slopeMoveDirection.z * speed); - 墙壁跳跃与蹬墙跳:这需要检测侧面的碰撞。可以通过在角色左右两侧放置触发器或进行射线检测来实现。当检测到侧面碰撞且玩家按下跳跃键时,施加一个远离墙壁的力和一个向上的力。
将这些技巧组合起来,你就能构建一个手感扎实、反应灵敏的物理角色控制器,它远比使用纯CharacterController或简单的AddForce要强大和灵活。关键在于理解每一帧物理状态的变化,并精确地施加干预,让物理引擎为你工作,而不是与你对抗。