第 41 章 物理系统:碰撞、刚体、射线,游戏世界的”牛顿”
我先抛个你绝对有直觉的现象:你玩任何一个 3D 游戏,丢出去的石头会往下掉、撞到墙会停住、被车撞飞会滚出去、子弹打中敌人会”啪”地命中。你作为写 Go 后端的,第一反应可能是:这些东西”掉下去、撞上来、弹开去”的位置,是不是得我每帧在 Update 里自己算?
我直接给你答案,免得你走弯路:不是你算的,是物理引擎替你算的。 你只要说”这块石头受重力、质量 2 公斤、形状是个球”,引擎就在每一物理步里按牛顿定律把它积分算出来——位置怎么变、速度多少、撞到别的物体怎么弹。这一章,我就把这层”游戏世界的牛顿”扒开给你看,而且我会特意把它和第 38 章要讲的主循环固定步长(FixedUpdate)咬死,因为物理能不能”确定、不穿模、不抖”,根子就在那个步长上。
先给你一个贯穿全章的类比,省得后面绕:物理引擎像一张台球桌的裁判。你(程序员)只负责”摆球、给母球一个力度”,裁判(物理引擎)负责”每 1/50 秒推演一次所有球的位置和碰撞,直到静止”。你不用手写每颗球何时撞哪颗——那是裁判的活。你写的代码,是”发球”和”读结果”,不是”逐帧当裁判”。
41.1 现象:东西会掉、会撞、会弹——引擎在 FixedUpdate 里积分算
现象层:一个没挂任何东西的方块,你运行游戏,它纹丝不动;你给它挂上”物理身体”,它立刻开始往下掉,落到地面”咚”地停住,你用力推它它会滚开。你没写一行”每帧 y 减 0.1”的代码,掉和停都是凭空发生的。
现实问题:重力、碰撞响应(撞墙停下、互撞弹开)这种事,涉及连续力学积分和大量物体两两检测,复杂度极高。让你在 Update 里手算?第一,你算不准(碰撞响应是带约束的微分方程,不是 y -= g*dt 那么简单);第二,帧率一波动,手算的物理就不确定(40 帧和 144 帧掉的速度不一样,联机直接乱套)。
工程师的方案,就是把”物理推演”从你的渲染逻辑里剥离出来,交给一个专门的物理引擎,并且放在固定步长(fixed timestep)里跑。这就是你第 38 章要深讲的 FixedUpdate 的用武之地——和你渲染的 Update(帧率随设备波动)不同,FixedUpdate 以恒定频率(默认每秒 50 次,即 0.02 秒一步)被调用。物理引擎就在每一步里,对所有带”物理身体”的物体做一遍:
对每个刚体:速度 += 力 / 质量 × 步长;位置 += 速度 × 步长;再解算碰撞约束
这套”速度 += 加速度×dt、位置 += 速度×dt”,就是最朴素的数值积分(integration)——你大学物理的 v = v0 + at、s = s0 + vt,被引擎每步重复。因为步长固定,所以无论你设备跑 40 帧还是 144 帧,物理世界的演化都是确定性的(同一输入同结果),这是联机同步和”回放一致”的命根子。
连回第 38 章:为什么物理”必须”在固定步算,而不能在
Update(变步长)算?因为变步长下位置 += 速度 × dt里那个dt每次不同,积分误差会累积且不可复现——球可能这次穿墙、下次没穿,纯看当时帧率。固定步把dt锁死,物理稳定可复现。你写 Go 时”定时任务用固定 ticker 而非 sleep 近似”是同一思路。记住这句话:物理的真相在 FixedUpdate,不在 Update。
落进引擎:Unity 的 PhysX(3D)/ Box2D(2D)物理引擎,就是那个”裁判”。你挂组件、设参数,它跑积分。下面几节把”挂什么组件”逐一拆开。
41.2 刚体 Rigidbody:给物体一个”物理身体”
先讲让物体”听物理的话”的组件:刚体(Rigidbody)。术语首现——“刚体”是物理学术语,指”受力后形状不变、只整体平移/旋转的硬物体”(区别于橡皮泥那种会形变的软体)。在游戏引擎里,Rigidbody 组件就是给 GameObject 贴一张”我是物理世界一员”的标签。
现象:一个只有 MeshRenderer(第 4 章的网格、第 18 章的显示组件)的方块,不会掉——因为它只是”能被画出来”,没说”受物理管”。给它加 Rigidbody,它立刻受重力下落。
工程师的方案:Rigidbody 持有一个物体的物理状态——质量(mass)、速度(velocity)、角速度(angular velocity)、是否受重力等。物理引擎每固定步读这些量,按牛顿第二定律 F = m·a(力 = 质量 × 加速度)更新速度和位置。你不用手算,你只”施力”或”设速度”。
连回第 18 章 18.4,关键参数和它们的”解决什么问题”:
- Mass(质量):决定”同样力下加速多快”(a = F/m)。重的车推得动得慢。调参直觉:人物 1、车 1000,别真的按公斤,按”手感”给相对值。
- Drag(空气阻力 / 阻尼):让物体自然减速(速度每步乘个小于 1 的系数)。没阻力,推力一停物体永远滑。
- Use Gravity(是否受重力):关掉,物体悬浮(比如太空场景、UI 漂浮物)。
- Is Kinematic(是否运动学):勾上后,物理引擎”不推它”(它不受力、不自己掉),但它能推动别的物体。典型用途:电梯、移动平台——你用代码精确控制它的位移(像第 40 章的”代码驱动”),但它碰到的箱子会被它带着走。
一个新手必踩的巨坑,我重点标红:你移动一个带 Rigidbody 的物体,千万别在 Update 里直接改 transform.position。为什么?因为物理引擎也在每固定步改这个物体的位置/速度(它维护着自己的速度积分)。你一手改 Transform、引擎一手改自己的物理状态,两者打架——结果就是物体抖动、穿透、被”弹回原位”。正确做法:在 FixedUpdate 里用 rigidbody.AddForce(力) 或 rigidbody.velocity = 速度 去驱动物体,把”位移真相”交给物理引擎(呼应第 40 章 40.6 说的”真相来源只能一个”)。
类比:你用手推一辆正在自己跑的车——你猛地把它挪到别处,车自己的引擎还按原速度算下一帧位置,两股力对撞,车就抽风了。要么你松手让车自己跑(用 AddForce/velocity),要么你把它设成 Kinematic 完全由你控(Is Kinematic)。中间态必乱。
给你一个”推开 / 跳起”的具体代码感受,对比错误与正确写法:
// 错误:在 Update 里直接改 Transform,会和物理引擎打架(见上段)
// transform.position += Vector3.forward * 5f * Time.deltaTime;
// 正确:在 FixedUpdate 里用速度 / 力驱动,位移真相交给引擎
void FixedUpdate()
{
if (Input.GetKeyDown(KeyCode.Space))
rb.AddForce(Vector3.up * 5f, ForceMode.Impulse); // 给一个瞬时的上跳力
}AddForce 的 ForceMode 还有 Force(持续力,按质量加速)、VelocityChange(直接改速度、不乘质量)等,你按”想模拟推力还是想直接设速度”来选。关键:调用时机是 FixedUpdate 而非 Update——只有固定步里的力,积分出来才确定、才和物理引擎同节拍(呼应 41.1 的固定步)。
主程还要知道两个 Rigidbody 的高频考点。其一是休眠(Sleeping):物理引擎不会让每个刚体每步都满负荷算——长时间静止的刚体(比如地上的箱子)会被引擎”休眠”,跳过积分,直到被别的东西撞醒,这省了大量 CPU;但代价是:你若用代码悄悄挪一个休眠刚体,它可能”没反应”,得先 rb.WakeUp() 唤醒它。其二是连续碰撞检测(CCD,Continuous Collision Detection):普通离散检测下,一个极快的子弹可能”一帧跳过”薄墙(步长太大,起点在墙前、终点在墙后,中间没采样到墙),造成穿墙;勾上 Rigidbody 的 Collision Detection = Continuous,引擎会沿运动轨迹做扫掠检测防穿透——代价更贵,只给真正快且薄的东西(子弹、飞镖)开。这两点,正是”物理稳定且便宜”的隐藏旋钮。
落进引擎的小结:Rigidbody 是”物理身体的身份证”。有它,物体才掉、才被推、才撞。移动它用
FixedUpdate+AddForce/velocity,别碰 Transform。这和第 40 章动画的”位移真相归谁”是同一类主程取舍。
41.3 碰撞体 Collider:物体的”物理外形”,只管碰不碰
光有 Rigidbody 还不够。你想,物理引擎要知道”两个东西撞没撞上”,得先知道”它们长啥样、边界在哪”。这就是碰撞体(Collider)。
先清一个你极易混的点:碰撞体 ≠ 显示模型。你第 4 章学的网格(Mesh)是”用来画的形状”,碰撞体是”用来算碰撞的形状”。一个角色显示用精细的人形网格(几万三角形),但物理碰撞用一个**胶囊体(Capsule)**就够了——因为碰撞检测要便宜、要稳,没必要用那个精细网格去算。
连回第 18 章 18.3,Collider 家族:
- Box Collider(盒):长方体。墙、箱子、地板用。最便宜。
- Sphere Collider(球):球。石头、弹珠用。很便宜。
- Capsule Collider(胶囊):两头半圆中间圆柱。人、怪物用——贴合”直立身体”又极便宜,且不会卡在地面缝隙里(球/盒的边角易卡)。
- Mesh Collider(网格碰撞体):贴着模型真实形状。最精确但最贵,而且默认只用于静态物体(地形、建筑)。动态物体慎用——每帧重建/检测精细网格碰撞,性能炸。
工程师的取舍很清楚:碰撞体是”物理外形”,目的是廉价且稳定地回答’碰没碰’,不是’画得像不像’。所以几乎所有动态物体都用简单体(盒/球/胶囊),只有静态复杂地形才偶尔用 Mesh Collider。你第 4 章的”网格决定显示”,和这里的”碰撞体决定碰撞”,是两条独立的线——一个管看,一个管碰。
连回第 27 章:碰撞体在世界里的位置,也是靠那个物体的 Transform(Model 矩阵)定位的。碰撞检测和渲染共用同一套”坐标变换链”——物体移动,它的碰撞体跟着 Transform 移到新世界位置,物理引擎据此检测。所以你第 2、27 章学的坐标链,在这里又派上用场:碰撞体的世界边界 = 它的局部形状 × 物体的 Model 矩阵。
一个判断口诀给你:看到”这东西要被看见”→ 想 MeshRenderer/MeshFilter(第 18 章 18.2);看到”这东西要能被撞/能挡路”→ 想 Collider。两个都挂,才是”既看得见又碰得着”的完整物体。
一个常被忽视的判定:“显不显示”和”碰不碰”完全正交,别默认绑死。你可以挂 MeshRenderer 但不挂 Collider(纯装饰的飘带、背景雕像,碰不到,省碰撞开销);也可以挂 Collider 但不挂 MeshRenderer(隐形的空气墙、触发区,常用来做”看不见的边界”把玩家挡在某个区域外)。后者在关卡设计里极常用——你看到角色走到地图边缘”撞上空气”停住,那道空气就是个没渲染的 Box Collider。所以”看得见”和”碰得着”是两根独立开关:该显示的不一定该碰(飘带),该碰的不一定该显示(空气墙)。把这层想通,你搭场景时就不会要么漏碰、要么白白给装饰物算碰撞。
41.4 碰撞 Collision vs 触发 Trigger:挡路的 vs 通知的
这是物理里最容易被混淆、面试最爱考的一对。你第 18 章 18.13 见过一眼,这里凿透。
现象与问题:两个物体碰到一起,游戏世界有两种完全不同的意图——
- 意图 A:物理上挡住。角色撞墙,应该”停住/弹开”,不能穿过去。这是碰撞(Collision)。
- 意图 B:逻辑上通知。玩家走进”陷阱区”,不挡路(人照常走),但触发”扣血”。这是触发(Trigger)。
工程师用两个开关 + 两套回调区分:
- 碰撞(Collision):两个 Collider 都没勾 Is Trigger,且至少一个有 Rigidbody。物理引擎会算出碰撞响应(弹开/停下),并回调
OnCollisionEnter(刚接触)、OnCollisionStay(持续接触)、OnCollisionExit(离开)。 - 触发(Trigger):把其中一个 Collider 勾上 Is Trigger。它变成”感应区”——不挡路(物体能穿过去),但每当有别的 Collider 进入/停留/离开,回调
OnTriggerEnter/OnTriggerStay/OnTriggerExit,纯粹发个”通知”给你写逻辑。
本质区别一句话:Collision 有物理响应(真的挡/弹),Trigger 只发逻辑通知(不挡路)。你第 40 章学的”表现与逻辑分离”在这里再一次出现:Collision 是物理层的事,Trigger 是”用物理做逻辑传感器的钩子”。
给个”踩陷阱扣血”的例子(Trigger 路线):
using UnityEngine;
public class TrapZone : MonoBehaviour
{
public int damage = 10;
// 别的 Collider 进入这个(勾了 Is Trigger 的)区域时触发
void OnTriggerEnter(Collider other)
{
// 只对有 "Player" 标签的物体扣血
if (other.CompareTag("Player"))
{
// 假设玩家有个 HP 组件
var hp = other.GetComponent<PlayerHP>();
if (hp != null)
{
hp.TakeDamage(damage);
Debug.Log("踩到陷阱,扣血 " + damage);
}
}
}
}逐行点:
- 这个陷阱 GameObject 的 Collider 勾了
Is Trigger(在 Inspector 里勾),所以它是感应区,玩家能走过去不卡住。 OnTriggerEnter是 Unity 在”有东西进入 Trigger”时自动调用的消息方法(名字固定,引擎反射调用,你只管写逻辑)。other.CompareTag("Player"):用标签过滤,避免扣到敌人/子弹身上。hp.TakeDamage(...):纯逻辑——扣血是游戏状态,和物理挡不挡路无关。这正是”Trigger 不挡路、只通知”的写法。
如果你想要”撞墙停下”的效果,那走 Collision 路线,挂 Rigidbody + 不勾 Is Trigger 的 Collider,写 OnCollisionEnter 去播”撞击音效/扣耐久”之类——墙的物理阻挡是引擎自动做的,你只需在碰撞时补自己的逻辑。
连回第 18 章 18.13:塔的”攻击范围检测”就该用 Trigger 球形碰撞体——敌人进入就加入可攻击列表,不挡敌人路;而”角色站在地面不掉落”靠的是 Collision(地面和脚底的 Collider 实打实挡着)。同一对概念,一个挡、一个感,全靠 Is Trigger 这一勾。你面试被问”Collision 和 Trigger 区别”,就把”挡 vs 通知 + Is Trigger 开关 + 两套回调”甩出去,比背定义强。
41.5 射线 Raycast:从一点发射一条线,检测打中什么
现象与问题:很多玩法需要”从某点沿某方向,问’第一个碰到的是什么’“——鼠标点选物体(从相机沿鼠标方向发射)、射击命中(从枪口沿瞄准方向)、AI 视线检测(怪看不看得见玩家)。你要手算”这条线穿过了哪些三角形、哪个最近”,又是几何地狱。
工程师的方案:射线检测(Raycast)。你给它”起点 + 方向”,物理引擎沿这条无限细的线,找出它击中的第一个碰撞体,返回命中点信息(撞到谁、撞在哪、距离多远)。类比最准的是激光笔 / 手电筒:你按下开关,一道光射出去,照到第一个物体就停,你读”照到的是谁、照在点哪”。
给个”鼠标点选物体”的例子:
using UnityEngine;
public class MousePicker : MonoBehaviour
{
void Update()
{
// 鼠标没按下就跳过
if (!Input.GetMouseButtonDown(0)) return;
// 1) 从主相机,经过鼠标屏幕位置,发出一条射线
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 2) 让物理引擎沿这条射线检测,命中结果写进 hit
if (Physics.Raycast(ray, out RaycastHit hit))
{
// 3) 读命中:hit.collider 是被打中的碰撞体
var enemy = hit.collider.GetComponent<Enemy>();
if (enemy != null)
{
enemy.Select(); // 选中敌人
Debug.Log("选中了:" + hit.collider.name
+ ",距离:" + hit.distance);
}
}
}
}逐行点:
Camera.main.ScreenPointToRay:把”屏幕上的鼠标像素点”反算成”世界里从相机出发、穿过该像素的一条射线”——这正用到了你第 7 章学的摄像机、第 27 章学的”屏幕 → 世界”那套逆变换。你点屏幕某个像素,本质是想选”那条视线穿过去的第一个物体”。Physics.Raycast(ray, out hit):核心调用。out RaycastHit hit是 C# 的 out 参数(类似你 Go 里传指针收结果),命中后hit里填满信息。hit.collider/hit.distance/hit.point:命中的碰撞体、距离、世界坐标点。你想做”子弹打在墙上留弹孔”,就用hit.point在那个世界坐标贴个痕迹。
Raycast 是”逻辑探针”,它不移动任何物体、不产生碰撞响应,只是”问一句”。所以它是做交互、做命中、做 AI 感知的最高频工具。性能上,一条射线很便宜(引擎用空间划分快速求交),但你如果每帧对全屏每个像素发射线(比如做某种后处理),那就贵了——点到为止。
连回前面:Raycast 检测的是”碰撞体(Collider)“,不是”显示的 Mesh”——又一次印证 41.3 说的”碰撞体独立于显示”。你射线打中的是那个胶囊碰撞体,哪怕它的显示模型是个精细人形。这也解释了为什么有时你”明明点到了模型的衣角,却没选中”——因为衣角那块没碰撞体,射线穿过去了。
41.6 关节、约束与物理材质:连接、摩擦、弹性
前面是”单个物体 + 碰撞”。真实游戏还常要”物体之间连起来”或”表面有摩擦/弹性”。三样东西:
关节 / 约束(Joint):把两个 Rigidbody 连起来,限制它们的相对运动。比如:
- Hinge Joint(铰链关节):像门,只能绕一根轴转。门、秋千用。
- Spring Joint(弹簧关节):像橡皮筋,两物体间有弹性拉力。
- Fixed Joint(固定关节):焊死,一起动。吊着的灯、连在车上的炮塔。
- Configurable Joint:万能关节,你能精细配”哪几个轴锁死、哪几个自由、加多大阻力”。布娃娃(Ragdoll)就是一堆关节把人体各骨头连起来,角色死亡时切换成布娃娃,身体软趴趴地瘫——你第 40 章 40.2 说的骨头,在这里被物理关节接管,动画让位给物理。
类比:关节像你生活里的连接件——门轴是铰链、橡皮筋是弹簧、焊点是固定。你不用手算连接后的力学,挂关节、设参数,引擎解算。
物理材质(Physic Material):贴到 Collider 上,定义表面的摩擦(friction)和弹性(bounciness)。冰面摩擦低(一推滑老远)、橡胶摩擦高(一推就停)、篮球弹性高(落地弹起)、泥地弹性 0(落地不弹)。这连回你第 18 章”参数解决什么问题”的思维:摩擦/弹性不是显示属性,是物理响应属性,所以用专门的 Physic Material 而非 Material(材质贴图那个)。
一个易混点提醒:Physic Material(物理材质,管摩擦弹性)和 Material(渲染材质,管贴图颜色,第 5 章)名字像、但完全是两回事,挂的地方也不同。你看到 Material 先想”它管画还是管碰”——碰,就是 Physic Material。
41.7 性能提醒:物理在 FixedUpdate 跑,贵起来要命
回到 41.1 的根:物理在固定步(FixedUpdate)里跑。这意味着即使你画面只渲染了 30 帧,物理可能已经悄悄走了 50 步——固定步和渲染帧率脱钩。好处是确定性,代价是:物理本身是个吃 CPU 的大户,以下三处最易爆:
- 大量刚体 + 复杂碰撞:每个动态 Rigidbody 每步要和其他可能碰撞的物体做”宽相(粗略筛)+ 窄相(精确测)“检测。几百个动态刚体互相检测,CPU 直接跪。对策:能用 Trigger/简单体就用;静态物体(地形、墙)用 Static Collider,引擎会把它放进加速结构,不参与”两两动态检测”。
- Mesh Collider 滥用:41.3 说过,动态物体用 Mesh Collider 每帧重建/检测精细网格,极贵。能不用就不用,非用不可就标 Convex(凸包近似)降成本。
- 和对象池配合(连回第 39 章 / 第 35 章的对象池):塔防里子弹、怪物死亡特效这类”频繁创建销毁”的物理物体,别真
Instantiate/Destroy(产生 GC 卡顿,你第 35 章诊断里提过”对象池治 GC”)。用对象池复用一个带 Rigidbody 的预制体,死亡时SetActive(false)收回、需要时激活——既避免 GC 抖动,也避免反复创建碰撞体的开销。
还有个主程必知的小开关:Layer Collision Matrix(层碰撞矩阵)。在 Project Settings 里你能规定”哪些 Layer 之间才检测碰撞”。比如”子弹”和”子弹”互相不碰、“特效”和”地面”不碰——把不需要互相检测的组合关掉,碰撞检测的 pairwise 数量指数级下降。这是物理性能调优的第一斧。
连回主线:你第 35 章给主程画的九大模块里,“动画/物理”是明确硬缺口,而”性能/对象池”也是。这一节把两者接上了——物理贵,所以要用对象池(复用)、用简单碰撞体、用层矩阵裁剪检测。主程不是”会挂 Rigidbody”就行,是”知道几百个刚体时怎么不让帧率崩”。这又是”广度兜底 + 关键深度”(第 35 章定义的主程本色)。
41.8 收个尾,连回主线
咱们把这章扣回全书的钥匙:游戏世界的”牛顿”不是你手写每帧算的,是物理引擎在固定步里做数值积分 + 碰撞解算的。你只挂组件、设参数、读回调。Rigidbody 给物理身体、Collider 给物理外形、Collision/Trigger 分”挡”与”通知”、Raycast 做逻辑探针、Joint/Physic Material 管连接与摩擦弹性。 而这一切稳定的前提,是物理跑在固定步(第 38 章)——变步长会让物理不确定、穿模、联机乱套。
- 自然现象 / 问题:物体会掉、会撞、会弹,逐帧手算不可行(算不准、帧率波动导致不确定)。
- 工程师的方案:剥离物理推演给专用引擎,放固定步积分;用 Rigidbody(受力身体)、Collider(物理外形)、Collision/Trigger(挡 vs 通知)、Raycast(探针)、Joint/Physic Material(连接/摩擦)。
- 落进引擎的参数:Rigidbody 的 Mass/Drag/Use Gravity/Is Kinematic;Collider 的类型与 Is Trigger;
OnCollision*/OnTrigger*回调;Physics.Raycast;Joint 类型;Physic Material 的 friction/bounciness;Layer Collision Matrix。
你带走的最核心一句话:物理的真相在 FixedUpdate,位移/碰撞交给引擎,你只施力、读结果;Collision 挡路、Trigger 通知,Raycast 探路——分清这三样,物理系统对你就是可控的乐高而非黑箱。 你从”手算每帧”的死路,走到”挂组件 + 设参数 + 收回调”,和动画系统(第 40 章)是同一套”别自己造轮子、用引擎给的机关”的主程思维。
【思考题】
- 第 41.1 节说物理必须放在固定步长(FixedUpdate)里跑,而不能放在变步长的 Update 里。请用”数值积分
位置 += 速度 × dt”解释:为什么变步长会让物理”不确定、甚至穿墙”?这和第 38 章要讲的主循环架构有什么关系? - 碰撞(Collision)和触发(Trigger)的本质区别是什么?各举一个你游戏里该用哪一种的例子,并说明:要让一个区域”只通知不挡路”,Inspector 里要勾哪个开关?对应的脚本回调方法叫什么?
- 你给一个带 Rigidbody 的角色做移动,为什么”在 Update 里直接改 transform.position”是错的?正确做法是什么?这和第 40 章 40.6 讲的”位移真相来源只能一个”有什么内在联系?
- 射线检测(Raycast)打中的是”碰撞体”还是”显示的网格”?为什么有时你”点到了模型的衣角却没选中物体”?如果用 Raycast 做”子弹命中”,你大概会从
RaycastHit的哪个字段拿到”打在墙上的世界坐标”去贴弹孔?
【参考答案】
- 积分
位置 += 速度 × dt里dt是步长。变步长下每次dt不同(40 帧时大、144 帧时小),积分误差累积且每次运行结果不同——球这次在墙前停、下次因步长不同直接穿过去(数值上”跳过”了墙的薄碰撞层)。固定步把dt锁死成常量(如 0.02s),物理演化确定可复现,联机/回放才能一致。这正连回第 38 章主循环:渲染帧率随设备波动该用 Update,但物理这类”要确定性”的演算必须独立成 FixedUpdate 固定 ticker,和渲染解耦。你 Go 里”定时任务用固定 ticker 而非 sleep 近似”是同一思路。 - 本质区别:Collision 有物理响应(真挡/弹),Trigger 只发逻辑通知(不挡路)。例子——Collision:角色撞墙要停下、车撞车要弹开;Trigger:陷阱区扣血、塔的攻击范围检测、出生点感应——都不该挡路。要让区域”只通知不挡路”,在 Inspector 给该 Collider 勾 Is Trigger;脚本里用
OnTriggerEnter(及 Stay/Exit)收通知,而非OnCollisionEnter。一句话:挡=Collision,感=Trigger,开关就是 Is Trigger。 - 错在”位移真相来源冲突”:Rigidbody 的物理引擎每固定步也在维护这个物体的速度积分、改它的位置;你又在同一物体上直接改 transform.position,两股力对撞 → 抖动、穿透、被弹回。正确做法:在
FixedUpdate里用rigidbody.AddForce(力)或rigidbody.velocity = 速度驱动物体,把位移真相交给物理引擎;若你要完全自己控位移(如电梯/平台),把它设Is Kinematic。这正好是第 40 章 40.6 的”位移真相来源只能一个”——动画根运动 vs 代码驱动二选一,这里物理引擎 vs 你手改 Transform 也是二选一,贪多必乱。 - Raycast 打中的是碰撞体(Collider),不是显示的 Mesh。点衣角没选中,是因为衣角那块没有碰撞体覆盖(角色通常只挂一个胶囊碰撞体罩住身体,衣角超出胶囊或本就没建碰撞几何),射线从衣角缝隙穿过去打空了。做子弹弹孔,用
RaycastHit.point——它是命中点的世界坐标,你在那Instantiate一个弹孔贴花即可;hit.collider告诉你打中谁,hit.distance是距离。