ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity物理实战:实现人肉盾牌阻挡高速列车的完整方案

Unity物理实战:实现人肉盾牌阻挡高速列车的完整方案 在长弓溪谷这张地图里玩家要操控角色像一堵墙一样正面挡住高速驶来的超星列车听起来像个恶搞挑战但从游戏开发者的视角看这其实是一个很典型的“高速物体撞击 阻挡判定”玩法原型。列车高速冲过来角色要站住、挡停、还不被撞飞背后涉及碰撞检测、刚体动力学、物理材质、关卡状态管理等 Unity 开发中的核心知识。本文就把这个挑战搬进 Unity 工程用完整脚本从零实现一遍并解释每个参数的作用。文章主要面向 Unity 初学者和游戏机制原型开发者不需要很强的编程功底只要了解 C# 基础语法和 Unity 基本操作就能跟上。完成本文实战后你会掌握如何搭建“高速物体阻挡”场景、如何避免列车高速穿模、如何通过质量、摩擦和约束让“人肉盾牌”看上去更合理以及如何用一个挑战管理器统一控制整条玩法流程。1. 背景与核心概念1.1 这个挑战玩法到底在做什么“人肉盾牌挡住超星列车”直接拆开来看就是让角色站在列车运行路径上与列车发生物理接触并让列车在接触后停止。更具体地分析这个玩法有三个关键点。第一列车必须处于运动状态并且最好由物理引擎驱动。第二角色必须参与碰撞响应不能只是一个静态的 3D 模型还需要挂载 Rigidbody 刚体组件。第三列车撞上角色之后要产生明确的结果例如速度清零、列车停止前进、角色被向后推出一点距离。如果没有这个结果反馈玩家会认为这只是两个物体重叠了一下完全感受不到“阻挡”的力度。这里真正的难点在于我们在现实生活中会下意识觉得高速列车不可阻挡角色挡上去应该是“当场蒸发”的效果。但挑战要求是挡住列车所以要调整刚体质量、碰撞检测模式、Constraints 约束和物理材质让最终表现符合设计预期。游戏开发中经常遇到这种“物理真实感”和“玩法需求”不一致的情况游戏程序员要做的就是通过参数和逻辑把物理引擎调整到符合玩法需要的状态。类似机制在很多游戏里都有变体比如角色使用盾牌格挡怪物冲锋、防守任务中阻挡敌方载具、关卡里推动巨石挡住洞口等。也许你以后不会真的做一个“挡火车”的玩法但只要掌握了这套碰撞阻挡思路就能自然迁移到这些场景中。1.2 背后涉及的核心技术这个机制的核心工作流可以分为四步移动、碰撞、响应、结算。移动是指让列车沿着轨道朝角色高速前进。这一步建议使用 Rigidbody 的 velocity 来驱动而不是直接修改 transform.position因为直接修改 Transform 坐标相当于“瞬间瞬移”物理引擎无法准确判断物体之间的真实接触位置速度越快越容易穿模。碰撞是指物理引擎检测到列车与角色的碰撞体发生接触。Unity 中只要双方都有 Collider且至少一方有 Rigidbody就会触发 OnCollisionEnter、OnCollisionStay、OnCollisionExit 这些回调方法。我们通常在 OnCollisionEnter 里编写一次性的阻挡逻辑。响应是指碰撞发生后物理引擎会按照质量、速度、摩擦系数计算新的速度产生反弹或推动效果。我们也可以主动干预比如把列车的 velocity 直接清零让它看起来像是被一堵墙挡住。结算是指根据碰撞结果判断挑战是否成功。比如列车是否停在指定区域之前、角色是否保持站立、挑战是否已经结束。一般的做法是设计一个 ChallengeManager 管理类负责监听列车碰撞事件、改变关卡状态、输出结果日志。这四步对任何“物体碰撞 结果判断”玩法都通用框架搭好后后续扩展动画、音效、UI 都只是在这个流程上增加表现层。1.3 这类机制的实际应用场景“人肉盾牌挡住高速列车”听起来非常特殊但它背后的技术模型可以应用到很多常见玩法里。盾牌格挡玩家用盾牌抵挡元素攻击或物理冲击需要敌人攻击物停下来角色只受到少量击退。阻挡怪物冲锋大型怪物向玩家冲刺玩家按下格挡键时怪物被挡停并进入硬直。推箱子、推石球角色推动大型物体时物体和角色都参与物理碰撞需要控制推动方向和阻力。车辆撞击模拟在测试场景中让一辆车撞向障碍物观察障碍物是否位移、车辆是否减速。机关触发让冲过来的物体触发某个开关用碰撞回调决定机关状态。只要理解了本文的核心实现把列车换成任意高速物体把角色换成任意目标碰撞体逻辑都能直接复用。2. 环境准备与版本说明2.1 开发环境要求本文示例基于 Unity 3D 项目。你本机的具体配置不需要和文章完全一致因为 Unity 的物理 API 相对稳定核心参数在较新版本中都能使用。推荐环境如下操作系统Windows 10/11、macOS 12、Ubuntu 20.04 均可。Unity 版本建议使用 Unity 2021.3 LTS 或 Unity 2022.3 LTS 长期支持版本。编程语言C#。IDEVisual Studio 2022、JetBrains Rider 或 Visual Studio Code 都可以。为什么推荐 LTS 版本LTSLong Term Support版本会持续获得官方修复和稳定性更新适合长期开发和项目沉淀。如果你使用的是 Unity 6 或者更早的 2019 系列版本以下脚本大概率也能直接运行但 Inspector 界面和组织方式可能会有细微不同。2.2 创建项目使用 Unity Hub 创建项目流程非常简单打开 Unity Hub。点击“New project”新建项目。在模板列表中选择“3D”模板使用默认的 Built-In Render Pipeline。项目名称填写LongbowShieldChallenge。选择磁盘路径后点击“Create project”。创建完成后进入编辑器Uniy 会自动生成一个默认场景包含 Main Camera 和 Directional Light这两个我们直接保留使用。2.3 项目目录结构为了让脚本和管理更清晰建议在Assets目录下建立以下结构Assets/ Scenes/ // 场景文件 Scripts/ // C# 脚本 Materials/ // 材质文件 Prefabs/ // 预置体这个结构不是强制的但项目变大后分类清晰能明显提升维护效率。接下来每一步创建的脚本都放到Scripts文件夹下。3. 核心原理与方案拆解3.1 碰撞检测的触发条件在开始写脚本之前有必要先弄清楚 Unity 物理系统的基础规则。一个物体如果要参与物理碰撞通常需要两个组件Collider碰撞体定义物体的物理形状比如 Box Collider、Capsule Collider、Sphere Collider。碰撞体可以设置触发模式触发模式下不产生物理阻挡只发送事件通知。Rigidbody刚体让物体接受物理引擎的控制包括重力、速度、推力、碰撞力等。两个物体之间要产生碰撞响应必须满足以下条件双方都必须有 Collider。双方中至少有一个物体有 Rigidbody。如果两个物体都只有 Collider没有 Rigidbody那它们都属于静态物体Unity 物理引擎不会计算它们之间的碰撞响应。在本文场景中列车需要移动所以列车必须有 Rigidbody。角色要参与碰撞响应所以角色也要有 Rigidbody。轨道作为静态边界只需要 Box Collider不需要 Rigidbody。物体组件作用列车Box Collider Rigidbody受物理引擎驱动产生撞击力角色Capsule Collider Rigidbody参与碰撞响应阻挡列车轨道Box Collider静态边界限制列车位置3.2 高速移动为什么容易穿模很多新手会遇到一个现象列车速度调得很快结果它像幽灵一样直接穿过了角色身体根本没有触发碰撞。这个问题的本质是物理引擎的离散碰撞检测机制。Unity 物理引擎默认每隔Fixed Timestep固定时间步长更新一次物理状态默认值是 0.02 秒也就是每秒更新 50 次。在这 0.02 秒内列车会前进一段距离。如果列车速度很快它在一帧物理更新前的位置和更新后的位置之间会跨越很长距离若这个距离大于角色碰撞体的厚度引擎就可能错过重叠瞬间导致列车穿模。为了解决这个问题Unity 提供了连续碰撞检测Continuous Collision Detection简称 CCD模式Discrete离散性能最好但高速物体容易穿模。Continuous连续用于检测动态碰撞体与静态碰撞体之间的碰撞。Continuous Dynamic动态连续适用于检测动量较大的碰撞体与其他碰撞体之间的碰撞效果更好但开销也更大。Continuous Speculative一种更平稳的连续检测模式适合在性能与准确度之间折中。由于列车和角色都是动态碰撞体我们需要把列车的 Collision Detection 设为 Continuous Dynamic 或 Continuous Speculative。如果列车速度特别高推荐 Continuous Dynamic如果感觉表现不稳也可以测试 Continuous Speculative。3.3 阻挡效果如何设计“挡住”效果不是让物理引擎自动完成的通常需要叠加一层逻辑干预。首先在碰撞发生时物理引擎会按照动量守恒计算列车和角色的速度变化。如果列车质量很大角色会被直接撞飞这虽然符合物理直觉但不符合“人肉盾牌”的挑战目标。所以我们可以在OnCollisionEnter回调里手动设置列车停止把列车的velocity和angularVelocity都置为零。这样列车在碰撞瞬间被“锁死”视觉上就是挡停。与此同时角色不能一点反馈都没有否则玩家会觉得不真实。可以给角色施加一个向后的冲量让角色被推后一段小距离最终呈现“列车被硬生生挡住角色被顶退了一点”的效果。这个冲量放在 PlayerShield 脚本中处理。为了让角色更稳定还需要做两件事适当调大角色 Rigidbody 的 mass例如从默认 1 改为 50增大角色对速度变化的抵抗能力。在 Rigidbody Constraints 中冻结 X 轴和 Z 轴旋转防止角色碰撞后像瓶子一样翻滚。这两步配合使用就能做出预期的“挡停”手感。3.4 物理参数如何影响手感下面这个表格可以当作手感调参的参考表。实际项目中参数没有绝对固定的标准因为每个项目的物体尺寸和视觉风格不同参数作用建议Mass物体质量影响碰撞后的速度变化角色适当调大列车保持较大值Drag阻力影响物体移动时速度衰减列车建议 0角色可设 0.1 左右Angular Drag角阻力影响物体旋转衰减角色建议 5防止旋转失控Bounciness物理材质的弹性系数阻挡玩法建议 0避免弹开Friction物理材质的摩擦系数角色和列车接触面可调大Constraints约束自由度角色冻结旋转列车冻结 Y 轴位置Collision Detection碰撞检测模式高速物体使用 Continuous Dynamic这些参数在 Inspector 上可以直接调整每调整一次都可以点击 Play 测试整个过程非常直观。4. 完整实战案例在长弓溪谷阻挡超星列车4.1 搭建基础场景现在开始搭建具体场景。整体布局可以概括为一条笔直轨道列车从远处高速驶来角色站在轨道中间等待撞击轨道尽头放置一个失败判定区如果列车穿过了角色冲过去就挑战失败。在 Unity 默认场景中依次创建以下物体第一步创建地面。在菜单栏选择GameObject - 3D Object - Plane命名为Ground位置保持(0, 0, 0)。把地面当作草地或站台平面。第二步创建轨道。选择GameObject - 3D Object - Cube命名为Track将 Scale 调整为(0.8f, 0.1f, 40f)位置设为(0f, 0.1f, 0f)。这样轨道是一条细长长方体被贴在地面上列车沿着轨道移动。第三步创建列车。为了简单起见我们先用一个 Cube 代替列车车身。选择GameObject - 3D Object - Cube命名为Train位置设为(0f, 1f, -15f)Scale 调整为(1f, 0.8f, 3f)。你可以继续添加多个 Cube 组成车头车尾不影响本文逻辑。第四步创建角色。选择GameObject - 3D Object - Capsule命名为Player位置设为(0f, 0.5f, 0f)Scale 调整为(1.5f, 1.5f, 1.5f)。胶囊体天生带有 Capsule Collider适合模拟人形角色。第五步创建失败判定区。选择GameObject - 3D Object - Cube命名为FailZone位置设为(0f, 0.5f, 6f)Scale 调整为(2f, 2f, 1f)然后取消勾选 MeshRenderer 组件或者保留一个半透明材质。关键是给它的 Collider 勾选Is Trigger让它变成触发器。场景搭建完成后如果镜头角度不太好可以选中 Main Camera在 Inspector 里把 Position 改为(8f, 8f, 2f)Rotation 改为(45f, -90f, 0f)让场景更直观。4.2 挂载组件与设置 Tag接下来给关键物体挂载组件。选中列车Train在 Inspector 中添加如下配置保持 Box Collider 默认尺寸不需要改动。点击Add Component搜索Rigidbody并添加。将 Collision Detection 切换为Continuous Dynamic。在 Constraints 中勾选Freeze Position Y因为列车不应该因为重力掉下轨道再勾选Freeze Rotation X和Freeze Rotation Z防止列车斜向侧翻。将 Tag 设置为Train。如果没有这个 Tag点击下拉框里的Add Tag...创建。选中角色Player在 Inspector 中进行如下配置保持 Capsule Collider。添加 Rigidbody。将 Mass 设置为50Drag 设置为0.1fAngular Drag 设置为5f。将 Collision Detection 切换为Continuous Dynamic。在 Constraints 中勾选Freeze Rotation X和Freeze Rotation Z。将 Tag 设置为Player。选中FailZone把 Tag 设置为FailZoneCollider 勾选Is Trigger。最后制作两个物理材质用来调节手感。在Project窗口的Materials文件夹下右键Create - Physics Material创建两个材质HighFrictionMat和NoBounceMat。将NoBounceMat的 Bounciness 设置为 0把HighFrictionMat的摩擦力设置为 0.8 左右。把这两个材质分别拖到列车和角色的 Collider 上具体看你想让“阻挡感”多强通常角色碰撞体挂 HighFrictionMat 效果更好。4.3 编写列车控制脚本现在创建第一个核心脚本。在Scripts文件夹下右键Create - C# Script命名为TrainController.cs双击打开写入以下代码。using UnityEngine; public class TrainController : MonoBehaviour { [Header(运行参数)] public float moveSpeed 12f; public Vector3 moveDirection new Vector3(0f, 0f, 1f); public bool startMoving false; [Header(外部引用)] public ChallengeManager challengeManager; private Rigidbody rb; private bool isBlocked false; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() { if (!startMoving || isBlocked) { return; } rb.velocity moveDirection.normalized * moveSpeed; } public void StartTrain() { startMoving true; isBlocked false; } public void StopTrain() { startMoving false; isBlocked true; rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(Player)) { StopTrain(); if (challengeManager ! null) { challengeManager.OnBlockedByPlayer(); } Debug.Log(列车被角色挡下碰撞成功); } } private void OnTriggerEnter(Collider other) { if (other.CompareTag(FailZone) !isBlocked) { if (challengeManager ! null) { challengeManager.OnChallengeFailed(); } } } }这段代码需要重点解释几个部分。moveSpeed和moveDirection是列车的速度和前进方向。由于列车沿 Z 轴正方向行驶moveDirection 就写成(0, 0, 1)。如果你希望列车换方向改成负数即可。FixedUpdate是物理更新方法适合修改 Rigidbody 的受力或速度。我们把 velocity 设置成方向乘以速度这样列车每帧都保持匀速运动。注意这里必须先检查startMoving和isBlocked防止列车在还没发车时就开始移动或者已经挡住了还继续推进。OnCollisionEnter检测到列车和玩家碰撞时调用StopTrain()把列车速度清零并通知 ChallengeManager 挑战成功。这里用的是 Tag 判断Tag 配置错的话事件不会触发需要注意。OnTriggerEnter用于失败判定。如果列车没有被玩家挡住一直开到 FailZone就会进入触发器调用OnChallengeFailed()宣布挑战失败。4.4 编写角色阻挡反馈脚本下一步创建角色脚本。在Scripts文件夹下创建PlayerShield.cs写入以下代码。using UnityEngine; public class PlayerShield : MonoBehaviour { [Header(阻挡反馈参数)] public float pushBackForce 6f; private Rigidbody rb; void Start() { rb GetComponentRigidbody(); } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag(Train)) { float impactSpeed collision.relativeVelocity.magnitude; Vector3 backDirection -collision.relativeVelocity.normalized; if (backDirection Vector3.zero) { backDirection Vector3.back; } rb.AddForce(backDirection * pushBackForce, ForceMode.Impulse); Debug.Log($检测到列车撞击相对速度 {impactSpeed:F2}); } } }这个脚本的核心逻辑在OnCollisionEnter里。当角色和列车发生碰撞先计算相对速度得到撞击强度。接着用撞击方向的反方向作为击退方向给角色施加一个冲量模拟角色被顶退的感觉。ForceMode.Impulse表示这个力是一次性冲量比较适合瞬间撞击的反馈。如果你希望角色被持续推动可以改成ForceMode.Force。需要说明的是这里只是给角色施加了一个向后的力。如果角色质量比较大这个力的效果会很小实际表现为轻微滑步如果质量不够大角色会被撞飞。建议保持质量 50 左右再微调 force 数值。4.5 编写挑战状态管理脚本为了把整个玩法流程组织起来还需要第三个脚本ChallengeManager.cs。它负责启动挑战、监听结果、输出状态。using UnityEngine; public class ChallengeManager : MonoBehaviour { [Header(外部引用)] public TrainController train; public PlayerShield player; [Header(挑战状态)] public bool challengeFinished false; [Header(发车延迟)] public float delaySeconds 2f; void Start() { challengeFinished false; train.StopTrain(); Invoke(nameof(BeginChallenge), delaySeconds); } void BeginChallenge() { if (challengeFinished) { return; } train.StartTrain(); Debug.Log(挑战开始超星列车已发车); } public void OnBlockedByPlayer() { if (challengeFinished) { return; } challengeFinished true; Debug.Log(挑战成功玩家成功挡住了超星列车); } public void OnChallengeFailed() { if (challengeFinished) { return; } challengeFinished true; train.StopTrain(); Debug.Log(挑战失败列车冲过了阻挡区); } }ChallengeManager 不直接处理碰撞它只负责状态流转。在 Start 中先让列车停止两秒后调用 BeginChallenge 开始发车这是为了给玩家一个准备时间。当列车被角色挡下时TrainController 会调用OnBlockedByPlayer()把 challengeFinished 设置为 true。当列车越过 FailZone 时会调用OnChallengeFailed()同样结束挑战。这里用challengeFinished做双重判断避免被重复调用。如果你以后要扩展 UI、音效、动画可以在这个状态管理脚本里增加事件委托或 UnityEvent把逻辑和表现层解耦。4.6 挂载脚本并配置引用脚本写好后需要将它们挂到对应物体上。选中Train物体添加Train Controller脚本。在 Inspector 中的Challenge Manager字段暂时留空因为还没创建管理物体。选中Player物体添加Player Shield脚本。接下来创建一个空物体管理挑战流程。在菜单栏选择GameObject - Create Empty命名为ChallengeManager。选中它添加Challenge Manager脚本。在ChallengeManager的 Inspector 里把Train物体拖到Train字段把Player物体拖到Player字段。再把Train物体上 TrainController 脚本的Challenge Manager字段拖入ChallengeManager物体。这样引用就配好了。这个步骤很容易漏掉。如果引用为空运行时报错或状态不更新需要回头检查。4.7 运行与验证现在点击编辑器顶部的 Play 按钮运行场景。预期流程如下场景启动后ChallengeManager 会先调用train.StopTrain()所以列车静止不动。两秒后控制台输出“挑战开始超星列车已发车”。列车开始沿轨道向角色移动。列车碰到角色时触发TrainController.OnCollisionEnter列车速度被清零PlayerShield 同时给角色一个向后的冲量。控制台输出“列车被角色挡下碰撞成功”和“挑战成功玩家成功挡住了超星列车”。如果你把列车速度调得特别高比如 50 以上需要额外检查是否发生穿模。如果列车穿过了角色就会继续前进进入 FailZone控制台输出“挑战失败列车冲过了阻挡区”。在实际测试中可以通过 Inspector 面板实时查看列车和角色的速度变化。运行时选中Train展开 Rigidbody 组件可以看到 Velocity 在碰撞前等于设定速度碰撞后变为 0。4.8 场景调整建议如果碰撞后角色还是被撞飞优先检查几件事角色 Mass 是否偏小建议逐步提高到 80 甚至 100 测试。是否在 Rigidbody Constraints 中冻结了旋转不冻结很容易翻滚。pushBackForce 是否过大先保持 5 到 8再根据效果微调。列车速度是否过高连续碰撞检测是否开启。这些调整都是即时的点击 Play 后每一轮测试都很快很适合作为入门物理实验。5. 常见问题与排查思路实际开发中新手最容易遇到的几个问题这里统一整理成表格方便直接按表排查。问题现象常见原因解决思路列车穿过角色碰撞不触发列车速度过快离散碰撞检测失效将列车的 Collision Detection 改为 Continuous Dynamic列车和角色根本不发生碰撞其中一个物体没有 Collider 或 Rigidbody检查双方是否都有 Collider至少一方有 Rigidbody碰撞事件方法没有被调用Tag 设置错误或脚本没有挂到对应物体确认 Player 和 Train 的 Tag确认脚本挂载正确角色被撞飞角色 mass 过小或未冻结旋转增大 mass冻结 Rotation X 和 Z列车被挡下后还在滑动没有清空 angularVelocity 和 velocityStopTrain 中同时清空两个速度字段挑战结果重复输出challengeFinished 判断缺失在状态方法开头检查 challengeFinished挑战一直不开始ChallengeManager 未配置 train 引用检查 Inspector 中的引用字段是否拖入FailZone 触发失败Is Trigger 未勾选或 Tag 不对给 FailZone 勾选 Is TriggerTag 设为 FailZone如果遇到“列车在轨道上抖动”的问题多半是因为在 Update 而不是 FixedUpdate 中设置速度或者同时使用了 Transform 移动和 Rigidbody 物理。正确的做法是只在 FixedUpdate 里修改 Rigidbody 的 velocity 或 AddForce避免两套移动逻辑打架。还可以在脚本中打印更多日志辅助排查。比如在OnCollisionEnter里打印collision.gameObject.name确认碰撞体是否正确在FixedUpdate里打印rb.velocity确认列车速度是否被正确设置。日志是定位物理问题最直接的帮手。6. 最佳实践与工程建议6.1 参数尽量暴露到 Inspector把 moveSpeed、pushBackForce、delaySeconds 这类参数写成 public 字段不要写死在代码里。这样策划或测试人员可以直接在 Inspector 中调整数值立即看到效果而不需要修改代码重新编译。使用[Header]
返回列表